From 0e6bf88beb861342ed33e2ae8dd78332873e4646 Mon Sep 17 00:00:00 2001 From: yml Date: Thu, 21 May 2026 22:57:48 +0800 Subject: [PATCH] [Snow Team] express-types: auto-commit work --- apps/backend/package-lock.json | 89 +++ apps/backend/package.json | 1 + apps/backend/src/app.ts | 3 +- apps/backend/src/middleware/access-log.ts | 16 +- apps/backend/src/middleware/cors.ts | 13 +- apps/backend/src/routes/admin/shared.ts | 33 +- apps/backend/src/routes/admin/tasks.ts | 674 ++++++++++++---------- apps/backend/src/routes/admin/users.ts | 8 +- apps/backend/src/types/express.d.ts | 12 + apps/backend/src/utils/http.ts | 253 ++++---- 10 files changed, 639 insertions(+), 463 deletions(-) create mode 100644 apps/backend/src/types/express.d.ts diff --git a/apps/backend/package-lock.json b/apps/backend/package-lock.json index ee21cd9f..9e587fd5 100644 --- a/apps/backend/package-lock.json +++ b/apps/backend/package-lock.json @@ -13,6 +13,7 @@ "playwright": "1.42.1" }, "devDependencies": { + "@types/express": "^5.0.6", "@types/node": "^25.6.0", "@types/pg": "^8.20.0", "tsx": "^4.22.3", @@ -461,6 +462,59 @@ "node": ">=18" } }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/express": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.6.tgz", + "integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^5.0.0", + "@types/serve-static": "^2" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.1.tgz", + "integrity": "sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/node": { "version": "25.6.0", "resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.0.tgz", @@ -483,6 +537,41 @@ "pg-types": "^2.2.0" } }, + "node_modules/@types/qs": { + "version": "6.15.1", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.1.tgz", + "integrity": "sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*" + } + }, "node_modules/accepts": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", diff --git a/apps/backend/package.json b/apps/backend/package.json index 4302ee3b..050fab74 100644 --- a/apps/backend/package.json +++ b/apps/backend/package.json @@ -23,6 +23,7 @@ "playwright": "1.42.1" }, "devDependencies": { + "@types/express": "^5.0.6", "@types/node": "^25.6.0", "@types/pg": "^8.20.0", "tsx": "^4.22.3", diff --git a/apps/backend/src/app.ts b/apps/backend/src/app.ts index 1053e348..d0464acd 100644 --- a/apps/backend/src/app.ts +++ b/apps/backend/src/app.ts @@ -1,3 +1,4 @@ +import type { Request, Response, NextFunction } from "express"; import express from "express"; import process from "node:process"; @@ -92,7 +93,7 @@ export function createApp({ app.use("/api/v1/claim", claimsRouter); app.use("/api/v1/admin", adminRouter); - app.use((err, _req, res, _next) => { + app.use((err: unknown, _req: Request, res: Response, _next: NextFunction) => { sendRouteError(res, err, "服务内部错误", "[global]"); }); diff --git a/apps/backend/src/middleware/access-log.ts b/apps/backend/src/middleware/access-log.ts index bfa02d2d..cd3caa36 100644 --- a/apps/backend/src/middleware/access-log.ts +++ b/apps/backend/src/middleware/access-log.ts @@ -1,6 +1,11 @@ +import type { Request, Response, NextFunction } from "express"; import { createRequestId, logInfo } from "../utils/logger.js"; -export function accessLogMiddleware(req, res, next) { +export function accessLogMiddleware( + req: Request, + res: Response, + next: NextFunction +): void { const startedAt = Date.now(); const requestId = resolveRequestId(req); @@ -32,12 +37,15 @@ export function accessLogMiddleware(req, res, next) { next(); } -function resolveRequestId(req) { +function resolveRequestId(req: Request): string { const fromHeader = String(req.headers["x-request-id"] || "").trim(); return fromHeader || createRequestId("req"); } -function shouldSkipAccessLog(originalUrl, statusCode) { +function shouldSkipAccessLog(originalUrl: string, statusCode: number): boolean { const pathname = String(originalUrl || "").split("?")[0]; - return ["/health", "/health/live", "/health/ready"].includes(pathname) && Number(statusCode) < 400; + return ( + ["/health", "/health/live", "/health/ready"].includes(pathname) && + Number(statusCode) < 400 + ); } diff --git a/apps/backend/src/middleware/cors.ts b/apps/backend/src/middleware/cors.ts index 570508ae..68ce7c7e 100644 --- a/apps/backend/src/middleware/cors.ts +++ b/apps/backend/src/middleware/cors.ts @@ -1,7 +1,16 @@ -export function corsMiddleware(req, res, next) { +import type { Request, Response, NextFunction } from "express"; + +export function corsMiddleware( + req: Request, + res: Response, + next: NextFunction +): void { res.setHeader("Access-Control-Allow-Origin", "*"); res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization"); - res.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS"); + res.setHeader( + "Access-Control-Allow-Methods", + "GET, POST, PUT, PATCH, DELETE, OPTIONS" + ); if (req.method === "OPTIONS") { res.sendStatus(204); diff --git a/apps/backend/src/routes/admin/shared.ts b/apps/backend/src/routes/admin/shared.ts index bd33a3b8..a00e302f 100644 --- a/apps/backend/src/routes/admin/shared.ts +++ b/apps/backend/src/routes/admin/shared.ts @@ -1,23 +1,10 @@ +import type { Request, Response, NextFunction } from 'express' import { requireAdminRole, verifyAdminSessionToken } from '../../services/admin/admin-auth-service.js' import { writeAdminAuditLog } from '../../services/admin/admin-audit-service.js' import { buildSuccessPayload, createHttpError, sendRouteError } from '../../utils/http.js' import { logWarn } from '../../utils/logger.js' import type { AdminSession } from '../../services/admin/admin-auth-service.js' -type NextFunction = (error?: unknown) => void -type ExpressResponse = { - json: (body: unknown) => unknown - sendFile: (path: string) => unknown -} -export type AdminExpressRequest = { - headers: { authorization?: string | string[] } - body?: any - params?: Record - query?: Record - adminSession?: AdminSession | null - [key: string]: unknown -} - type AdminAuditPayload = { action?: string targetType?: string @@ -29,7 +16,7 @@ type AdminJsonHandlerOptions = { successMessage?: string errorMessage?: string scope?: string - audit?: (req: AdminExpressRequest, data: unknown) => AdminAuditPayload | null | undefined + audit?: (req: Request, data: unknown) => AdminAuditPayload | null | undefined } type AdminFileHandlerOptions = { @@ -38,10 +25,10 @@ type AdminFileHandlerOptions = { } export function createJsonHandler( - action: (req: AdminExpressRequest, res: ExpressResponse) => unknown | Promise, + action: (req: Request, res: Response) => unknown | Promise, { successMessage = 'ok', errorMessage, scope, audit }: AdminJsonHandlerOptions = {}, ) { - return async (req, res) => { + return async (req: Request, res: Response): Promise => { try { const data = await action(req, res) await recordAdminAudit(req.adminSession, req, data, audit) @@ -53,10 +40,10 @@ export function createJsonHandler( } export function createFileHandler( - action: (req: AdminExpressRequest, res: ExpressResponse) => string | Promise, + action: (req: Request, res: Response) => string | Promise, { errorMessage, scope }: AdminFileHandlerOptions = {}, ) { - return async (req, res) => { + return async (req: Request, res: Response): Promise => { try { const filePath = await action(req, res) res.sendFile(filePath) @@ -66,7 +53,7 @@ export function createFileHandler( } } -export async function requireAdminSession(req: AdminExpressRequest, res: ExpressResponse, next: NextFunction) { +export async function requireAdminSession(req: Request, res: Response, next: NextFunction): Promise { try { req.adminSession = await verifyAdminSessionToken(extractBearerToken(req)) next() @@ -76,7 +63,7 @@ export async function requireAdminSession(req: AdminExpressRequest, res: Express } export function requireAdminRoles(allowedRoles: string[]) { - return (req, res, next) => { + return (req: Request, res: Response, next: NextFunction): void => { try { requireAdminRole(req.adminSession, allowedRoles) next() @@ -88,7 +75,7 @@ export function requireAdminRoles(allowedRoles: string[]) { async function recordAdminAudit( session: AdminSession | null | undefined, - req: AdminExpressRequest, + req: Request, data: unknown, audit: AdminJsonHandlerOptions['audit'], ) { @@ -114,7 +101,7 @@ async function recordAdminAudit( } } -export function extractBearerToken(req: AdminExpressRequest) { +export function extractBearerToken(req: Request): string { const authorization = String(req.headers.authorization || '').trim() const matched = authorization.match(/^Bearer\s+(.+)$/i) diff --git a/apps/backend/src/routes/admin/tasks.ts b/apps/backend/src/routes/admin/tasks.ts index 75a60fc6..f60f7b1d 100644 --- a/apps/backend/src/routes/admin/tasks.ts +++ b/apps/backend/src/routes/admin/tasks.ts @@ -1,4 +1,4 @@ -import { Router } from 'express' +import { Router } from "express"; import { closeAdminTask, @@ -14,378 +14,420 @@ import { releaseAdminTaskInventory, returnNumberAdminTaskKuaishouCloudFulfillment, retryAdminTask, -} from '../../services/admin/admin-write-service.js' +} from "../../services/admin/admin-write-service.js"; import { getAdminTaskDetail, getAdminTaskScreenshotPath, getAdminTasks, -} from '../../services/admin/admin-read-service.js' -import { createFileHandler, createJsonHandler, requireAdminRoles } from './shared.js' -import type { AdminExpressRequest } from './shared.js' +} from "../../services/admin/admin-read-service.js"; +import { + createFileHandler, + createJsonHandler, + requireAdminRoles, +} from "./shared.js"; +import type { Request } from "express"; import type { AdminTaskKuaishouCloudDispatchRouteBody, AdminTaskManualDispatchRouteBody, AdminTaskRouteParams, AdminTaskRouteQuery, -} from '../../types/admin-route-inputs.js' +} from "../../types/admin-route-inputs.js"; import type { AdminTaskActionResponse, AdminTaskBindingReleaseResponse, AdminTaskManualDispatchResponse, -} from '../../types/admin-write-models.js' +} from "../../types/admin-write-models.js"; -const router = Router() +const router = Router(); -function getTaskId(req: AdminExpressRequest): string | undefined { - return (req.params as AdminTaskRouteParams).taskId +function getTaskId(req: Request): string | undefined { + return (req.params as AdminTaskRouteParams).taskId; } -function getBindingId(req: AdminExpressRequest): string | undefined { - return (req.params as AdminTaskRouteParams).bindingId +function getBindingId(req: Request): string | undefined { + return (req.params as AdminTaskRouteParams).bindingId; } -router.get('/tasks', createJsonHandler( - (req) => getAdminTasks( - req.query as AdminTaskRouteQuery, - req.adminSession || null, - ), - { - successMessage: 'ok', - errorMessage: '读取任务列表失败', - scope: '[admin/tasks]', - }, -)) +router.get( + "/tasks", + createJsonHandler( + (req) => + getAdminTasks(req.query as AdminTaskRouteQuery, req.adminSession || null), + { + successMessage: "ok", + errorMessage: "读取任务列表失败", + scope: "[admin/tasks]", + } + ) +); -router.get('/tasks/:taskId', createJsonHandler( - (req) => getAdminTaskDetail( - getTaskId(req), - req.adminSession || null, - ), - { - successMessage: 'ok', - errorMessage: '读取任务详情失败', - scope: '[admin/tasks/:taskId]', - }, -)) +router.get( + "/tasks/:taskId", + createJsonHandler( + (req) => getAdminTaskDetail(getTaskId(req), req.adminSession || null), + { + successMessage: "ok", + errorMessage: "读取任务详情失败", + scope: "[admin/tasks/:taskId]", + } + ) +); -router.get('/tasks/:taskId/screenshot', createFileHandler( - (req) => getAdminTaskScreenshotPath( - getTaskId(req), - req.adminSession || null, - ), - { - errorMessage: '读取任务截图失败', - scope: '[admin/tasks/:taskId/screenshot]', - }, -)) +router.get( + "/tasks/:taskId/screenshot", + createFileHandler( + (req) => + getAdminTaskScreenshotPath(getTaskId(req), req.adminSession || null), + { + errorMessage: "读取任务截图失败", + scope: "[admin/tasks/:taskId/screenshot]", + } + ) +); -router.post('/tasks/:taskId/retry', requireAdminRoles(['admin', 'operator']), createJsonHandler( - (req) => retryAdminTask(getTaskId(req)), - { - successMessage: '任务已重试', - errorMessage: '重试任务失败', - scope: '[admin/tasks/:taskId/retry]', - }, -)) +router.post( + "/tasks/:taskId/retry", + requireAdminRoles(["admin", "operator"]), + createJsonHandler((req) => retryAdminTask(getTaskId(req)), { + successMessage: "任务已重试", + errorMessage: "重试任务失败", + scope: "[admin/tasks/:taskId/retry]", + }) +); -router.post('/tasks/:taskId/release-inventory', requireAdminRoles(['admin']), createJsonHandler( - (req) => releaseAdminTaskInventory(getTaskId(req)), - { - successMessage: '库存凭据已释放', - errorMessage: '释放库存凭据失败', - scope: '[admin/tasks/:taskId/release-inventory]', +router.post( + "/tasks/:taskId/release-inventory", + requireAdminRoles(["admin"]), + createJsonHandler((req) => releaseAdminTaskInventory(getTaskId(req)), { + successMessage: "库存凭据已释放", + errorMessage: "释放库存凭据失败", + scope: "[admin/tasks/:taskId/release-inventory]", audit: (req, data) => { - const result = data as AdminTaskActionResponse + const result = data as AdminTaskActionResponse; return { - action: 'task_release_inventory', - targetType: 'task', + action: "task_release_inventory", + targetType: "task", targetId: String(getTaskId(req)), data: { taskId: result.task.taskId, taskNo: result.task.taskNo, inventoryItemId: result.task.inventoryItemId, }, - } + }; }, - }, -)) + }) +); -router.post('/tasks/:taskId/inventory-bindings/:bindingId/release', requireAdminRoles(['admin']), createJsonHandler( - (req) => releaseAdminTaskInventoryBinding( - getTaskId(req), - getBindingId(req), - ), - { - successMessage: '库存绑定已释放', - errorMessage: '释放库存绑定失败', - scope: '[admin/tasks/:taskId/inventory-bindings/:bindingId/release]', - audit: (req, data) => { - const result = data as AdminTaskBindingReleaseResponse - return { - action: 'task_release_inventory_binding', - targetType: 'task_inventory_binding', - targetId: String(getBindingId(req)), - data: { - taskId: result.task.taskId, - taskNo: result.task.taskNo, - bindingId: result.bindingId, - inventoryItemId: result.inventoryItemId, - }, - } - }, - }, -)) +router.post( + "/tasks/:taskId/inventory-bindings/:bindingId/release", + requireAdminRoles(["admin"]), + createJsonHandler( + (req) => + releaseAdminTaskInventoryBinding(getTaskId(req), getBindingId(req)), + { + successMessage: "库存绑定已释放", + errorMessage: "释放库存绑定失败", + scope: "[admin/tasks/:taskId/inventory-bindings/:bindingId/release]", + audit: (req, data) => { + const result = data as AdminTaskBindingReleaseResponse; + return { + action: "task_release_inventory_binding", + targetType: "task_inventory_binding", + targetId: String(getBindingId(req)), + data: { + taskId: result.task.taskId, + taskNo: result.task.taskNo, + bindingId: result.bindingId, + inventoryItemId: result.inventoryItemId, + }, + }; + }, + } + ) +); -router.post('/tasks/:taskId/regenerate-claim-link', requireAdminRoles(['admin', 'operator', 'support']), createJsonHandler( - (req) => regenerateAdminTaskClaimLink( - getTaskId(req), - req.adminSession || null, - ), - { - successMessage: '领取链接已重新生成', - errorMessage: '重新生成领取链接失败', - scope: '[admin/tasks/:taskId/regenerate-claim-link]', - audit: (req, data) => { - const result = data as AdminTaskActionResponse - return { - action: 'task_regenerate_claim_link', - targetType: 'task', - targetId: String(getTaskId(req)), - data: { - taskId: result.task.taskId, - taskNo: result.task.taskNo, - primaryClaimTokenId: result.task.primaryClaimTokenId, - }, - } - }, - }, -)) +router.post( + "/tasks/:taskId/regenerate-claim-link", + requireAdminRoles(["admin", "operator", "support"]), + createJsonHandler( + (req) => + regenerateAdminTaskClaimLink(getTaskId(req), req.adminSession || null), + { + successMessage: "领取链接已重新生成", + errorMessage: "重新生成领取链接失败", + scope: "[admin/tasks/:taskId/regenerate-claim-link]", + audit: (req, data) => { + const result = data as AdminTaskActionResponse; + return { + action: "task_regenerate_claim_link", + targetType: "task", + targetId: String(getTaskId(req)), + data: { + taskId: result.task.taskId, + taskNo: result.task.taskNo, + primaryClaimTokenId: result.task.primaryClaimTokenId, + }, + }; + }, + } + ) +); -router.post('/tasks/:taskId/support-confirm-role', requireAdminRoles(['admin', 'operator', 'support']), createJsonHandler( - (req) => confirmAdminTaskAssistedRole( - getTaskId(req), - req.adminSession || null, - ), - { - successMessage: '角色已确认', - errorMessage: '客服确认角色失败', - scope: '[admin/tasks/:taskId/support-confirm-role]', +router.post( + "/tasks/:taskId/support-confirm-role", + requireAdminRoles(["admin", "operator", "support"]), + createJsonHandler( + (req) => + confirmAdminTaskAssistedRole(getTaskId(req), req.adminSession || null), + { + successMessage: "角色已确认", + errorMessage: "客服确认角色失败", + scope: "[admin/tasks/:taskId/support-confirm-role]", + audit: (req, data) => { + const result = data as AdminTaskActionResponse; + return { + action: "task_support_confirm_role", + targetType: "task", + targetId: String(getTaskId(req)), + data: { + taskId: result.task.taskId, + taskNo: result.task.taskNo, + status: result.task.status, + }, + }; + }, + } + ) +); + +router.post( + "/tasks/:taskId/support-redeem", + requireAdminRoles(["admin", "operator", "support"]), + createJsonHandler( + (req) => redeemAdminTaskAssisted(getTaskId(req), req.adminSession || null), + { + successMessage: "兑换任务已启动", + errorMessage: "客服发起兑换失败", + scope: "[admin/tasks/:taskId/support-redeem]", + audit: (req, data) => { + const result = data as AdminTaskActionResponse; + return { + action: "task_support_redeem", + targetType: "task", + targetId: String(getTaskId(req)), + data: { + taskId: result.task.taskId, + taskNo: result.task.taskNo, + status: result.task.status, + deliveryStatus: result.task.deliveryStatus, + }, + }; + }, + } + ) +); + +router.post( + "/tasks/:taskId/close", + requireAdminRoles(["admin", "operator", "support"]), + createJsonHandler((req) => closeAdminTask(getTaskId(req)), { + successMessage: "任务已关闭", + errorMessage: "关闭任务失败", + scope: "[admin/tasks/:taskId/close]", audit: (req, data) => { - const result = data as AdminTaskActionResponse + const result = data as AdminTaskActionResponse; return { - action: 'task_support_confirm_role', - targetType: 'task', + action: "task_closed", + targetType: "task", targetId: String(getTaskId(req)), data: { taskId: result.task.taskId, taskNo: result.task.taskNo, status: result.task.status, }, - } + }; }, - }, -)) + }) +); -router.post('/tasks/:taskId/support-redeem', requireAdminRoles(['admin', 'operator', 'support']), createJsonHandler( - (req) => redeemAdminTaskAssisted( - getTaskId(req), - req.adminSession || null, - ), - { - successMessage: '兑换任务已启动', - errorMessage: '客服发起兑换失败', - scope: '[admin/tasks/:taskId/support-redeem]', +router.post( + "/tasks/:taskId/mark-manual-review", + requireAdminRoles(["admin", "operator"]), + createJsonHandler((req) => markAdminTaskManualReview(getTaskId(req)), { + successMessage: "任务已转人工处理", + errorMessage: "标记人工处理失败", + scope: "[admin/tasks/:taskId/mark-manual-review]", audit: (req, data) => { - const result = data as AdminTaskActionResponse + const result = data as AdminTaskActionResponse; return { - action: 'task_support_redeem', - targetType: 'task', - targetId: String(getTaskId(req)), - data: { - taskId: result.task.taskId, - taskNo: result.task.taskNo, - status: result.task.status, - deliveryStatus: result.task.deliveryStatus, - }, - } - }, - }, -)) - -router.post('/tasks/:taskId/close', requireAdminRoles(['admin', 'operator', 'support']), createJsonHandler( - (req) => closeAdminTask(getTaskId(req)), - { - successMessage: '任务已关闭', - errorMessage: '关闭任务失败', - scope: '[admin/tasks/:taskId/close]', - audit: (req, data) => { - const result = data as AdminTaskActionResponse - return { - action: 'task_closed', - targetType: 'task', + action: "task_mark_manual_review", + targetType: "task", targetId: String(getTaskId(req)), data: { taskId: result.task.taskId, taskNo: result.task.taskNo, status: result.task.status, }, - } + }; }, - }, -)) + }) +); -router.post('/tasks/:taskId/mark-manual-review', requireAdminRoles(['admin', 'operator']), createJsonHandler( - (req) => markAdminTaskManualReview(getTaskId(req)), - { - successMessage: '任务已转人工处理', - errorMessage: '标记人工处理失败', - scope: '[admin/tasks/:taskId/mark-manual-review]', - audit: (req, data) => { - const result = data as AdminTaskActionResponse - return { - action: 'task_mark_manual_review', - targetType: 'task', - targetId: String(getTaskId(req)), - data: { - taskId: result.task.taskId, - taskNo: result.task.taskNo, - status: result.task.status, - }, - } - }, - }, -)) +router.post( + "/tasks/:taskId/complete-manual-dispatch", + requireAdminRoles(["admin", "operator"]), + createJsonHandler( + (req) => + completeAdminTaskManualDispatch( + getTaskId(req), + req.body as AdminTaskManualDispatchRouteBody, + req.adminSession || null + ), + { + successMessage: "人工履约结果已回写", + errorMessage: "回写人工履约结果失败", + scope: "[admin/tasks/:taskId/complete-manual-dispatch]", + audit: (req, data) => { + const result = data as AdminTaskManualDispatchResponse; + return { + action: "task_complete_manual_dispatch", + targetType: "task", + targetId: String(getTaskId(req)), + data: { + taskId: result.task.taskId, + taskNo: result.task.taskNo, + outcome: result.outcome, + deliveryStatus: result.task.deliveryStatus, + resultCode: result.task.resultCode, + }, + }; + }, + } + ) +); -router.post('/tasks/:taskId/complete-manual-dispatch', requireAdminRoles(['admin', 'operator']), createJsonHandler( - (req) => completeAdminTaskManualDispatch( - getTaskId(req), - req.body as AdminTaskManualDispatchRouteBody, - req.adminSession || null, - ), - { - successMessage: '人工履约结果已回写', - errorMessage: '回写人工履约结果失败', - scope: '[admin/tasks/:taskId/complete-manual-dispatch]', - audit: (req, data) => { - const result = data as AdminTaskManualDispatchResponse - return { - action: 'task_complete_manual_dispatch', - targetType: 'task', - targetId: String(getTaskId(req)), - data: { - taskId: result.task.taskId, - taskNo: result.task.taskNo, - outcome: result.outcome, - deliveryStatus: result.task.deliveryStatus, - resultCode: result.task.resultCode, - }, - } - }, - }, -)) +router.post( + "/tasks/:taskId/kuaishou-cloud/prepare", + requireAdminRoles(["admin", "operator"]), + createJsonHandler( + (req) => + prepareAdminTaskKuaishouCloudFulfillment( + getTaskId(req), + req.adminSession || null + ), + { + successMessage: "绑定资源已准备完成", + errorMessage: "准备绑定资源失败", + scope: "[admin/tasks/:taskId/kuaishou-cloud/prepare]", + audit: (req, data) => { + const result = data as AdminTaskActionResponse; + return { + action: "task_kuaishou_cloud_prepare", + targetType: "task", + targetId: String(getTaskId(req)), + data: { + taskId: result.task.taskId, + taskNo: result.task.taskNo, + status: result.task.status, + }, + }; + }, + } + ) +); -router.post('/tasks/:taskId/kuaishou-cloud/prepare', requireAdminRoles(['admin', 'operator']), createJsonHandler( - (req) => prepareAdminTaskKuaishouCloudFulfillment( - getTaskId(req), - req.adminSession || null, - ), - { - successMessage: '绑定资源已准备完成', - errorMessage: '准备绑定资源失败', - scope: '[admin/tasks/:taskId/kuaishou-cloud/prepare]', - audit: (req, data) => { - const result = data as AdminTaskActionResponse - return { - action: 'task_kuaishou_cloud_prepare', - targetType: 'task', - targetId: String(getTaskId(req)), - data: { - taskId: result.task.taskId, - taskNo: result.task.taskNo, - status: result.task.status, - }, - } - }, - }, -)) +router.post( + "/tasks/:taskId/kuaishou-cloud/refresh-role-info", + requireAdminRoles(["admin", "operator", "support"]), + createJsonHandler( + (req) => + refreshAdminTaskKuaishouCloudRoleInfo( + getTaskId(req), + req.adminSession || null + ), + { + successMessage: "角色信息已刷新", + errorMessage: "刷新角色信息失败", + scope: "[admin/tasks/:taskId/kuaishou-cloud/refresh-role-info]", + audit: (req, data) => { + const result = data as AdminTaskActionResponse; + return { + action: "task_kuaishou_cloud_role_info_refreshed", + targetType: "task", + targetId: String(getTaskId(req)), + data: { + taskId: result.task.taskId, + taskNo: result.task.taskNo, + status: result.task.status, + }, + }; + }, + } + ) +); -router.post('/tasks/:taskId/kuaishou-cloud/refresh-role-info', requireAdminRoles(['admin', 'operator', 'support']), createJsonHandler( - (req) => refreshAdminTaskKuaishouCloudRoleInfo( - getTaskId(req), - req.adminSession || null, - ), - { - successMessage: '角色信息已刷新', - errorMessage: '刷新角色信息失败', - scope: '[admin/tasks/:taskId/kuaishou-cloud/refresh-role-info]', - audit: (req, data) => { - const result = data as AdminTaskActionResponse - return { - action: 'task_kuaishou_cloud_role_info_refreshed', - targetType: 'task', - targetId: String(getTaskId(req)), - data: { - taskId: result.task.taskId, - taskNo: result.task.taskNo, - status: result.task.status, - }, - } - }, - }, -)) +router.post( + "/tasks/:taskId/kuaishou-cloud/dispatch", + requireAdminRoles(["admin", "operator"]), + createJsonHandler( + (req) => + dispatchAdminTaskKuaishouCloudFulfillment( + getTaskId(req), + req.body as AdminTaskKuaishouCloudDispatchRouteBody, + req.adminSession || null + ), + { + successMessage: "已完成绑定确认并发货", + errorMessage: "执行发货失败", + scope: "[admin/tasks/:taskId/kuaishou-cloud/dispatch]", + audit: (req, data) => { + const result = data as AdminTaskActionResponse; + const body = req.body as AdminTaskKuaishouCloudDispatchRouteBody; + return { + action: "task_kuaishou_cloud_dispatch", + targetType: "task", + targetId: String(getTaskId(req)), + data: { + taskId: result.task.taskId, + taskNo: result.task.taskNo, + status: result.task.status, + deliveryStatus: result.task.deliveryStatus, + ticketCodeProvided: Boolean(String(body.ticketCode || "").trim()), + }, + }; + }, + } + ) +); -router.post('/tasks/:taskId/kuaishou-cloud/dispatch', requireAdminRoles(['admin', 'operator']), createJsonHandler( - (req) => dispatchAdminTaskKuaishouCloudFulfillment( - getTaskId(req), - req.body as AdminTaskKuaishouCloudDispatchRouteBody, - req.adminSession || null, - ), - { - successMessage: '已完成绑定确认并发货', - errorMessage: '执行发货失败', - scope: '[admin/tasks/:taskId/kuaishou-cloud/dispatch]', - audit: (req, data) => { - const result = data as AdminTaskActionResponse - const body = req.body as AdminTaskKuaishouCloudDispatchRouteBody - return { - action: 'task_kuaishou_cloud_dispatch', - targetType: 'task', - targetId: String(getTaskId(req)), - data: { - taskId: result.task.taskId, - taskNo: result.task.taskNo, - status: result.task.status, - deliveryStatus: result.task.deliveryStatus, - ticketCodeProvided: Boolean(String(body.ticketCode || '').trim()), - }, - } - }, - }, -)) +router.post( + "/tasks/:taskId/kuaishou-cloud/return-number", + requireAdminRoles(["admin", "operator"]), + createJsonHandler( + (req) => + returnNumberAdminTaskKuaishouCloudFulfillment( + getTaskId(req), + req.adminSession || null + ), + { + successMessage: "号码已退还", + errorMessage: "退还号码失败", + scope: "[admin/tasks/:taskId/kuaishou-cloud/return-number]", + audit: (req, data) => { + const result = data as AdminTaskActionResponse; + return { + action: "task_kuaishou_cloud_return_number", + targetType: "task", + targetId: String(getTaskId(req)), + data: { + taskId: result.task.taskId, + taskNo: result.task.taskNo, + status: result.task.status, + }, + }; + }, + } + ) +); -router.post('/tasks/:taskId/kuaishou-cloud/return-number', requireAdminRoles(['admin', 'operator']), createJsonHandler( - (req) => returnNumberAdminTaskKuaishouCloudFulfillment( - getTaskId(req), - req.adminSession || null, - ), - { - successMessage: '号码已退还', - errorMessage: '退还号码失败', - scope: '[admin/tasks/:taskId/kuaishou-cloud/return-number]', - audit: (req, data) => { - const result = data as AdminTaskActionResponse - return { - action: 'task_kuaishou_cloud_return_number', - targetType: 'task', - targetId: String(getTaskId(req)), - data: { - taskId: result.task.taskId, - taskNo: result.task.taskNo, - status: result.task.status, - }, - } - }, - }, -)) - -export default router +export default router; diff --git a/apps/backend/src/routes/admin/users.ts b/apps/backend/src/routes/admin/users.ts index f3115f32..bf55f15f 100644 --- a/apps/backend/src/routes/admin/users.ts +++ b/apps/backend/src/routes/admin/users.ts @@ -56,7 +56,7 @@ router.post('/users', createJsonHandler( )) router.post('/users/:userId/role', createJsonHandler( - (req) => updateManagedAdminUserRole(req.params.userId, req.body, req.adminSession), + (req) => updateManagedAdminUserRole(String(req.params.userId), req.body, req.adminSession), { successMessage: '用户角色已更新', errorMessage: '更新用户角色失败', @@ -77,7 +77,7 @@ router.post('/users/:userId/role', createJsonHandler( )) router.post('/users/:userId/status', createJsonHandler( - (req) => updateManagedAdminUserStatus(req.params.userId, req.body, req.adminSession), + (req) => updateManagedAdminUserStatus(String(req.params.userId), req.body, req.adminSession), { successMessage: '用户状态已更新', errorMessage: '更新用户状态失败', @@ -98,7 +98,7 @@ router.post('/users/:userId/status', createJsonHandler( )) router.post('/users/:userId/inventory-groups', createJsonHandler( - (req) => updateManagedAdminUserInventoryGroups(req.params.userId, req.body, req.adminSession), + (req) => updateManagedAdminUserInventoryGroups(String(req.params.userId), req.body, req.adminSession), { successMessage: '用户库存组已更新', errorMessage: '更新用户库存组失败', @@ -119,7 +119,7 @@ router.post('/users/:userId/inventory-groups', createJsonHandler( )) router.post('/users/:userId/reset-password', createJsonHandler( - (req) => resetManagedAdminUserPassword(req.params.userId, req.body), + (req) => resetManagedAdminUserPassword(String(req.params.userId), req.body), { successMessage: '用户密码已重置', errorMessage: '重置用户密码失败', diff --git a/apps/backend/src/types/express.d.ts b/apps/backend/src/types/express.d.ts new file mode 100644 index 00000000..595b6684 --- /dev/null +++ b/apps/backend/src/types/express.d.ts @@ -0,0 +1,12 @@ +import type { AdminSession } from '../services/admin/admin-auth-service.js' + +declare global { + namespace Express { + interface Request { + adminSession?: AdminSession | null + requestId?: string + } + } +} + +export {} diff --git a/apps/backend/src/utils/http.ts b/apps/backend/src/utils/http.ts index 344e6ec4..325dec22 100644 --- a/apps/backend/src/utils/http.ts +++ b/apps/backend/src/utils/http.ts @@ -1,33 +1,34 @@ -import { logError, logWarn } from './logger.js' +import type { Request, Response } from "express"; +import { logError, logWarn } from "./logger.js"; -const RESPONSE_TIME_ZONE = 'Asia/Shanghai' +const RESPONSE_TIME_ZONE = "Asia/Shanghai"; export type HttpErrorLike = { - statusCode?: number - errorCode?: string - cause?: unknown - context?: unknown - [key: string]: unknown -} + statusCode?: number; + errorCode?: string; + cause?: unknown; + context?: unknown; + [key: string]: unknown; +}; type CreateHttpErrorOptions = { - statusCode?: number - errorCode?: string - cause?: unknown - context?: unknown -} + statusCode?: number; + errorCode?: string; + cause?: unknown; + context?: unknown; +}; -export function buildSuccessPayload(data, msg = 'ok') { +export function buildSuccessPayload(data: unknown, msg: string = "ok") { return { code: 0, msg, time: Math.floor(Date.now() / 1000), data: normalizeResponseDateTime(data), - } + }; } -export function buildErrorPayload(error, fallbackMessage) { - const classification = classifyRouteError(error) +export function buildErrorPayload(error: unknown, fallbackMessage: string) { + const classification = classifyRouteError(error); return { code: 1, @@ -35,214 +36,240 @@ export function buildErrorPayload(error, fallbackMessage) { errorCode: classification.errorCode, time: Math.floor(Date.now() / 1000), data: null, - } + }; } -export function sendRouteError(res, error, fallbackMessage, scope) { - const classification = classifyRouteError(error) - const logger = classification.statusCode >= 500 ? logError : logWarn +export function sendRouteError( + res: Response, + error: unknown, + fallbackMessage: string, + scope: string +): void { + const classification = classifyRouteError(error); + const logger = classification.statusCode >= 500 ? logError : logWarn; logger(scope, fallbackMessage, { statusCode: classification.statusCode, errorCode: classification.errorCode, error, - }) - res.status(classification.statusCode).json(buildErrorPayload(error, fallbackMessage)) + }); + res + .status(classification.statusCode) + .json(buildErrorPayload(error, fallbackMessage)); } -export function buildNotFoundPayload(req) { +export function buildNotFoundPayload(req: Request) { return { code: 1, msg: `未实现接口: ${req.method} ${req.originalUrl}`, time: Math.floor(Date.now() / 1000), data: null, - } + }; } -export function createHttpError(message: string, options: CreateHttpErrorOptions = {}): Error & HttpErrorLike { - const { statusCode = 500, errorCode = '', cause, context } = options - const error = new Error(message) as Error & HttpErrorLike - error.statusCode = statusCode - error.errorCode = errorCode || defaultErrorCodeForStatus(statusCode) +export function createHttpError( + message: string, + options: CreateHttpErrorOptions = {} +): Error & HttpErrorLike { + const { statusCode = 500, errorCode = "", cause, context } = options; + const error = new Error(message) as Error & HttpErrorLike; + error.statusCode = statusCode; + error.errorCode = errorCode || defaultErrorCodeForStatus(statusCode); - if (typeof cause !== 'undefined') { - error.cause = cause + if (typeof cause !== "undefined") { + error.cause = cause; } - if (typeof context !== 'undefined') { - error.context = context + if (typeof context !== "undefined") { + error.context = context; } - return error + return error; } -function classifyRouteError(error) { - if (error && typeof error === 'object') { - const currentError = error as HttpErrorLike - const statusCode = Number(currentError.statusCode) - const errorCode = String(currentError.errorCode || '').trim() +function classifyRouteError(error: unknown): { + statusCode: number; + errorCode: string; +} { + if (error && typeof error === "object") { + const currentError = error as HttpErrorLike; + const statusCode = Number(currentError.statusCode); + const errorCode = String(currentError.errorCode || "").trim(); if (Number.isInteger(statusCode) && statusCode >= 400 && statusCode < 600) { return { statusCode, errorCode: errorCode || defaultErrorCodeForStatus(statusCode), - } + }; } } - const message = error instanceof Error ? error.message : String(error || '') + const message = error instanceof Error ? error.message : String(error || ""); - if (message.includes('缺少') || message.includes('无效') || message.includes('验签失败')) { - return { statusCode: 400, errorCode: 'invalid_request' } + if ( + message.includes("缺少") || + message.includes("无效") || + message.includes("验签失败") + ) { + return { statusCode: 400, errorCode: "invalid_request" }; } - if (message.includes('不存在')) { - return { statusCode: 404, errorCode: 'not_found' } + if (message.includes("不存在")) { + return { statusCode: 404, errorCode: "not_found" }; } - if (message.includes('已关闭')) { - return { statusCode: 410, errorCode: 'gone' } + if (message.includes("已关闭")) { + return { statusCode: 410, errorCode: "gone" }; } - if (message.includes('不能') || message.includes('冲突')) { - return { statusCode: 409, errorCode: 'conflict' } + if (message.includes("不能") || message.includes("冲突")) { + return { statusCode: 409, errorCode: "conflict" }; } if ( - message.includes('OCR') || - message.includes('uv sync') || - message.includes('spawn uv ENOENT') || + message.includes("OCR") || + message.includes("uv sync") || + message.includes("spawn uv ENOENT") || message.includes("Executable doesn't exist") || - message.includes('please run the following command to download new browsers') + message.includes( + "please run the following command to download new browsers" + ) ) { - return { statusCode: 503, errorCode: 'dependency_unavailable' } + return { statusCode: 503, errorCode: "dependency_unavailable" }; } - return { statusCode: 500, errorCode: 'internal_error' } + return { statusCode: 500, errorCode: "internal_error" }; } -function defaultErrorCodeForStatus(statusCode) { +function defaultErrorCodeForStatus(statusCode: number): string { if (statusCode === 400) { - return 'invalid_request' + return "invalid_request"; } if (statusCode === 401) { - return 'unauthorized' + return "unauthorized"; } if (statusCode === 403) { - return 'forbidden' + return "forbidden"; } if (statusCode === 404) { - return 'not_found' + return "not_found"; } if (statusCode === 409) { - return 'conflict' + return "conflict"; } if (statusCode === 410) { - return 'gone' + return "gone"; } if (statusCode === 503) { - return 'dependency_unavailable' + return "dependency_unavailable"; } - return 'internal_error' + return "internal_error"; } -function normalizeResponseDateTime(value) { +function normalizeResponseDateTime(value: unknown): unknown { if (value instanceof Date) { - return formatResponseDateTime(value.toISOString()) + return formatResponseDateTime(value.toISOString()); } if (Array.isArray(value)) { - return value.map((item) => normalizeResponseDateTime(item)) + return value.map((item) => normalizeResponseDateTime(item)); } - if (value && typeof value === 'object') { + if (value && typeof value === "object") { return Object.fromEntries( - Object.entries(value).map(([key, currentValue]) => [key, normalizeResponseDateTime(currentValue)]), - ) + Object.entries(value).map(([key, currentValue]) => [ + key, + normalizeResponseDateTime(currentValue), + ]) + ); } if (isIsoDateTimeString(value)) { - return formatResponseDateTime(value) + return formatResponseDateTime(value as string); } - return value + return value; } -function isIsoDateTimeString(value) { +function isIsoDateTimeString(value: unknown): boolean { return ( - typeof value === 'string' && - /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})?$/.test(value.trim()) - ) + typeof value === "string" && + /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})?$/.test( + value.trim() + ) + ); } -function formatResponseDateTime(value) { - const date = new Date(value) +function formatResponseDateTime(value: string): string { + const date = new Date(value); if (Number.isNaN(date.getTime())) { - return String(value || '').replace('T', ' ').replace('Z', '') + return String(value || "") + .replace("T", " ") + .replace("Z", ""); } - const parts = new Intl.DateTimeFormat('zh-CN', { + const parts = new Intl.DateTimeFormat("zh-CN", { timeZone: RESPONSE_TIME_ZONE, hour12: false, - year: 'numeric', - month: '2-digit', - day: '2-digit', - hour: '2-digit', - minute: '2-digit', - second: '2-digit', - }).formatToParts(date) + year: "numeric", + month: "2-digit", + day: "2-digit", + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + }).formatToParts(date); const tokens = Object.fromEntries( parts - .filter((item) => item.type !== 'literal') - .map((item) => [item.type, item.value]), - ) + .filter((item) => item.type !== "literal") + .map((item) => [item.type, item.value]) + ); - return `${tokens.year}-${tokens.month}-${tokens.day} ${tokens.hour}:${tokens.minute}:${tokens.second}` + return `${tokens.year}-${tokens.month}-${tokens.day} ${tokens.hour}:${tokens.minute}:${tokens.second}`; } type RouteHandlerOptions = { - successMessage?: string - errorMessage?: string - scope?: string -} + successMessage?: string; + errorMessage?: string; + scope?: string; +}; type RouteFileHandlerOptions = { - errorMessage?: string - scope?: string -} + errorMessage?: string; + scope?: string; +}; export function createRouteHandler( - action: (req: any, res: any) => unknown | Promise, - { successMessage = 'ok', errorMessage, scope }: RouteHandlerOptions = {}, + action: (req: Request, res: Response) => unknown | Promise, + { successMessage = "ok", errorMessage, scope }: RouteHandlerOptions = {} ) { - return async (req, res) => { + return async (req: Request, res: Response): Promise => { try { - const data = await action(req, res) - res.json(buildSuccessPayload(data, successMessage)) + const data = await action(req, res); + res.json(buildSuccessPayload(data, successMessage)); } catch (error) { - sendRouteError(res, error, errorMessage, scope) + sendRouteError(res, error, errorMessage, scope); } - } + }; } export function createRouteFileHandler( - action: (req: any, res: any) => string | Promise, - { errorMessage, scope }: RouteFileHandlerOptions = {}, + action: (req: Request, res: Response) => string | Promise, + { errorMessage, scope }: RouteFileHandlerOptions = {} ) { - return async (req, res) => { + return async (req: Request, res: Response): Promise => { try { - const filePath = await action(req, res) - res.sendFile(filePath) + const filePath = await action(req, res); + res.sendFile(filePath); } catch (error) { - sendRouteError(res, error, errorMessage, scope) + sendRouteError(res, error, errorMessage, scope); } - } + }; }