统一前后端代码格式化配置

This commit is contained in:
yml2213
2026-08-16 17:27:12 +08:00
parent 8705f6a6a1
commit 5c7c3e14e3
278 changed files with 6386 additions and 5500 deletions
+13
View File
@@ -0,0 +1,13 @@
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
indent_style = space
indent_size = 2
trim_trailing_whitespace = true
[*.md]
trim_trailing_whitespace = false
+36
View File
@@ -0,0 +1,36 @@
name: 代码质量
on:
push:
pull_request:
jobs:
backend:
runs-on: ubuntu-latest
defaults:
run:
working-directory: apps/backend
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
cache-dependency-path: apps/backend/package-lock.json
- run: npm ci
- run: npm run check
frontend:
runs-on: ubuntu-latest
defaults:
run:
working-directory: apps/frontend
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
cache-dependency-path: apps/frontend/package-lock.json
- run: npm ci
- run: npm run check
+8
View File
@@ -0,0 +1,8 @@
**/node_modules
**/dist
**/.vite
**/coverage
**/data
**/package-lock.json
**/.env*
@@ -4,3 +4,4 @@
"printWidth": 100,
"trailingComma": "all"
}
+4
View File
@@ -0,0 +1,4 @@
{
"recommendations": ["esbenp.prettier-vscode", "editorconfig.editorconfig"]
}
+5
View File
@@ -0,0 +1,5 @@
{
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true,
"prettier.configPath": ".prettierrc.json"
}
+8 -3
View File
@@ -88,8 +88,11 @@ bash deploy/mac-dev.sh
```bash
docker compose -f docker-compose.dev.yml exec -T backend npm test
docker compose -f docker-compose.dev.yml exec -T backend npm run format:check
docker compose -f docker-compose.dev.yml exec -T backend npm run typecheck
docker compose -f docker-compose.dev.yml exec -T backend npm run build
docker compose -f docker-compose.dev.yml exec -T frontend npm test
docker compose -f docker-compose.dev.yml exec -T frontend npm run format:check
docker compose -f docker-compose.dev.yml exec -T frontend npm run typecheck
docker compose -f docker-compose.dev.yml exec -T frontend npm run build
```
@@ -249,11 +252,13 @@ bash deploy/ubuntu-deploy.sh
## 提交前检查
```bash
docker compose -f docker-compose.dev.yml exec -T backend npm test
docker compose -f docker-compose.dev.yml exec -T backend npm run typecheck
docker compose -f docker-compose.dev.yml exec -T frontend npm run typecheck
docker compose -f docker-compose.dev.yml exec -T backend npm run check
docker compose -f docker-compose.dev.yml exec -T frontend npm run check
```
代码格式由仓库根目录的 Prettier 配置统一管理。修改代码后可在对应应用目录执行
`npm run format`,提交前使用 `npm run format:check` 只检查、不修改文件。
确认:
- 未提交真实账号、Cookie、token、手机号或生产配置
+1 -1
View File
@@ -23,7 +23,7 @@
"@types/multer": "^2.2.0",
"@types/node": "^25.6.0",
"@types/pg": "^8.20.0",
"prettier": "^3.8.3",
"prettier": "3.8.3",
"tsx": "^4.22.3",
"typescript": "^6.0.2"
}
+4 -2
View File
@@ -10,7 +10,8 @@
"db:migrate:create": "tsx scripts/create-migration.ts",
"db:migrate:status": "tsx scripts/migration-status.ts",
"dev": "tsx watch --clear-screen=false src/index.ts",
"format": "prettier --write .",
"format": "prettier --config ../../.prettierrc.json --ignore-path ../../.prettierignore --write \"src/**/*.{ts,js,json}\" \"scripts/**/*.{ts,js}\" package.json tsconfig.json tsconfig.build.json",
"format:check": "prettier --config ../../.prettierrc.json --ignore-path ../../.prettierignore --check \"src/**/*.{ts,js,json}\" \"scripts/**/*.{ts,js}\" package.json tsconfig.json tsconfig.build.json",
"mock:claim": "tsx scripts/mock-kuaishou-cloud-claim.ts",
"mock:open91": "tsx scripts/mock-open91-order.ts",
"test": "node --import tsx --test $(find src \\( -name '*.test.ts' -o -name '*.test.js' \\) -print)",
@@ -19,6 +20,7 @@
"test:industry:gen": "tsx scripts/gen-test-cases.ts",
"test:industry:curl": "tsx scripts/curl-send-callback.ts",
"typecheck": "tsc -p tsconfig.json --noEmit",
"check": "npm run format:check && npm run typecheck && npm test && npm run build",
"start": "node dist/index.js",
"start:src": "tsx src/index.ts",
"mock:feifei": "tsx scripts/mock-feifei-claim.ts"
@@ -39,7 +41,7 @@
"@types/multer": "^2.2.0",
"@types/node": "^25.6.0",
"@types/pg": "^8.20.0",
"prettier": "^3.8.3",
"prettier": "3.8.3",
"tsx": "^4.22.3",
"typescript": "^6.0.2"
}
+8 -3
View File
@@ -36,7 +36,10 @@ try {
console.log('\n已清空测试业务数据。')
console.log('保留内容:fulfillment profiles/bindings、admin users、配置文件。')
} catch (error) {
console.error('\n清理失败:', error instanceof Error ? error.message : String(error || '未知错误'))
console.error(
'\n清理失败:',
error instanceof Error ? error.message : String(error || '未知错误'),
)
process.exitCode = 1
} finally {
await closeDb()
@@ -64,7 +67,8 @@ function parseArgs(argv) {
}
function printHelp() {
console.log(`
console.log(
`
用法:
npm run cleanup:dev-data -- [--apply]
@@ -80,7 +84,8 @@ function printHelp() {
admin_users / admin_audit_logs
本地配置文件
数据库连接: ${String(runtimeConfig.database?.url || '').trim() || '(未配置 DATABASE_URL)'}
`.trim())
`.trim(),
)
}
function printSummary(counts) {
+27 -13
View File
@@ -20,10 +20,7 @@ const CURRENT_DIR = path.dirname(fileURLToPath(import.meta.url))
const PROJECT_ROOT = path.resolve(CURRENT_DIR, '..')
const WORKSPACE_ROOT = path.resolve(PROJECT_ROOT, '../..')
loadEnvFiles([
path.join(WORKSPACE_ROOT, '.env'),
path.join(PROJECT_ROOT, '.env'),
])
loadEnvFiles([path.join(WORKSPACE_ROOT, '.env'), path.join(PROJECT_ROOT, '.env')])
const accessToken = String(process.env.KUASHOU_INDUSTRY_ACCESS_TOKEN || 'your_access_token').trim()
const appKey = String(process.env.KUASHOU_INDUSTRY_APP_KEY || 'your_app_key').trim()
@@ -41,7 +38,8 @@ const KUAISHOU_OPEN_API = 'https://openapi.kwaixiaodian.com'
const now = Date.now()
const validEnd = now + 30 * 24 * 60 * 60 * 1000
const bizParams: JsonObject = mode === 'consume'
const bizParams: JsonObject =
mode === 'consume'
? {
oid,
etickets: Array.from({ length: sendNum }, (_, i) => ({
@@ -81,13 +79,15 @@ const bizParams: JsonObject = mode === 'consume'
token,
}
const method = mode === 'consume'
const method =
mode === 'consume'
? 'integration.callback.virtual.eticket.consume'
: mode === 'destroy'
? 'integration.callback.virtual.eticket.destroy'
: 'integration.callback.virtual.eticket.send'
const endpoint = mode === 'consume'
const endpoint =
mode === 'consume'
? '/integration/callback/virtual/eticket/consume'
: mode === 'destroy'
? '/integration/callback/virtual/eticket/destroy'
@@ -112,7 +112,9 @@ const modeLabel = mode === 'consume' ? '核销' : mode === 'destroy' ? '销毁'
console.log(`# 电子凭证${modeLabel}回调 curl 命令`)
console.log(`# oid=${oid} sendNum=${sendNum} access_token=${bodyAccessToken.slice(0, 10)}...`)
console.log(`# 先测试网络连通性:`)
console.log(`curl -s -o /dev/null -w "%{http_code}" --connect-timeout 5 https://openapi.kwaixiaodian.com/`)
console.log(
`curl -s -o /dev/null -w "%{http_code}" --connect-timeout 5 https://openapi.kwaixiaodian.com/`,
)
console.log('# 如果返回 000,说明网络不通,需要关闭代理或换服务器')
console.log('')
console.log('curl -X POST \\')
@@ -130,10 +132,15 @@ console.log('')
// 本地签名校验(不调快手)
console.log('# 本地签名校验(不会真正调用快手):')
const localEndpoint = mode === 'consume' ? 'consume-code' : mode === 'destroy' ? 'destroy-code' : 'send-code'
console.log(`curl -s -X POST http://127.0.0.1:3000/api/v1/open/kuaishou-industry/${localEndpoint} \\`)
const localEndpoint =
mode === 'consume' ? 'consume-code' : mode === 'destroy' ? 'destroy-code' : 'send-code'
console.log(
`curl -s -X POST http://127.0.0.1:3000/api/v1/open/kuaishou-industry/${localEndpoint} \\`,
)
console.log(" -H 'Content-Type: application/x-www-form-urlencoded' \\")
console.log(` -d 'appkey=${appKey}&version=1&timestamp=${now}&signMethod=MD5&access_token=&method=&sign=${sign}&param=${encodeURIComponent(paramStr)}'`)
console.log(
` -d 'appkey=${appKey}&version=1&timestamp=${now}&signMethod=MD5&access_token=&method=&sign=${sign}&param=${encodeURIComponent(paramStr)}'`,
)
console.log('')
function signPayload(params: JsonObject, secret: string): string {
@@ -141,7 +148,11 @@ function signPayload(params: JsonObject, secret: string): string {
.filter(([k]) => k !== 'sign' && k !== 'signSecret')
.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
const qs = entries.map(([k, v]) => `${k}=${stringify(v)}`).join('&')
return crypto.createHash('md5').update(`${qs}&signSecret=${secret}`, 'utf8').digest('hex').toLowerCase()
return crypto
.createHash('md5')
.update(`${qs}&signSecret=${secret}`, 'utf8')
.digest('hex')
.toLowerCase()
}
function stringify(v: unknown): string {
@@ -164,7 +175,10 @@ function parseArgs(raw: string[]): JsonObject {
if (!arg) continue
const n = arg.startsWith('--') ? arg.slice(2) : arg
const eq = n.indexOf('=')
if (eq < 0) { p[n] = true; continue }
if (eq < 0) {
p[n] = true
continue
}
p[n.slice(0, eq).trim()] = n.slice(eq + 1).trim()
}
return p
+20 -10
View File
@@ -19,16 +19,15 @@ const CURRENT_DIR = path.dirname(fileURLToPath(import.meta.url))
const PROJECT_ROOT = path.resolve(CURRENT_DIR, '..')
const WORKSPACE_ROOT = path.resolve(PROJECT_ROOT, '../..')
loadEnvFiles([
path.join(WORKSPACE_ROOT, '.env'),
path.join(PROJECT_ROOT, '.env'),
])
loadEnvFiles([path.join(WORKSPACE_ROOT, '.env'), path.join(PROJECT_ROOT, '.env')])
const args = parseArgs(process.argv.slice(2))
const baseUrl = String(args.baseUrl || 'http://127.0.0.1').replace(/\/+$/, '')
const appKey = String(args.appKey || process.env.KUASHOU_INDUSTRY_APP_KEY || '').trim()
const signSecret = String(args.signSecret || process.env.KUASHOU_INDUSTRY_SIGN_SECRET || '').trim()
const signMethod = String(args.signMethod || 'MD5').trim().toUpperCase()
const signMethod = String(args.signMethod || 'MD5')
.trim()
.toUpperCase()
if (!appKey || !signSecret) {
console.error('请配置 appKey 和 signSecret')
@@ -85,7 +84,8 @@ function signKuaishou(params: JsonObject, method: string): string {
.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
const qs = entries.map(([k, v]) => `${k}=${stringifyVal(v)}`).join('&')
const src = `${qs}&signSecret=${signSecret}`
if (method === 'HMAC_SHA256') return crypto.createHmac('sha256', signSecret).update(src, 'utf8').digest('base64')
if (method === 'HMAC_SHA256')
return crypto.createHmac('sha256', signSecret).update(src, 'utf8').digest('base64')
return crypto.createHash('md5').update(src, 'utf8').digest('hex').toLowerCase()
}
@@ -106,7 +106,8 @@ async function call(endpoint: string, params: JsonObject) {
body.append('param', String(params.param || '{}'))
const started = Date.now()
let status = 0; let json: JsonObject = {}
let status = 0
let json: JsonObject = {}
try {
const res = await fetch(url, {
method: 'POST',
@@ -115,8 +116,14 @@ async function call(endpoint: string, params: JsonObject) {
})
status = res.status
const text = await res.text()
try { json = JSON.parse(text) } catch { json = { raw: text } }
} catch (err) { json = { error: String(err) } }
try {
json = JSON.parse(text)
} catch {
json = { raw: text }
}
} catch (err) {
json = { error: String(err) }
}
return { status, json, ms: Date.now() - started }
}
@@ -409,7 +416,10 @@ function parseArgs(rawArgs: string[]) {
if (!arg) continue
const n = arg.startsWith('--') ? arg.slice(2) : arg
const eq = n.indexOf('=')
if (eq < 0) { parsed[n] = true; continue }
if (eq < 0) {
parsed[n] = true
continue
}
parsed[n.slice(0, eq).trim()] = n.slice(eq + 1).trim()
}
return parsed
+6 -4
View File
@@ -40,7 +40,11 @@ async function main() {
let appliedNames = new Set<string>()
try {
const result = await query<{ name: string | null; filename: string | null; run_on: string | null }>(
const result = await query<{
name: string | null
filename: string | null
run_on: string | null
}>(
`
SELECT name, filename, run_on
FROM schema_migrations
@@ -49,9 +53,7 @@ async function main() {
)
appliedNames = new Set(
result.rows
.map((row) => normalizeMigrationName(row.filename || row.name))
.filter(Boolean),
result.rows.map((row) => normalizeMigrationName(row.filename || row.name)).filter(Boolean),
)
} catch (error) {
const message = error instanceof Error ? error.message : String(error)
@@ -20,7 +20,9 @@ if (args.help) {
}
if (!isDevMockEnabled() && args.force !== true) {
console.error('当前是生产环境,已拒绝生成 mock 数据。如确实要执行,请追加 --force,或设置 ENABLE_DEV_MOCK=1。')
console.error(
'当前是生产环境,已拒绝生成 mock 数据。如确实要执行,请追加 --force,或设置 ENABLE_DEV_MOCK=1。',
)
process.exit(1)
}
+9 -7
View File
@@ -10,10 +10,7 @@ const CURRENT_DIR = path.dirname(fileURLToPath(import.meta.url))
const PROJECT_ROOT = path.resolve(CURRENT_DIR, '..')
const WORKSPACE_ROOT = path.resolve(PROJECT_ROOT, '../..')
loadEnvFiles([
path.join(WORKSPACE_ROOT, '.env'),
path.join(PROJECT_ROOT, '.env'),
])
loadEnvFiles([path.join(WORKSPACE_ROOT, '.env'), path.join(PROJECT_ROOT, '.env')])
const args = parseArgs(process.argv.slice(2))
@@ -24,7 +21,9 @@ if (args.help) {
const secret = readEnv('KAQUAN91_SECRET')
const version = readEnv('KAQUAN91_VERSION') || '1.0'
const baseUrl = String(args.baseUrl || process.env.OPEN91_MOCK_BASE_URL || 'http://127.0.0.1').replace(/\/$/, '')
const baseUrl = String(
args.baseUrl || process.env.OPEN91_MOCK_BASE_URL || 'http://127.0.0.1',
).replace(/\/$/, '')
const mode = String(args.mode || 'create').trim()
if (!secret || secret.length !== 32) {
@@ -40,7 +39,8 @@ if (!['create', 'query'].includes(mode)) {
const orderNo = String(args.orderNo || `MOCK91${Date.now()}`).trim()
const timestamp = Number(args.timestamp || Math.floor(Date.now() / 1000))
const payload = mode === 'query'
const payload =
mode === 'query'
? {
orderNo,
timestamp,
@@ -75,7 +75,9 @@ const response = await fetch(endpoint, {
}).catch((error) => {
console.error('请求 91 mock 接口失败,请确认后端入口可访问。')
console.error(`当前地址:${endpoint}`)
console.error('Docker 开发环境通常使用 http://127.0.0.1;本地直启后端通常使用 http://127.0.0.1:3000。')
console.error(
'Docker 开发环境通常使用 http://127.0.0.1;本地直启后端通常使用 http://127.0.0.1:3000。',
)
throw error
})
@@ -34,10 +34,7 @@ const CURRENT_DIR = path.dirname(fileURLToPath(import.meta.url))
const PROJECT_ROOT = path.resolve(CURRENT_DIR, '..')
const WORKSPACE_ROOT = path.resolve(PROJECT_ROOT, '../..')
loadEnvFiles([
path.join(WORKSPACE_ROOT, '.env'),
path.join(PROJECT_ROOT, '.env'),
])
loadEnvFiles([path.join(WORKSPACE_ROOT, '.env'), path.join(PROJECT_ROOT, '.env')])
const args = parseArgs(process.argv.slice(2))
@@ -65,13 +62,21 @@ if (args.provider && providerPresets[String(args.provider)]) {
console.log(` 预设环境: ${preset.description}`)
}
} else {
baseUrl = String(args.baseUrl || process.env.KUASHOU_INDUSTRY_TEST_BASE_URL || 'http://127.0.0.1:3000').replace(/\/+$/, '')
baseUrl = String(
args.baseUrl || process.env.KUASHOU_INDUSTRY_TEST_BASE_URL || 'http://127.0.0.1:3000',
).replace(/\/+$/, '')
}
// 签名配置:CLI 参数 > 环境变量 > 默认值
const appKey = (String(args.appKey || '') || process.env.KUASHOU_INDUSTRY_APP_KEY || '').trim()
const signSecret = (String(args.signSecret || '') || process.env.KUASHOU_INDUSTRY_SIGN_SECRET || '').trim()
const signMethod = (String(args.signMethod || 'MD5').trim().toUpperCase())
const signSecret = (
String(args.signSecret || '') ||
process.env.KUASHOU_INDUSTRY_SIGN_SECRET ||
''
).trim()
const signMethod = String(args.signMethod || 'MD5')
.trim()
.toUpperCase()
const isLoadTest = Boolean(args.load)
const targetTps = normalizePositiveInteger(args.tps, 100)
@@ -119,7 +124,9 @@ if (!isLoadTest) {
let failed = 0
// 1. 通知商家发码
const sendParams = buildSignedParams(appKey, {
const sendParams = buildSignedParams(
appKey,
{
oid: testOid,
sellerId: '2174425348',
num: testNum,
@@ -133,10 +140,13 @@ if (!isLoadTest) {
certExpDays: 30,
certActualStartTime: Date.now(),
certActualEndTime: Date.now() + 30 * 24 * 60 * 60 * 1000,
}, signMethod)
},
signMethod,
)
const r1 = await post('/send-code', sendParams, { oid: testOid })
if (assertResult(1, '通知商家发码', r1)) passed++; else failed++
if (assertResult(1, '通知商家发码', r1)) passed++
else failed++
if (r1.ok) {
const len = r1.data?.data?.etickets?.length || 0
const firstId = r1.data?.data?.etickets?.[0]?.id
@@ -145,12 +155,16 @@ if (!isLoadTest) {
// 2. 重复发码(幂等性)
const r2 = await post('/send-code', sendParams, { oid: testOid })
if (assertResult(2, '重复发码(幂等性)', r2)) passed++; else failed++
if (assertResult(2, '重复发码(幂等性)', r2)) passed++
else failed++
// 3. 查询全部卡券
const qBiz: JsonObject = { oid: testOid, sendType: 'VIRTUAL', eticketType: 'DINING_OPEN_TICKET' }
const r3 = await post('/query-code', buildSignedParams(appKey, qBiz, signMethod), { oid: testOid })
if (assertResult(3, '查询全部卡券', r3)) passed++; else failed++
const r3 = await post('/query-code', buildSignedParams(appKey, qBiz, signMethod), {
oid: testOid,
})
if (assertResult(3, '查询全部卡券', r3)) passed++
else failed++
if (r3.ok) {
console.log(` 卡券数: ${r3.data?.data?.etickets?.length || 0}`)
r3.data?.data?.etickets?.forEach((e: JsonObject, i: number) => {
@@ -161,13 +175,31 @@ if (!isLoadTest) {
// 4. 查询单个卡券
const firstEticketId = r3.data?.data?.etickets?.[0]?.id
if (firstEticketId) {
const r4 = await post('/query-code', buildSignedParams(appKey, { oid: testOid, eticketId: String(firstEticketId), sendType: 'VIRTUAL', eticketType: 'DINING_OPEN_TICKET' }, signMethod), { oid: testOid })
if (assertResult(4, `查询单个卡券 (eticketId=${firstEticketId})`, r4)) passed++; else failed++
const r4 = await post(
'/query-code',
buildSignedParams(
appKey,
{
oid: testOid,
eticketId: String(firstEticketId),
sendType: 'VIRTUAL',
eticketType: 'DINING_OPEN_TICKET',
},
signMethod,
),
{ oid: testOid },
)
if (assertResult(4, `查询单个卡券 (eticketId=${firstEticketId})`, r4)) passed++
else failed++
}
// 5. 查询不存在的订单
const fakeOid = `FAKE_${Date.now()}`
const r5 = await post('/query-code', buildSignedParams(appKey, { oid: fakeOid, sendType: 'VIRTUAL' }, signMethod), { oid: fakeOid })
const r5 = await post(
'/query-code',
buildSignedParams(appKey, { oid: fakeOid, sendType: 'VIRTUAL' }, signMethod),
{ oid: fakeOid },
)
const expect4012002 = r5.data?.result === 4012002
if (expect4012002) {
passed++
@@ -179,24 +211,52 @@ if (!isLoadTest) {
// 6. 销毁指定卡券
if (firstEticketId) {
const r6 = await post('/destroy-code', buildSignedParams(appKey, { oid: testOid, reason: 'USER_APPLY_REFUND', etickets: [{ id: String(firstEticketId), num: 1, goodsValue: 100 }] }, signMethod), { oid: testOid })
if (assertResult(6, `销毁卡券 (id=${firstEticketId})`, r6)) passed++; else failed++
const r6 = await post(
'/destroy-code',
buildSignedParams(
appKey,
{
oid: testOid,
reason: 'USER_APPLY_REFUND',
etickets: [{ id: String(firstEticketId), num: 1, goodsValue: 100 }],
},
signMethod,
),
{ oid: testOid },
)
if (assertResult(6, `销毁卡券 (id=${firstEticketId})`, r6)) passed++
else failed++
// 7. 验证销毁后状态
const r7 = await post('/query-code', buildSignedParams(appKey, { oid: testOid, eticketId: String(firstEticketId), sendType: 'VIRTUAL' }, signMethod), { oid: testOid })
const r7 = await post(
'/query-code',
buildSignedParams(
appKey,
{ oid: testOid, eticketId: String(firstEticketId), sendType: 'VIRTUAL' },
signMethod,
),
{ oid: testOid },
)
const destroyed = r7.data?.result === 1 && r7.data?.data?.etickets?.[0]?.status === 'DESTROYED'
if (destroyed) {
passed++
console.log(` ✅ [7] 销毁后验证: status=DESTROYED`)
} else {
failed++
console.log(` ❌ [7] 销毁后验证: 期望 DESTROYED 实际 ${r7.data?.data?.etickets?.[0]?.status}`)
console.log(
` ❌ [7] 销毁后验证: 期望 DESTROYED 实际 ${r7.data?.data?.etickets?.[0]?.status}`,
)
}
}
// 8. 整单销毁
const r8 = await post('/destroy-code', buildSignedParams(appKey, { oid: testOid, reason: 'ETICKET_EXPIRED' }, signMethod), { oid: testOid })
if (assertResult(8, '整单销毁', r8)) passed++; else failed++
const r8 = await post(
'/destroy-code',
buildSignedParams(appKey, { oid: testOid, reason: 'ETICKET_EXPIRED' }, signMethod),
{ oid: testOid },
)
if (assertResult(8, '整单销毁', r8)) passed++
else failed++
// 9. 签名错误测试
const badParams = buildSignedParams(appKey, { oid: testOid, sendType: 'VIRTUAL' }, signMethod)
@@ -213,7 +273,9 @@ if (!isLoadTest) {
console.log('')
console.log('╔══════════════════════════════════════════════════════╗')
console.log(`║ HTTP 功能测试: ${String(passed).padStart(2)}/${passed + failed} 通过, ${String(failed).padStart(2)}/${passed + failed} 失败 ║`)
console.log(
`║ HTTP 功能测试: ${String(passed).padStart(2)}/${passed + failed} 通过, ${String(failed).padStart(2)}/${passed + failed} 失败 ║`,
)
console.log('╚══════════════════════════════════════════════════════╝')
console.log('')
process.exit(failed > 0 ? 1 : 0)
@@ -222,16 +284,16 @@ if (!isLoadTest) {
// ─────────────────────────────────────────────────────────────
// 压力测试
// ─────────────────────────────────────────────────────────────
const endpointsToTest = testEndpoint
? [testEndpoint]
: ['send-code', 'query-code', 'destroy-code']
const endpointsToTest = testEndpoint ? [testEndpoint] : ['send-code', 'query-code', 'destroy-code']
const loadTestOid = `LOAD_${Date.now()}`
// 先发码创建订单以供查询/销毁
console.log('')
console.log(` 准备测试数据: 通知商家发码 (oid=${loadTestOid})...`)
const prepParams = buildSignedParams(appKey, {
const prepParams = buildSignedParams(
appKey,
{
oid: loadTestOid,
sellerId: '2174425348',
num: 10,
@@ -245,12 +307,16 @@ const prepParams = buildSignedParams(appKey, {
certExpDays: 30,
certActualStartTime: Date.now(),
certActualEndTime: Date.now() + 30 * 24 * 60 * 60 * 1000,
}, signMethod)
},
signMethod,
)
const prepResult = await post('/send-code', prepParams, {})
if (prepResult.data?.result === 1) {
console.log(` 数据准备完成 (${prepResult.ms}ms)`)
} else {
console.log(` ⚠ 数据准备失败: result=${prepResult.data?.result} error_msg="${prepResult.data?.error_msg}" (${prepResult.ms}ms)`)
console.log(
` ⚠ 数据准备失败: result=${prepResult.data?.result} error_msg="${prepResult.data?.error_msg}" (${prepResult.ms}ms)`,
)
console.log(` 请确认服务端已配置正确的 appKey/signSecret 且未触发限流`)
}
@@ -258,14 +324,18 @@ for (const endpoint of endpointsToTest) {
console.log('')
console.log('╔══════════════════════════════════════════════════════╗')
console.log(`║ 压测: ${endpoint.padEnd(46)}`)
console.log(`║ 目标: ${String(targetTps).padEnd(4)} TPS, 持续 ${String(testDuration).padEnd(3)}s ║`)
console.log(
`║ 目标: ${String(targetTps).padEnd(4)} TPS, 持续 ${String(testDuration).padEnd(3)}s ║`,
)
console.log('╚══════════════════════════════════════════════════════╝')
const result = await runLoadTest(endpoint, targetTps, testDuration, loadTestOid)
console.log(` 成功 TPS : ${result.successTps.toFixed(1)} (业务 result=1)`)
console.log(` 总请求数 : ${result.totalReqs}`)
console.log(` 业务成功 : ${result.successes}`)
console.log(` 业务失败 : ${result.bizErrors}${result.bizSample ? ` (示例: ${result.bizSample})` : ''}`)
console.log(
` 业务失败 : ${result.bizErrors}${result.bizSample ? ` (示例: ${result.bizSample})` : ''}`,
)
console.log(` 被限流 : ${result.rateLimited}`)
console.log(` 平均耗时 : ${result.avgMs.toFixed(1)} ms`)
console.log(` P50 : ${result.p50Ms.toFixed(1)} ms`)
@@ -275,7 +345,9 @@ for (const endpoint of endpointsToTest) {
console.log(` 最大耗时 : ${result.maxMs.toFixed(1)} ms`)
if (result.rateLimited > result.totalReqs * 0.1) {
console.log(` ⚠ 注意: ${((result.rateLimited / result.totalReqs) * 100).toFixed(0)}% 请求被限流,建议调高 KUASHOU_INDUSTRY_RATE_LIMIT_MAX`)
console.log(
` ⚠ 注意: ${((result.rateLimited / result.totalReqs) * 100).toFixed(0)}% 请求被限流,建议调高 KUASHOU_INDUSTRY_RATE_LIMIT_MAX`,
)
}
}
@@ -311,7 +383,11 @@ async function post(
})
status = res.status
const text = await res.text()
try { data = JSON.parse(text) } catch { data = { raw: text } }
try {
data = JSON.parse(text)
} catch {
data = { raw: text }
}
ok = res.ok && data.result === 1
} catch (err) {
data = { error: String(err) }
@@ -455,9 +531,7 @@ function signKuaishouIndustry(params: JsonObject, method: string): string {
.filter(([key]) => key !== 'sign' && key !== 'signSecret')
.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
const queryString = entries
.map(([key, value]) => `${key}=${stringifySignVal(value)}`)
.join('&')
const queryString = entries.map(([key, value]) => `${key}=${stringifySignVal(value)}`).join('&')
const source = `${queryString}&signSecret=${signSecret}`
@@ -472,23 +546,30 @@ function stringifySignVal(value: unknown): string {
if (typeof value === 'number' && Number.isFinite(value)) return String(value)
if (typeof value === 'boolean') return value ? 'true' : 'false'
if (value == null) return ''
if (typeof value === 'object') return JSON.stringify(value, Object.keys(value as JsonObject).sort())
if (typeof value === 'object')
return JSON.stringify(value, Object.keys(value as JsonObject).sort())
return String(value)
}
// ─────────────────── 工具 ───────────────────
function assertResult(num: number, label: string, r: { ok: boolean; status: number; data: JsonObject; ms: number }) {
function assertResult(
num: number,
label: string,
r: { ok: boolean; status: number; data: JsonObject; ms: number },
) {
if (r.ok) {
console.log(` ✅ [${num}] ${label}: result=1 (${r.ms}ms)`)
return true
}
console.log(` ❌ [${num}] ${label}: HTTP ${r.status} result=${r.data?.result} error_msg="${r.data?.error_msg}" (${r.ms}ms)`)
console.log(
` ❌ [${num}] ${label}: HTTP ${r.status} result=${r.data?.result} error_msg="${r.data?.error_msg}" (${r.ms}ms)`,
)
return false
}
function delay(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms))
return new Promise((resolve) => setTimeout(resolve, ms))
}
function avg(arr: number[]): number {
@@ -512,10 +593,16 @@ function parseArgs(rawArgs: string[]): JsonObject {
for (const rawArg of rawArgs) {
const arg = String(rawArg || '').trim()
if (!arg) continue
if (arg === '--help' || arg === '-h') { parsed.help = true; continue }
if (arg === '--help' || arg === '-h') {
parsed.help = true
continue
}
const normalized = arg.startsWith('--') ? arg.slice(2) : arg
const eqIdx = normalized.indexOf('=')
if (eqIdx < 0) { parsed[normalized] = true; continue }
if (eqIdx < 0) {
parsed[normalized] = true
continue
}
const key = normalized.slice(0, eqIdx).trim()
const value = normalized.slice(eqIdx + 1).trim()
if (key) parsed[key] = value
+31 -15
View File
@@ -21,13 +21,12 @@ const CURRENT_DIR = path.dirname(fileURLToPath(import.meta.url))
const PROJECT_ROOT = path.resolve(CURRENT_DIR, '..')
const WORKSPACE_ROOT = path.resolve(PROJECT_ROOT, '../..')
loadEnvFiles([
path.join(WORKSPACE_ROOT, '.env'),
path.join(PROJECT_ROOT, '.env'),
])
loadEnvFiles([path.join(WORKSPACE_ROOT, '.env'), path.join(PROJECT_ROOT, '.env')])
process.env.KUASHOU_INDUSTRY_APP_KEY = process.env.KUASHOU_INDUSTRY_APP_KEY || 'test_industry_app_key'
process.env.KUASHOU_INDUSTRY_SIGN_SECRET = process.env.KUASHOU_INDUSTRY_SIGN_SECRET || 'test_industry_sign_secret'
process.env.KUASHOU_INDUSTRY_APP_KEY =
process.env.KUASHOU_INDUSTRY_APP_KEY || 'test_industry_app_key'
process.env.KUASHOU_INDUSTRY_SIGN_SECRET =
process.env.KUASHOU_INDUSTRY_SIGN_SECRET || 'test_industry_sign_secret'
process.env.KUASHOU_INDUSTRY_SHOP_ID = process.env.KUASHOU_INDUSTRY_SHOP_ID || 'test_shop'
const args = parseArgs(process.argv.slice(2))
@@ -56,7 +55,9 @@ const [
const config = getKuaishouIndustryConfig()
const testOid = String(args.oid || `TEST_KS${Date.now()}`).trim()
const testNum = normalizePositiveInteger(args.num, 3)
const signMethod = String(args.signMethod || 'MD5').trim().toUpperCase()
const signMethod = String(args.signMethod || 'MD5')
.trim()
.toUpperCase()
console.log('')
console.log('╔══════════════════════════════════════════════════════╗')
@@ -149,7 +150,9 @@ try {
console.log(` 已发货数量: ${queryCodeResult.data?.sendNum || 0}`)
console.log(` 卡券列表数: ${queryCodeResult.data?.etickets?.length || 0}`)
queryCodeResult.data?.etickets?.forEach((eticket: Record<string, unknown>, index: number) => {
console.log(` [${index + 1}] id=${eticket.id} status=${eticket.status} num=${eticket.num}`)
console.log(
` [${index + 1}] id=${eticket.id} status=${eticket.status} num=${eticket.num}`,
)
})
} else {
failed++
@@ -206,7 +209,9 @@ try {
console.log(` 错误信息: ${queryMissingResult.error_msg}`)
} else {
failed++
console.log(` └─ ❌ 查询不存在订单: 期望 result=4012002,实际 result=${queryMissingResult.result}`)
console.log(
` └─ ❌ 查询不存在订单: 期望 result=4012002,实际 result=${queryMissingResult.result}`,
)
}
// ─────────────────────────────────────────────────────────
@@ -214,7 +219,9 @@ try {
// ─────────────────────────────────────────────────────────
if (queryCodeResult.data?.etickets?.length > 0) {
total++
const destroyTargetIds = queryCodeResult.data.etickets.slice(0, 1).map((e: Record<string, unknown>) => ({
const destroyTargetIds = queryCodeResult.data.etickets
.slice(0, 1)
.map((e: Record<string, unknown>) => ({
id: String(e.id),
num: 1,
goodsValue: 100,
@@ -248,7 +255,11 @@ try {
eticketType: 'DINING_OPEN_TICKET',
}
const verifyDestroyParams = buildRequestParams(config.appKey, verifyDestroyBizParams, signMethod)
const verifyDestroyParams = buildRequestParams(
config.appKey,
verifyDestroyBizParams,
signMethod,
)
const verifyDestroyResult = await handleQueryCode(verifyDestroyParams)
printResult('销毁后查询', verifyDestroyResult)
@@ -258,7 +269,9 @@ try {
console.log(` 卡券状态: ${verifyStatus}`)
} else {
failed++
console.log(` 期望 status=DESTROYED,实际 status=${verifyStatus}, result=${verifyDestroyResult.result}`)
console.log(
` 期望 status=DESTROYED,实际 status=${verifyStatus}, result=${verifyDestroyResult.result}`,
)
}
} else {
console.log(' ⚠ 跳过(无卡券可销毁)')
@@ -284,7 +297,6 @@ try {
} else {
failed++
}
} finally {
await closeDb()
}
@@ -292,7 +304,9 @@ try {
// ─────────────────────────────────────────────────────────
console.log('')
console.log('╔══════════════════════════════════════════════════════╗')
console.log(`║ 测试完成: ${String(passed).padStart(2)}/${total} 通过, ${String(failed).padStart(2)}/${total} 失败 ║`)
console.log(
`║ 测试完成: ${String(passed).padStart(2)}/${total} 通过, ${String(failed).padStart(2)}/${total} 失败 ║`,
)
console.log('╚══════════════════════════════════════════════════════╝')
console.log('')
@@ -338,7 +352,9 @@ function printStep(stepNum: number, title: string) {
function printResult(label: string, result: Record<string, unknown>) {
const ok = result.result === 1
const icon = ok ? '✅' : '❌'
const resultText = ok ? `result=${result.result}` : `result=${result.result} error_msg="${result.error_msg}"`
const resultText = ok
? `result=${result.result}`
: `result=${result.result} error_msg="${result.error_msg}"`
console.log(` └─ ${icon} ${label}: ${resultText}`)
}
+20 -5
View File
@@ -27,8 +27,18 @@ async function main() {
const config = runtimeConfig.worker.sms
console.log('当前短信配置:')
console.log(' provider :', config.provider)
console.log(' accessKeyId :', config.accessKeyId ? `${config.accessKeyId.slice(0, 4)}...${config.accessKeyId.slice(-4)}` : '(空)')
console.log(' accessKeySecret :', config.accessKeySecret ? `${config.accessKeySecret.slice(0, 4)}...${config.accessKeySecret.slice(-4)}(长度 ${config.accessKeySecret.length}` : '(空)')
console.log(
' accessKeyId :',
config.accessKeyId
? `${config.accessKeyId.slice(0, 4)}...${config.accessKeyId.slice(-4)}`
: '(空)',
)
console.log(
' accessKeySecret :',
config.accessKeySecret
? `${config.accessKeySecret.slice(0, 4)}...${config.accessKeySecret.slice(-4)}(长度 ${config.accessKeySecret.length}`
: '(空)',
)
console.log(' signName :', config.signName)
console.log(' templateCode :', config.templateCode)
console.log('')
@@ -64,13 +74,18 @@ async function main() {
const url = `https://dysmsapi.aliyuncs.com/?${queryString}`
console.log('stringToSign:')
console.log(' ', `GET&%2F&${percentEncode(
console.log(
' ',
`GET&%2F&${percentEncode(
Object.entries(params)
.filter(([key]) => key !== 'Signature')
.map(([key, value]) => `${percentEncode(key)}=${percentEncode(value)}`)
.sort((left, right) => (String(left[0]) < String(right[0]) ? -1 : String(left[0]) > String(right[0]) ? 1 : 0))
.sort((left, right) =>
String(left[0]) < String(right[0]) ? -1 : String(left[0]) > String(right[0]) ? 1 : 0,
)
.join('&'),
)}`)
)}`,
)
console.log('')
console.log(`发送测试短信到 ${phone}(验证码 123456...`)
+36 -36
View File
@@ -1,98 +1,98 @@
import fs from "node:fs";
import process from "node:process";
import fs from 'node:fs'
import process from 'node:process'
export function loadEnvFiles(filePaths: string[]): void {
for (const filePath of filePaths) {
loadEnvFile(filePath);
loadEnvFile(filePath)
}
}
function loadEnvFile(filePath: string): void {
if (!fs.existsSync(filePath)) {
return;
return
}
const rawText = fs.readFileSync(filePath, "utf8");
const lines = rawText.split(/\r?\n/);
const rawText = fs.readFileSync(filePath, 'utf8')
const lines = rawText.split(/\r?\n/)
for (const rawLine of lines) {
const line = rawLine.trim();
const line = rawLine.trim()
if (!line || line.startsWith("#")) {
continue;
if (!line || line.startsWith('#')) {
continue
}
const separatorIndex = line.indexOf("=");
const separatorIndex = line.indexOf('=')
if (separatorIndex <= 0) {
continue;
continue
}
const key = line.slice(0, separatorIndex).trim();
const key = line.slice(0, separatorIndex).trim()
if (!key || key in process.env) {
continue;
continue
}
process.env[key] = parseEnvValue(line.slice(separatorIndex + 1));
process.env[key] = parseEnvValue(line.slice(separatorIndex + 1))
}
}
function parseEnvValue(rawValue: string): string {
const value = String(rawValue || "").trim();
const value = String(rawValue || '').trim()
if (!value) {
return "";
return ''
}
const quote = value[0];
const quote = value[0]
if ((quote === '"' || quote === "'") && value.endsWith(quote)) {
return value.slice(1, -1);
return value.slice(1, -1)
}
return value;
return value
}
export function parseBoolean(rawValue: unknown): boolean | null {
const normalized = String(rawValue || "")
const normalized = String(rawValue || '')
.trim()
.toLowerCase();
.toLowerCase()
if (!normalized) {
return null;
return null
}
if (["1", "true", "yes", "on"].includes(normalized)) {
return true;
if (['1', 'true', 'yes', 'on'].includes(normalized)) {
return true
}
if (["0", "false", "no", "off"].includes(normalized)) {
return false;
if (['0', 'false', 'no', 'off'].includes(normalized)) {
return false
}
return null;
return null
}
export function parseInteger(rawValue: unknown): number | null {
const normalized = String(rawValue || "").trim();
const normalized = String(rawValue || '').trim()
if (!normalized) {
return null;
return null
}
const parsed = Number(normalized);
return Number.isFinite(parsed) ? parsed : null;
const parsed = Number(normalized)
return Number.isFinite(parsed) ? parsed : null
}
export function parseJsonArray<T = unknown>(rawValue: unknown): T[] | null {
const normalized = String(rawValue || "").trim();
const normalized = String(rawValue || '').trim()
if (!normalized) {
return null;
return null
}
try {
const parsed = JSON.parse(normalized);
return Array.isArray(parsed) ? (parsed as T[]) : null;
const parsed = JSON.parse(normalized)
return Array.isArray(parsed) ? (parsed as T[]) : null
} catch {
return null;
return null
}
}
+10 -13
View File
@@ -1,17 +1,14 @@
import path from "node:path";
import { fileURLToPath } from "node:url";
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import { createDefaultRuntimeConfig } from "./defaults.js";
import { applyEnvOverrides } from "./env-overrides.js";
import { loadEnvFiles } from "./runtime-env.js";
import { createDefaultRuntimeConfig } from './defaults.js'
import { applyEnvOverrides } from './env-overrides.js'
import { loadEnvFiles } from './runtime-env.js'
const CURRENT_DIR = path.dirname(fileURLToPath(import.meta.url));
export const PROJECT_ROOT = path.resolve(CURRENT_DIR, "../..");
const WORKSPACE_ROOT = path.resolve(PROJECT_ROOT, "../..");
const CURRENT_DIR = path.dirname(fileURLToPath(import.meta.url))
export const PROJECT_ROOT = path.resolve(CURRENT_DIR, '../..')
const WORKSPACE_ROOT = path.resolve(PROJECT_ROOT, '../..')
loadEnvFiles([
path.join(WORKSPACE_ROOT, ".env"),
path.join(PROJECT_ROOT, ".env"),
]);
loadEnvFiles([path.join(WORKSPACE_ROOT, '.env'), path.join(PROJECT_ROOT, '.env')])
export const runtimeConfig = applyEnvOverrides(createDefaultRuntimeConfig(PROJECT_ROOT));
export const runtimeConfig = applyEnvOverrides(createDefaultRuntimeConfig(PROJECT_ROOT))
+1 -3
View File
@@ -76,9 +76,7 @@ function assertMigrationFilesOrdered() {
const invalid = files.filter((name) => !MIGRATION_FILE_PATTERN.test(name))
if (invalid.length > 0) {
throw new Error(
`迁移文件命名必须为 NNN_name.sql(三位序号),非法文件: ${invalid.join(', ')}`,
)
throw new Error(`迁移文件命名必须为 NNN_name.sql(三位序号),非法文件: ${invalid.join(', ')}`)
}
const versions = files.map((name) => Number(name.slice(0, 3)))
+12 -3
View File
@@ -11,11 +11,20 @@ import {
} from './task-status.js'
test('任务状态机声明 kuaishou-lewan 主流程跳转', () => {
assert.equal(canTaskTransition(TASK_STATUS.PENDING_BINDING_PREPARE, TASK_STATUS.WAITING_BINDING), true)
assert.equal(
canTaskTransition(TASK_STATUS.PENDING_BINDING_PREPARE, TASK_STATUS.WAITING_BINDING),
true,
)
assert.equal(canTaskTransition(TASK_STATUS.WAITING_BINDING, TASK_STATUS.ROLE_CONFIRMED), true)
assert.equal(canTaskTransition(TASK_STATUS.ROLE_CONFIRMED, TASK_STATUS.REDEEMING), true)
assert.equal(canTaskTransition(TASK_STATUS.REDEEMING, TASK_STATUS.DISPATCHED_PENDING_RETURN), true)
assert.equal(canTaskTransition(TASK_STATUS.DISPATCHED_PENDING_RETURN, TASK_STATUS.COMPLETED), true)
assert.equal(
canTaskTransition(TASK_STATUS.REDEEMING, TASK_STATUS.DISPATCHED_PENDING_RETURN),
true,
)
assert.equal(
canTaskTransition(TASK_STATUS.DISPATCHED_PENDING_RETURN, TASK_STATUS.COMPLETED),
true,
)
})
test('任务状态机收敛领取和 91 查询的业务判断', () => {
+1 -4
View File
@@ -259,10 +259,7 @@ export function isKuaishouCloudRedeemSettledStatus(status: unknown): boolean {
export function canRedeemKuaishouCloudClaimStatus(status: unknown): boolean {
const normalized = normalizeTaskStatus(status)
// 允许 WAITING_BINDINGUID 匹配后可一键确认+兑换
return (
normalized === TASK_STATUS.ROLE_CONFIRMED ||
normalized === TASK_STATUS.WAITING_BINDING
)
return normalized === TASK_STATUS.ROLE_CONFIRMED || normalized === TASK_STATUS.WAITING_BINDING
}
export function canRegenerateClaimLinkStatus(status: unknown): boolean {
+3 -1
View File
@@ -9,7 +9,9 @@ export const WORK_ORDER_STATUS = {
CANCELLED: 'cancelled',
} as const
export type WorkOrderStatus = (typeof WORK_ORDER_STATUS)[keyof typeof WORK_ORDER_STATUS] | (string & {})
export type WorkOrderStatus =
| (typeof WORK_ORDER_STATUS)[keyof typeof WORK_ORDER_STATUS]
| (string & {})
const FINAL_STATUSES = new Set<WorkOrderStatus>([
WORK_ORDER_STATUS.ACCEPTED,
+37 -43
View File
@@ -1,69 +1,63 @@
import process from "node:process";
import process from 'node:process'
import { createApp } from "./app.js";
import { runtimeConfig } from "./config/runtime.js";
import { assertRuntimeConfigValid } from "./config/runtime-validation.js";
import { bootstrapCoreServices } from "./startup/bootstrap.js";
import { createShutdownController } from "./startup/shutdown.js";
import { createStartupState, formatStartupError } from "./startup/state.js";
import { logError, logInfo } from "./utils/logger.js";
import { createApp } from './app.js'
import { runtimeConfig } from './config/runtime.js'
import { assertRuntimeConfigValid } from './config/runtime-validation.js'
import { bootstrapCoreServices } from './startup/bootstrap.js'
import { createShutdownController } from './startup/shutdown.js'
import { createStartupState, formatStartupError } from './startup/state.js'
import { logError, logInfo } from './utils/logger.js'
const port = Number(runtimeConfig.server.port || 3000);
const host = "0.0.0.0";
const startupState = createStartupState();
const port = Number(runtimeConfig.server.port || 3000)
const host = '0.0.0.0'
const startupState = createStartupState()
let shutdownController: ReturnType<typeof createShutdownController> | null = null;
let shutdownController: ReturnType<typeof createShutdownController> | null = null
try {
assertRuntimeConfigValid(runtimeConfig);
assertRuntimeConfigValid(runtimeConfig)
} catch (error) {
logError("[startup]", "运行时配置校验失败,服务停止启动", error);
process.exit(1);
logError('[startup]', '运行时配置校验失败,服务停止启动', error)
process.exit(1)
}
const app = createApp({
startupState,
isShutdownStarted: () => shutdownController?.isShutdownStarted() || false,
config: runtimeConfig,
});
})
const server = app.listen(port, host, () => {
logInfo(
"[startup]",
`order-site-backend kuaishou-lite listening on http://${host}:${port}`
);
void bootstrapCoreServices(
startupState,
() => shutdownController?.isShutdownStarted() || false
);
});
logInfo('[startup]', `order-site-backend kuaishou-lite listening on http://${host}:${port}`)
void bootstrapCoreServices(startupState, () => shutdownController?.isShutdownStarted() || false)
})
shutdownController = createShutdownController(server, startupState);
shutdownController = createShutdownController(server, startupState)
server.on("error", (error) => {
logError("[startup]", "HTTP server failed", error);
});
server.on('error', (error) => {
logError('[startup]', 'HTTP server failed', error)
})
process.on("SIGINT", () => {
void shutdownController?.shutdown("SIGINT");
});
process.on('SIGINT', () => {
void shutdownController?.shutdown('SIGINT')
})
process.on("SIGTERM", () => {
void shutdownController?.shutdown("SIGTERM");
});
process.on('SIGTERM', () => {
void shutdownController?.shutdown('SIGTERM')
})
process.on("unhandledRejection", (reason) => {
process.on('unhandledRejection', (reason) => {
startupState.process.lastUnhandledRejection = {
time: new Date().toISOString(),
message: formatStartupError(reason),
};
logError("[process]", "unhandled promise rejection", reason);
});
}
logError('[process]', 'unhandled promise rejection', reason)
})
process.on("uncaughtException", (error) => {
process.on('uncaughtException', (error) => {
startupState.process.lastUncaughtException = {
time: new Date().toISOString(),
message: formatStartupError(error),
};
logError("[process]", "uncaught exception captured", error);
});
}
logError('[process]', 'uncaught exception captured', error)
})
+24 -31
View File
@@ -1,51 +1,44 @@
import type { Request, Response, NextFunction } from "express";
import { createRequestId, logInfo } from "../utils/logger.js";
import type { Request, Response, NextFunction } from 'express'
import { createRequestId, logInfo } from '../utils/logger.js'
export function accessLogMiddleware(
req: Request,
res: Response,
next: NextFunction
): void {
const startedAt = Date.now();
const requestId = resolveRequestId(req);
export function accessLogMiddleware(req: Request, res: Response, next: NextFunction): void {
const startedAt = Date.now()
const requestId = resolveRequestId(req)
req.requestId = requestId;
res.setHeader("X-Request-Id", requestId);
req.requestId = requestId
res.setHeader('X-Request-Id', requestId)
res.on("finish", () => {
res.on('finish', () => {
if (shouldSkipAccessLog(req.originalUrl, res.statusCode)) {
return;
return
}
logInfo("[http/access]", "request completed", {
logInfo('[http/access]', 'request completed', {
requestId,
method: req.method,
originalUrl: req.originalUrl,
statusCode: res.statusCode,
durationMs: Date.now() - startedAt,
ip: req.ip,
forwardedFor: String(req.headers["x-forwarded-for"] || ""),
userAgent: String(req.headers["user-agent"] || ""),
referer: String(req.headers.referer || ""),
contentLength: Number(res.getHeader("content-length") || 0),
actorUserId: req.adminSession?.userId || "",
actorUsername: req.adminSession?.username || "",
actorRole: req.adminSession?.role || "",
});
});
forwardedFor: String(req.headers['x-forwarded-for'] || ''),
userAgent: String(req.headers['user-agent'] || ''),
referer: String(req.headers.referer || ''),
contentLength: Number(res.getHeader('content-length') || 0),
actorUserId: req.adminSession?.userId || '',
actorUsername: req.adminSession?.username || '',
actorRole: req.adminSession?.role || '',
})
})
next();
next()
}
function resolveRequestId(req: Request): string {
const fromHeader = String(req.headers["x-request-id"] || "").trim();
return fromHeader || createRequestId("req");
const fromHeader = String(req.headers['x-request-id'] || '').trim()
return fromHeader || createRequestId('req')
}
function shouldSkipAccessLog(originalUrl: string, statusCode: number): boolean {
const pathname = String(originalUrl || "").split("?")[0] || "";
return (
["/health", "/health/live", "/health/ready"].includes(pathname) &&
Number(statusCode) < 400
);
const pathname = String(originalUrl || '').split('?')[0] || ''
return ['/health', '/health/live', '/health/ready'].includes(pathname) && Number(statusCode) < 400
}
+18 -29
View File
@@ -1,43 +1,32 @@
import type { Request, Response, NextFunction } from "express";
import type { RuntimeConfig } from "../types/runtime-config.js";
import type { Request, Response, NextFunction } from 'express'
import type { RuntimeConfig } from '../types/runtime-config.js'
export function createCorsMiddleware(
config: RuntimeConfig
config: RuntimeConfig,
): (req: Request, res: Response, next: NextFunction) => void {
const allowedOrigins = config.cors.allowedOrigins;
const isWildcard =
allowedOrigins.length === 1 && allowedOrigins[0] === "*";
const allowedOrigins = config.cors.allowedOrigins
const isWildcard = allowedOrigins.length === 1 && allowedOrigins[0] === '*'
return function corsMiddleware(
req: Request,
res: Response,
next: NextFunction
): void {
return function corsMiddleware(req: Request, res: Response, next: NextFunction): void {
if (isWildcard) {
res.setHeader("Access-Control-Allow-Origin", "*");
res.setHeader('Access-Control-Allow-Origin', '*')
} else {
const origin = req.headers.origin;
const origin = req.headers.origin
if (origin && allowedOrigins.includes(origin)) {
res.setHeader("Access-Control-Allow-Origin", origin);
res.setHeader("Access-Control-Allow-Credentials", "true");
res.setHeader('Access-Control-Allow-Origin', origin)
res.setHeader('Access-Control-Allow-Credentials', 'true')
}
}
res.setHeader(
"Access-Control-Allow-Headers",
"Content-Type, Authorization"
);
res.setHeader(
"Access-Control-Allow-Methods",
"GET, POST, PUT, PATCH, DELETE, OPTIONS"
);
res.setHeader("Access-Control-Max-Age", "86400");
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization')
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, PATCH, DELETE, OPTIONS')
res.setHeader('Access-Control-Max-Age', '86400')
if (req.method === "OPTIONS") {
res.sendStatus(204);
return;
if (req.method === 'OPTIONS') {
res.sendStatus(204)
return
}
next();
};
next()
}
}
+64 -67
View File
@@ -1,116 +1,113 @@
import test from "node:test";
import assert from "node:assert/strict";
import type { NextFunction, Request, Response } from "express";
import test from 'node:test'
import assert from 'node:assert/strict'
import type { NextFunction, Request, Response } from 'express'
import {
createRateLimitMiddleware,
resetRateLimitBucketsForTest,
} from "./rate-limit.js";
import { createRateLimitMiddleware, resetRateLimitBucketsForTest } from './rate-limit.js'
test("createRateLimitMiddleware allows requests within the window", () => {
resetRateLimitBucketsForTest();
test('createRateLimitMiddleware allows requests within the window', () => {
resetRateLimitBucketsForTest()
const limiter = createRateLimitMiddleware({
scope: "test:allow",
scope: 'test:allow',
windowMs: 60_000,
max: 2,
});
const req = createMockRequest();
const res = createMockResponse();
let nextCount = 0;
})
const req = createMockRequest()
const res = createMockResponse()
let nextCount = 0
const next: NextFunction = () => {
nextCount += 1;
};
nextCount += 1
}
limiter(req, res, next);
limiter(req, res, next);
limiter(req, res, next)
limiter(req, res, next)
assert.equal(nextCount, 2);
assert.equal(res.statusCode, 200);
});
assert.equal(nextCount, 2)
assert.equal(res.statusCode, 200)
})
test("createRateLimitMiddleware blocks requests over the limit", () => {
resetRateLimitBucketsForTest();
test('createRateLimitMiddleware blocks requests over the limit', () => {
resetRateLimitBucketsForTest()
const limiter = createRateLimitMiddleware({
scope: "test:block",
scope: 'test:block',
windowMs: 60_000,
max: 1,
});
const req = createMockRequest();
const res = createMockResponse();
let nextCount = 0;
})
const req = createMockRequest()
const res = createMockResponse()
let nextCount = 0
const next: NextFunction = () => {
nextCount += 1;
};
nextCount += 1
}
limiter(req, res, next);
limiter(req, res, next);
limiter(req, res, next)
limiter(req, res, next)
assert.equal(nextCount, 1);
assert.equal(res.statusCode, 429);
assert.equal(res.headers["Retry-After"], "60");
assert.equal(res.body.errorCode, "rate_limited");
});
assert.equal(nextCount, 1)
assert.equal(res.statusCode, 429)
assert.equal(res.headers['Retry-After'], '60')
assert.equal(res.body.errorCode, 'rate_limited')
})
test("createRateLimitMiddleware supports custom limit responses", () => {
resetRateLimitBucketsForTest();
test('createRateLimitMiddleware supports custom limit responses', () => {
resetRateLimitBucketsForTest()
const limiter = createRateLimitMiddleware({
scope: "test:custom",
scope: 'test:custom',
windowMs: 60_000,
max: 1,
onLimit: (_req, res) => {
res.status(200).json({ code: 429, message: "limited" });
res.status(200).json({ code: 429, message: 'limited' })
},
});
const req = createMockRequest();
const res = createMockResponse();
})
const req = createMockRequest()
const res = createMockResponse()
limiter(req, res, () => {});
limiter(req, res, () => {});
limiter(req, res, () => {})
limiter(req, res, () => {})
assert.equal(res.statusCode, 200);
assert.deepEqual(res.body, { code: 429, message: "limited" });
});
assert.equal(res.statusCode, 200)
assert.deepEqual(res.body, { code: 429, message: 'limited' })
})
function createMockRequest(): Request {
return {
ip: "127.0.0.1",
originalUrl: "/test",
ip: '127.0.0.1',
originalUrl: '/test',
headers: {},
socket: {
remoteAddress: "127.0.0.1",
remoteAddress: '127.0.0.1',
},
} as Request;
} as Request
}
function createMockResponse(): Response & {
statusCode: number;
headers: Record<string, string>;
body: any;
statusCode: number
headers: Record<string, string>
body: any
} {
const res = {
statusCode: 200,
headers: {} as Record<string, string>,
body: null as any,
setHeader(name: string, value: string) {
this.headers[name] = value;
return this;
this.headers[name] = value
return this
},
status(statusCode: number) {
this.statusCode = statusCode;
return this;
this.statusCode = statusCode
return this
},
json(body: any) {
this.body = body;
return this;
this.body = body
return this
},
};
}
return res as Response & {
statusCode: number;
headers: Record<string, string>;
body: any;
};
statusCode: number
headers: Record<string, string>
body: any
}
}
+56 -55
View File
@@ -1,30 +1,30 @@
import type { NextFunction, Request, Response } from "express";
import type { NextFunction, Request, Response } from 'express'
import { buildErrorPayload, createHttpError } from "../utils/http.js";
import { logWarn } from "../utils/logger.js";
import { buildErrorPayload, createHttpError } from '../utils/http.js'
import { logWarn } from '../utils/logger.js'
type RateLimitKeyResolver = (req: Request) => string;
type RateLimitKeyResolver = (req: Request) => string
type RateLimitExceededContext = {
scope: string;
retryAfterSeconds: number;
};
scope: string
retryAfterSeconds: number
}
type RateLimitOptions = {
scope: string;
windowMs: number;
max: number;
key?: RateLimitKeyResolver;
onLimit?: (req: Request, res: Response, context: RateLimitExceededContext) => void;
};
scope: string
windowMs: number
max: number
key?: RateLimitKeyResolver
onLimit?: (req: Request, res: Response, context: RateLimitExceededContext) => void
}
type RateLimitBucket = {
count: number;
resetAt: number;
};
count: number
resetAt: number
}
const buckets = new Map<string, RateLimitBucket>();
let lastCleanupAt = 0;
const buckets = new Map<string, RateLimitBucket>()
let lastCleanupAt = 0
export function createRateLimitMiddleware({
scope,
@@ -33,90 +33,91 @@ export function createRateLimitMiddleware({
key = defaultRateLimitKey,
onLimit,
}: RateLimitOptions) {
const normalizedScope = String(scope || "default").trim() || "default";
const normalizedWindowMs = Math.max(1000, Number(windowMs || 0));
const normalizedMax = Math.max(1, Number(max || 0));
const normalizedScope = String(scope || 'default').trim() || 'default'
const normalizedWindowMs = Math.max(1000, Number(windowMs || 0))
const normalizedMax = Math.max(1, Number(max || 0))
return (req: Request, res: Response, next: NextFunction): void => {
const now = Date.now();
cleanupExpiredBuckets(now);
const now = Date.now()
cleanupExpiredBuckets(now)
const bucketKey = `${normalizedScope}:${key(req)}`;
const current = buckets.get(bucketKey);
const bucket = current && current.resetAt > now
? current
: { count: 0, resetAt: now + normalizedWindowMs };
const bucketKey = `${normalizedScope}:${key(req)}`
const current = buckets.get(bucketKey)
const bucket =
current && current.resetAt > now ? current : { count: 0, resetAt: now + normalizedWindowMs }
bucket.count += 1;
buckets.set(bucketKey, bucket);
bucket.count += 1
buckets.set(bucketKey, bucket)
if (bucket.count <= normalizedMax) {
next();
return;
next()
return
}
const retryAfterSeconds = Math.max(1, Math.ceil((bucket.resetAt - now) / 1000));
res.setHeader("Retry-After", String(retryAfterSeconds));
const retryAfterSeconds = Math.max(1, Math.ceil((bucket.resetAt - now) / 1000))
res.setHeader('Retry-After', String(retryAfterSeconds))
logWarn("[rate-limit]", "请求触发限流", {
logWarn('[rate-limit]', '请求触发限流', {
scope: normalizedScope,
ip: req.ip,
originalUrl: req.originalUrl,
retryAfterSeconds,
});
})
if (onLimit) {
onLimit(req, res, {
scope: normalizedScope,
retryAfterSeconds,
});
return;
})
return
}
const error = createHttpError("请求过于频繁,请稍后再试", {
const error = createHttpError('请求过于频繁,请稍后再试', {
statusCode: 429,
errorCode: "rate_limited",
});
res.status(429).json(buildErrorPayload(error, "请求过于频繁,请稍后再试"));
};
errorCode: 'rate_limited',
})
res.status(429).json(buildErrorPayload(error, '请求过于频繁,请稍后再试'))
}
}
export function resetRateLimitBucketsForTest(): void {
buckets.clear();
lastCleanupAt = 0;
buckets.clear()
lastCleanupAt = 0
}
export function getBodyFieldRateLimitKey(fieldName: string): RateLimitKeyResolver {
return (req) => [defaultRateLimitKey(req), normalizeKeyPart(req.body?.[fieldName])].join(":");
return (req) => [defaultRateLimitKey(req), normalizeKeyPart(req.body?.[fieldName])].join(':')
}
export function getParamRateLimitKey(paramName: string): RateLimitKeyResolver {
return (req) => [defaultRateLimitKey(req), normalizeKeyPart(req.params?.[paramName])].join(":");
return (req) => [defaultRateLimitKey(req), normalizeKeyPart(req.params?.[paramName])].join(':')
}
function defaultRateLimitKey(req: Request): string {
return normalizeKeyPart(
req.ip ||
String(req.headers["x-forwarded-for"] || "").split(",")[0] ||
String(req.headers['x-forwarded-for'] || '').split(',')[0] ||
req.socket.remoteAddress ||
"unknown",
);
'unknown',
)
}
function normalizeKeyPart(value: unknown): string {
const normalized = String(value || "").trim().toLowerCase();
return normalized || "unknown";
const normalized = String(value || '')
.trim()
.toLowerCase()
return normalized || 'unknown'
}
function cleanupExpiredBuckets(now: number): void {
if (now - lastCleanupAt < 60_000) {
return;
return
}
lastCleanupAt = now;
lastCleanupAt = now
for (const [key, bucket] of buckets.entries()) {
if (bucket.resetAt <= now) {
buckets.delete(key);
buckets.delete(key)
}
}
}
@@ -39,7 +39,9 @@ type AdminAuditLogListResult = {
total: number
}
export async function createAdminAuditLog(input: AdminAuditLogCreateInput): Promise<AdminAuditLogRow | null> {
export async function createAdminAuditLog(
input: AdminAuditLogCreateInput,
): Promise<AdminAuditLogRow | null> {
const result = await query<AdminAuditLogRow>(
`
INSERT INTO admin_audit_logs (
@@ -69,12 +71,19 @@ export async function createAdminAuditLog(input: AdminAuditLogCreateInput): Prom
return result.rows[0] || null
}
export async function getAdminAuditLogById(logId: number | string): Promise<AdminAuditLogRow | null> {
const result = await query<AdminAuditLogRow>('SELECT * FROM admin_audit_logs WHERE id = $1 LIMIT 1', [Number(logId)])
export async function getAdminAuditLogById(
logId: number | string,
): Promise<AdminAuditLogRow | null> {
const result = await query<AdminAuditLogRow>(
'SELECT * FROM admin_audit_logs WHERE id = $1 LIMIT 1',
[Number(logId)],
)
return result.rows[0] || null
}
export async function listAdminAuditLogs(queryInput: AdminAuditLogListInput = {}): Promise<AdminAuditLogListResult> {
export async function listAdminAuditLogs(
queryInput: AdminAuditLogListInput = {},
): Promise<AdminAuditLogListResult> {
const conditions: string[] = []
const params: unknown[] = []
@@ -108,7 +117,7 @@ export async function listAdminAuditLogs(queryInput: AdminAuditLogListInput = {}
const pageSize = Number(queryInput.pageSize) || 20
const offset = (page - 1) * pageSize
const totalResult = await query<{ [column: string]: unknown, total: number }>(
const totalResult = await query<{ [column: string]: unknown; total: number }>(
`SELECT COUNT(*)::int AS total FROM admin_audit_logs ${whereClause}`,
params,
)
@@ -20,10 +20,12 @@ type AdminUserCreateInput = {
updatedAt: string
}
type AdminUserPatch = Partial<Pick<
type AdminUserPatch = Partial<
Pick<
AdminUserRow,
'username' | 'password_hash' | 'role' | 'status' | 'session_version' | 'updated_at'
>>
>
>
type AdminUserListInput = {
page?: number
@@ -44,23 +46,23 @@ const ADMIN_USER_SELECT = `
`
export async function getAdminUserById(userId: number | string): Promise<AdminUserRow | null> {
const result = await query<AdminUserRow>(
`${ADMIN_USER_SELECT} WHERE au.id = $1 LIMIT 1`,
[Number(userId)],
)
const result = await query<AdminUserRow>(`${ADMIN_USER_SELECT} WHERE au.id = $1 LIMIT 1`, [
Number(userId),
])
return result.rows[0] || null
}
export async function getAdminUserByUsername(username: string): Promise<AdminUserRow | null> {
const result = await query<AdminUserRow>(
`${ADMIN_USER_SELECT} WHERE au.username = $1 LIMIT 1`,
[String(username || '').trim().toLowerCase()],
)
const result = await query<AdminUserRow>(`${ADMIN_USER_SELECT} WHERE au.username = $1 LIMIT 1`, [
String(username || '')
.trim()
.toLowerCase(),
])
return result.rows[0] || null
}
export async function createAdminUser(input: AdminUserCreateInput): Promise<AdminUserRow | null> {
const result = await query<{ [column: string]: unknown, id: number }>(
const result = await query<{ [column: string]: unknown; id: number }>(
`
INSERT INTO admin_users (
username,
@@ -97,7 +99,7 @@ export async function updateAdminUser(
}
const next = { ...current, ...patch }
const result = await query<{ [column: string]: unknown, id: number }>(
const result = await query<{ [column: string]: unknown; id: number }>(
`
UPDATE admin_users
SET
@@ -151,7 +153,7 @@ export async function listAdminUsers({
}
const whereClause = filters.length > 0 ? `WHERE ${filters.join(' AND ')}` : ''
const totalResult = await query<{ [column: string]: unknown, total: number }>(
const totalResult = await query<{ [column: string]: unknown; total: number }>(
`SELECT COUNT(*)::int AS total FROM admin_users ${whereClause}`,
params,
)
@@ -175,7 +177,7 @@ export async function listAdminUsers({
}
export async function countActiveAdminUsers(): Promise<number> {
const result = await query<{ [column: string]: unknown, total: number }>(
const result = await query<{ [column: string]: unknown; total: number }>(
`SELECT COUNT(*)::int AS total FROM admin_users WHERE role = 'admin' AND status = 'active'`,
)
return Number(result.rows[0]?.total || 0)
@@ -26,12 +26,16 @@ type ClaimTokenCreateInput = {
updatedAt: string
}
type ClaimTokenPatch = Partial<Pick<
type ClaimTokenPatch = Partial<
Pick<
ClaimTokenRow,
'status' | 'expired_at' | 'used_at' | 'max_use_count' | 'used_count' | 'updated_at'
>>
>
>
export async function createClaimToken(input: ClaimTokenCreateInput): Promise<ClaimTokenRow | null> {
export async function createClaimToken(
input: ClaimTokenCreateInput,
): Promise<ClaimTokenRow | null> {
const result = await query<ClaimTokenRow>(
`
INSERT INTO claim_tokens (
@@ -74,16 +78,23 @@ export async function createClaimToken(input: ClaimTokenCreateInput): Promise<Cl
}
export async function getClaimTokenById(tokenId: number | string): Promise<ClaimTokenRow | null> {
const result = await query<ClaimTokenRow>('SELECT * FROM claim_tokens WHERE id = $1 LIMIT 1', [Number(tokenId)])
const result = await query<ClaimTokenRow>('SELECT * FROM claim_tokens WHERE id = $1 LIMIT 1', [
Number(tokenId),
])
return result.rows[0] || null
}
export async function findClaimTokenByToken(token: string): Promise<ClaimTokenRow | null> {
const result = await query<ClaimTokenRow>('SELECT * FROM claim_tokens WHERE token = $1 LIMIT 1', [String(token || '').trim()])
const result = await query<ClaimTokenRow>('SELECT * FROM claim_tokens WHERE token = $1 LIMIT 1', [
String(token || '').trim(),
])
return result.rows[0] || null
}
export async function updateClaimToken(tokenId: number | string, patch: ClaimTokenPatch): Promise<ClaimTokenRow | null> {
export async function updateClaimToken(
tokenId: number | string,
patch: ClaimTokenPatch,
): Promise<ClaimTokenRow | null> {
const current = await getClaimTokenById(tokenId)
if (!current) {
return null
@@ -47,7 +47,9 @@ export type FulfillmentProfileRequirementInput = {
configJson?: string | Record<string, unknown>
}
export async function getFulfillmentProfileByKey(profileKey: string): Promise<FulfillmentProfileRow | null> {
export async function getFulfillmentProfileByKey(
profileKey: string,
): Promise<FulfillmentProfileRow | null> {
const result = await query<FulfillmentProfileRow>(
'SELECT * FROM fulfillment_profiles WHERE profile_key = $1 LIMIT 1',
[String(profileKey || '').trim()],
@@ -104,10 +106,9 @@ export async function replaceFulfillmentProfileRequirements(
timestamp: string,
): Promise<void> {
await withTransaction(async (client) => {
await client.query(
'DELETE FROM fulfillment_profile_requirements WHERE profile_id = $1',
[Number(profileId)],
)
await client.query('DELETE FROM fulfillment_profile_requirements WHERE profile_id = $1', [
Number(profileId),
])
for (const requirement of requirements) {
await client.query(
@@ -10,7 +10,8 @@ test('syncKuaishouCloudTaskStateForTask 将 Date 更新时间规范化为 ISO
return { rows: [] }
}
await syncKuaishouCloudTaskStateForTask({
await syncKuaishouCloudTaskStateForTask(
{
taskId: 9,
executorKey: 'kuaishou_ct_assisted',
contextJson: {
@@ -35,7 +36,9 @@ test('syncKuaishouCloudTaskStateForTask 将 Date 更新时间规范化为 ISO
},
},
updatedAt: new Date('2026-05-29T09:05:46.658Z'),
}, executor)
},
executor,
)
assert.equal(calls.length, 1)
assert.match(calls[0].text, /INSERT INTO kuaishou_cloud_task_states/)
@@ -64,7 +64,10 @@ test('resolveOrderItemSyncPlan matches by identity before falling back to positi
const plan = resolveOrderItemSyncPlan(existingItems, nextItems)
assert.deepEqual(plan.updates.map((item) => item.orderItemId), [22, 21])
assert.deepEqual(
plan.updates.map((item) => item.orderItemId),
[22, 21],
)
assert.deepEqual(plan.creates, [])
assert.deepEqual(plan.deletes, [])
})
@@ -88,7 +91,10 @@ test('resolveOrderItemSyncPlan keeps surplus existing ids for conditional cleanu
const plan = resolveOrderItemSyncPlan(existingItems, nextItems)
assert.deepEqual(plan.updates.map((item) => item.orderItemId), [31])
assert.deepEqual(
plan.updates.map((item) => item.orderItemId),
[31],
)
assert.deepEqual(plan.creates, [])
assert.deepEqual(plan.deletes, [32])
})
@@ -204,6 +204,8 @@ async function listOrderItemsByOrderIdWithExecutor(
}
export async function getOrderItemById(orderItemId: number | string): Promise<OrderItemRow | null> {
const result = await query<OrderItemRow>('SELECT * FROM order_items WHERE id = $1 LIMIT 1', [Number(orderItemId)])
const result = await query<OrderItemRow>('SELECT * FROM order_items WHERE id = $1 LIMIT 1', [
Number(orderItemId),
])
return result.rows[0] || null
}
@@ -29,12 +29,7 @@ export async function createTaskEvent(
) VALUES ($1, $2, $3::jsonb, $4)
RETURNING *
`,
[
Number(taskId),
String(eventType || '').trim(),
JSON.stringify(payload || {}),
createdAt,
],
[Number(taskId), String(eventType || '').trim(), JSON.stringify(payload || {}), createdAt],
)
return result.rows[0] || null
+53 -22
View File
@@ -9,10 +9,7 @@ import type {
TaskRuntimeContextPatch,
TaskUpdatePatch,
} from '../types/repository/inputs.js'
import type {
TaskListQueryResult,
TaskRow,
} from '../types/repository/rows.js'
import type { TaskListQueryResult, TaskRow } from '../types/repository/rows.js'
type TaskQueryExecutor = (text: string, params?: unknown[]) => Promise<{ rows: unknown[] }>
const KUAISHOU_FEIFEI_EXECUTOR_KEY = 'kuaishou_feifei'
@@ -150,21 +147,32 @@ export async function createTask(input: TaskCreateInput): Promise<TaskRow | null
const taskId = Number(taskResult.rows[0]?.id || 0)
if (input.runtimeContext) {
await upsertTaskRuntimeContextWithClient(client, taskId, input.runtimeContext, input.createdAt)
await upsertTaskRuntimeContextWithClient(
client,
taskId,
input.runtimeContext,
input.createdAt,
)
}
await syncKuaishouCloudTaskStateForTask({
await syncKuaishouCloudTaskStateForTask(
{
taskId,
executorKey: input.executorKey,
contextJson: input.contextJson || '{}',
updatedAt: input.updatedAt,
}, client.query.bind(client) as TaskQueryExecutor)
},
client.query.bind(client) as TaskQueryExecutor,
)
return getTaskByIdWithExecutor(client.query.bind(client) as TaskQueryExecutor, taskId)
})
}
export async function updateTask(taskId: number | string, patch: TaskUpdatePatch): Promise<TaskRow | null> {
export async function updateTask(
taskId: number | string,
patch: TaskUpdatePatch,
): Promise<TaskRow | null> {
const current = await getTaskById(taskId)
if (!current) {
return null
@@ -218,26 +226,38 @@ export async function updateTask(taskId: number | string, patch: TaskUpdatePatch
if (containsRuntimeContextPatch(patch)) {
const runtimeContextPatch: TaskRuntimeContextPatch = {}
if (patch.runtime_session_id !== undefined) runtimeContextPatch.runtimeSessionId = patch.runtime_session_id
if (patch.runtime_session_id !== undefined)
runtimeContextPatch.runtimeSessionId = patch.runtime_session_id
if (patch.login_type !== undefined) runtimeContextPatch.loginType = patch.login_type
if (patch.nickname !== undefined) runtimeContextPatch.nickname = patch.nickname
if (patch.role_id !== undefined) runtimeContextPatch.roleId = patch.role_id
if (patch.role_name !== undefined) runtimeContextPatch.roleName = patch.role_name
if (patch.area !== undefined) runtimeContextPatch.area = patch.area
if (patch.partition_name !== undefined) runtimeContextPatch.partitionName = patch.partition_name
if (patch.screenshot_path !== undefined) runtimeContextPatch.screenshotPath = patch.screenshot_path
if (patch.artifacts_json !== undefined) runtimeContextPatch.artifactsJson = patch.artifacts_json
if (patch.partition_name !== undefined)
runtimeContextPatch.partitionName = patch.partition_name
if (patch.screenshot_path !== undefined)
runtimeContextPatch.screenshotPath = patch.screenshot_path
if (patch.artifacts_json !== undefined)
runtimeContextPatch.artifactsJson = patch.artifacts_json
if (patch.state_json !== undefined) runtimeContextPatch.stateJson = patch.state_json
await upsertTaskRuntimeContextWithClient(client, Number(taskId), runtimeContextPatch, patch.updated_at || current.updated_at)
await upsertTaskRuntimeContextWithClient(
client,
Number(taskId),
runtimeContextPatch,
patch.updated_at || current.updated_at,
)
}
await syncKuaishouCloudTaskStateForTask({
await syncKuaishouCloudTaskStateForTask(
{
taskId,
executorKey: next.executor_key,
contextJson: next.context_json,
updatedAt: next.updated_at,
}, client.query.bind(client) as TaskQueryExecutor)
},
client.query.bind(client) as TaskQueryExecutor,
)
return getTaskByIdWithExecutor(client.query.bind(client) as TaskQueryExecutor, taskId)
})
@@ -296,12 +316,15 @@ export async function updateTaskStatusIfCurrent(
return null
}
await syncKuaishouCloudTaskStateForTask({
await syncKuaishouCloudTaskStateForTask(
{
taskId: updated.id,
executorKey: updated.executor_key,
contextJson: updated.context_json,
updatedAt: updated.updated_at,
}, client.query.bind(client) as TaskQueryExecutor)
},
client.query.bind(client) as TaskQueryExecutor,
)
return getTaskByIdWithExecutor(client.query.bind(client) as TaskQueryExecutor, taskId)
})
@@ -311,7 +334,9 @@ export async function getTaskById(taskId: number | string): Promise<TaskRow | nu
return getTaskByIdWithExecutor(query, taskId)
}
export async function findTaskByClaimTokenId(claimTokenId: number | string): Promise<TaskRow | null> {
export async function findTaskByClaimTokenId(
claimTokenId: number | string,
): Promise<TaskRow | null> {
const result = await query<TaskRow>(
`${buildTaskSelect()}
JOIN claim_tokens ctf ON ctf.task_id = ft.id
@@ -441,7 +466,7 @@ export async function listTasks({
}
const whereClause = filters.length > 0 ? `WHERE ${filters.join(' AND ')}` : ''
const totalResult = await query<{ [column: string]: unknown, total: number }>(
const totalResult = await query<{ [column: string]: unknown; total: number }>(
`
SELECT COUNT(*)::int AS total
FROM fulfillment_tasks ft
@@ -525,8 +550,12 @@ async function upsertTaskRuntimeContextWithClient(
next.area,
next.partition_name,
next.screenshot_path,
typeof next.artifacts_json === 'string' ? next.artifacts_json : JSON.stringify(next.artifacts_json || {}),
typeof next.state_json === 'string' ? next.state_json : JSON.stringify(next.state_json || {}),
typeof next.artifacts_json === 'string'
? next.artifacts_json
: JSON.stringify(next.artifacts_json || {}),
typeof next.state_json === 'string'
? next.state_json
: JSON.stringify(next.state_json || {}),
timestamp,
timestamp,
],
@@ -560,7 +589,9 @@ async function upsertTaskRuntimeContextWithClient(
next.area,
next.partition_name,
next.screenshot_path,
typeof next.artifacts_json === 'string' ? next.artifacts_json : JSON.stringify(next.artifacts_json || {}),
typeof next.artifacts_json === 'string'
? next.artifacts_json
: JSON.stringify(next.artifacts_json || {}),
typeof next.state_json === 'string' ? next.state_json : JSON.stringify(next.state_json || {}),
timestamp,
Number(taskId),
@@ -2,7 +2,6 @@ import type { PoolClient } from 'pg'
import type { WorkerWalletRow } from './types.js'
export async function ensureWorkerWalletWithClient(
client: PoolClient,
workerId: number,
@@ -1,5 +1,3 @@
export type WorkerLevelRow = {
id: number
level_key: string
@@ -1,10 +1,25 @@
import { query, withTransaction } from '../../db/client.js'
import { ensureWorkerWalletWithClient, getWorkerWalletWithClient, toJsonString, toPositiveInteger } from './shared.js'
import type { CreateWorkOrderInput, GrabWorkOrderResult, ListInput, ProblemWorkOrderResolutionAction, ProductRuleListInput, WorkCategoryRow, WorkerDepositUnfreezeRow, WorkOrderRow, WorkOrderShareRow, WorkProductRuleRow } from './types.js'
import {
ensureWorkerWalletWithClient,
getWorkerWalletWithClient,
toJsonString,
toPositiveInteger,
} from './shared.js'
import type {
CreateWorkOrderInput,
GrabWorkOrderResult,
ListInput,
ProblemWorkOrderResolutionAction,
ProductRuleListInput,
WorkCategoryRow,
WorkerDepositUnfreezeRow,
WorkOrderRow,
WorkOrderShareRow,
WorkProductRuleRow,
} from './types.js'
import type { PoolClient } from 'pg'
import { maybeUpgradeWorkerLevelWithClient } from './worker-repo.js'
const WORK_ORDER_SELECT = `
SELECT
wo.*,
@@ -248,7 +263,10 @@ export async function submitWorkOrderShareAcceptance(input: {
workerId: number
acceptanceJson: string
now: string
}): Promise<{ share: WorkOrderShareRow | null; failureReason: 'share_not_found' | 'share_status_invalid' | null }> {
}): Promise<{
share: WorkOrderShareRow | null
failureReason: 'share_not_found' | 'share_status_invalid' | null
}> {
return withTransaction(async (client) => {
const lockResult = await client.query<{ id: number }>(
`
@@ -436,7 +454,9 @@ export async function getWorkCategoryByKey(categoryKey: string): Promise<WorkCat
return result.rows[0] || null
}
export async function getWorkCategoryById(categoryId: number | string): Promise<WorkCategoryRow | null> {
export async function getWorkCategoryById(
categoryId: number | string,
): Promise<WorkCategoryRow | null> {
const result = await query<WorkCategoryRow>(
'SELECT * FROM work_categories WHERE id = $1 LIMIT 1',
[Number(categoryId)],
@@ -747,8 +767,7 @@ export async function listWorkOrders({
pinnedFirst = false,
sort = 'id_desc',
}: ListInput = {}): Promise<{ items: WorkOrderRow[]; total: number }> {
const effectiveStatuses =
statuses.length > 0 ? statuses : status.trim() ? [status.trim()] : []
const effectiveStatuses = statuses.length > 0 ? statuses : status.trim() ? [status.trim()] : []
const { whereClause, params } = buildWorkOrderWhere({
statuses: effectiveStatuses,
keyword,
@@ -787,9 +806,7 @@ export async function listWorkOrders({
) DESC NULLS LAST, wo.id DESC`
})()
: 'wo.id DESC'
const effectiveOrderBy = pinnedFirst
? `wo.pinned_at DESC NULLS LAST, ${orderBy}`
: orderBy
const effectiveOrderBy = pinnedFirst ? `wo.pinned_at DESC NULLS LAST, ${orderBy}` : orderBy
params.push(pageSize, offset)
const itemsResult = await query<WorkOrderRow>(
`${WORK_ORDER_SELECT}
@@ -911,14 +928,8 @@ export async function updateWorkOrderBasic(
String((patch.productName ?? current.product_name) || '').trim(),
patch.categoryId === undefined ? current.category_id : patch.categoryId,
toPositiveInteger(patch.rewardAmount ?? current.reward_amount, 0),
toPositiveInteger(
patch.requiredDepositAmount ?? current.required_deposit_amount,
0,
),
toPositiveInteger(
patch.depositThresholdAmount ?? current.deposit_threshold_amount,
0,
),
toPositiveInteger(patch.requiredDepositAmount ?? current.required_deposit_amount, 0),
toPositiveInteger(patch.depositThresholdAmount ?? current.deposit_threshold_amount, 0),
toJsonString(patch.requirementJson ?? current.requirement_json),
toPositiveInteger(patch.timeoutMinutes ?? current.timeout_minutes, 0),
String((patch.timeoutPolicy ?? current.timeout_policy) || 'reopen').trim(),
@@ -1196,10 +1207,7 @@ export async function cancelWorkerWorkOrder(input: {
if (releaseAmount > 0) {
const nextAvailable = Number(wallet?.available_amount || 0) + releaseAmount
const nextFrozen = Math.max(
0,
Number(wallet?.frozen_deposit_amount || 0) - releaseAmount,
)
const nextFrozen = Math.max(0, Number(wallet?.frozen_deposit_amount || 0) - releaseAmount)
await client.query(
`
UPDATE worker_wallets
@@ -1237,10 +1245,7 @@ export async function cancelWorkerWorkOrder(input: {
})
}
export async function unassignWorkOrder(input: {
workOrderId: number
now: string
}): Promise<{
export async function unassignWorkOrder(input: { workOrderId: number; now: string }): Promise<{
order: WorkOrderRow | null
failureReason: 'work_order_not_in_progress' | 'work_order_no_worker' | null
}> {
@@ -1286,10 +1291,7 @@ export async function unassignWorkOrder(input: {
if (releaseAmount > 0) {
const nextAvailable = Number(wallet?.available_amount || 0) + releaseAmount
const nextFrozen = Math.max(
0,
Number(wallet?.frozen_deposit_amount || 0) - releaseAmount,
)
const nextFrozen = Math.max(0, Number(wallet?.frozen_deposit_amount || 0) - releaseAmount)
await client.query(
`
UPDATE worker_wallets
@@ -1378,10 +1380,7 @@ export async function returnAssignedWorkOrderToHall(input: {
if (releaseAmount > 0) {
const nextAvailable = Number(wallet?.available_amount || 0) + releaseAmount
const nextFrozen = Math.max(
0,
Number(wallet?.frozen_deposit_amount || 0) - releaseAmount,
)
const nextFrozen = Math.max(0, Number(wallet?.frozen_deposit_amount || 0) - releaseAmount)
await client.query(
`
UPDATE worker_wallets
@@ -1474,10 +1473,7 @@ export async function cancelAssignedWorkOrder(input: {
if (releaseAmount > 0) {
const nextAvailable = Number(wallet?.available_amount || 0) + releaseAmount
const nextFrozen = Math.max(
0,
Number(wallet?.frozen_deposit_amount || 0) - releaseAmount,
)
const nextFrozen = Math.max(0, Number(wallet?.frozen_deposit_amount || 0) - releaseAmount)
await client.query(
`
UPDATE worker_wallets
@@ -1608,10 +1604,7 @@ export async function acceptWorkOrderAndSettle(input: {
Number(wallet?.available_amount || 0) +
shareReward +
(shouldDelayUnfreeze ? 0 : releaseAmount)
const nextFrozen = Math.max(
0,
Number(wallet?.frozen_deposit_amount || 0) - releaseAmount,
)
const nextFrozen = Math.max(0, Number(wallet?.frozen_deposit_amount || 0) - releaseAmount)
await client.query(
`
@@ -1829,7 +1822,10 @@ export async function acceptWorkOrderAndSettle(input: {
await maybeUpgradeWorkerLevelWithClient(client, workerId, input.now)
}
return { order: await getWorkOrderByIdWithClient(client, input.workOrderId), failureReason: null }
return {
order: await getWorkOrderByIdWithClient(client, input.workOrderId),
failureReason: null,
}
})
}
@@ -2263,7 +2259,10 @@ export async function listDueDepositUnfreezes({
export async function releaseDepositUnfreeze({
unfreezeId,
now,
}: { unfreezeId: number; now: string }): Promise<WorkerDepositUnfreezeRow | null> {
}: {
unfreezeId: number
now: string
}): Promise<WorkerDepositUnfreezeRow | null> {
return withTransaction(async (client) => {
const currentResult = await client.query<WorkerDepositUnfreezeRow>(
`
@@ -2295,10 +2294,7 @@ export async function releaseDepositUnfreeze({
if (amount > 0) {
const nextAvailable = Number(wallet?.available_amount || 0) + amount
const nextPending = Math.max(
0,
Number(wallet?.pending_unfreeze_amount || 0) - amount,
)
const nextPending = Math.max(0, Number(wallet?.pending_unfreeze_amount || 0) - amount)
await client.query(
`
UPDATE worker_wallets
@@ -2337,7 +2333,13 @@ export async function releaseDepositUnfreeze({
async function enqueueDepositUnfreezeWithClient(
client: PoolClient,
input: { workerId: number; workOrderId: number; amount: number; unfreezeDays: number; now: string },
input: {
workerId: number
workOrderId: number
amount: number
unfreezeDays: number
now: string
},
) {
if (input.amount <= 0) return
const unfreezeAt = new Date(
@@ -2375,7 +2377,9 @@ export async function countWorkerActiveOrders(workerId: number | string): Promis
return Number(result.rows[0]?.total || 0)
}
export async function countTimeoutEventsByWorkerIds(workerIds: number[]): Promise<Map<number, number>> {
export async function countTimeoutEventsByWorkerIds(
workerIds: number[],
): Promise<Map<number, number>> {
const counts = new Map<number, number>()
const uniqueIds = [...new Set(workerIds.map((id) => Number(id)).filter((id) => id > 0))]
if (uniqueIds.length === 0) return counts
@@ -2481,7 +2485,10 @@ async function getOutstandingDepositAmountWithClient(
return resolveOutstandingDepositAmount(result.rows[0])
}
async function countWorkerActiveOrdersWithClient(client: PoolClient, workerId: number): Promise<number> {
async function countWorkerActiveOrdersWithClient(
client: PoolClient,
workerId: number,
): Promise<number> {
const result = await client.query<{ total: number }>(
`
SELECT (
@@ -2542,11 +2549,7 @@ function buildWorkOrderWhere({
const filters: string[] = []
const params: unknown[] = []
const normalizedStatuses = [
...new Set(
statuses
.map((item) => String(item || '').trim())
.filter(Boolean),
),
...new Set(statuses.map((item) => String(item || '').trim()).filter(Boolean)),
]
if (normalizedStatuses.length > 0) {
params.push(normalizedStatuses)
@@ -1,9 +1,18 @@
import { query, withTransaction } from '../../db/client.js'
import { ensureWorkerWalletWithClient, getWorkerWalletWithClient } from './shared.js'
import type { CreateWorkerInput, FinanceRequestListInput, ListInput, WalletLedgerListInput, WorkerFinanceRequestRow, WorkerLevelRow, WorkerUserRow, WorkerWalletLedgerRow, WorkerWalletRow } from './types.js'
import type {
CreateWorkerInput,
FinanceRequestListInput,
ListInput,
WalletLedgerListInput,
WorkerFinanceRequestRow,
WorkerLevelRow,
WorkerUserRow,
WorkerWalletLedgerRow,
WorkerWalletRow,
} from './types.js'
import type { PoolClient } from 'pg'
const WORKER_USER_SELECT = `
SELECT
wu.*,
@@ -119,10 +128,9 @@ export async function getWorkerUserByUsername(username: string): Promise<WorkerU
}
export async function getWorkerUserByPhone(phone: string): Promise<WorkerUserRow | null> {
const result = await query<WorkerUserRow>(
`${WORKER_USER_SELECT} WHERE wu.phone = $1 LIMIT 1`,
[String(phone || '').trim()],
)
const result = await query<WorkerUserRow>(`${WORKER_USER_SELECT} WHERE wu.phone = $1 LIMIT 1`, [
String(phone || '').trim(),
])
return result.rows[0] || null
}
@@ -136,12 +144,14 @@ export async function getWorkerUserByDisplayName(
return result.rows[0] || null
}
export async function getWorkerUserByInviteCode(
inviteCode: string,
): Promise<WorkerUserRow | null> {
export async function getWorkerUserByInviteCode(inviteCode: string): Promise<WorkerUserRow | null> {
const result = await query<WorkerUserRow>(
`${WORKER_USER_SELECT} WHERE wu.invite_code = $1 LIMIT 1`,
[String(inviteCode || '').trim().toUpperCase()],
[
String(inviteCode || '')
.trim()
.toUpperCase(),
],
)
return result.rows[0] || null
}
@@ -827,23 +837,18 @@ export async function maybeUpgradeWorkerLevelWithClient(
return null
}
await client.query(
'UPDATE worker_users SET level_id = $1, updated_at = $2 WHERE id = $3',
[nextLevelId, now, workerId],
)
await client.query('UPDATE worker_users SET level_id = $1, updated_at = $2 WHERE id = $3', [
nextLevelId,
now,
workerId,
])
await client.query(
`
INSERT INTO worker_level_logs (
worker_id, from_level_id, to_level_id, reason, payload_json, created_at
) VALUES ($1, $2, $3, 'auto', $4::jsonb, $5)
`,
[
workerId,
currentRow.level_id || null,
nextLevelId,
JSON.stringify({ acceptedCount }),
now,
],
[workerId, currentRow.level_id || null, nextLevelId, JSON.stringify({ acceptedCount }), now],
)
return getWorkerUserByIdWithClient(client, workerId)
}
@@ -903,10 +908,7 @@ function buildWorkerUserWhere({
}
}
function buildWorkerWalletLedgerWhere({
workerId = 0,
ledgerType = '',
}: WalletLedgerListInput) {
function buildWorkerWalletLedgerWhere({ workerId = 0, ledgerType = '' }: WalletLedgerListInput) {
const filters: string[] = []
const params: unknown[] = []
if (workerId) {
+5 -5
View File
@@ -10,13 +10,13 @@ const router = Router()
router.use('/audit-logs', requireAdminRoles(['admin']))
router.get('/audit-logs', createJsonHandler(
(req) => getAdminAuditLogs(req.query as AdminAuditLogRouteQuery),
{
router.get(
'/audit-logs',
createJsonHandler((req) => getAdminAuditLogs(req.query as AdminAuditLogRouteQuery), {
successMessage: 'ok',
errorMessage: '读取操作审计日志失败',
scope: '[admin/audit-logs]',
},
))
}),
)
export default router
+19 -14
View File
@@ -11,13 +11,17 @@ import { createJsonHandler, extractBearerToken } from './session.js'
const router = Router()
router.post('/auth/login', createRateLimitMiddleware({
router.post(
'/auth/login',
createRateLimitMiddleware({
scope: 'admin:login',
windowMs: 60_000,
max: 10,
key: getBodyFieldRateLimitKey('username'),
}), createJsonHandler(
(req) => loginAdmin(req.body?.username, req.body?.password, {
}),
createJsonHandler(
(req) =>
loginAdmin(req.body?.username, req.body?.password, {
ip: resolveClientIp(req),
userAgent: resolveUserAgent(req),
location: resolveClientLocation(req),
@@ -27,24 +31,25 @@ router.post('/auth/login', createRateLimitMiddleware({
errorMessage: '后台登录失败',
scope: '[admin/auth/login]',
},
))
),
)
router.get('/auth/session', createJsonHandler(
(req) => getAdminSessionSummary(extractBearerToken(req)),
{
router.get(
'/auth/session',
createJsonHandler((req) => getAdminSessionSummary(extractBearerToken(req)), {
successMessage: 'ok',
errorMessage: '读取后台登录态失败',
scope: '[admin/auth/session]',
},
))
}),
)
router.post('/auth/logout', createJsonHandler(
() => ({ success: true }),
{
router.post(
'/auth/logout',
createJsonHandler(() => ({ success: true }), {
successMessage: '已退出登录',
errorMessage: '后台退出失败',
scope: '[admin/auth/logout]',
},
))
}),
)
export default router
@@ -14,7 +14,12 @@ import { createHttpError, sendRouteError } from '../../utils/http.js'
import type { AdminCloudtentaclesDeliveryRecordQueryRouteBody } from '../../types/admin/route-inputs.js'
const router = Router()
const RECORD_IMAGE_CACHE_DIR = path.join(PROJECT_ROOT, 'data', 'cache', 'cloudtentacles-record-images')
const RECORD_IMAGE_CACHE_DIR = path.join(
PROJECT_ROOT,
'data',
'cache',
'cloudtentacles-record-images',
)
router.get(
'/cloudtentacles-records/sources',
@@ -155,7 +160,10 @@ function buildRecordImageCacheMeta(imageUrl: string) {
return {
candidateFilePaths: candidateTypes.map((contentType) => ({
contentType,
filePath: path.join(RECORD_IMAGE_CACHE_DIR, `${digest}.${extensionForContentType(contentType)}`),
filePath: path.join(
RECORD_IMAGE_CACHE_DIR,
`${digest}.${extensionForContentType(contentType)}`,
),
})),
filePathForContentType(contentType: string) {
return path.join(RECORD_IMAGE_CACHE_DIR, `${digest}.${extensionForContentType(contentType)}`)
@@ -164,7 +172,12 @@ function buildRecordImageCacheMeta(imageUrl: string) {
}
function normalizeImageContentType(contentType: string) {
return String(contentType || 'image/jpeg').split(';')[0]?.trim().toLowerCase() || 'image/jpeg'
return (
String(contentType || 'image/jpeg')
.split(';')[0]
?.trim()
.toLowerCase() || 'image/jpeg'
)
}
function extensionForContentType(contentType: string) {
+10 -10
View File
@@ -7,22 +7,22 @@ import { createJsonHandler } from './session.js'
const router = Router()
router.get('/dashboard/summary', createJsonHandler(
() => getAdminDashboardSummary(),
{
router.get(
'/dashboard/summary',
createJsonHandler(() => getAdminDashboardSummary(), {
successMessage: 'ok',
errorMessage: '读取后台概览失败',
scope: '[admin/dashboard/summary]',
},
))
}),
)
router.get('/dashboard/login-logs', createJsonHandler(
(req) => getAdminLoginLogs(req.query as JsonObject),
{
router.get(
'/dashboard/login-logs',
createJsonHandler((req) => getAdminLoginLogs(req.query as JsonObject), {
successMessage: 'ok',
errorMessage: '读取登录记录失败',
scope: '[admin/dashboard/login-logs]',
},
))
}),
)
export default router
+1 -5
View File
@@ -15,11 +15,7 @@ import { createJsonHandler, requireAdminRoles } from './session.js'
const router = Router()
function requireDevMockEnabled() {
return (
_req: unknown,
_res: unknown,
next: (error?: unknown) => void,
) => {
return (_req: unknown, _res: unknown, next: (error?: unknown) => void) => {
if (!isDevMockEnabled()) {
next(
createHttpError('开发 Mock 仅在非 production 环境可用(或设置 ENABLE_DEV_MOCK=1', {
@@ -57,7 +57,8 @@ router.post(
successMessage: '售后单列表已查询',
errorMessage: '查询快手售后单列表失败',
scope: '[admin/kuaishou-industry/refunds/list]',
audit: (req, data) => buildKuaishouIndustryAudit('kuaishou_industry_refund_list', req.body, data),
audit: (req, data) =>
buildKuaishouIndustryAudit('kuaishou_industry_refund_list', req.body, data),
},
),
)
@@ -72,7 +73,8 @@ router.post(
successMessage: '同意退款接口已执行',
errorMessage: '执行同意退款失败',
scope: '[admin/kuaishou-industry/refunds/approve]',
audit: (req, data) => buildKuaishouIndustryAudit('kuaishou_industry_refund_approve', req.body, data),
audit: (req, data) =>
buildKuaishouIndustryAudit('kuaishou_industry_refund_approve', req.body, data),
},
),
)
@@ -87,7 +89,8 @@ router.post(
successMessage: '不同意退款接口已执行',
errorMessage: '执行不同意退款失败',
scope: '[admin/kuaishou-industry/refunds/disagree]',
audit: (req, data) => buildKuaishouIndustryAudit('kuaishou_industry_refund_disagree', req.body, data),
audit: (req, data) =>
buildKuaishouIndustryAudit('kuaishou_industry_refund_disagree', req.body, data),
},
),
)
@@ -104,7 +107,8 @@ router.post(
successMessage: '电子凭证有效性已检查',
errorMessage: '检查电子凭证有效性失败',
scope: '[admin/kuaishou-industry/vouchers/check-available]',
audit: (req, data) => buildKuaishouIndustryAudit('kuaishou_industry_voucher_check_available', req.body, data),
audit: (req, data) =>
buildKuaishouIndustryAudit('kuaishou_industry_voucher_check_available', req.body, data),
},
),
)
@@ -119,7 +123,8 @@ router.post(
successMessage: '电子凭证冲正回调已执行',
errorMessage: '执行电子凭证冲正失败',
scope: '[admin/kuaishou-industry/vouchers/reverse]',
audit: (req, data) => buildKuaishouIndustryAudit('kuaishou_industry_voucher_reverse', req.body, data),
audit: (req, data) =>
buildKuaishouIndustryAudit('kuaishou_industry_voucher_reverse', req.body, data),
},
),
)
@@ -136,7 +141,8 @@ router.post(
successMessage: '电子凭证已手动核销',
errorMessage: '手动核销电子凭证失败',
scope: '[admin/kuaishou-industry/vouchers/consume]',
audit: (req, data) => buildKuaishouIndustryAudit('kuaishou_industry_voucher_consume', req.body, data),
audit: (req, data) =>
buildKuaishouIndustryAudit('kuaishou_industry_voucher_consume', req.body, data),
},
),
)
@@ -153,7 +159,8 @@ router.post(
successMessage: '电子凭证发码回调已重发',
errorMessage: '重发电子凭证发码回调失败',
scope: '[admin/kuaishou-industry/vouchers/resend-code]',
audit: (req, data) => buildKuaishouIndustryAudit('kuaishou_industry_voucher_resend_code', req.body, data),
audit: (req, data) =>
buildKuaishouIndustryAudit('kuaishou_industry_voucher_resend_code', req.body, data),
},
),
)
@@ -170,14 +177,15 @@ router.post(
successMessage: '电子凭证已手动销毁',
errorMessage: '手动销毁电子凭证失败',
scope: '[admin/kuaishou-industry/vouchers/destroy]',
audit: (req, data) => buildKuaishouIndustryAudit('kuaishou_industry_voucher_destroy', req.body, data),
audit: (req, data) =>
buildKuaishouIndustryAudit('kuaishou_industry_voucher_destroy', req.body, data),
},
),
)
function buildKuaishouIndustryAudit(action: string, body: unknown, data: unknown) {
const payload = body && typeof body === 'object' ? body as Record<string, unknown> : {}
const result = data && typeof data === 'object' ? data as Record<string, unknown> : {}
const payload = body && typeof body === 'object' ? (body as Record<string, unknown>) : {}
const result = data && typeof data === 'object' ? (data as Record<string, unknown>) : {}
return {
action,
+12 -12
View File
@@ -3,23 +3,22 @@ import { Router } from 'express'
import { getAdminOrderDetail, getAdminOrders } from '../../services/admin/admin-read-service.js'
import { resendAdminOrderKuaishouIndustryVoucherCodes } from '../../services/admin/write/kuaishou-cloud-actions.js'
import { createJsonHandler, requireAdminRoles } from './session.js'
import type {
AdminOrderRouteParams,
AdminOrderRouteQuery,
} from '../../types/admin/route-inputs.js'
import type { AdminOrderRouteParams, AdminOrderRouteQuery } from '../../types/admin/route-inputs.js'
const router = Router()
router.get('/orders', createJsonHandler(
(req) => getAdminOrders(req.query as AdminOrderRouteQuery),
{
router.get(
'/orders',
createJsonHandler((req) => getAdminOrders(req.query as AdminOrderRouteQuery), {
successMessage: 'ok',
errorMessage: '读取订单列表失败',
scope: '[admin/orders]',
},
))
}),
)
router.get('/orders/:orderId', createJsonHandler(
router.get(
'/orders/:orderId',
createJsonHandler(
(req) =>
getAdminOrderDetail(
String((req.params as AdminOrderRouteParams).orderId || ''),
@@ -30,7 +29,8 @@ router.get('/orders/:orderId', createJsonHandler(
errorMessage: '读取订单详情失败',
scope: '[admin/orders/:orderId]',
},
))
),
)
router.post(
'/orders/:orderId/kuaishou-industry/resend-code',
@@ -49,7 +49,7 @@ router.post(
action: 'order_kuaishou_industry_resend_code',
targetType: 'order',
targetId: String((req.params as AdminOrderRouteParams).orderId || ''),
data: data && typeof data === 'object' ? data as Record<string, unknown> : {},
data: data && typeof data === 'object' ? (data as Record<string, unknown>) : {},
}),
},
),
@@ -1,23 +1,23 @@
import { Router } from "express";
import { Router } from 'express'
import { requireAdminRoles } from "./session.js";
import cloudtentaclesRouter from "./platform-config/cloudtentacles.js";
import kuaishouIndustryRouter from "./platform-config/kuaishou-industry.js";
import kuaishouFeifeiRouter from "./platform-config/kuaishou-feifei.js";
import ninetyoneRouter from "./platform-config/ninetyone.js";
import notificationsRouter from "./platform-config/notifications.js";
import fulfillmentRoutingRouter from "./platform-config/fulfillment-routing.js";
import affiliateDashRouter from "./platform-config/affiliate-dash.js";
import { requireAdminRoles } from './session.js'
import cloudtentaclesRouter from './platform-config/cloudtentacles.js'
import kuaishouIndustryRouter from './platform-config/kuaishou-industry.js'
import kuaishouFeifeiRouter from './platform-config/kuaishou-feifei.js'
import ninetyoneRouter from './platform-config/ninetyone.js'
import notificationsRouter from './platform-config/notifications.js'
import fulfillmentRoutingRouter from './platform-config/fulfillment-routing.js'
import affiliateDashRouter from './platform-config/affiliate-dash.js'
const router = Router();
const router = Router()
router.use("/platform-config", kuaishouIndustryRouter);
router.use("/platform-config", requireAdminRoles(["admin"]));
router.use("/platform-config", notificationsRouter);
router.use("/platform-config", kuaishouFeifeiRouter);
router.use("/platform-config", ninetyoneRouter);
router.use("/platform-config", fulfillmentRoutingRouter);
router.use("/platform-config", cloudtentaclesRouter);
router.use("/platform-config", affiliateDashRouter);
router.use('/platform-config', kuaishouIndustryRouter)
router.use('/platform-config', requireAdminRoles(['admin']))
router.use('/platform-config', notificationsRouter)
router.use('/platform-config', kuaishouFeifeiRouter)
router.use('/platform-config', ninetyoneRouter)
router.use('/platform-config', fulfillmentRoutingRouter)
router.use('/platform-config', cloudtentaclesRouter)
router.use('/platform-config', affiliateDashRouter)
export default router;
export default router
@@ -1,4 +1,4 @@
import { Router } from "express";
import { Router } from 'express'
import {
getAdminAffiliateDashConfig,
@@ -6,84 +6,77 @@ import {
listAdminAffiliateDashProducts,
matchAdminAffiliateDashSku,
updateAdminAffiliateDashConfig,
} from "../../../services/admin/platform-config/affiliate-dash-service.js";
import { createJsonHandler } from "../session.js";
import type { JsonRecord } from "../../../types/json.js";
} from '../../../services/admin/platform-config/affiliate-dash-service.js'
import { createJsonHandler } from '../session.js'
import type { JsonRecord } from '../../../types/json.js'
const router = Router();
const router = Router()
router.get(
"/affiliate-dash",
'/affiliate-dash',
createJsonHandler(() => getAdminAffiliateDashConfig(), {
successMessage: "ok",
errorMessage: "读取 affiliate-dash 配置失败",
scope: "[admin/platform-config/affiliate-dash]",
})
);
successMessage: 'ok',
errorMessage: '读取 affiliate-dash 配置失败',
scope: '[admin/platform-config/affiliate-dash]',
}),
)
router.post(
"/affiliate-dash",
createJsonHandler(
(req) => updateAdminAffiliateDashConfig(req.body as JsonRecord),
{
successMessage: "affiliate-dash 配置已保存",
errorMessage: "保存 affiliate-dash 配置失败",
scope: "[admin/platform-config/affiliate-dash]",
'/affiliate-dash',
createJsonHandler((req) => updateAdminAffiliateDashConfig(req.body as JsonRecord), {
successMessage: 'affiliate-dash 配置已保存',
errorMessage: '保存 affiliate-dash 配置失败',
scope: '[admin/platform-config/affiliate-dash]',
audit: (_req, data) => {
const result = data as JsonRecord;
const source = result.source as JsonRecord | undefined;
const effective = result.effective as JsonRecord | undefined;
const result = data as JsonRecord
const source = result.source as JsonRecord | undefined
const effective = result.effective as JsonRecord | undefined
return {
action: "platform_affiliate_dash_config_updated",
targetType: "platform_config",
targetId: "affiliate_dash",
action: 'platform_affiliate_dash_config_updated',
targetType: 'platform_config',
targetId: 'affiliate_dash',
data: {
enabled: effective?.enabled !== false,
hasAppKey: Boolean(effective?.hasAppKey),
hasAppSecret: Boolean(effective?.hasAppSecret),
hasCallbackSecret: Boolean(effective?.hasCallbackSecret),
skuMappingCount: Number(source?.skuMapping && typeof source.skuMapping === "object"
skuMappingCount: Number(
source?.skuMapping && typeof source.skuMapping === 'object'
? Object.keys(source.skuMapping).length
: 0),
},
};
: 0,
),
},
}
},
}),
)
);
router.post(
"/affiliate-dash/match",
createJsonHandler(
(req) => matchAdminAffiliateDashSku(req.body as JsonRecord),
{
successMessage: "ok",
errorMessage: "匹配 affiliate-dash sku 失败",
scope: "[admin/platform-config/affiliate-dash/match]",
}
'/affiliate-dash/match',
createJsonHandler((req) => matchAdminAffiliateDashSku(req.body as JsonRecord), {
successMessage: 'ok',
errorMessage: '匹配 affiliate-dash sku 失败',
scope: '[admin/platform-config/affiliate-dash/match]',
}),
)
);
router.post(
"/affiliate-dash/products",
createJsonHandler(
(req) => listAdminAffiliateDashProducts(req.body as JsonRecord),
{
successMessage: "ok",
errorMessage: "查询 affiliate-dash 商品失败",
scope: "[admin/platform-config/affiliate-dash/products]",
}
'/affiliate-dash/products',
createJsonHandler((req) => listAdminAffiliateDashProducts(req.body as JsonRecord), {
successMessage: 'ok',
errorMessage: '查询 affiliate-dash 商品失败',
scope: '[admin/platform-config/affiliate-dash/products]',
}),
)
);
router.post(
"/affiliate-dash/wallet",
'/affiliate-dash/wallet',
createJsonHandler(() => getAdminAffiliateDashWallet(), {
successMessage: "ok",
errorMessage: "查询 affiliate-dash 钱包失败",
scope: "[admin/platform-config/affiliate-dash/wallet]",
})
);
successMessage: 'ok',
errorMessage: '查询 affiliate-dash 钱包失败',
scope: '[admin/platform-config/affiliate-dash/wallet]',
}),
)
export default router;
export default router
@@ -1,4 +1,4 @@
import { Router } from "express";
import { Router } from 'express'
import {
appointAdminCloudtentaclesVirtualNumber,
@@ -23,7 +23,7 @@ import {
useAdminCloudtentaclesSku,
validateAdminCloudtentaclesSession,
verifyAdminCloudtentaclesLoginCode,
} from "../../../services/admin/platform-config/cloudtentacles/index.js";
} from '../../../services/admin/platform-config/cloudtentacles/index.js'
import type {
AdminCloudtentaclesCatalogQueryRouteBody,
AdminCloudtentaclesFullFlowRouteBody,
@@ -35,398 +35,353 @@ import type {
AdminCloudtentaclesTestLoginRouteBody,
AdminCloudtentaclesValidateSessionRouteBody,
AdminCloudtentaclesVirtualNumberRouteBody,
} from "../../../types/admin/route-inputs.js";
import { createJsonHandler } from "../session.js";
import type { JsonRecord } from "../../../types/json.js";
} from '../../../types/admin/route-inputs.js'
import { createJsonHandler } from '../session.js'
import type { JsonRecord } from '../../../types/json.js'
const router = Router();
const router = Router()
router.get(
"/cloudtentacles-source",
'/cloudtentacles-source',
createJsonHandler(() => listAdminCloudtentaclesSources(), {
successMessage: "ok",
errorMessage: "读取 cloudtentacles 履约平台配置失败",
scope: "[admin/platform-config/cloudtentacles-source]",
})
);
successMessage: 'ok',
errorMessage: '读取 cloudtentacles 履约平台配置失败',
scope: '[admin/platform-config/cloudtentacles-source]',
}),
)
router.delete(
"/cloudtentacles-source/:sourceKey",
'/cloudtentacles-source/:sourceKey',
createJsonHandler(
(req) =>
deleteAdminCloudtentaclesSource(
String(req.params.sourceKey || "").trim()
),
(req) => deleteAdminCloudtentaclesSource(String(req.params.sourceKey || '').trim()),
{
successMessage: "cloudtentacles 履约平台配置已删除",
errorMessage: "删除 cloudtentacles 履约平台配置失败",
scope: "[admin/platform-config/cloudtentacles-source/:sourceKey]",
successMessage: 'cloudtentacles 履约平台配置已删除',
errorMessage: '删除 cloudtentacles 履约平台配置失败',
scope: '[admin/platform-config/cloudtentacles-source/:sourceKey]',
audit: (req) => ({
action: "platform_cloudtentacles_source_deleted",
targetType: "platform_config",
targetId: String(req.params.sourceKey || "").trim(),
action: 'platform_cloudtentacles_source_deleted',
targetType: 'platform_config',
targetId: String(req.params.sourceKey || '').trim(),
data: {},
}),
}
},
),
)
);
router.post(
"/cloudtentacles-source",
'/cloudtentacles-source',
createJsonHandler(
(req) =>
updateAdminCloudtentaclesSourceConfig(
req.body as AdminCloudtentaclesSourceConfigRouteBody
),
updateAdminCloudtentaclesSourceConfig(req.body as AdminCloudtentaclesSourceConfigRouteBody),
{
successMessage: "cloudtentacles 履约平台配置已保存",
errorMessage: "保存 cloudtentacles 履约平台配置失败",
scope: "[admin/platform-config/cloudtentacles-source]",
successMessage: 'cloudtentacles 履约平台配置已保存',
errorMessage: '保存 cloudtentacles 履约平台配置失败',
scope: '[admin/platform-config/cloudtentacles-source]',
audit: (_req, data) => {
const result = data as JsonRecord;
const result = data as JsonRecord
return {
action: "platform_cloudtentacles_source_updated",
targetType: "platform_config",
targetId: "cloudtentacles_source",
action: 'platform_cloudtentacles_source_updated',
targetType: 'platform_config',
targetId: 'cloudtentacles_source',
data: {
filePath: String(result.filePath || "").trim(),
username: String(result.source?.username || "").trim(),
filePath: String(result.filePath || '').trim(),
username: String(result.source?.username || '').trim(),
enabled: Boolean(result.source?.enabled),
},
};
},
}
},
},
),
)
);
router.get(
"/cloudtentacles/override-rules",
'/cloudtentacles/override-rules',
createJsonHandler(() => getAdminCloudtentaclesOverrideRules(), {
successMessage: "ok",
errorMessage: "读取 cloudtentacles 覆盖规则失败",
scope: "[admin/platform-config/cloudtentacles/override-rules]",
})
);
successMessage: 'ok',
errorMessage: '读取 cloudtentacles 覆盖规则失败',
scope: '[admin/platform-config/cloudtentacles/override-rules]',
}),
)
router.post(
"/cloudtentacles/override-rules",
'/cloudtentacles/override-rules',
createJsonHandler(
(req) =>
updateAdminCloudtentaclesOverrideRules(
req.body as AdminCloudtentaclesOverrideRuleConfigRouteBody
req.body as AdminCloudtentaclesOverrideRuleConfigRouteBody,
),
{
successMessage: "cloudtentacles 覆盖规则已保存",
errorMessage: "保存 cloudtentacles 覆盖规则失败",
scope: "[admin/platform-config/cloudtentacles/override-rules]",
successMessage: 'cloudtentacles 覆盖规则已保存',
errorMessage: '保存 cloudtentacles 覆盖规则失败',
scope: '[admin/platform-config/cloudtentacles/override-rules]',
audit: (_req, data) => {
const result = data as JsonRecord;
const result = data as JsonRecord
return {
action: "platform_cloudtentacles_override_rules_updated",
targetType: "platform_config",
targetId: "cloudtentacles_override_rules",
action: 'platform_cloudtentacles_override_rules_updated',
targetType: 'platform_config',
targetId: 'cloudtentacles_override_rules',
data: {
filePath: String(result.filePath || "").trim(),
filePath: String(result.filePath || '').trim(),
enabled: result.enabled !== false,
ruleCount: Array.isArray(result.rules) ? result.rules.length : 0,
},
};
},
}
},
},
),
)
);
router.post(
"/cloudtentacles/send-sms-code",
'/cloudtentacles/send-sms-code',
createJsonHandler(
(req) =>
sendAdminCloudtentaclesSmsCode(
req.body as AdminCloudtentaclesSendSmsCodeRouteBody
),
(req) => sendAdminCloudtentaclesSmsCode(req.body as AdminCloudtentaclesSendSmsCodeRouteBody),
{
successMessage: "cloudtentacles 短信验证码已发送",
errorMessage: "cloudtentacles 发送短信验证码失败",
scope: "[admin/platform-config/cloudtentacles/send-sms-code]",
successMessage: 'cloudtentacles 短信验证码已发送',
errorMessage: 'cloudtentacles 发送短信验证码失败',
scope: '[admin/platform-config/cloudtentacles/send-sms-code]',
audit: (req, data) => {
const body = req.body as AdminCloudtentaclesSendSmsCodeRouteBody;
const result = data as JsonRecord;
const body = req.body as AdminCloudtentaclesSendSmsCodeRouteBody
const result = data as JsonRecord
return {
action: "platform_cloudtentacles_send_sms_code",
targetType: "platform_config",
targetId:
String(result.username || body.username || "").trim() ||
"cloudtentacles",
action: 'platform_cloudtentacles_send_sms_code',
targetType: 'platform_config',
targetId: String(result.username || body.username || '').trim() || 'cloudtentacles',
data: {
baseUrl: result.baseUrl || String(body.baseUrl || "").trim(),
phoneMasked: result.phoneMasked || "",
},
};
baseUrl: result.baseUrl || String(body.baseUrl || '').trim(),
phoneMasked: result.phoneMasked || '',
},
}
},
},
),
)
);
router.post(
"/cloudtentacles/test-login",
'/cloudtentacles/test-login',
createJsonHandler(
(req) =>
testAdminCloudtentaclesLogin(
req.body as AdminCloudtentaclesTestLoginRouteBody
),
(req) => testAdminCloudtentaclesLogin(req.body as AdminCloudtentaclesTestLoginRouteBody),
{
successMessage: "cloudtentacles 登录测试成功",
errorMessage: "cloudtentacles 登录测试失败",
scope: "[admin/platform-config/cloudtentacles/test-login]",
successMessage: 'cloudtentacles 登录测试成功',
errorMessage: 'cloudtentacles 登录测试失败',
scope: '[admin/platform-config/cloudtentacles/test-login]',
audit: (req, data) => {
const body = req.body as AdminCloudtentaclesTestLoginRouteBody;
const result = data as JsonRecord;
const body = req.body as AdminCloudtentaclesTestLoginRouteBody
const result = data as JsonRecord
return {
action: "platform_cloudtentacles_test_login",
targetType: "platform_config",
targetId:
String(result.username || body.username || "").trim() ||
"cloudtentacles",
action: 'platform_cloudtentacles_test_login',
targetType: 'platform_config',
targetId: String(result.username || body.username || '').trim() || 'cloudtentacles',
data: {
baseUrl: result.baseUrl || String(body.baseUrl || "").trim(),
baseUrl: result.baseUrl || String(body.baseUrl || '').trim(),
permissionCount: Number(result.session?.permissionCount || 0),
},
};
},
}
},
},
),
)
);
router.post(
"/cloudtentacles/validate-session",
'/cloudtentacles/validate-session',
createJsonHandler(
(req) =>
validateAdminCloudtentaclesSession(
req.body as AdminCloudtentaclesValidateSessionRouteBody
),
validateAdminCloudtentaclesSession(req.body as AdminCloudtentaclesValidateSessionRouteBody),
{
successMessage: "cloudtentacles 会话校验成功",
errorMessage: "cloudtentacles 会话校验失败",
scope: "[admin/platform-config/cloudtentacles/validate-session]",
successMessage: 'cloudtentacles 会话校验成功',
errorMessage: 'cloudtentacles 会话校验失败',
scope: '[admin/platform-config/cloudtentacles/validate-session]',
audit: (_req, data) => {
const result = data as JsonRecord;
const result = data as JsonRecord
return {
action: "platform_cloudtentacles_validate_session",
targetType: "platform_config",
targetId: "cloudtentacles_session",
action: 'platform_cloudtentacles_validate_session',
targetType: 'platform_config',
targetId: 'cloudtentacles_session',
data: {
baseUrl: String(result.baseUrl || "").trim(),
baseUrl: String(result.baseUrl || '').trim(),
permissionCount: Number(result.session?.permissionCount || 0),
},
};
}
},
},
}
)
);
router.post(
"/cloudtentacles/asset",
createJsonHandler(
(req) =>
getAdminCloudtentaclesAsset(
req.body as AdminCloudtentaclesCatalogQueryRouteBody
),
{
successMessage: "cloudtentacles 余额查询成功",
errorMessage: "cloudtentacles 余额查询失败",
scope: "[admin/platform-config/cloudtentacles/asset]",
}
)
);
router.post(
"/cloudtentacles/categories",
'/cloudtentacles/asset',
createJsonHandler(
(req) =>
getAdminCloudtentaclesCategories(
req.body as AdminCloudtentaclesCatalogQueryRouteBody
(req) => getAdminCloudtentaclesAsset(req.body as AdminCloudtentaclesCatalogQueryRouteBody),
{
successMessage: 'cloudtentacles 余额查询成功',
errorMessage: 'cloudtentacles 余额查询失败',
scope: '[admin/platform-config/cloudtentacles/asset]',
},
),
{
successMessage: "cloudtentacles 分类查询成功",
errorMessage: "cloudtentacles 分类查询失败",
scope: "[admin/platform-config/cloudtentacles/categories]",
}
)
);
router.post(
"/cloudtentacles/sku/list",
'/cloudtentacles/categories',
createJsonHandler(
(req) =>
getAdminCloudtentaclesSkuList(
req.body as AdminCloudtentaclesCatalogQueryRouteBody
(req) => getAdminCloudtentaclesCategories(req.body as AdminCloudtentaclesCatalogQueryRouteBody),
{
successMessage: 'cloudtentacles 分类查询成功',
errorMessage: 'cloudtentacles 分类查询失败',
scope: '[admin/platform-config/cloudtentacles/categories]',
},
),
{
successMessage: "cloudtentacles SKU 列表查询成功",
errorMessage: "cloudtentacles SKU 列表查询失败",
scope: "[admin/platform-config/cloudtentacles/sku/list]",
}
)
);
router.post(
"/cloudtentacles/sku/buy",
'/cloudtentacles/sku/list',
createJsonHandler(
(req) =>
buyAdminCloudtentaclesSku(req.body as AdminCloudtentaclesSkuBuyRouteBody),
(req) => getAdminCloudtentaclesSkuList(req.body as AdminCloudtentaclesCatalogQueryRouteBody),
{
successMessage: "cloudtentacles SKU 购买成功",
errorMessage: "cloudtentacles SKU 购买失败",
scope: "[admin/platform-config/cloudtentacles/sku/buy]",
}
)
);
router.post(
"/cloudtentacles/sku/use",
createJsonHandler(
(req) =>
useAdminCloudtentaclesSku(req.body as AdminCloudtentaclesSkuUseRouteBody),
{
successMessage: "cloudtentacles 发货成功",
errorMessage: "cloudtentacles 发货失败",
scope: "[admin/platform-config/cloudtentacles/sku/use]",
}
)
);
router.post(
"/cloudtentacles/knapsack",
createJsonHandler(
(req) =>
getAdminCloudtentaclesKnapsack(
req.body as AdminCloudtentaclesCatalogQueryRouteBody
successMessage: 'cloudtentacles SKU 列表查询成功',
errorMessage: 'cloudtentacles SKU 列表查询失败',
scope: '[admin/platform-config/cloudtentacles/sku/list]',
},
),
{
successMessage: "cloudtentacles 背包查询成功",
errorMessage: "cloudtentacles 背包查询失败",
scope: "[admin/platform-config/cloudtentacles/knapsack]",
}
)
);
router.post(
"/cloudtentacles/vn/list",
'/cloudtentacles/sku/buy',
createJsonHandler(
(req) => buyAdminCloudtentaclesSku(req.body as AdminCloudtentaclesSkuBuyRouteBody),
{
successMessage: 'cloudtentacles SKU 购买成功',
errorMessage: 'cloudtentacles SKU 购买失败',
scope: '[admin/platform-config/cloudtentacles/sku/buy]',
},
),
)
router.post(
'/cloudtentacles/sku/use',
createJsonHandler(
(req) => useAdminCloudtentaclesSku(req.body as AdminCloudtentaclesSkuUseRouteBody),
{
successMessage: 'cloudtentacles 发货成功',
errorMessage: 'cloudtentacles 发货失败',
scope: '[admin/platform-config/cloudtentacles/sku/use]',
},
),
)
router.post(
'/cloudtentacles/knapsack',
createJsonHandler(
(req) => getAdminCloudtentaclesKnapsack(req.body as AdminCloudtentaclesCatalogQueryRouteBody),
{
successMessage: 'cloudtentacles 背包查询成功',
errorMessage: 'cloudtentacles 背包查询失败',
scope: '[admin/platform-config/cloudtentacles/knapsack]',
},
),
)
router.post(
'/cloudtentacles/vn/list',
createJsonHandler(
(req) =>
listAdminCloudtentaclesVirtualNumbers(
req.body as AdminCloudtentaclesVirtualNumberRouteBody
),
listAdminCloudtentaclesVirtualNumbers(req.body as AdminCloudtentaclesVirtualNumberRouteBody),
{
successMessage: "cloudtentacles 虚拟号列表查询成功",
errorMessage: "cloudtentacles 虚拟号列表查询失败",
scope: "[admin/platform-config/cloudtentacles/vn/list]",
}
successMessage: 'cloudtentacles 虚拟号列表查询成功',
errorMessage: 'cloudtentacles 虚拟号列表查询失败',
scope: '[admin/platform-config/cloudtentacles/vn/list]',
},
),
)
);
router.post(
"/cloudtentacles/vn/appoint",
'/cloudtentacles/vn/appoint',
createJsonHandler(
(req) =>
appointAdminCloudtentaclesVirtualNumber(
req.body as AdminCloudtentaclesVirtualNumberRouteBody
req.body as AdminCloudtentaclesVirtualNumberRouteBody,
),
{
successMessage: "cloudtentacles 虚拟号申请成功",
errorMessage: "cloudtentacles 虚拟号申请失败",
scope: "[admin/platform-config/cloudtentacles/vn/appoint]",
}
successMessage: 'cloudtentacles 虚拟号申请成功',
errorMessage: 'cloudtentacles 虚拟号申请失败',
scope: '[admin/platform-config/cloudtentacles/vn/appoint]',
},
),
)
);
router.post(
"/cloudtentacles/vn/generate-login-code",
'/cloudtentacles/vn/generate-login-code',
createJsonHandler(
(req) =>
generateAdminCloudtentaclesLoginCode(
req.body as AdminCloudtentaclesVirtualNumberRouteBody
),
generateAdminCloudtentaclesLoginCode(req.body as AdminCloudtentaclesVirtualNumberRouteBody),
{
successMessage: "cloudtentacles 登录码生成成功",
errorMessage: "cloudtentacles 登录码生成失败",
scope: "[admin/platform-config/cloudtentacles/vn/generate-login-code]",
}
successMessage: 'cloudtentacles 登录码生成成功',
errorMessage: 'cloudtentacles 登录码生成失败',
scope: '[admin/platform-config/cloudtentacles/vn/generate-login-code]',
},
),
)
);
router.post(
"/cloudtentacles/vn/fetch-code",
'/cloudtentacles/vn/fetch-code',
createJsonHandler(
(req) =>
fetchAdminCloudtentaclesVirtualNumberCode(
req.body as AdminCloudtentaclesVirtualNumberRouteBody
req.body as AdminCloudtentaclesVirtualNumberRouteBody,
),
{
successMessage: "cloudtentacles 验证码获取成功",
errorMessage: "cloudtentacles 验证码获取失败",
scope: "[admin/platform-config/cloudtentacles/vn/fetch-code]",
}
successMessage: 'cloudtentacles 验证码获取成功',
errorMessage: 'cloudtentacles 验证码获取失败',
scope: '[admin/platform-config/cloudtentacles/vn/fetch-code]',
},
),
)
);
router.post(
"/cloudtentacles/vn/verify-code",
'/cloudtentacles/vn/verify-code',
createJsonHandler(
(req) =>
verifyAdminCloudtentaclesLoginCode(
req.body as AdminCloudtentaclesVirtualNumberRouteBody
),
verifyAdminCloudtentaclesLoginCode(req.body as AdminCloudtentaclesVirtualNumberRouteBody),
{
successMessage: "cloudtentacles 登录码校验成功",
errorMessage: "cloudtentacles 登录码校验失败",
scope: "[admin/platform-config/cloudtentacles/vn/verify-code]",
}
successMessage: 'cloudtentacles 登录码校验成功',
errorMessage: 'cloudtentacles 登录码校验失败',
scope: '[admin/platform-config/cloudtentacles/vn/verify-code]',
},
),
)
);
router.post(
"/cloudtentacles/vn/bind-url",
'/cloudtentacles/vn/bind-url',
createJsonHandler(
(req) =>
getAdminCloudtentaclesBindUrl(
req.body as AdminCloudtentaclesVirtualNumberRouteBody
),
(req) => getAdminCloudtentaclesBindUrl(req.body as AdminCloudtentaclesVirtualNumberRouteBody),
{
successMessage: "cloudtentacles 兑换链接获取成功",
errorMessage: "cloudtentacles 兑换链接获取失败",
scope: "[admin/platform-config/cloudtentacles/vn/bind-url]",
}
successMessage: 'cloudtentacles 兑换链接获取成功',
errorMessage: 'cloudtentacles 兑换链接获取失败',
scope: '[admin/platform-config/cloudtentacles/vn/bind-url]',
},
),
)
);
router.post(
"/cloudtentacles/vn/back",
'/cloudtentacles/vn/back',
createJsonHandler(
(req) =>
backAdminCloudtentaclesVirtualNumber(
req.body as AdminCloudtentaclesVirtualNumberRouteBody
),
backAdminCloudtentaclesVirtualNumber(req.body as AdminCloudtentaclesVirtualNumberRouteBody),
{
successMessage: "cloudtentacles 号码退还成功",
errorMessage: "cloudtentacles 号码退还失败",
scope: "[admin/platform-config/cloudtentacles/vn/back]",
}
successMessage: 'cloudtentacles 号码退还成功',
errorMessage: 'cloudtentacles 号码退还失败',
scope: '[admin/platform-config/cloudtentacles/vn/back]',
},
),
)
);
router.post(
"/cloudtentacles/debug/full-flow",
'/cloudtentacles/debug/full-flow',
createJsonHandler(
(req) =>
runAdminCloudtentaclesFullFlow(
req.body as AdminCloudtentaclesFullFlowRouteBody
),
(req) => runAdminCloudtentaclesFullFlow(req.body as AdminCloudtentaclesFullFlowRouteBody),
{
successMessage: "cloudtentacles 完整调试流程执行成功",
errorMessage: "cloudtentacles 完整调试流程执行失败",
scope: "[admin/platform-config/cloudtentacles/debug/full-flow]",
}
successMessage: 'cloudtentacles 完整调试流程执行成功',
errorMessage: 'cloudtentacles 完整调试流程执行失败',
scope: '[admin/platform-config/cloudtentacles/debug/full-flow]',
},
),
)
);
export default router;
export default router
@@ -21,9 +21,7 @@ router.get(
router.post(
'/fulfillment-routing',
createJsonHandler(
(req) => updateAdminFulfillmentRoutingConfig(req.body as JsonRecord),
{
createJsonHandler((req) => updateAdminFulfillmentRoutingConfig(req.body as JsonRecord), {
successMessage: '履约路由配置已保存',
errorMessage: '保存履约路由配置失败',
scope: '[admin/platform-config/fulfillment-routing]',
@@ -40,20 +38,16 @@ router.post(
},
}
},
},
),
}),
)
router.post(
'/fulfillment-routing/preview',
createJsonHandler(
(req) => previewAdminFulfillmentRouting(req.body as JsonRecord),
{
createJsonHandler((req) => previewAdminFulfillmentRouting(req.body as JsonRecord), {
successMessage: 'ok',
errorMessage: '预览履约路由失败',
scope: '[admin/platform-config/fulfillment-routing/preview]',
},
),
}),
)
export default router
@@ -1,4 +1,4 @@
import { Router } from "express";
import { Router } from 'express'
import {
createAdminKuaishouFeifeiTestOrder,
@@ -8,137 +8,117 @@ import {
queryAdminKuaishouFeifeiTestOrder,
syncAdminKuaishouFeifeiProductRules,
updateAdminKuaishouFeifeiConfig,
} from "../../../services/admin/platform-config/kuaishou-feifei-service.js";
import { createJsonHandler } from "../session.js";
import type { JsonRecord } from "../../../types/json.js";
} from '../../../services/admin/platform-config/kuaishou-feifei-service.js'
import { createJsonHandler } from '../session.js'
import type { JsonRecord } from '../../../types/json.js'
const router = Router();
const router = Router()
router.get(
"/kuaishou-feifei",
'/kuaishou-feifei',
createJsonHandler(() => getAdminKuaishouFeifeiConfig(), {
successMessage: "ok",
errorMessage: "读取 kuaishou-feifei 配置失败",
scope: "[admin/platform-config/kuaishou-feifei]",
})
);
successMessage: 'ok',
errorMessage: '读取 kuaishou-feifei 配置失败',
scope: '[admin/platform-config/kuaishou-feifei]',
}),
)
router.post(
"/kuaishou-feifei",
createJsonHandler(
(req) => updateAdminKuaishouFeifeiConfig(req.body as JsonRecord),
{
successMessage: "kuaishou-feifei 配置已保存",
errorMessage: "保存 kuaishou-feifei 配置失败",
scope: "[admin/platform-config/kuaishou-feifei]",
'/kuaishou-feifei',
createJsonHandler((req) => updateAdminKuaishouFeifeiConfig(req.body as JsonRecord), {
successMessage: 'kuaishou-feifei 配置已保存',
errorMessage: '保存 kuaishou-feifei 配置失败',
scope: '[admin/platform-config/kuaishou-feifei]',
audit: (_req, data) => {
const result = data as JsonRecord;
const source = result.source as JsonRecord | undefined;
const result = data as JsonRecord
const source = result.source as JsonRecord | undefined
return {
action: "platform_kuaishou_feifei_config_updated",
targetType: "platform_config",
targetId: "kuaishou_feifei",
action: 'platform_kuaishou_feifei_config_updated',
targetType: 'platform_config',
targetId: 'kuaishou_feifei',
data: {
filePath: String(result.filePath || "").trim(),
filePath: String(result.filePath || '').trim(),
enabled: source?.enabled !== false,
ruleCount: Array.isArray(source?.productRules)
? source.productRules.length
: 0,
},
};
ruleCount: Array.isArray(source?.productRules) ? source.productRules.length : 0,
},
}
},
}),
)
);
router.post(
"/kuaishou-feifei/match",
createJsonHandler(
(req) => matchAdminKuaishouFeifeiProduct(req.body as JsonRecord),
{
successMessage: "ok",
errorMessage: "匹配 kuaishou-feifei 商品失败",
scope: "[admin/platform-config/kuaishou-feifei/match]",
}
'/kuaishou-feifei/match',
createJsonHandler((req) => matchAdminKuaishouFeifeiProduct(req.body as JsonRecord), {
successMessage: 'ok',
errorMessage: '匹配 kuaishou-feifei 商品失败',
scope: '[admin/platform-config/kuaishou-feifei/match]',
}),
)
);
router.post(
"/kuaishou-feifei/products",
createJsonHandler(
(req) => listAdminKuaishouFeifeiProducts(req.body as JsonRecord),
{
successMessage: "ok",
errorMessage: "查询 kuaishou-feifei 商品失败",
scope: "[admin/platform-config/kuaishou-feifei/products]",
}
'/kuaishou-feifei/products',
createJsonHandler((req) => listAdminKuaishouFeifeiProducts(req.body as JsonRecord), {
successMessage: 'ok',
errorMessage: '查询 kuaishou-feifei 商品失败',
scope: '[admin/platform-config/kuaishou-feifei/products]',
}),
)
);
router.post(
"/kuaishou-feifei/sync-products",
createJsonHandler(
(req) => syncAdminKuaishouFeifeiProductRules(req.body as JsonRecord),
{
successMessage: "kuaishou-feifei 商品映射已同步",
errorMessage: "同步 kuaishou-feifei 商品映射失败",
scope: "[admin/platform-config/kuaishou-feifei/sync-products]",
'/kuaishou-feifei/sync-products',
createJsonHandler((req) => syncAdminKuaishouFeifeiProductRules(req.body as JsonRecord), {
successMessage: 'kuaishou-feifei 商品映射已同步',
errorMessage: '同步 kuaishou-feifei 商品映射失败',
scope: '[admin/platform-config/kuaishou-feifei/sync-products]',
audit: (_req, data) => {
const result = data as JsonRecord;
const sync = result.sync as JsonRecord | undefined;
const result = data as JsonRecord
const sync = result.sync as JsonRecord | undefined
return {
action: "platform_kuaishou_feifei_products_synced",
targetType: "platform_config",
targetId: "kuaishou_feifei",
action: 'platform_kuaishou_feifei_products_synced',
targetType: 'platform_config',
targetId: 'kuaishou_feifei',
data: {
productCount: Number(sync?.productCount || 0) || 0,
ruleCount: Number(sync?.ruleCount || 0) || 0,
status: String(sync?.status || "").trim(),
},
};
status: String(sync?.status || '').trim(),
},
}
},
}),
)
);
router.post(
"/kuaishou-feifei/test-order",
createJsonHandler(
(req) => createAdminKuaishouFeifeiTestOrder(req.body as JsonRecord),
{
successMessage: "kuaishou-feifei 测试单已创建",
errorMessage: "创建 kuaishou-feifei 测试单失败",
scope: "[admin/platform-config/kuaishou-feifei/test-order]",
'/kuaishou-feifei/test-order',
createJsonHandler((req) => createAdminKuaishouFeifeiTestOrder(req.body as JsonRecord), {
successMessage: 'kuaishou-feifei 测试单已创建',
errorMessage: '创建 kuaishou-feifei 测试单失败',
scope: '[admin/platform-config/kuaishou-feifei/test-order]',
audit: (_req, data) => {
const result = data as JsonRecord;
const result = data as JsonRecord
return {
action: "platform_kuaishou_feifei_test_order_created",
targetType: "platform_config",
targetId: "kuaishou_feifei",
action: 'platform_kuaishou_feifei_test_order_created',
targetType: 'platform_config',
targetId: 'kuaishou_feifei',
data: {
orderNo: String(result.orderNo || "").trim(),
platformOrderNo: String(result.platformOrderNo || "").trim(),
productCode: String(result.productCode || "").trim(),
orderNo: String(result.orderNo || '').trim(),
platformOrderNo: String(result.platformOrderNo || '').trim(),
productCode: String(result.productCode || '').trim(),
rechargeStatus: Number(result.rechargeStatus || 0) || 0,
},
};
},
}
},
}),
)
);
router.post(
"/kuaishou-feifei/query-order",
createJsonHandler(
(req) => queryAdminKuaishouFeifeiTestOrder(req.body as JsonRecord),
{
successMessage: "ok",
errorMessage: "查询 kuaishou-feifei 订单失败",
scope: "[admin/platform-config/kuaishou-feifei/query-order]",
}
'/kuaishou-feifei/query-order',
createJsonHandler((req) => queryAdminKuaishouFeifeiTestOrder(req.body as JsonRecord), {
successMessage: 'ok',
errorMessage: '查询 kuaishou-feifei 订单失败',
scope: '[admin/platform-config/kuaishou-feifei/query-order]',
}),
)
);
export default router;
export default router
@@ -1,77 +1,75 @@
import { Router } from "express";
import { Router } from 'express'
import {
failAdminNinetyoneOrder,
getAdminNinetyoneOrders,
retryAdminNinetyoneOrder,
} from "../../../services/admin/platform-config/ninetyone-service.js";
import type { AdminEntityRouteParams } from "../../../types/admin/route-inputs.js";
import { createJsonHandler } from "../session.js";
import type { JsonRecord } from "../../../types/json.js";
} from '../../../services/admin/platform-config/ninetyone-service.js'
import type { AdminEntityRouteParams } from '../../../types/admin/route-inputs.js'
import { createJsonHandler } from '../session.js'
import type { JsonRecord } from '../../../types/json.js'
const router = Router();
const router = Router()
router.get(
"/ninetyone/orders",
'/ninetyone/orders',
createJsonHandler((req) => getAdminNinetyoneOrders(req.query), {
successMessage: "ok",
errorMessage: "读取 91卡券订单失败",
scope: "[admin/platform-config/ninetyone/orders]",
})
);
successMessage: 'ok',
errorMessage: '读取 91卡券订单失败',
scope: '[admin/platform-config/ninetyone/orders]',
}),
)
router.post(
"/ninetyone/orders/:id/retry",
'/ninetyone/orders/:id/retry',
createJsonHandler(
(req) => retryAdminNinetyoneOrder(String((req.params as AdminEntityRouteParams).id || "")),
(req) => retryAdminNinetyoneOrder(String((req.params as AdminEntityRouteParams).id || '')),
{
successMessage: "91卡券订单已重试",
errorMessage: "重试 91卡券订单失败",
scope: "[admin/platform-config/ninetyone/orders/:id/retry]",
successMessage: '91卡券订单已重试',
errorMessage: '重试 91卡券订单失败',
scope: '[admin/platform-config/ninetyone/orders/:id/retry]',
audit: (_req, data) => {
const result = data as JsonRecord;
const result = data as JsonRecord
return {
action: "platform_ninetyone_order_retried",
targetType: "order",
targetId: String(result.orderId || "").trim(),
action: 'platform_ninetyone_order_retried',
targetType: 'order',
targetId: String(result.orderId || '').trim(),
data: {
orderNo: String(result.orderNo || "").trim(),
orderNo: String(result.orderNo || '').trim(),
taskCount: Number(result.taskCount || 0),
},
};
},
}
},
},
),
)
);
router.post(
"/ninetyone/orders/:id/fail",
'/ninetyone/orders/:id/fail',
createJsonHandler(
(req) =>
failAdminNinetyoneOrder(
String((req.params as AdminEntityRouteParams).id || ""),
req.body as { reason?: string }
String((req.params as AdminEntityRouteParams).id || ''),
req.body as { reason?: string },
),
{
successMessage: "91卡券订单已标记失败",
errorMessage: "标记 91卡券订单失败",
scope: "[admin/platform-config/ninetyone/orders/:id/fail]",
successMessage: '91卡券订单已标记失败',
errorMessage: '标记 91卡券订单失败',
scope: '[admin/platform-config/ninetyone/orders/:id/fail]',
audit: (req, data) => {
const result = data as JsonRecord;
const result = data as JsonRecord
return {
action: "platform_ninetyone_order_failed",
targetType: "order",
targetId: String(result.orderId || "").trim(),
action: 'platform_ninetyone_order_failed',
targetType: 'order',
targetId: String(result.orderId || '').trim(),
data: {
orderNo: String(result.orderNo || "").trim(),
reason: String(
(req.body as { reason?: string }).reason || ""
).trim(),
},
};
orderNo: String(result.orderNo || '').trim(),
reason: String((req.body as { reason?: string }).reason || '').trim(),
},
}
},
},
),
)
);
export default router;
export default router
@@ -1,4 +1,4 @@
import { Router } from "express";
import { Router } from 'express'
import {
getAdminNotificationConfig,
@@ -7,152 +7,132 @@ import {
testAdminNotification,
updateAdminNotificationConfig,
updateAdminScheduledJobsConfig,
} from "../../../services/admin/platform-config/notification-service.js";
} from '../../../services/admin/platform-config/notification-service.js'
import type {
AdminEntityRouteParams,
AdminNotificationConfigRouteBody,
AdminNotificationTestRouteBody,
AdminScheduledJobsConfigRouteBody,
} from "../../../types/admin/route-inputs.js";
import { createJsonHandler } from "../session.js";
import type { JsonRecord } from "../../../types/json.js";
} from '../../../types/admin/route-inputs.js'
import { createJsonHandler } from '../session.js'
import type { JsonRecord } from '../../../types/json.js'
const router = Router();
const router = Router()
router.get(
"/notifications",
'/notifications',
createJsonHandler(() => getAdminNotificationConfig(), {
successMessage: "ok",
errorMessage: "读取内部通知配置失败",
scope: "[admin/platform-config/notifications]",
})
);
successMessage: 'ok',
errorMessage: '读取内部通知配置失败',
scope: '[admin/platform-config/notifications]',
}),
)
router.post(
"/notifications",
'/notifications',
createJsonHandler(
(req) =>
updateAdminNotificationConfig(
req.body as AdminNotificationConfigRouteBody
),
(req) => updateAdminNotificationConfig(req.body as AdminNotificationConfigRouteBody),
{
successMessage: "内部通知配置已保存",
errorMessage: "保存内部通知配置失败",
scope: "[admin/platform-config/notifications]",
successMessage: '内部通知配置已保存',
errorMessage: '保存内部通知配置失败',
scope: '[admin/platform-config/notifications]',
audit: (_req, data) => {
const result = data as JsonRecord;
const result = data as JsonRecord
return {
action: "platform_notification_config_updated",
targetType: "platform_config",
targetId: "notifications",
action: 'platform_notification_config_updated',
targetType: 'platform_config',
targetId: 'notifications',
data: {
filePath: String(result.filePath || "").trim(),
filePath: String(result.filePath || '').trim(),
enabled: Boolean(result.source?.enabled),
barkRecipientCount: Array.isArray(
result.source?.channels?.bark?.recipients
)
barkRecipientCount: Array.isArray(result.source?.channels?.bark?.recipients)
? result.source.channels.bark.recipients.length
: 0,
wpushRecipientCount: Array.isArray(
result.source?.channels?.wpush?.recipients
)
wpushRecipientCount: Array.isArray(result.source?.channels?.wpush?.recipients)
? result.source.channels.wpush.recipients.length
: 0,
},
};
},
}
},
},
),
)
);
router.post(
"/notifications/test",
createJsonHandler(
(req) => testAdminNotification(req.body as AdminNotificationTestRouteBody),
{
successMessage: "内部通知测试已执行",
errorMessage: "内部通知测试失败",
scope: "[admin/platform-config/notifications/test]",
'/notifications/test',
createJsonHandler((req) => testAdminNotification(req.body as AdminNotificationTestRouteBody), {
successMessage: '内部通知测试已执行',
errorMessage: '内部通知测试失败',
scope: '[admin/platform-config/notifications/test]',
audit: (_req, data) => {
const result = data as JsonRecord;
const result = data as JsonRecord
return {
action: "platform_notification_test_sent",
targetType: "platform_config",
targetId: "notifications",
action: 'platform_notification_test_sent',
targetType: 'platform_config',
targetId: 'notifications',
data: {
channel: String(result.channel || "").trim(),
channel: String(result.channel || '').trim(),
successCount: Number(result.successCount || 0),
failedCount: Number(result.failedCount || 0),
},
};
},
}
},
}),
)
);
router.get(
"/scheduled-jobs",
'/scheduled-jobs',
createJsonHandler(() => getAdminScheduledJobsConfig(), {
successMessage: "ok",
errorMessage: "读取定时任务配置失败",
scope: "[admin/platform-config/scheduled-jobs]",
})
);
successMessage: 'ok',
errorMessage: '读取定时任务配置失败',
scope: '[admin/platform-config/scheduled-jobs]',
}),
)
router.post(
"/scheduled-jobs",
'/scheduled-jobs',
createJsonHandler(
(req) =>
updateAdminScheduledJobsConfig(
req.body as AdminScheduledJobsConfigRouteBody
),
(req) => updateAdminScheduledJobsConfig(req.body as AdminScheduledJobsConfigRouteBody),
{
successMessage: "定时任务配置已保存",
errorMessage: "保存定时任务配置失败",
scope: "[admin/platform-config/scheduled-jobs]",
successMessage: '定时任务配置已保存',
errorMessage: '保存定时任务配置失败',
scope: '[admin/platform-config/scheduled-jobs]',
audit: (_req, data) => {
const result = data as JsonRecord;
const result = data as JsonRecord
return {
action: "platform_scheduled_jobs_updated",
targetType: "platform_config",
targetId: "scheduled_jobs",
action: 'platform_scheduled_jobs_updated',
targetType: 'platform_config',
targetId: 'scheduled_jobs',
data: {
filePath: String(result.filePath || "").trim(),
filePath: String(result.filePath || '').trim(),
enabled: Boolean(result.source?.enabled),
jobCount: Array.isArray(result.source?.jobs)
? result.source.jobs.length
: 0,
},
};
jobCount: Array.isArray(result.source?.jobs) ? result.source.jobs.length : 0,
},
}
},
},
),
)
);
router.post(
"/scheduled-jobs/:id/run",
createJsonHandler(
(req) => runAdminScheduledJobNow((req.params as AdminEntityRouteParams).id),
{
successMessage: "定时任务已执行",
errorMessage: "执行定时任务失败",
scope: "[admin/platform-config/scheduled-jobs/:id/run]",
'/scheduled-jobs/:id/run',
createJsonHandler((req) => runAdminScheduledJobNow((req.params as AdminEntityRouteParams).id), {
successMessage: '定时任务已执行',
errorMessage: '执行定时任务失败',
scope: '[admin/platform-config/scheduled-jobs/:id/run]',
audit: (req, data) => {
const result = data as JsonRecord;
const result = data as JsonRecord
return {
action: "platform_scheduled_job_run",
targetType: "platform_config",
targetId: String(
(req.params as AdminEntityRouteParams).id || ""
).trim(),
action: 'platform_scheduled_job_run',
targetType: 'platform_config',
targetId: String((req.params as AdminEntityRouteParams).id || '').trim(),
data: {
status: String(result.result?.status || "").trim(),
message: String(result.result?.message || "").trim(),
},
};
status: String(result.result?.status || '').trim(),
message: String(result.result?.message || '').trim(),
},
}
},
}),
)
);
export default router;
export default router
+9 -2
View File
@@ -1,5 +1,8 @@
import type { Request, Response, NextFunction } from 'express'
import { requireAdminRole, verifyAdminSessionToken } from '../../services/admin/admin-auth-service.js'
import {
requireAdminRole,
verifyAdminSessionToken,
} from '../../services/admin/admin-auth-service.js'
import { writeAdminAuditLog } from '../../services/admin/admin-audit-service.js'
import { buildSuccessPayload, createHttpError, sendRouteError } from '../../utils/http.js'
import { logWarn } from '../../utils/logger.js'
@@ -53,7 +56,11 @@ export function createFileHandler(
}
}
export async function requireAdminSession(req: Request, res: Response, next: NextFunction): Promise<void> {
export async function requireAdminSession(
req: Request,
res: Response,
next: NextFunction,
): Promise<void> {
try {
req.adminSession = await verifyAdminSessionToken(extractBearerToken(req))
next()
+2 -7
View File
@@ -105,11 +105,7 @@ router.post(
'/tasks/:taskId/kuaishou-cloud/dispatch',
requireAdminRoles(['admin', 'operator']),
createJsonHandler(
(req) =>
dispatchAdminTaskKuaishouCloudFulfillment(
getTaskId(req),
req.adminSession || null,
),
(req) => dispatchAdminTaskKuaishouCloudFulfillment(getTaskId(req), req.adminSession || null),
{
successMessage: '已完成绑定确认并发货',
errorMessage: '执行发货失败',
@@ -152,8 +148,7 @@ router.post(
'/tasks/:taskId/kuaishou-industry/resend-code',
requireAdminRoles(['admin', 'operator', 'support']),
createJsonHandler(
(req) =>
resendAdminTaskKuaishouIndustryVoucherCode(getTaskId(req), req.adminSession || null),
(req) => resendAdminTaskKuaishouIndustryVoucherCode(getTaskId(req), req.adminSession || null),
{
successMessage: '电子凭证发码回调已重发',
errorMessage: '重发电子凭证发码回调失败',
+37 -18
View File
@@ -22,18 +22,18 @@ const router = Router()
router.use('/users', requireAdminRoles(['admin']))
router.get('/users', createJsonHandler(
(req) => getAdminUserList(req.query),
{
router.get(
'/users',
createJsonHandler((req) => getAdminUserList(req.query), {
successMessage: 'ok',
errorMessage: '读取后台用户列表失败',
scope: '[admin/users]',
},
))
}),
)
router.post('/users', createJsonHandler(
(req) => createManagedAdminUser(req.body),
{
router.post(
'/users',
createJsonHandler((req) => createManagedAdminUser(req.body), {
successMessage: '后台用户已创建',
errorMessage: '创建后台用户失败',
scope: '[admin/users:create]',
@@ -50,11 +50,18 @@ router.post('/users', createJsonHandler(
},
}
},
},
))
}),
)
router.post('/users/:userId/role', createJsonHandler(
(req) => updateManagedAdminUserRole(String(req.params.userId || ''), req.body, getRequiredAdminSession(req)),
router.post(
'/users/:userId/role',
createJsonHandler(
(req) =>
updateManagedAdminUserRole(
String(req.params.userId || ''),
req.body,
getRequiredAdminSession(req),
),
{
successMessage: '用户角色已更新',
errorMessage: '更新用户角色失败',
@@ -72,10 +79,18 @@ router.post('/users/:userId/role', createJsonHandler(
}
},
},
))
),
)
router.post('/users/:userId/status', createJsonHandler(
(req) => updateManagedAdminUserStatus(String(req.params.userId || ''), req.body, getRequiredAdminSession(req)),
router.post(
'/users/:userId/status',
createJsonHandler(
(req) =>
updateManagedAdminUserStatus(
String(req.params.userId || ''),
req.body,
getRequiredAdminSession(req),
),
{
successMessage: '用户状态已更新',
errorMessage: '更新用户状态失败',
@@ -93,9 +108,12 @@ router.post('/users/:userId/status', createJsonHandler(
}
},
},
))
),
)
router.post('/users/:userId/reset-password', createJsonHandler(
router.post(
'/users/:userId/reset-password',
createJsonHandler(
(req) => resetManagedAdminUserPassword(String(req.params.userId || ''), req.body),
{
successMessage: '用户密码已重置',
@@ -113,6 +131,7 @@ router.post('/users/:userId/reset-password', createJsonHandler(
}
},
},
))
),
)
export default router
@@ -266,8 +266,7 @@ router.post(
'/worker-platform/finance-requests/:requestId/review',
requireAdminRoles(['admin', 'operator']),
createJsonHandler(
(req) =>
reviewAdminWorkerFinanceRequest(String(req.params.requestId || ''), req.body || {}),
(req) => reviewAdminWorkerFinanceRequest(String(req.params.requestId || ''), req.body || {}),
{
successMessage: '资金申请已处理',
errorMessage: '处理资金申请失败',
@@ -387,7 +386,9 @@ router.post(
router.post(
'/worker-platform/orders/:workOrderId/cancel',
requireAdminRoles(['admin', 'operator', 'support']),
createJsonHandler((req) => cancelAdminWorkOrder(String(req.params.workOrderId || ''), req.body || {}), {
createJsonHandler(
(req) => cancelAdminWorkOrder(String(req.params.workOrderId || ''), req.body || {}),
{
successMessage: '撤单处理完成,打手额度已恢复',
errorMessage: '撤单失败',
scope: '[admin/worker-platform/orders/:workOrderId/cancel]',
@@ -400,7 +401,8 @@ router.post(
targetId: String(req.params.workOrderId || ''),
data: data && typeof data === 'object' ? (data as Record<string, unknown>) : {},
}),
}),
},
),
)
router.delete(
@@ -576,10 +578,7 @@ router.post(
requireAdminRoles(['admin', 'operator']),
createJsonHandler(
(req) =>
deductAdminWorkOrderPendingDeposit(
String(req.params.workOrderId || ''),
req.body || {},
),
deductAdminWorkOrderPendingDeposit(String(req.params.workOrderId || ''), req.body || {}),
{
successMessage: '待解冻押金已扣减',
errorMessage: '扣减待解冻押金失败',
+7 -2
View File
@@ -44,11 +44,16 @@ router.post('/', notifyRateLimit, async (req, res) => {
})
res.status(200).json({ code: 0 })
} catch (error) {
logIntegration('[affiliate-dash/notify]', '订单回调处理失败', {
logIntegration(
'[affiliate-dash/notify]',
'订单回调处理失败',
{
requestId,
durationMs: Date.now() - startedAt,
error,
}, { level: 'error' })
},
{ level: 'error' },
)
sendRouteError(res, error, 'affiliate-dash 回调处理失败', '[affiliate-dash/notify]')
}
})
+6 -3
View File
@@ -42,18 +42,21 @@ router.post(
'/upload',
collectRateLimit,
uploadSingleFile,
createRouteHandler((req) => {
createRouteHandler(
(req) => {
return uploadFileAsset({
file: req.file,
scene: req.body?.scene || 'collect-material',
uploaderType: 'collect',
uploaderId: String(req.body?.orderNo || 'anonymous'),
})
}, {
},
{
successMessage: '文件已上传',
errorMessage: '文件上传失败',
scope: '[collect/upload]',
}),
},
),
)
router.use((req, res) => {
+7 -2
View File
@@ -45,11 +45,16 @@ router.post('/notify', notifyRateLimit, async (req, res) => {
})
res.status(200).json({ code: 0 })
} catch (error) {
logIntegration('[kuaishou-feifei/notify]', '订单通知处理失败', {
logIntegration(
'[kuaishou-feifei/notify]',
'订单通知处理失败',
{
requestId,
durationMs: Date.now() - startedAt,
error,
}, { level: 'error' })
},
{ level: 'error' },
)
sendRouteError(res, error, 'kuaishou-feifei 通知处理失败', '[kuaishou-feifei/notify]')
}
})
+28 -8
View File
@@ -45,11 +45,16 @@ router.post('/send-code', industryRateLimit, async (req, res) => {
res.status(200).json(result)
} catch (error) {
const message = error instanceof Error ? error.message : '系统异常'
logIntegration('[kuaishou-industry/send-code]', '发码处理失败', {
logIntegration(
'[kuaishou-industry/send-code]',
'发码处理失败',
{
requestId,
durationMs: Date.now() - startedAt,
error,
}, { level: 'error' })
},
{ level: 'error' },
)
res.status(200).json(buildIndustryErrorResponse(4010003, message))
}
})
@@ -78,11 +83,16 @@ router.post('/destroy-code', industryRateLimit, async (req, res) => {
res.status(200).json(result)
} catch (error) {
const message = error instanceof Error ? error.message : '系统异常'
logIntegration('[kuaishou-industry/destroy-code]', '销毁处理失败', {
logIntegration(
'[kuaishou-industry/destroy-code]',
'销毁处理失败',
{
requestId,
durationMs: Date.now() - startedAt,
error,
}, { level: 'error' })
},
{ level: 'error' },
)
res.status(200).json(buildIndustryErrorResponse(4010003, message))
}
})
@@ -111,11 +121,16 @@ router.post('/query-code', industryRateLimit, async (req, res) => {
res.status(200).json(result)
} catch (error) {
const message = error instanceof Error ? error.message : '系统异常'
logIntegration('[kuaishou-industry/query-code]', '查询处理失败', {
logIntegration(
'[kuaishou-industry/query-code]',
'查询处理失败',
{
requestId,
durationMs: Date.now() - startedAt,
error,
}, { level: 'error' })
},
{ level: 'error' },
)
res.status(200).json(buildIndustryErrorResponse(4010003, message))
}
})
@@ -144,11 +159,16 @@ router.post('/consume-code', industryRateLimit, async (req, res) => {
res.status(200).json(result)
} catch (error) {
const message = error instanceof Error ? error.message : '系统异常'
logIntegration('[kuaishou-industry/consume-code]', '核销处理失败', {
logIntegration(
'[kuaishou-industry/consume-code]',
'核销处理失败',
{
requestId,
durationMs: Date.now() - startedAt,
error,
}, { level: 'error' })
},
{ level: 'error' },
)
res.status(200).json(buildIndustryErrorResponse(4010003, message))
}
})
+14 -4
View File
@@ -47,11 +47,16 @@ router.post('/orders/create', open91RateLimit, async (req, res) => {
} catch (error) {
const message = error instanceof Error ? error.message : '系统错误'
const code = resolveErrorStatusCode(error) >= 500 ? 500 : 400
logIntegration('[open-91/create]', '91卡券异步下单处理失败', {
logIntegration(
'[open-91/create]',
'91卡券异步下单处理失败',
{
requestId,
durationMs: Date.now() - startedAt,
error,
}, { level: 'error' })
},
{ level: 'error' },
)
res.status(200).json(buildOpen91ErrorResponse(message, code))
}
})
@@ -79,11 +84,16 @@ router.post('/orders/query', open91RateLimit, async (req, res) => {
} catch (error) {
const message = error instanceof Error ? error.message : '系统错误'
const code = resolveErrorStatusCode(error) >= 500 ? 500 : 400
logIntegration('[open-91/query]', '91卡券订单查询处理失败', {
logIntegration(
'[open-91/query]',
'91卡券订单查询处理失败',
{
requestId,
durationMs: Date.now() - startedAt,
error,
}, { level: 'error' })
},
{ level: 'error' },
)
res.status(200).json(buildOpen91ErrorResponse(message, code))
}
})
+7 -2
View File
@@ -25,12 +25,17 @@ router.get('/:code', async (req, res) => {
})
res.redirect(302, row.target_url)
} catch (error) {
logIntegration('[short-link]', '短链跳转失败', {
logIntegration(
'[short-link]',
'短链跳转失败',
{
requestId,
code: req.params.code,
durationMs: Date.now() - startedAt,
error,
}, { level: 'warn' })
},
{ level: 'warn' },
)
sendRouteError(res, error, '短链不可用', '[short-link]')
}
})
+20 -8
View File
@@ -111,40 +111,52 @@ router.get(
router.get(
'/profile/wallet-ledgers',
createRouteHandler((req) => listWorkerProfileWalletLedgers(req.query, getRequiredWorkerSession(req)), {
createRouteHandler(
(req) => listWorkerProfileWalletLedgers(req.query, getRequiredWorkerSession(req)),
{
successMessage: 'ok',
errorMessage: '读取钱包流水失败',
scope: '[worker/profile/wallet-ledgers]',
}),
},
),
)
router.get(
'/profile/finance-requests',
createRouteHandler((req) => listWorkerProfileFinanceRequests(req.query, getRequiredWorkerSession(req)), {
createRouteHandler(
(req) => listWorkerProfileFinanceRequests(req.query, getRequiredWorkerSession(req)),
{
successMessage: 'ok',
errorMessage: '读取资金申请记录失败',
scope: '[worker/profile/finance-requests]',
}),
},
),
)
router.post(
'/profile/recharge-requests',
requireActiveWorker,
createRouteHandler((req) => createWorkerRechargeRequest(req.body || {}, getRequiredWorkerSession(req)), {
createRouteHandler(
(req) => createWorkerRechargeRequest(req.body || {}, getRequiredWorkerSession(req)),
{
successMessage: '充值申请已提交',
errorMessage: '提交充值申请失败',
scope: '[worker/profile/recharge-requests]',
}),
},
),
)
router.post(
'/profile/withdraw-requests',
requireActiveWorker,
createRouteHandler((req) => createWorkerWithdrawRequest(req.body || {}, getRequiredWorkerSession(req)), {
createRouteHandler(
(req) => createWorkerWithdrawRequest(req.body || {}, getRequiredWorkerSession(req)),
{
successMessage: '提现申请已提交',
errorMessage: '提交提现申请失败',
scope: '[worker/profile/withdraw-requests]',
}),
},
),
)
router.post(
+5 -1
View File
@@ -7,7 +7,11 @@ import {
} from '../../services/worker-platform/index.js'
import { createHttpError, sendRouteError } from '../../utils/http.js'
export async function requireWorkerSession(req: Request, res: Response, next: NextFunction): Promise<void> {
export async function requireWorkerSession(
req: Request,
res: Response,
next: NextFunction,
): Promise<void> {
try {
req.workerSession = await verifyWorkerSessionToken(extractWorkerBearerToken(req))
next()
@@ -1,9 +1,17 @@
import { createAdminAuditLog, listAdminAuditLogs } from '../../repositories/admin-audit-log-repo.js'
import { nowIso } from '../../utils/time.js'
import { normalizeDateQuery, normalizePage, normalizePageSize, safeParseJson } from './admin-query-utils.js'
import {
normalizeDateQuery,
normalizePage,
normalizePageSize,
safeParseJson,
} from './admin-query-utils.js'
import type { JsonObject } from '../../types/json.js'
export async function writeAdminAuditLog(session: JsonObject | null | undefined, payload: JsonObject = {}) {
export async function writeAdminAuditLog(
session: JsonObject | null | undefined,
payload: JsonObject = {},
) {
if (!session?.userId) {
return null
}
@@ -32,10 +32,14 @@ type AdminUserStatus = 'active' | 'disabled'
export async function ensureAdminUsersBootstrapped(): Promise<void> {
ensureAdminAuthConfigured()
const configuredUsers = Array.isArray(runtimeConfig.admin?.defaultUsers) ? runtimeConfig.admin.defaultUsers : []
const configuredUsers = Array.isArray(runtimeConfig.admin?.defaultUsers)
? runtimeConfig.admin.defaultUsers
: []
for (const configuredUser of configuredUsers) {
const username = String(configuredUser?.username || '').trim().toLowerCase()
const username = String(configuredUser?.username || '')
.trim()
.toLowerCase()
const password = String(configuredUser?.password || '').trim()
const role = normalizeAdminRole(configuredUser?.role)
@@ -70,7 +74,9 @@ export async function loginAdmin(
): Promise<JsonObject> {
ensureAdminAuthConfigured()
const normalizedUsername = String(username || '').trim().toLowerCase()
const normalizedUsername = String(username || '')
.trim()
.toLowerCase()
const normalizedPassword = String(password || '').trim()
if (!normalizedUsername || !normalizedPassword) {
@@ -87,7 +93,11 @@ export async function loginAdmin(
}
const user = await getAdminUserByUsername(normalizedUsername)
if (!user || user.status !== 'active' || !verifyAdminPassword(normalizedPassword, user.password_hash)) {
if (
!user ||
user.status !== 'active' ||
!verifyAdminPassword(normalizedPassword, user.password_hash)
) {
await recordAdminLoginLog({
userId: user ? Number(user.id) : null,
username: normalizedUsername,
@@ -201,7 +211,10 @@ export async function getAdminSessionSummary(token: unknown): Promise<JsonObject
}
}
export function requireAdminRole(session: { role?: string } | null | undefined, allowedRoles: string[]): void {
export function requireAdminRole(
session: { role?: string } | null | undefined,
allowedRoles: string[],
): void {
if (session && allowedRoles.includes(session.role || '')) {
return
}
@@ -342,7 +355,10 @@ export async function updateManagedAdminUserStatus(
}
}
export async function resetManagedAdminUserPassword(userId: number | string, payload: JsonObject = {}): Promise<JsonObject> {
export async function resetManagedAdminUserPassword(
userId: number | string,
payload: JsonObject = {},
): Promise<JsonObject> {
const user = await getRequiredAdminUser(userId)
const password = normalizePassword(payload.password)
@@ -373,7 +389,9 @@ export async function resetManagedAdminUserPassword(userId: number | string, pay
export function ensureAdminAuthConfigured(): void {
const sessionSecret = String(runtimeConfig.admin?.sessionSecret || '').trim()
const configuredUsers = Array.isArray(runtimeConfig.admin?.defaultUsers) ? runtimeConfig.admin.defaultUsers : []
const configuredUsers = Array.isArray(runtimeConfig.admin?.defaultUsers)
? runtimeConfig.admin.defaultUsers
: []
if (sessionSecret && configuredUsers.length > 0) {
return
@@ -435,7 +453,9 @@ function signPayload(encodedPayload: string): string {
}
export function normalizeAdminRole(role: unknown): AdminRole {
const normalized = String(role || '').trim().toLowerCase()
const normalized = String(role || '')
.trim()
.toLowerCase()
if (normalized === 'admin') {
return 'admin'
@@ -449,7 +469,11 @@ export function normalizeAdminRole(role: unknown): AdminRole {
}
export function normalizeAdminUserStatus(status: unknown): AdminUserStatus {
return String(status || '').trim().toLowerCase() === 'disabled' ? 'disabled' : 'active'
return String(status || '')
.trim()
.toLowerCase() === 'disabled'
? 'disabled'
: 'active'
}
function safeCompare(input: unknown, expected: unknown): boolean {
@@ -464,17 +488,23 @@ function safeCompare(input: unknown, expected: unknown): boolean {
}
function normalizeRoleQuery(role: unknown): string {
const normalized = String(role || '').trim().toLowerCase()
const normalized = String(role || '')
.trim()
.toLowerCase()
return ['admin', 'operator', 'support'].includes(normalized) ? normalized : ''
}
function normalizeStatusQuery(status: unknown): string {
const normalized = String(status || '').trim().toLowerCase()
const normalized = String(status || '')
.trim()
.toLowerCase()
return ['active', 'disabled'].includes(normalized) ? normalized : ''
}
function normalizeUsername(username: unknown): string {
return String(username || '').trim().toLowerCase()
return String(username || '')
.trim()
.toLowerCase()
}
function normalizePassword(password: unknown): string {
@@ -523,7 +553,7 @@ async function getRequiredAdminUser(userId: number | string): Promise<AdminUserR
async function ensureAdminUserChangeAllowed(
user: AdminUserRow,
options: { nextRole?: AdminRole, nextStatus?: AdminUserStatus } = {},
options: { nextRole?: AdminRole; nextStatus?: AdminUserStatus } = {},
session: AdminSession,
): Promise<void> {
const nextRole = options.nextRole || user.role
@@ -536,7 +566,11 @@ async function ensureAdminUserChangeAllowed(
})
}
if (user.role === 'admin' && (nextRole !== 'admin' || nextStatus !== 'active') && await countActiveAdminUsers() <= 1) {
if (
user.role === 'admin' &&
(nextRole !== 'admin' || nextStatus !== 'active') &&
(await countActiveAdminUsers()) <= 1
) {
throw createHttpError('至少保留一个启用中的管理员账号', {
statusCode: 409,
errorCode: 'admin_user_last_admin_not_allowed',
@@ -555,11 +589,13 @@ function mapAdminUser(user: AdminUserRow): JsonObject {
}
}
function pickLoginMeta(meta: {
function pickLoginMeta(
meta: {
ip?: string
userAgent?: string
location?: string
} = {}) {
} = {},
) {
const result: {
ip?: string
userAgent?: string
@@ -11,15 +11,8 @@ export async function getAdminDashboardSummary() {
TASK_STATUS.PENDING_BINDING_PREPARE,
TASK_STATUS.WAITING_BINDING,
]
const claimingStatuses = [
TASK_STATUS.CLAIMED,
TASK_STATUS.ROLE_CONFIRMED,
TASK_STATUS.REDEEMING,
]
const abnormalStatuses = [
TASK_STATUS.RETRY_PENDING,
TASK_STATUS.MANUAL_REVIEW,
]
const claimingStatuses = [TASK_STATUS.CLAIMED, TASK_STATUS.ROLE_CONFIRMED, TASK_STATUS.REDEEMING]
const abnormalStatuses = [TASK_STATUS.RETRY_PENDING, TASK_STATUS.MANUAL_REVIEW]
const result = await query(
`
SELECT
@@ -1,17 +1,10 @@
import type { Request } from 'express'
import type { JsonObject } from '../../types/json.js'
import {
createAdminLoginLog,
listAdminLoginLogs,
} from '../../repositories/admin-login-log-repo.js'
import { createAdminLoginLog, listAdminLoginLogs } from '../../repositories/admin-login-log-repo.js'
import { logWarn } from '../../utils/logger.js'
import { nowIso } from '../../utils/time.js'
import {
normalizeDateQuery,
normalizePage,
normalizePageSize,
} from './admin-query-utils.js'
import { normalizeDateQuery, normalizePage, normalizePageSize } from './admin-query-utils.js'
type RecordAdminLoginInput = {
userId?: number | null
@@ -108,16 +101,8 @@ export function resolveClientLocation(req: Request) {
'cloudfront-viewer-country',
'x-country-code',
])
const city = firstHeader(req, [
'cf-ipcity',
'x-vercel-ip-city',
'x-city',
])
const region = firstHeader(req, [
'cf-region',
'x-vercel-ip-country-region',
'x-region',
])
const city = firstHeader(req, ['cf-ipcity', 'x-vercel-ip-city', 'x-city'])
const region = firstHeader(req, ['cf-region', 'x-vercel-ip-country-region', 'x-region'])
return [country, region, city].filter(Boolean).join(' · ')
}
@@ -34,7 +34,10 @@ export async function mapAdminOrderListItem(item: OrderListRow): Promise<AdminOr
createdAt: item.created_at,
updatedAt: item.updated_at,
itemCount: orderItems.length,
totalQuantity: orderItems.reduce((sum, orderItem) => sum + Math.max(1, Number(orderItem.quantity || 1)), 0),
totalQuantity: orderItems.reduce(
(sum, orderItem) => sum + Math.max(1, Number(orderItem.quantity || 1)),
0,
),
itemSummary,
taskCount: Number(item.task_count || tasks.length || 0),
resourceStatus: fulfillmentProgress.resourceStatus,
@@ -52,8 +55,9 @@ export function summarizeOrderItems(items: OrderItemRow[] | null | undefined): s
}
const [firstItem] = normalizedItems
const firstLabel = resolveOrderItemTitle(firstItem)
|| String(firstItem?.sku_name || firstItem?.sku_code || '').trim()
const firstLabel =
resolveOrderItemTitle(firstItem) ||
String(firstItem?.sku_name || firstItem?.sku_code || '').trim()
if (normalizedItems.length === 1) {
return firstLabel
@@ -66,10 +66,7 @@ import type {
AdminTaskListQueryInput,
AdminViewerSessionInput,
} from '../../types/admin/read-inputs.js'
import type {
KuaishouIndustryVoucherRow,
TaskRow,
} from '../../types/repository/rows.js'
import type { KuaishouIndustryVoucherRow, TaskRow } from '../../types/repository/rows.js'
export async function getAdminOrders(
query: AdminOrderListQueryInput = {},
@@ -229,7 +226,8 @@ export async function getAdminTaskDetail(
}
const primaryClaimTokenId = getTaskPrimaryClaimTokenId(task)
const [order, claimToken, taskEvents, taskKuaishouIndustryVouchers, oidKuaishouIndustryVouchers] = await Promise.all([
const [order, claimToken, taskEvents, taskKuaishouIndustryVouchers, oidKuaishouIndustryVouchers] =
await Promise.all([
getOrderById(task.order_id),
primaryClaimTokenId ? getClaimTokenById(primaryClaimTokenId) : Promise.resolve(null),
listTaskEventsByTaskId(task.id),
@@ -252,10 +250,7 @@ export async function getAdminTaskDetail(
cloudSourceLabelMap,
}) || mapKuaishouCloudTaskStateProjection(task, { cloudSourceLabelMap })
const claimIdentity = buildClaimIdentityAdminSummary(taskContext, {
flowLike:
taskContext.kuaishouCloudFulfillment ||
taskContext.kuaishouFeifei ||
null,
flowLike: taskContext.kuaishouCloudFulfillment || taskContext.kuaishouFeifei || null,
taskRoleId: task.role_id,
taskRoleName: task.role_name,
})
@@ -265,17 +260,20 @@ export async function getAdminTaskDetail(
taskKuaishouIndustryVouchers,
oidKuaishouIndustryVouchers,
)
const kuaishouIndustryVoucher =
firstKuaishouIndustryVoucher
const kuaishouIndustryVoucher = firstKuaishouIndustryVoucher
? mapAdminKuaishouIndustryVoucher(firstKuaishouIndustryVoucher)
: mapKuaishouIndustryVoucherContext(taskContext.kuaishouIndustryVoucher)
const hasKuaishouIndustryVoucher = Boolean(
kuaishouIndustryVoucher && String(kuaishouIndustryVoucher.voucherCode || '').trim(),
)
const kuaishouIndustryVoucherStatus =
String(kuaishouIndustryVoucher?.status || '').trim().toUpperCase()
const kuaishouIndustryVoucherSendCallbackStatus =
String(kuaishouIndustryVoucher?.sendCallbackStatus || '').trim().toLowerCase()
const kuaishouIndustryVoucherStatus = String(kuaishouIndustryVoucher?.status || '')
.trim()
.toUpperCase()
const kuaishouIndustryVoucherSendCallbackStatus = String(
kuaishouIndustryVoucher?.sendCallbackStatus || '',
)
.trim()
.toLowerCase()
return {
task: mapAdminTaskListItem(
@@ -435,7 +433,7 @@ function resolveKuaishouIndustryVoucherForTask(
taskContext.kuaishouIndustryVoucher &&
typeof taskContext.kuaishouIndustryVoucher === 'object' &&
!Array.isArray(taskContext.kuaishouIndustryVoucher)
? taskContext.kuaishouIndustryVoucher as JsonRecord
? (taskContext.kuaishouIndustryVoucher as JsonRecord)
: {}
const voucherCode = String(voucherContext.voucherCode || voucherContext.eticketId || '').trim()
if (voucherCode) {
@@ -457,7 +455,7 @@ function resolveKuaishouIndustryVoucherForTask(
}
}
return oidVouchers.length === 1 ? (oidVouchers[0] || null) : null
return oidVouchers.length === 1 ? oidVouchers[0] || null : null
}
function buildCloudSourceLabelMap() {
@@ -15,7 +15,10 @@ test('resolveAdminTaskScreenshotUrl lets support view final redeemed screenshot'
runtime_session_id: '',
}
const screenshotUrl = await resolveAdminTaskScreenshotUrl(task, createAdminViewerContext({ role: 'support' }))
const screenshotUrl = await resolveAdminTaskScreenshotUrl(
task,
createAdminViewerContext({ role: 'support' }),
)
assert.equal(screenshotUrl, '/api/v1/admin/tasks/12/screenshot')
})
@@ -27,7 +30,10 @@ test('resolveAdminTaskScreenshotUrl falls back to review screenshot when runtime
runtime_session_id: 'runtime-session-13',
}
const screenshotUrl = await resolveAdminTaskScreenshotUrl(task, createAdminViewerContext({ role: 'support' }))
const screenshotUrl = await resolveAdminTaskScreenshotUrl(
task,
createAdminViewerContext({ role: 'support' }),
)
assert.equal(screenshotUrl, '/api/v1/admin/tasks/13/screenshot')
})
@@ -79,8 +79,7 @@ export function mapKuaishouCloudFulfillmentContext(
const role = asJsonObject(record.role)
const purchase = asJsonObject(record.purchase)
const dispatch = asJsonObject(record.dispatch)
const returnNumber =
asJsonObject(record.returnNumber)
const returnNumber = asJsonObject(record.returnNumber)
const consume = asJsonObject(record.consume)
const cloudSourceKeys = Array.isArray(binding.cloudSourceKeys)
? binding.cloudSourceKeys.map((value: unknown) => String(value || '').trim()).filter(Boolean)
@@ -373,7 +372,8 @@ export function createAdminViewerContext(
canViewSensitiveTaskData: role === 'admin' || role === 'operator',
canManageTaskLifecycle: role === 'admin' || role === 'operator',
canOperateAssistedTask: role === 'admin' || role === 'operator' || role === 'support',
canOperateKuaishouIndustryVoucher: role === 'admin' || role === 'operator' || role === 'support',
canOperateKuaishouIndustryVoucher:
role === 'admin' || role === 'operator' || role === 'support',
}
}
@@ -17,10 +17,7 @@ import type {
AdminTaskListItem,
} from '../../types/admin/read-models.js'
import type { AdminTaskActionPayload } from '../../types/admin/write-models.js'
import type {
TaskEventRow,
TaskRow,
} from '../../types/repository/rows.js'
import type { TaskEventRow, TaskRow } from '../../types/repository/rows.js'
import type { AdminViewerContext } from './admin-read-shared-helpers.js'
type TaskFulfillmentState = {
@@ -28,9 +25,7 @@ type TaskFulfillmentState = {
customerStatus: string
}
export function mapAdminTaskSummary(
task: TaskRow,
): JsonRecord {
export function mapAdminTaskSummary(task: TaskRow): JsonRecord {
const fulfillment = buildTaskFulfillmentState(task)
return {
@@ -111,8 +106,10 @@ export function mapAdminTaskListItem(
lastError: task.last_error,
createdAt: task.created_at,
updatedAt: task.updated_at,
claimToken: viewerContext.canViewSensitiveTaskData ? (task.primary_claim_token || task.claim_token || '') : '',
screenshotPath: viewerContext.role === 'support' ? '' : (task.screenshot_path || ''),
claimToken: viewerContext.canViewSensitiveTaskData
? task.primary_claim_token || task.claim_token || ''
: '',
screenshotPath: viewerContext.role === 'support' ? '' : task.screenshot_path || '',
}
}
@@ -134,7 +131,9 @@ export function buildOrderFulfillmentProgress(
const totalTaskCount = normalizedTasks.length
const taskFulfillments = normalizedTasks.map((task) => buildTaskFulfillmentState(task))
const preparedTaskCount = normalizedTasks.filter((task) => isTaskResourcePrepared(task)).length
const completedTaskCount = normalizedTasks.filter((task) => isTaskFulfillmentCompleted(task)).length
const completedTaskCount = normalizedTasks.filter((task) =>
isTaskFulfillmentCompleted(task),
).length
let resourceStatus = 'pending_prepare'
let customerStatus = 'not_started'
@@ -152,7 +151,11 @@ export function buildOrderFulfillmentProgress(
if (normalizedTasks.every((task) => isTaskFulfillmentCompleted(task))) {
resourceStatus = 'resource_ready'
customerStatus = 'customer_completed'
} else if (taskFulfillments.some((item) => ['customer_processing', 'customer_confirmed', 'link_opened'].includes(item.customerStatus))) {
} else if (
taskFulfillments.some((item) =>
['customer_processing', 'customer_confirmed', 'link_opened'].includes(item.customerStatus),
)
) {
resourceStatus = preparedTaskCount > 0 ? 'resource_ready' : 'pending_prepare'
customerStatus = 'customer_processing'
} else if (taskFulfillments.some((item) => item.customerStatus === 'waiting_customer')) {
@@ -163,7 +166,11 @@ export function buildOrderFulfillmentProgress(
customerStatus = 'customer_exception'
} else if (preparedTaskCount > 0) {
resourceStatus = 'resource_ready'
} else if (taskFulfillments.some((item) => ['manual_review', 'retry_pending'].includes(item.resourceStatus))) {
} else if (
taskFulfillments.some((item) =>
['manual_review', 'retry_pending'].includes(item.resourceStatus),
)
) {
resourceStatus = 'resource_exception'
}
@@ -1,8 +1,6 @@
import { getAffiliateDashConfig } from '../../platforms/affiliate-dash/config.js'
import type { JsonObject } from '../../../types/json.js'
import {
getAffiliateDashWallet,
} from '../../platforms/affiliate-dash/order-service.js'
import { getAffiliateDashWallet } from '../../platforms/affiliate-dash/order-service.js'
import { listAffiliateDashProducts } from '../../platforms/affiliate-dash/product-service.js'
import {
getAffiliateDashSourceConfig,
@@ -35,9 +33,7 @@ export async function updateAdminAffiliateDashConfig(payload: JsonObject = {}) {
export function matchAdminAffiliateDashSku(payload: JsonObject = {}) {
const productNo = String(payload.productNo || payload.product_no || '').trim()
const source = getAdminEditableAffiliateDashConfig()
const sku = productNo
? String(source.skuMapping[productNo] || '').trim()
: ''
const sku = productNo ? String(source.skuMapping[productNo] || '').trim() : ''
return {
productNo,
@@ -52,7 +52,13 @@ test('resolveCloudtentaclesAdminContext merges payload source and persisted sess
test('hasCloudtentaclesCredentialContextChanged detects normalized credential changes', () => {
assert.equal(
hasCloudtentaclesCredentialContextChanged(
{ baseUrl: ' https://a ', username: 'u1', phone: '13812345678', deviceId: 'd1', deviceType: 1 },
{
baseUrl: ' https://a ',
username: 'u1',
phone: '13812345678',
deviceId: 'd1',
deviceType: 1,
},
{ baseUrl: 'https://a', username: 'u1', phone: '13812345678', deviceId: 'd1', deviceType: 1 },
),
false,
@@ -1,70 +1,57 @@
export function pickFirstNonEmpty(values: unknown[]) {
for (const value of values) {
const normalized = String(value || "").trim();
const normalized = String(value || '').trim()
if (normalized) {
return normalized;
return normalized
}
}
return "";
return ''
}
export function isPlainObject(value: unknown): value is JsonObject {
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
return Boolean(value) && typeof value === 'object' && !Array.isArray(value)
}
import { getCloudtentaclesSourceByKey } from "../../../platforms/cloudtentacles/source-config-service.js";
import { getCloudtentaclesSessionStateByKey } from "../../../platforms/cloudtentacles/session-state-service.js";
import { getCloudtentaclesSourceByKey } from '../../../platforms/cloudtentacles/source-config-service.js'
import { getCloudtentaclesSessionStateByKey } from '../../../platforms/cloudtentacles/session-state-service.js'
import type { JsonObject } from '../../../../types/json.js'
import {
normalizeCloudtentaclesDeviceId,
normalizeCloudtentaclesDeviceType,
} from "../../../platforms/cloudtentacles/defaults.js";
} from '../../../platforms/cloudtentacles/defaults.js'
export function resolveCloudtentaclesAdminContext(payload: JsonObject = {}, options: JsonObject = {}) {
const sourceKey = String(
payload.sourceKey || options.sourceKey || "default"
).trim();
const savedSource = options.savedSource || getCloudtentaclesSourceByKey(sourceKey) || {};
const persistedSession = options.persistedSession ||
getCloudtentaclesSessionStateByKey(sourceKey) ||
{};
const defaultBaseUrl = String(
options.defaultBaseUrl || "https://123.207.217.176"
).trim();
export function resolveCloudtentaclesAdminContext(
payload: JsonObject = {},
options: JsonObject = {},
) {
const sourceKey = String(payload.sourceKey || options.sourceKey || 'default').trim()
const savedSource = options.savedSource || getCloudtentaclesSourceByKey(sourceKey) || {}
const persistedSession =
options.persistedSession || getCloudtentaclesSessionStateByKey(sourceKey) || {}
const defaultBaseUrl = String(options.defaultBaseUrl || 'https://123.207.217.176').trim()
return {
sourceKey,
baseUrl: pickFirstNonEmpty([
payload.baseUrl,
savedSource.baseUrl,
defaultBaseUrl,
]),
baseUrl: pickFirstNonEmpty([payload.baseUrl, savedSource.baseUrl, defaultBaseUrl]),
token: pickFirstNonEmpty([payload.token, persistedSession.token]),
deviceId: normalizeCloudtentaclesDeviceId(
pickFirstNonEmpty([
payload.deviceId,
savedSource.deviceId,
persistedSession.deviceId,
])
pickFirstNonEmpty([payload.deviceId, savedSource.deviceId, persistedSession.deviceId]),
),
deviceType: normalizeCloudtentaclesDeviceType(
payload.deviceType ?? savedSource.deviceType ?? persistedSession.deviceType
payload.deviceType ?? savedSource.deviceType ?? persistedSession.deviceType,
),
};
}
}
export function hasCloudtentaclesCredentialContextChanged(
current: JsonObject = {},
next: JsonObject = {}
next: JsonObject = {},
) {
return (
String(current.baseUrl || "").trim() !==
String(next.baseUrl || "").trim() ||
String(current.username || "").trim() !==
String(next.username || "").trim() ||
String(current.phone || "").trim() !== String(next.phone || "").trim() ||
String(current.deviceId || "").trim() !==
String(next.deviceId || "").trim() ||
String(current.baseUrl || '').trim() !== String(next.baseUrl || '').trim() ||
String(current.username || '').trim() !== String(next.username || '').trim() ||
String(current.phone || '').trim() !== String(next.phone || '').trim() ||
String(current.deviceId || '').trim() !== String(next.deviceId || '').trim() ||
Number(current.deviceType || 0) !== Number(next.deviceType || 0)
);
)
}
File diff suppressed because it is too large Load Diff
@@ -1,11 +1,7 @@
import test from 'node:test'
import assert from 'node:assert/strict'
import {
mapAdminCloudtentaclesSession,
maskPhone,
maskSecret,
} from './mappers.js'
import { mapAdminCloudtentaclesSession, maskPhone, maskSecret } from './mappers.js'
test('maskSecret preserves edges while hiding middle characters', () => {
assert.equal(maskSecret('abcdef1234567890'), 'abcdef****567890')
@@ -2,44 +2,44 @@ import type { JsonObject } from '../../../../types/json.js'
import {
maskPhone as maskPhoneValue,
maskSecret as maskSecretValue,
} from "../../../../utils/masking.js";
} from '../../../../utils/masking.js'
import {
normalizeCloudtentaclesDeviceId,
normalizeCloudtentaclesDeviceType,
} from "../../../platforms/cloudtentacles/defaults.js";
} from '../../../platforms/cloudtentacles/defaults.js'
export function maskSecret(value: unknown) {
return maskSecretValue(value);
return maskSecretValue(value)
}
export function maskPhone(value: unknown) {
return maskPhoneValue(value, { maskShort: false });
return maskPhoneValue(value, { maskShort: false })
}
export function mapAdminCloudtentaclesSourceConfig(config: JsonObject = {}) {
return {
key: String(config.key || "").trim(),
label: String(config.label || "").trim(),
key: String(config.key || '').trim(),
label: String(config.label || '').trim(),
enabled: config.enabled !== false,
baseUrl: String(config.baseUrl || "").trim(),
username: String(config.username || "").trim(),
password: String(config.password || "").trim(),
phone: String(config.phone || "").trim(),
baseUrl: String(config.baseUrl || '').trim(),
username: String(config.username || '').trim(),
password: String(config.password || '').trim(),
phone: String(config.phone || '').trim(),
deviceId: normalizeCloudtentaclesDeviceId(config.deviceId),
deviceType: normalizeCloudtentaclesDeviceType(config.deviceType),
};
}
}
export function mapAdminCloudtentaclesSession(session: JsonObject = {}) {
return {
token: String(session.token || "").trim(),
token: String(session.token || '').trim(),
tokenMasked: maskSecret(session.token),
baseUrl: String(session.baseUrl || "").trim(),
username: String(session.username || "").trim(),
baseUrl: String(session.baseUrl || '').trim(),
username: String(session.username || '').trim(),
phoneMasked: maskPhone(session.phone),
loggedInAt: String(session.loggedInAt || "").trim(),
loggedInAt: String(session.loggedInAt || '').trim(),
deviceId: normalizeCloudtentaclesDeviceId(session.deviceId),
deviceType: normalizeCloudtentaclesDeviceType(session.deviceType),
hasToken: Boolean(String(session.token || "").trim()),
};
hasToken: Boolean(String(session.token || '').trim()),
}
}
@@ -1,96 +1,80 @@
import { getCloudtentaclesSourceByKey } from "../../../platforms/cloudtentacles/source-config-service.js";
import { getCloudtentaclesSessionStateByKey } from "../../../platforms/cloudtentacles/session-state-service.js";
import { getCloudtentaclesSourceByKey } from '../../../platforms/cloudtentacles/source-config-service.js'
import { getCloudtentaclesSessionStateByKey } from '../../../platforms/cloudtentacles/session-state-service.js'
import type { JsonObject } from '../../../../types/json.js'
import {
normalizeCloudtentaclesDeviceId,
normalizeCloudtentaclesDeviceType,
} from "../../../platforms/cloudtentacles/defaults.js";
import {
pickFirstNonEmpty,
resolveCloudtentaclesAdminContext,
} from "./context.js";
import { maskPhone, maskSecret } from "./mappers.js";
} from '../../../platforms/cloudtentacles/defaults.js'
import { pickFirstNonEmpty, resolveCloudtentaclesAdminContext } from './context.js'
import { maskPhone, maskSecret } from './mappers.js'
const DEFAULT_CLOUDTENTACLES_BASE_URL = "https://123.207.217.176";
const DEFAULT_CLOUDTENTACLES_BASE_URL = 'https://123.207.217.176'
function _resolveSourceKey(payload: JsonObject = {}) {
return String(payload.sourceKey || "").trim() || "default";
return String(payload.sourceKey || '').trim() || 'default'
}
export function resolveAdminCloudtentaclesCredentialPayload(
payload: JsonObject = {},
options: JsonObject = {}
options: JsonObject = {},
) {
const sourceKey = _resolveSourceKey(payload);
const savedSource = options.savedSource || getCloudtentaclesSourceByKey(sourceKey) || {};
const defaultBaseUrl = String(
options.defaultBaseUrl || DEFAULT_CLOUDTENTACLES_BASE_URL
).trim();
const sourceKey = _resolveSourceKey(payload)
const savedSource = options.savedSource || getCloudtentaclesSourceByKey(sourceKey) || {}
const defaultBaseUrl = String(options.defaultBaseUrl || DEFAULT_CLOUDTENTACLES_BASE_URL).trim()
return {
sourceKey,
baseUrl: pickFirstNonEmpty([
payload.baseUrl,
savedSource.baseUrl,
defaultBaseUrl,
]),
baseUrl: pickFirstNonEmpty([payload.baseUrl, savedSource.baseUrl, defaultBaseUrl]),
username: pickFirstNonEmpty([payload.username, savedSource.username]),
password: pickFirstNonEmpty([payload.password, savedSource.password]),
phone: pickFirstNonEmpty([payload.phone, savedSource.phone]),
deviceId: normalizeCloudtentaclesDeviceId(
pickFirstNonEmpty([payload.deviceId, savedSource.deviceId])
pickFirstNonEmpty([payload.deviceId, savedSource.deviceId]),
),
deviceType: normalizeCloudtentaclesDeviceType(
payload.deviceType ?? savedSource.deviceType
),
};
deviceType: normalizeCloudtentaclesDeviceType(payload.deviceType ?? savedSource.deviceType),
}
}
export function resolveAdminCloudtentaclesSessionPayload(
payload: JsonObject = {},
options: JsonObject = {}
options: JsonObject = {},
) {
const sourceKey = _resolveSourceKey(payload);
const savedSource = options.savedSource || getCloudtentaclesSourceByKey(sourceKey) || {};
const persistedSession = options.persistedSession ||
getCloudtentaclesSessionStateByKey(sourceKey) ||
{};
const sourceKey = _resolveSourceKey(payload)
const savedSource = options.savedSource || getCloudtentaclesSourceByKey(sourceKey) || {}
const persistedSession =
options.persistedSession || getCloudtentaclesSessionStateByKey(sourceKey) || {}
return resolveCloudtentaclesAdminContext(payload, {
savedSource,
persistedSession,
sourceKey,
defaultBaseUrl: options.defaultBaseUrl || DEFAULT_CLOUDTENTACLES_BASE_URL,
});
})
}
export function buildAdminCloudtentaclesPersistedSessionPayload(
session: JsonObject = {},
options: JsonObject = {}
options: JsonObject = {},
) {
return {
token: String(session.token || "").trim(),
baseUrl: String(session.baseUrl || "").trim(),
token: String(session.token || '').trim(),
baseUrl: String(session.baseUrl || '').trim(),
username: pickFirstNonEmpty([options.username, session.username]),
phone: pickFirstNonEmpty([options.phone, session.phone]),
loggedInAt: String(session.loggedInAt || "").trim(),
loggedInAt: String(session.loggedInAt || '').trim(),
deviceId: normalizeCloudtentaclesDeviceId(
pickFirstNonEmpty([options.deviceId, session.deviceId])
pickFirstNonEmpty([options.deviceId, session.deviceId]),
),
deviceType: normalizeCloudtentaclesDeviceType(
options.deviceType ?? session.deviceType
),
};
deviceType: normalizeCloudtentaclesDeviceType(options.deviceType ?? session.deviceType),
}
}
export function buildAdminCloudtentaclesSessionSummary(
session: JsonObject = {},
savedSession: JsonObject = {},
sourceKey = "default"
sourceKey = 'default',
) {
const permissions = Array.isArray(session.permissions)
? session.permissions
: [];
const permissions = Array.isArray(session.permissions) ? session.permissions : []
return {
sourceKey,
@@ -98,47 +82,39 @@ export function buildAdminCloudtentaclesSessionSummary(
permissionCount: permissions.length,
permissions,
persisted: Boolean(savedSession.token),
};
}
}
export function buildAdminCloudtentaclesLoginResult(
session: JsonObject = {},
savedSession: JsonObject = {},
sourceKey = "default"
sourceKey = 'default',
) {
return {
sourceKey,
baseUrl: String(session.baseUrl || "").trim(),
username: String(session.username || "").trim(),
baseUrl: String(session.baseUrl || '').trim(),
username: String(session.username || '').trim(),
phoneMasked: maskPhone(session.phone),
loggedInAt: String(session.loggedInAt || "").trim(),
responseMessage: String(session.responseMessage || "").trim(),
token: String(session.token || "").trim(),
session: buildAdminCloudtentaclesSessionSummary(
session,
savedSession,
sourceKey
),
loggedInAt: String(session.loggedInAt || '').trim(),
responseMessage: String(session.responseMessage || '').trim(),
token: String(session.token || '').trim(),
session: buildAdminCloudtentaclesSessionSummary(session, savedSession, sourceKey),
userInfo: session.userInfo,
asset: session.asset,
};
}
}
export function buildAdminCloudtentaclesValidateResult(
session: JsonObject = {},
savedSession: JsonObject = {},
sourceKey = "default"
sourceKey = 'default',
) {
return {
sourceKey,
baseUrl: String(session.baseUrl || "").trim(),
loggedInAt: String(session.loggedInAt || "").trim(),
session: buildAdminCloudtentaclesSessionSummary(
session,
savedSession,
sourceKey
),
baseUrl: String(session.baseUrl || '').trim(),
loggedInAt: String(session.loggedInAt || '').trim(),
session: buildAdminCloudtentaclesSessionSummary(session, savedSession, sourceKey),
userInfo: session.userInfo,
asset: session.asset,
};
}
}
@@ -2,22 +2,19 @@ import type { JsonObject } from '../../../../types/json.js'
import {
normalizeCloudtentaclesDeviceId,
normalizeCloudtentaclesDeviceType,
} from "../../../platforms/cloudtentacles/defaults.js";
} from '../../../platforms/cloudtentacles/defaults.js'
export function normalizeAdminCloudtentaclesSourceConfigPayload(
payload: JsonObject = {}
) {
const sourceKey =
String(payload.sourceKey || payload.key || "").trim() || "default";
export function normalizeAdminCloudtentaclesSourceConfigPayload(payload: JsonObject = {}) {
const sourceKey = String(payload.sourceKey || payload.key || '').trim() || 'default'
return {
key: sourceKey,
label: String(payload.label || "").trim(),
label: String(payload.label || '').trim(),
enabled: payload.enabled !== false,
baseUrl: String(payload.baseUrl || "").trim() || "https://123.207.217.176",
username: String(payload.username || "").trim(),
password: String(payload.password || "").trim(),
phone: String(payload.phone || "").trim(),
baseUrl: String(payload.baseUrl || '').trim() || 'https://123.207.217.176',
username: String(payload.username || '').trim(),
password: String(payload.password || '').trim(),
phone: String(payload.phone || '').trim(),
deviceId: normalizeCloudtentaclesDeviceId(payload.deviceId),
deviceType: normalizeCloudtentaclesDeviceType(payload.deviceType),
};
}
}
@@ -60,14 +60,18 @@ export async function listAdminKuaishouFeifeiProducts(payload: JsonObject = {})
page: Number(payload.page || 1) || 1,
perPage: Number(payload.perPage || payload.per_page || 20) || 20,
status: String(payload.status || 'on_sale').trim(),
supplyProductName: String(payload.supplyProductName || payload.supply_product_name || '').trim(),
supplyProductName: String(
payload.supplyProductName || payload.supply_product_name || '',
).trim(),
})
}
export async function syncAdminKuaishouFeifeiProductRules(payload: JsonObject = {}) {
const source = getAdminEditableKuaishouFeifeiConfig()
const status = String(payload.status || 'on_sale').trim() || 'on_sale'
const supplyProductName = String(payload.supplyProductName || payload.supply_product_name || '').trim()
const supplyProductName = String(
payload.supplyProductName || payload.supply_product_name || '',
).trim()
const products = await listAllKuaishouFeifeiProducts({
status,
supplyProductName,
@@ -185,10 +189,7 @@ function mapEffectiveKuaishouFeifeiConfig(config: ReturnType<typeof getKuaishouF
}
}
async function listAllKuaishouFeifeiProducts(input: {
status: string
supplyProductName: string
}) {
async function listAllKuaishouFeifeiProducts(input: { status: string; supplyProductName: string }) {
const perPage = 100
const firstPage = await listKuaishouFeifeiProducts({
page: 1,
@@ -14,16 +14,9 @@ import {
refreshKuaishouIndustryAccessToken,
} from '../../platforms/kuaishou-industry/token-service.js'
const SECRET_FIELDS = [
'appSecret',
'signSecret',
'messageSecret',
] as const
const SECRET_FIELDS = ['appSecret', 'signSecret', 'messageSecret'] as const
const SHOP_SECRET_FIELDS = [
'accessToken',
'refreshToken',
] as const
const SHOP_SECRET_FIELDS = ['accessToken', 'refreshToken'] as const
export function getAdminKuaishouIndustrySourceConfig() {
const config = getKuaishouIndustrySourceConfig()
@@ -38,15 +31,23 @@ export async function updateAdminKuaishouIndustrySourceConfig(payload: JsonObjec
const current = getKuaishouIndustrySourceConfig()
const saved = await saveKuaishouIndustrySourceConfig({
...current,
enabled: hasPayloadField(payload, 'enabled') ? payload.enabled !== false : current.enabled !== false,
enabled: hasPayloadField(payload, 'enabled')
? payload.enabled !== false
: current.enabled !== false,
baseUrl: readConfigString(payload, 'baseUrl', current.baseUrl),
authBaseUrl: readConfigString(payload, 'authBaseUrl', current.authBaseUrl),
redirectUri: readConfigString(payload, 'redirectUri', current.redirectUri, { allowBlank: true }),
scopes: normalizeScopeText(readConfigString(payload, 'scopes', current.scopes, { allowBlank: true })),
redirectUri: readConfigString(payload, 'redirectUri', current.redirectUri, {
allowBlank: true,
}),
scopes: normalizeScopeText(
readConfigString(payload, 'scopes', current.scopes, { allowBlank: true }),
),
authState: readConfigString(payload, 'authState', current.authState, { allowBlank: true }),
appKey: readConfigString(payload, 'appKey', current.appKey, { allowBlank: true }),
openId: readConfigString(payload, 'openId', current.openId, { allowBlank: true }),
grantedScopes: normalizeScopeText(readConfigString(payload, 'grantedScopes', current.grantedScopes, { allowBlank: true })),
grantedScopes: normalizeScopeText(
readConfigString(payload, 'grantedScopes', current.grantedScopes, { allowBlank: true }),
),
sellerId: readConfigString(payload, 'sellerId', current.sellerId, { allowBlank: true }),
provider: readConfigString(payload, 'provider', current.provider),
platform: readConfigString(payload, 'platform', current.platform),
@@ -245,33 +246,77 @@ function normalizeShopConfigPayload(
const payload = rawShop as JsonObject
const current = resolveCurrentShopConfig(payload, index, currentShops)
const sellerId = readConfigString(payload, 'sellerId', current?.sellerId || '', { allowBlank: true })
const shopId = readConfigString(payload, 'shopId', current?.shopId || sellerId, { allowBlank: true }) || sellerId
const shopName = readConfigString(payload, 'shopName', current?.shopName || '', { allowBlank: true })
const customShopName = readConfigString(payload, 'customShopName', current?.customShopName || '', { allowBlank: true })
const sellerId = readConfigString(payload, 'sellerId', current?.sellerId || '', {
allowBlank: true,
})
const shopId =
readConfigString(payload, 'shopId', current?.shopId || sellerId, { allowBlank: true }) ||
sellerId
const shopName = readConfigString(payload, 'shopName', current?.shopName || '', {
allowBlank: true,
})
const customShopName = readConfigString(
payload,
'customShopName',
current?.customShopName || '',
{ allowBlank: true },
)
const accessToken = readSecretString(payload, 'accessToken', current?.accessToken || '')
const refreshToken = readSecretString(payload, 'refreshToken', current?.refreshToken || '')
const openId = readConfigString(payload, 'openId', current?.openId || '', { allowBlank: true })
if (!sellerId && !shopId && !shopName && !customShopName && !accessToken && !refreshToken && !openId) {
if (
!sellerId &&
!shopId &&
!shopName &&
!customShopName &&
!accessToken &&
!refreshToken &&
!openId
) {
return null
}
return {
enabled: hasPayloadField(payload, 'enabled') ? payload.enabled !== false : current?.enabled !== false,
enabled: hasPayloadField(payload, 'enabled')
? payload.enabled !== false
: current?.enabled !== false,
sellerId,
shopId,
shopName,
customShopName,
authState: readConfigString(payload, 'authState', current?.authState || '', { allowBlank: true }),
authState: readConfigString(payload, 'authState', current?.authState || '', {
allowBlank: true,
}),
accessToken,
refreshToken,
accessTokenExpiresAt: readConfigString(payload, 'accessTokenExpiresAt', current?.accessTokenExpiresAt || '', { allowBlank: true }),
refreshTokenExpiresAt: readConfigString(payload, 'refreshTokenExpiresAt', current?.refreshTokenExpiresAt || '', { allowBlank: true }),
accessTokenExpiresAt: readConfigString(
payload,
'accessTokenExpiresAt',
current?.accessTokenExpiresAt || '',
{ allowBlank: true },
),
refreshTokenExpiresAt: readConfigString(
payload,
'refreshTokenExpiresAt',
current?.refreshTokenExpiresAt || '',
{ allowBlank: true },
),
openId,
grantedScopes: normalizeScopeText(readConfigString(payload, 'grantedScopes', current?.grantedScopes || '', { allowBlank: true })),
lastRefreshedAt: readConfigString(payload, 'lastRefreshedAt', current?.lastRefreshedAt || '', { allowBlank: true }),
lastRefreshError: readConfigString(payload, 'lastRefreshError', current?.lastRefreshError || '', { allowBlank: true }),
grantedScopes: normalizeScopeText(
readConfigString(payload, 'grantedScopes', current?.grantedScopes || '', {
allowBlank: true,
}),
),
lastRefreshedAt: readConfigString(payload, 'lastRefreshedAt', current?.lastRefreshedAt || '', {
allowBlank: true,
}),
lastRefreshError: readConfigString(
payload,
'lastRefreshError',
current?.lastRefreshError || '',
{ allowBlank: true },
),
...resolveShopSecretPatch(payload, current),
}
}
@@ -305,16 +350,14 @@ function resolveShopSecretPatch(
return patch
}
function readSecretString(
payload: JsonObject,
field: string,
fallback: string,
): string {
function readSecretString(payload: JsonObject, field: string, fallback: string): string {
const text = String(payload[field] || '').trim()
return text || fallback
}
function resolveAccessTokenStatus(config: Pick<KuaishouIndustrySourceConfig, 'accessToken' | 'accessTokenExpiresAt'>) {
function resolveAccessTokenStatus(
config: Pick<KuaishouIndustrySourceConfig, 'accessToken' | 'accessTokenExpiresAt'>,
) {
if (!config.accessToken) {
return {
status: 'missing',
@@ -94,23 +94,27 @@ function mapAdminNotificationConfig(config: JsonObject = {}) {
bark: {
enabled: bark.enabled !== false,
serverUrl: String(bark.serverUrl || 'https://api.day.app').trim(),
recipients: (Array.isArray(bark.recipients) ? bark.recipients : []).map((item: JsonObject) => ({
recipients: (Array.isArray(bark.recipients) ? bark.recipients : []).map(
(item: JsonObject) => ({
id: String(item.id || '').trim(),
name: String(item.name || '').trim(),
deviceKey: String(item.deviceKey || '').trim(),
deviceKeyMasked: maskSecret(item.deviceKey),
enabled: item.enabled !== false,
})),
}),
),
},
wpush: {
enabled: wpush.enabled !== false,
recipients: (Array.isArray(wpush.recipients) ? wpush.recipients : []).map((item: JsonObject) => ({
recipients: (Array.isArray(wpush.recipients) ? wpush.recipients : []).map(
(item: JsonObject) => ({
id: String(item.id || '').trim(),
name: String(item.name || '').trim(),
apiKey: String(item.apiKey || item.apikey || '').trim(),
apiKeyMasked: maskSecret(item.apiKey || item.apikey),
enabled: item.enabled !== false,
})),
}),
),
},
},
}
@@ -118,8 +122,7 @@ function mapAdminNotificationConfig(config: JsonObject = {}) {
function mapAdminScheduledJobsConfig(config: JsonObject = {}) {
const cloudtentaclesAccountMap = new Map(
listAdminCloudtentaclesMonitorAccounts()
.map((item) => [item.sourceKey, item]),
listAdminCloudtentaclesMonitorAccounts().map((item) => [item.sourceKey, item]),
)
return {
@@ -147,7 +150,8 @@ function listAdminCloudtentaclesMonitorAccounts() {
const sessionsConfig = getAllCloudtentaclesSessionStates()
const sessions: Record<string, JsonObject> = sessionsConfig.sessions || {}
return (Array.isArray(sourcesConfig.sources) ? sourcesConfig.sources : []).map((source) => {
return (Array.isArray(sourcesConfig.sources) ? sourcesConfig.sources : [])
.map((source) => {
const sourceKey = String(source.key || '').trim()
const session = sessions[sourceKey] || {}
const label = String(source.label || source.username || sourceKey).trim() || sourceKey
@@ -161,7 +165,8 @@ function listAdminCloudtentaclesMonitorAccounts() {
hasToken: Boolean(String(session.token || '').trim()),
loggedInAt: String(session.loggedInAt || '').trim(),
}
}).filter((item) => item.sourceKey)
})
.filter((item) => item.sourceKey)
}
function mapScheduledJobCloudtentaclesAccounts(
@@ -193,30 +198,35 @@ function mapScheduledJobCloudtentaclesAccounts(
},
] as const
})
.filter(Boolean) as Array<readonly [string, {
.filter(Boolean) as Array<
readonly [
string,
{
sourceKey: string
label: string
enabled: boolean
assetThreshold: number
}]>,
},
]
>,
)
// 读配置时合并全部 kuaishou-lewan 账号,避免前端/任务只看到历史 default。
const merged = Array.from(cloudtentaclesAccountMap.values()).map((option) => {
const merged = Array.from(cloudtentaclesAccountMap.values())
.map((option) => {
const sourceKey = String(option.sourceKey || '').trim()
const configured = configuredMap.get(sourceKey)
return {
sourceKey,
label: String(configured?.label || option.label || sourceKey).trim(),
enabled: configured
? configured.enabled !== false
: option.enabled !== false,
enabled: configured ? configured.enabled !== false : option.enabled !== false,
assetThreshold: normalizeNonNegativeNumber(
configured?.assetThreshold,
defaultAssetThreshold,
),
}
}).filter((item) => item.sourceKey)
})
.filter((item) => item.sourceKey)
const mergedKeys = new Set(merged.map((item) => item.sourceKey))
for (const [sourceKey, configured] of configuredMap.entries()) {
@@ -1,8 +1,5 @@
import { buildClaimUrl, createTaskClaimToken } from '../claim/claim-service.js'
import {
maskCode as maskCodeValue,
maskPhone as maskPhoneValue,
} from '../../utils/masking.js'
import { maskCode as maskCodeValue, maskPhone as maskPhoneValue } from '../../utils/masking.js'
import type { TaskRow } from '../../types/repository/rows.js'
@@ -31,7 +28,9 @@ export function isRecoverableTaskSessionCloseError(error: ErrorLike | null | und
}
export function normalizeManualDispatchOutcome(value: unknown): 'delivered' | 'failed' {
const normalized = String(value || '').trim().toLowerCase()
const normalized = String(value || '')
.trim()
.toLowerCase()
if (normalized === 'failed') {
return 'failed'
@@ -25,18 +25,13 @@ import {
refreshFulfillmentRole,
returnFulfillmentNumber,
} from '../../fulfillment/executors/registry.js'
import {
isKuaishouCloudTask,
normalizeKuaishouCloudFlow,
} from './kuaishou-cloud-helpers.js'
import { isKuaishouCloudTask, normalizeKuaishouCloudFlow } from './kuaishou-cloud-helpers.js'
import type {
AdminEntityIdInput,
AdminViewerSessionInput,
} from '../../../types/admin/read-inputs.js'
import type {
AdminTaskKuaishouIndustryConsumeInput,
} from '../../../types/admin/write-inputs.js'
import type { AdminTaskKuaishouIndustryConsumeInput } from '../../../types/admin/write-inputs.js'
import type { AdminTaskActionResponse } from '../../../types/admin/write-models.js'
import type { KuaishouIndustryVoucherRow, TaskRow } from '../../../types/repository/rows.js'
@@ -491,7 +486,9 @@ export async function consumeAdminTaskKuaishouIndustryVoucher(
return { task: mapTaskActionPayload(updatedTask || consumeTargetTask) }
}
async function getRequiredIndustryVoucherForTask(task: TaskRow): Promise<KuaishouIndustryVoucherRow> {
async function getRequiredIndustryVoucherForTask(
task: TaskRow,
): Promise<KuaishouIndustryVoucherRow> {
const vouchers = await listKuaishouIndustryVouchersByTaskId(task.id)
const firstVoucher = vouchers[0]
if (firstVoucher) {
@@ -499,7 +496,9 @@ async function getRequiredIndustryVoucherForTask(task: TaskRow): Promise<Kuaisho
}
const taskContext = parseTaskContext(task)
const voucherContext = normalizeAdminTaskIndustryVoucherContext(taskContext.kuaishouIndustryVoucher)
const voucherContext = normalizeAdminTaskIndustryVoucherContext(
taskContext.kuaishouIndustryVoucher,
)
const voucherCode = String(voucherContext.voucherCode || voucherContext.eticketId || '').trim()
if (voucherCode) {
const voucher = await findKuaishouIndustryVoucherByCode(voucherCode, task.platform_order_id)
@@ -539,12 +538,12 @@ async function getRequiredIndustryVoucherForTask(task: TaskRow): Promise<Kuaisho
}
function normalizeAdminTaskIndustryVoucherContext(value: unknown): JsonObject {
return value && typeof value === 'object' && !Array.isArray(value)
? value as JsonObject
: {}
return value && typeof value === 'object' && !Array.isArray(value) ? (value as JsonObject) : {}
}
async function resolveIndustryVouchersForOrder(platformOrderId: string): Promise<KuaishouIndustryVoucherRow[]> {
async function resolveIndustryVouchersForOrder(
platformOrderId: string,
): Promise<KuaishouIndustryVoucherRow[]> {
const normalizedOid = String(platformOrderId || '').trim()
if (!normalizedOid) {
return []
@@ -52,7 +52,9 @@ export type PreparedKuaishouCloudBindResource = {
bindUrl: string
}
export function resolvePersistedCloudtentaclesContext(sourceKeys: unknown[]): CloudtentaclesContext {
export function resolvePersistedCloudtentaclesContext(
sourceKeys: unknown[],
): CloudtentaclesContext {
try {
return resolvePersistedCloudtentaclesContextBySourceKeys(sourceKeys)
} catch (error) {
@@ -4,14 +4,8 @@ import { createTaskEvent } from '../../../repositories/task-event-repo.js'
import { createHttpError } from '../../../utils/http.js'
import { nowIso } from '../../../utils/time.js'
import { TASK_STATUS } from '../../../domain/task-status.js'
import {
canViewerCloseTask,
createAdminViewerContext,
} from '../admin-read-shared-helpers.js'
import {
getRequiredTask,
mapTaskActionPayload,
} from '../admin-task-read-helpers.js'
import { canViewerCloseTask, createAdminViewerContext } from '../admin-read-shared-helpers.js'
import { getRequiredTask, mapTaskActionPayload } from '../admin-task-read-helpers.js'
import type {
AdminEntityIdInput,
@@ -62,7 +56,10 @@ export async function closeAdminTask(
})
}
await createTaskEvent(task.id, 'task_closed', {
await createTaskEvent(
task.id,
'task_closed',
{
closedBy: session
? {
userId: session.userId,
@@ -71,7 +68,9 @@ export async function closeAdminTask(
}
: null,
claimTokenClosed: claimTokenId > 0,
}, now)
},
now,
)
return {
task: mapTaskActionPayload(updatedTask),
@@ -69,7 +69,11 @@ export function getClaimIdentityFromTask(task: Partial<TaskRow> | null | undefin
}
export function hasClaimExpectedUid(taskOrContext: unknown): boolean {
if (taskOrContext && typeof taskOrContext === 'object' && 'context_json' in (taskOrContext as object)) {
if (
taskOrContext &&
typeof taskOrContext === 'object' &&
'context_json' in (taskOrContext as object)
) {
return Boolean(getClaimIdentityFromTask(taskOrContext as TaskRow).expectedUid)
}
return Boolean(getClaimIdentityFromContext(taskOrContext).expectedUid)
@@ -158,16 +162,11 @@ export function buildClaimIdentityAdminSummary(
role.name || binding.roleName || options.taskRoleName || '',
).trim()
const boundUid = useShipped ? shippedUid : liveBoundUid
const boundRoleName = useShipped
? shippedRoleName || liveBoundRoleName
: liveBoundRoleName
const boundRoleName = useShipped ? shippedRoleName || liveBoundRoleName : liveBoundRoleName
const ready = Boolean(identity.expectedUid)
const uidMatched = ready && boundUid ? isClaimUidMatched(identity.expectedUid, boundUid) : null
const liveMismatched =
useShipped &&
liveBoundUid &&
shippedUid &&
!isClaimUidMatched(shippedUid, liveBoundUid)
useShipped && liveBoundUid && shippedUid && !isClaimUidMatched(shippedUid, liveBoundUid)
let note = '旧单或未提交 UID:用户须先打开领取页填写 UID,否则禁止自动发货'
if (ready) {
@@ -90,8 +90,5 @@ test('assertBoundUidMatchesExpected 在不匹配时抛错', () => {
})
test('assertClaimExpectedUidReady 要求 claimIdentity', () => {
assert.throws(
() => assertClaimExpectedUidReady({ context_json: '{}' }),
/填写游戏 UID/,
)
assert.throws(() => assertClaimExpectedUidReady({ context_json: '{}' }), /填写游戏 UID/)
})
@@ -42,9 +42,7 @@ export function resolveClaimTokenExpiration(
tokenTtlHours: unknown,
): string | null {
const ttlHours = Number(tokenTtlHours)
return Number.isFinite(ttlHours) && ttlHours > 0
? addHours(createdAt, ttlHours)
: null
return Number.isFinite(ttlHours) && ttlHours > 0 ? addHours(createdAt, ttlHours) : null
}
export function buildClaimUrl(token: string): string {
@@ -157,7 +157,12 @@ export function buildClaimDetailPayload({ claimToken, task, order, orderItem }:
}
}
function buildAffiliateDashClaimDetailPayload({ claimToken, task, order, orderItem }: ClaimContext) {
function buildAffiliateDashClaimDetailPayload({
claimToken,
task,
order,
orderItem,
}: ClaimContext) {
const context = parseTaskContext(task)
const claimIdentity = buildClaimIdentityPayload(context.claimIdentity)
const flow = normalizeAffiliateDashFlow(context.affiliateDash)
@@ -169,11 +174,13 @@ function buildAffiliateDashClaimDetailPayload({ claimToken, task, order, orderIt
skuCode: String(orderItem.sku_code || '').trim(),
quantity: Math.max(1, Number(orderItem.quantity || 1) || 1),
isBundle: false,
items: [{
items: [
{
cloudSkuId: 0,
name: productTitle,
quantity: Math.max(1, Number(orderItem.quantity || 1) || 1),
}],
},
],
}
return {
@@ -227,7 +234,12 @@ function buildAffiliateDashClaimDetailPayload({ claimToken, task, order, orderIt
}
}
function buildKuaishouFeifeiClaimDetailPayload({ claimToken, task, order, orderItem }: ClaimContext) {
function buildKuaishouFeifeiClaimDetailPayload({
claimToken,
task,
order,
orderItem,
}: ClaimContext) {
const context = parseTaskContext(task)
const claimIdentity = buildClaimIdentityPayload(context.claimIdentity)
const expectedUid = getClaimIdentityFromContext(context).expectedUid
@@ -237,11 +249,13 @@ function buildKuaishouFeifeiClaimDetailPayload({ claimToken, task, order, orderI
skuCode: String(orderItem.sku_code || '').trim(),
quantity: Math.max(1, Number(orderItem.quantity || 1) || 1),
isBundle: false,
items: [{
items: [
{
cloudSkuId: 0,
name: String(flow.productName || orderItem.sku_name || orderItem.sku_code || '').trim(),
quantity: Math.max(1, Number(orderItem.quantity || 1) || 1),
}],
},
],
}
return {
@@ -362,13 +376,16 @@ function buildClaimProductPayload(
quantity: item.quantity,
}))
.filter((item) => item.name)
const normalizedDeliveryItems = deliveryItems.length > 0
const normalizedDeliveryItems =
deliveryItems.length > 0
? mergeClaimProductItems(deliveryItems)
: [{
: [
{
cloudSkuId: 0,
name: displaySkuName,
quantity: Math.max(1, Number(orderItem.quantity || 1) || 1),
}]
},
]
return {
title: displaySkuName,
@@ -452,7 +469,9 @@ export function mapClaimKuaishouCloudFulfillment(task: TaskRow, order: OrderRow)
binding: {
prepareStatus: String(binding.prepareStatus || 'pending').trim() || 'pending',
cloudSourceKeys: Array.isArray(binding.cloudSourceKeys)
? binding.cloudSourceKeys.map((value: unknown) => String(value || '').trim()).filter(Boolean)
? binding.cloudSourceKeys
.map((value: unknown) => String(value || '').trim())
.filter(Boolean)
: [],
resolvedSourceKey: String(binding.resolvedSourceKey || '').trim(),
skuId: Number(binding.skuId || 0) || 0,
@@ -566,7 +585,9 @@ function normalizeClaimDeliveryItems(value: unknown, binding: JsonObject) {
const rawItems = Array.isArray(value) ? value : []
const items = rawItems
.map((item) => normalizeClaimDeliveryItem(item))
.filter((item): item is { cloudSkuId: number; cloudSkuName: string; quantity: number } => Boolean(item))
.filter((item): item is { cloudSkuId: number; cloudSkuName: string; quantity: number } =>
Boolean(item),
)
if (items.length > 0) {
return mergeClaimDeliveryItems(items)
@@ -577,11 +598,13 @@ function normalizeClaimDeliveryItems(value: unknown, binding: JsonObject) {
return []
}
return [{
return [
{
cloudSkuId,
cloudSkuName: String(binding.skuName || '').trim(),
quantity: 1,
}]
},
]
}
function normalizeClaimDeliveryItem(value: unknown) {
@@ -633,12 +656,13 @@ async function expireClaimContext(claimToken: ClaimTokenRow, task: TaskRow) {
let nextTask = task
if (!isTaskFinalStatus(task.task_status)) {
nextTask = await updateTask(task.id, {
nextTask =
(await updateTask(task.id, {
task_status: TASK_STATUS.EXPIRED,
user_action_status: TASK_STATUS.EXPIRED,
last_error: '领取链接已过期',
updated_at: now,
}) || task
})) || task
}
return {
@@ -61,7 +61,9 @@ async function requireExecutorAction<T>(
}
function isKuaishouCloudBindingReady(flow: ReturnType<typeof normalizeKuaishouCloudFlow>) {
return flow.binding.prepareStatus === 'ready' && Boolean(String(flow.binding.bindUrl || '').trim())
return (
flow.binding.prepareStatus === 'ready' && Boolean(String(flow.binding.bindUrl || '').trim())
)
}
/**
@@ -160,7 +162,8 @@ async function verifyIndustryVoucherTicket(
shopId: context.order.shop_id,
shopName: context.order.shop_name,
autoConsumeEnabled: true,
consumedAt: voucherContext.status === 'CONSUMED'
consumedAt:
voucherContext.status === 'CONSUMED'
? voucherContext.consumedAt || flow.consume.consumedAt || now
: flow.consume.consumedAt,
},
@@ -242,8 +245,10 @@ export async function getKuaishouCloudClaimDetail(token: unknown): Promise<Claim
// 已核销也要走 verify:内部会补 prepare 绑定资源
if (flow.consume.status !== 'success' || !isKuaishouCloudBindingReady(flow)) {
const now = nowIso()
const prepared: ClaimDetailPayload | null = await verifyIndustryVoucherTicket(context, now)
.catch(() => null)
const prepared: ClaimDetailPayload | null = await verifyIndustryVoucherTicket(
context,
now,
).catch(() => null)
if (prepared) {
return prepared
}
@@ -255,15 +260,14 @@ export async function getKuaishouCloudClaimDetail(token: unknown): Promise<Claim
const flow = normalizeKuaishouCloudFlow(taskContext.kuaishouCloudFulfillment)
const needsIndustryVoucherPrepare =
hasUsableIndustryVoucher(taskContext) &&
(
flow.ticket.status !== 'verified' ||
!isKuaishouCloudBindingReady(flow)
)
(flow.ticket.status !== 'verified' || !isKuaishouCloudBindingReady(flow))
if (needsIndustryVoucherPrepare) {
const now = nowIso()
const prepared: ClaimDetailPayload | null = await verifyIndustryVoucherTicket(context, now)
.catch(() => null)
const prepared: ClaimDetailPayload | null = await verifyIndustryVoucherTicket(
context,
now,
).catch(() => null)
if (prepared) {
return prepared
}
@@ -273,7 +277,8 @@ export async function getKuaishouCloudClaimDetail(token: unknown): Promise<Claim
if (!isKuaishouCloudMockTask(task)) {
const latestFlow = normalizeKuaishouCloudFlow(parseTaskContext(task).kuaishouCloudFulfillment)
if (
(latestFlow.ticket.status === 'verified' || hasUsableIndustryVoucher(parseTaskContext(task))) &&
(latestFlow.ticket.status === 'verified' ||
hasUsableIndustryVoucher(parseTaskContext(task))) &&
!isKuaishouCloudBindingReady(latestFlow)
) {
task = await ensureKuaishouCloudBindingPrepared(task, {
@@ -923,10 +928,11 @@ function hasUsableIndustryVoucher(context: JsonObject = {}) {
}
function normalizeIndustryVoucherContext(value: unknown): JsonObject {
const source = value && typeof value === 'object' && !Array.isArray(value)
? value as JsonObject
: {}
const status = String(source.status || 'UNUSED').trim().toUpperCase()
const source =
value && typeof value === 'object' && !Array.isArray(value) ? (value as JsonObject) : {}
const status = String(source.status || 'UNUSED')
.trim()
.toUpperCase()
return {
...source,
@@ -1,9 +1,6 @@
import fs from 'node:fs'
import {
listAppConfigEntries,
upsertAppConfigEntry,
} from '../../repositories/app-config-repo.js'
import { listAppConfigEntries, upsertAppConfigEntry } from '../../repositories/app-config-repo.js'
import { readJsonFile } from '../../utils/json-file-store.js'
type NormalizeJsonValue<T> = (value: unknown) => T
@@ -67,8 +67,8 @@ const JSON_CONFIG_MIGRATION_ITEMS: JsonConfigMigrationItem[] = [
]
export async function migrateJsonConfigFilesToDatabase() {
const migrated: Array<{ configKey: string, filePath: string }> = []
const skipped: Array<{ configKey: string, filePath: string, reason: string }> = []
const migrated: Array<{ configKey: string; filePath: string }> = []
const skipped: Array<{ configKey: string; filePath: string; reason: string }> = []
for (const item of JSON_CONFIG_MIGRATION_ITEMS) {
const filePath = path.join(DATA_DIR, item.fileName)
@@ -65,7 +65,11 @@ export function isDevMockEnabled(env: NodeJS.ProcessEnv = process.env): boolean
if (String(env.ENABLE_DEV_MOCK || '').trim() === '1') {
return true
}
if (String(env.ENABLE_DEV_MOCK || '').trim().toLowerCase() === 'true') {
if (
String(env.ENABLE_DEV_MOCK || '')
.trim()
.toLowerCase() === 'true'
) {
return true
}
return !isProductionLike(env)
@@ -111,14 +115,16 @@ export function getDevMockStatus() {
}
}
export async function createLewanMockClaim(input: {
export async function createLewanMockClaim(
input: {
step?: unknown
orderNo?: unknown
productNo?: unknown
uid?: unknown
items?: unknown
frontendBaseUrl?: unknown
} = {}): Promise<DevMockCreateResult> {
} = {},
): Promise<DevMockCreateResult> {
assertDevMockEnabled()
const step = normalizeLewanStep(input.step)
@@ -192,7 +198,10 @@ export async function createLewanMockClaim(input: {
})
if (!task) {
throw createHttpError('履约任务创建失败', { statusCode: 500, errorCode: 'dev_mock_task_failed' })
throw createHttpError('履约任务创建失败', {
statusCode: 500,
errorCode: 'dev_mock_task_failed',
})
}
const claimToken = await createTaskClaimToken(task.id)
@@ -223,7 +232,8 @@ export async function createLewanMockClaim(input: {
})
}
export async function createFeifeiMockClaim(input: {
export async function createFeifeiMockClaim(
input: {
step?: unknown
orderNo?: unknown
productName?: unknown
@@ -231,7 +241,8 @@ export async function createFeifeiMockClaim(input: {
uid?: unknown
h5Url?: unknown
frontendBaseUrl?: unknown
} = {}): Promise<DevMockCreateResult> {
} = {},
): Promise<DevMockCreateResult> {
assertDevMockEnabled()
const step = normalizeFeifeiStep(input.step)
@@ -296,7 +307,12 @@ export async function createFeifeiMockClaim(input: {
? TASK_STATUS.MANUAL_REVIEW
: TASK_STATUS.LINK_GENERATED,
deliveryStatus: step === 'completed' ? 'delivered' : 'pending',
resultCode: step === 'completed' ? 'kuaishou_feifei_completed' : step === 'failed' ? 'kuaishou_feifei_status_40' : '',
resultCode:
step === 'completed'
? 'kuaishou_feifei_completed'
: step === 'failed'
? 'kuaishou_feifei_status_40'
: '',
resultMessage: rechargeStatusLabel,
claimToken: '',
claimExpiresAt: null,
@@ -349,7 +365,10 @@ export async function createFeifeiMockClaim(input: {
})
if (!task) {
throw createHttpError('履约任务创建失败', { statusCode: 500, errorCode: 'dev_mock_task_failed' })
throw createHttpError('履约任务创建失败', {
statusCode: 500,
errorCode: 'dev_mock_task_failed',
})
}
const claimToken = await createTaskClaimToken(task.id)
@@ -383,14 +402,16 @@ type AffiliateDashMockStep = 'uid' | 'bind' | 'submitted' | 'completed' | 'faile
* 生成 affiliate-dash 领取 mock:不调用真实 affiliate-dash 平台。
* 上下文带 mock 标记,sync/refresh/submit 全部短路,领取页可走完四步。
*/
export async function createAffiliateDashMockClaim(input: {
export async function createAffiliateDashMockClaim(
input: {
step?: unknown
orderNo?: unknown
productName?: unknown
productSku?: unknown
uid?: unknown
frontendBaseUrl?: unknown
} = {}): Promise<DevMockCreateResult> {
} = {},
): Promise<DevMockCreateResult> {
assertDevMockEnabled()
const step = normalizeAffiliateDashStep(input.step)
@@ -457,11 +478,7 @@ export async function createAffiliateDashMockClaim(input: {
? TASK_STATUS.REDEEMING
: TASK_STATUS.LINK_GENERATED,
deliveryStatus:
step === 'completed'
? 'delivered'
: step === 'submitted'
? 'delivering'
: 'pending',
step === 'completed' ? 'delivered' : step === 'submitted' ? 'delivering' : 'pending',
resultCode:
step === 'completed'
? 'affiliate_dash_delivered'
@@ -541,7 +558,10 @@ export async function createAffiliateDashMockClaim(input: {
})
if (!task) {
throw createHttpError('履约任务创建失败', { statusCode: 500, errorCode: 'dev_mock_task_failed' })
throw createHttpError('履约任务创建失败', {
statusCode: 500,
errorCode: 'dev_mock_task_failed',
})
}
const claimToken = await createTaskClaimToken(task.id)
@@ -573,9 +593,11 @@ export async function createAffiliateDashMockClaim(input: {
* 生成电子凭证列表测试数据,不调用快手接口。
* 覆盖未使用、已核销、已销毁和发码失败等运营页面常见状态。
*/
export async function createKuaishouIndustryVoucherMockData(input: {
export async function createKuaishouIndustryVoucherMockData(
input: {
sellerId?: unknown
} = {}): Promise<DevMockKuaishouIndustryVoucherResult> {
} = {},
): Promise<DevMockKuaishouIndustryVoucherResult> {
assertDevMockEnabled()
const now = nowIso()
@@ -668,11 +690,13 @@ export async function createKuaishouIndustryVoucherMockData(input: {
/**
* 生成 91 查询请求体(带签名),可选对本机发起查询。
*/
export async function buildOpen91QueryMock(input: {
export async function buildOpen91QueryMock(
input: {
orderNo?: unknown
execute?: unknown
baseUrl?: unknown
} = {}) {
} = {},
) {
assertDevMockEnabled()
const orderNo = String(input.orderNo || '').trim()
@@ -767,7 +791,10 @@ async function ensureProfile(profileKey: string, name: string, requiresClaim: bo
})
if (!profile) {
throw createHttpError('履约配置创建失败', { statusCode: 500, errorCode: 'dev_mock_profile_failed' })
throw createHttpError('履约配置创建失败', {
statusCode: 500,
errorCode: 'dev_mock_profile_failed',
})
}
return profile
}
@@ -885,7 +912,10 @@ async function createBaseOrderItem(input: {
])
if (!orderItem) {
throw createHttpError('订单商品创建失败', { statusCode: 500, errorCode: 'dev_mock_item_failed' })
throw createHttpError('订单商品创建失败', {
statusCode: 500,
errorCode: 'dev_mock_item_failed',
})
}
return orderItem
}
@@ -931,9 +961,7 @@ function buildLewanMockContext(input: {
configId: `mock:${input.productName}`,
internalSkuCode: input.productName,
internalSkuName: input.productName,
deliveryItems: input.deliveryItems.length
? input.deliveryItems
: [primaryItem],
deliveryItems: input.deliveryItems.length ? input.deliveryItems : [primaryItem],
mock: {
enabled: true,
orderNo: input.orderNo,
@@ -960,9 +988,7 @@ function buildLewanMockContext(input: {
vnKey: '1',
vnId: roleReady ? 900001 : 0,
vnPhone: roleReady ? '13800000000' : '',
bindUrl: roleReady
? `https://example.com/mock-kuaishou-cloud-bind/${input.orderNo}`
: '',
bindUrl: roleReady ? `https://example.com/mock-kuaishou-cloud-bind/${input.orderNo}` : '',
bindPreparedAt: roleReady ? input.timestamp : null,
bindExpiresAt: roleReady ? addHours(input.timestamp, 24) : null,
bindProbeAt: roleReady ? input.timestamp : null,
@@ -1190,7 +1216,10 @@ function buildAffiliateDashTips(step: AffiliateDashMockStep, uid: string) {
return [`打开领取链接,Step1 填 UID${uid}(或自定义)`, '提交后进入绑定步,mock 会给出二维码']
}
if (step === 'bind') {
return [`已预填 UID=${uid},绑定二维码为 mock 生成(扫描无效)`, '正常流程:扫码完成真实绑定后自动进入下一步']
return [
`已预填 UID=${uid},绑定二维码为 mock 生成(扫描无效)`,
'正常流程:扫码完成真实绑定后自动进入下一步',
]
}
if (step === 'submitted') {
return ['已模拟绑定成功,可点「提交发货」(mock 直接模拟发货成功)']

Some files were not shown because too many files have changed in this diff Show More