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