Add LINE push integration and target mode config

This commit is contained in:
2026-04-15 16:22:19 +08:00
parent 1687223631
commit 2bed4f6df0
5 changed files with 256 additions and 1 deletions

View File

@@ -17,6 +17,14 @@ const distReady = existsSync(path.join(distDir, 'index.html'))
const requiredEnv = ['DB_HOST', 'DB_PORT', 'DB_USER', 'DB_PASSWORD', 'DB_DATABASE']
const missingEnv = requiredEnv.filter((key) => !process.env[key])
const lineAccessToken =
process.env.LINE_CHANNEL_ACCESS_TOKEN ?? process.env.channelAccessToken ?? ''
const lineTargetMode = (process.env.LINE_TARGET_MODE ?? 'local').toLowerCase()
const lineTargetId =
process.env.LINE_TARGET_ID ??
(lineTargetMode === 'prod'
? process.env.LINE_TARGET_ID_PROD ?? ''
: process.env.LINE_TARGET_ID_LOCAL ?? '')
const pool =
missingEnv.length === 0
@@ -39,10 +47,64 @@ app.get('/api/health', (_request, response) => {
ok: true,
dbReady: Boolean(pool),
distReady,
lineReady: Boolean(lineAccessToken && lineTargetId),
lineTargetMode,
missingEnv,
})
})
app.post('/api/line/push-match-results', async (request, response) => {
const { time, teams } = request.body ?? {}
if (typeof time !== 'string' || !Array.isArray(teams)) {
response.status(400).json({
ok: false,
message: '送出的 LINE 訊息資料格式不正確。',
})
return
}
if (!lineAccessToken || !lineTargetId) {
response.status(500).json({
ok: false,
message:
'LINE 推播環境變數缺少,請檢查 LINE_CHANNEL_ACCESS_TOKEN 與 LINE_TARGET_ID_LOCAL / LINE_TARGET_ID_PROD。',
})
return
}
try {
const message = buildLineFlexMessage(time, teams)
const lineResponse = await fetch('https://api.line.me/v2/bot/message/push', {
method: 'POST',
headers: {
Authorization: `Bearer ${lineAccessToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
to: lineTargetId,
messages: [message],
}),
})
if (!lineResponse.ok) {
const errorText = await lineResponse.text()
throw new Error(`LINE 推播失敗:${errorText}`)
}
response.json({
ok: true,
message: '已推送到 LINE。',
})
} catch (error) {
console.error('line push error:', error)
response.status(500).json({
ok: false,
message: error instanceof Error ? error.message : 'LINE 推播失敗。',
})
}
})
app.get('/api/match-results/:time', async (request, response) => {
if (!pool) {
response.status(500).json({
@@ -172,6 +234,7 @@ if (distReady) {
app.listen(port, () => {
console.log(`Server ready on http://localhost:${port}`)
console.log(`Static files: ${distReady ? 'loaded' : 'missing'}`)
console.log(`LINE target mode: ${lineTargetMode}`)
if (missingEnv.length > 0) {
console.log(`Missing env: ${missingEnv.join(', ')}`)
}
@@ -187,3 +250,102 @@ async function ensureTable(poolInstance, currentTableName) {
) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci
`)
}
function buildLineFlexMessage(time, rounds) {
const dateText = `${time.slice(0, 4)}-${time.slice(4, 6)}-${time.slice(6, 8)}`
return {
type: 'flex',
altText: `${dateText} 羽球隊伍配對`,
contents: {
type: 'carousel',
contents: rounds.map((round, roundIndex) => ({
type: 'bubble',
size: 'micro',
body: {
type: 'box',
layout: 'vertical',
contents: [
{
type: 'text',
text: '勝皇羽球團',
weight: 'bold',
color: '#1DB446',
size: 'sm',
},
{
type: 'text',
text: dateText,
weight: 'bold',
size: 'xl',
margin: 'sm',
},
{
type: 'text',
text: `${roundIndex + 1}`,
weight: 'bold',
size: 'xxl',
margin: 'sm',
},
{
type: 'separator',
margin: 'md',
},
{
type: 'box',
layout: 'horizontal',
contents: [
{
type: 'text',
text: '一號隊友',
size: 'sm',
flex: 0,
},
{
type: 'text',
text: '二號隊友',
size: 'sm',
align: 'center',
},
],
margin: 'md',
},
{
type: 'separator',
margin: 'sm',
},
{
type: 'box',
layout: 'vertical',
spacing: 'sm',
margin: 'md',
contents: round.teams.map((team) => ({
type: 'box',
layout: 'horizontal',
contents: [
{
type: 'text',
text: team.a,
size: 'sm',
flex: 0,
},
{
type: 'text',
text: team.b,
size: 'sm',
align: 'center',
},
],
})),
},
],
},
styles: {
footer: {
separator: true,
},
},
})),
},
}
}