拆分快手轻量后端入口
This commit is contained in:
@@ -10,11 +10,15 @@
|
||||
"cleanup:dev-data": "tsx scripts/cleanup-dev-data.ts",
|
||||
"seed:dev-data": "tsx scripts/seed-dev-data.ts",
|
||||
"build": "tsc -p tsconfig.build.json && rm -rf dist/db/migrations && mkdir -p dist/db && cp -R src/db/migrations dist/db/migrations",
|
||||
"build:kuaishou": "tsc -p tsconfig.kuaishou.build.json && rm -rf dist/db/migrations && mkdir -p dist/db && cp -R src/db/migrations dist/db/migrations",
|
||||
"db:migrate": "tsx src/db/migrate.ts",
|
||||
"dev": "tsx watch --clear-screen=false src/index.ts",
|
||||
"dev:kuaishou": "tsx watch --clear-screen=false src/index-kuaishou.ts",
|
||||
"test": "node --import tsx --test $(find src \\( -name '*.test.ts' -o -name '*.test.js' \\) -print)",
|
||||
"typecheck": "tsc -p tsconfig.json --noEmit",
|
||||
"typecheck:kuaishou": "tsc -p tsconfig.kuaishou.json --noEmit",
|
||||
"start": "node dist/index.js",
|
||||
"start:kuaishou": "node dist/index-kuaishou.js",
|
||||
"start:src": "tsx src/index.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "order-site-backend-kuaishou",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build:kuaishou": "tsc -p tsconfig.kuaishou.build.json && rm -rf dist/db/migrations && mkdir -p dist/db && cp -R src/db/migrations dist/db/migrations",
|
||||
"db:migrate": "tsx src/db/migrate.ts",
|
||||
"start:kuaishou": "node dist/index-kuaishou.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"express": "^5.1.0",
|
||||
"pg": "^8.16.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/express": "^5.0.6",
|
||||
"@types/node": "^25.6.0",
|
||||
"@types/pg": "^8.20.0",
|
||||
"tsx": "^4.22.3",
|
||||
"typescript": "^6.0.2"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
import type { NextFunction, Request, Response } from "express";
|
||||
import express from "express";
|
||||
import process from "node:process";
|
||||
|
||||
import adminRouter from "./routes/admin-kuaishou.js";
|
||||
import claimsRouter from "./routes/claims-kuaishou.js";
|
||||
import open91Router from "./routes/open-91.js";
|
||||
import webhooksRouter from "./routes/webhooks.js";
|
||||
import { accessLogMiddleware } from "./middleware/access-log.js";
|
||||
import { createCorsMiddleware } from "./middleware/cors.js";
|
||||
import { buildHealthPayload, type StartupState } from "./startup/state.js";
|
||||
import type { RuntimeConfig } from "./types/runtime-config.js";
|
||||
import { buildSuccessPayload, sendRouteError } from "./utils/http.js";
|
||||
|
||||
type CreateKuaishouAppOptions = {
|
||||
startupState: StartupState;
|
||||
isShutdownStarted: () => boolean;
|
||||
config: RuntimeConfig;
|
||||
};
|
||||
|
||||
export function createKuaishouApp({
|
||||
startupState,
|
||||
isShutdownStarted,
|
||||
config,
|
||||
}: CreateKuaishouAppOptions) {
|
||||
const app = express();
|
||||
|
||||
app.use(accessLogMiddleware);
|
||||
app.use(createCorsMiddleware(config));
|
||||
app.use(express.json({ limit: "2mb" }));
|
||||
app.use(express.urlencoded({ extended: true }));
|
||||
|
||||
app.get("/health", (_req, res) => {
|
||||
res.json(
|
||||
buildSuccessPayload(
|
||||
buildHealthPayload(startupState, isShutdownStarted()),
|
||||
startupState.core.ready ? "ready" : "starting"
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
app.get("/health/live", (_req, res) => {
|
||||
res.json(
|
||||
buildSuccessPayload({
|
||||
status: isShutdownStarted() ? "shutting_down" : "alive",
|
||||
pid: process.pid,
|
||||
mode: "kuaishou-lite",
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
app.get("/health/ready", (_req, res) => {
|
||||
if (startupState.core.ready) {
|
||||
res.json(
|
||||
buildSuccessPayload(
|
||||
buildHealthPayload(startupState, isShutdownStarted()),
|
||||
"ready"
|
||||
)
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
res.status(503).json({
|
||||
code: 1,
|
||||
msg: startupState.core.lastError || "服务启动中,请稍后重试",
|
||||
errorCode: "service_not_ready",
|
||||
time: Math.floor(Date.now() / 1000),
|
||||
data: buildHealthPayload(startupState, isShutdownStarted()),
|
||||
});
|
||||
});
|
||||
|
||||
app.use((req, res, next) => {
|
||||
if (startupState.core.ready) {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
|
||||
res.status(503).json({
|
||||
code: 1,
|
||||
msg: startupState.core.lastError || "服务启动中,请稍后重试",
|
||||
errorCode: "service_not_ready",
|
||||
time: Math.floor(Date.now() / 1000),
|
||||
data: {
|
||||
startup: {
|
||||
phase: startupState.phase,
|
||||
attemptCount: startupState.core.attemptCount,
|
||||
lastAttemptAt: startupState.core.lastAttemptAt,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
app.use("/api/v1/open/91", open91Router);
|
||||
app.use("/api/v1/webhooks", webhooksRouter);
|
||||
app.use("/api/v1/claim", claimsRouter);
|
||||
app.use("/api/v1/admin", adminRouter);
|
||||
|
||||
app.use((err: unknown, _req: Request, res: Response, _next: NextFunction) => {
|
||||
sendRouteError(res, err, "服务内部错误", "[global:kuaishou]");
|
||||
});
|
||||
|
||||
return app;
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
import process from "node:process";
|
||||
|
||||
import { createKuaishouApp } from "./app-kuaishou.js";
|
||||
import { runtimeConfig } from "./config/runtime.js";
|
||||
import { assertRuntimeConfigValid } from "./config/runtime-validation.js";
|
||||
import { bootstrapKuaishouCoreServices } from "./startup/bootstrap-kuaishou.js";
|
||||
import { createKuaishouShutdownController } from "./startup/shutdown-kuaishou.js";
|
||||
import { createStartupState, formatStartupError } from "./startup/state.js";
|
||||
import { logError, logInfo } from "./utils/logger.js";
|
||||
|
||||
const port = Number(runtimeConfig.server.port || 3000);
|
||||
const host = "0.0.0.0";
|
||||
const startupState = createStartupState();
|
||||
|
||||
let shutdownController: ReturnType<
|
||||
typeof createKuaishouShutdownController
|
||||
> | null = null;
|
||||
|
||||
try {
|
||||
assertRuntimeConfigValid(runtimeConfig);
|
||||
} catch (error) {
|
||||
logError("[startup:kuaishou]", "运行时配置校验失败,服务停止启动", error);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const app = createKuaishouApp({
|
||||
startupState,
|
||||
isShutdownStarted: () => shutdownController?.isShutdownStarted() || false,
|
||||
config: runtimeConfig,
|
||||
});
|
||||
|
||||
const server = app.listen(port, host, () => {
|
||||
logInfo(
|
||||
"[startup:kuaishou]",
|
||||
`order-site-backend kuaishou-lite listening on http://${host}:${port}`
|
||||
);
|
||||
void bootstrapKuaishouCoreServices(
|
||||
startupState,
|
||||
() => shutdownController?.isShutdownStarted() || false
|
||||
);
|
||||
});
|
||||
|
||||
shutdownController = createKuaishouShutdownController(server, startupState);
|
||||
|
||||
server.on("error", (error) => {
|
||||
logError("[startup:kuaishou]", "HTTP server failed", error);
|
||||
});
|
||||
|
||||
process.on("SIGINT", () => {
|
||||
void shutdownController?.shutdown("SIGINT");
|
||||
});
|
||||
|
||||
process.on("SIGTERM", () => {
|
||||
void shutdownController?.shutdown("SIGTERM");
|
||||
});
|
||||
|
||||
process.on("unhandledRejection", (reason) => {
|
||||
startupState.process.lastUnhandledRejection = {
|
||||
time: new Date().toISOString(),
|
||||
message: formatStartupError(reason),
|
||||
};
|
||||
logError("[process:kuaishou]", "unhandled promise rejection", reason);
|
||||
});
|
||||
|
||||
process.on("uncaughtException", (error) => {
|
||||
startupState.process.lastUncaughtException = {
|
||||
time: new Date().toISOString(),
|
||||
message: formatStartupError(error),
|
||||
};
|
||||
logError("[process:kuaishou]", "uncaught exception captured", error);
|
||||
});
|
||||
@@ -0,0 +1,34 @@
|
||||
import { Router } from "express";
|
||||
|
||||
import authRouter from "./admin/auth.js";
|
||||
import auditLogsRouter from "./admin/audit-logs.js";
|
||||
import dashboardRouter from "./admin/dashboard.js";
|
||||
import inventoryRouter from "./admin/inventory-kuaishou.js";
|
||||
import messageDeliveriesRouter from "./admin/message-deliveries.js";
|
||||
import ordersRouter from "./admin/orders.js";
|
||||
import platformConfigRouter from "./admin/platform-config.js";
|
||||
import { requireAdminSession } from "./admin/shared.js";
|
||||
import tasksRouter from "./admin/tasks-kuaishou.js";
|
||||
import usersRouter from "./admin/users.js";
|
||||
import webhookEventsRouter from "./admin/webhook-events-kuaishou.js";
|
||||
import { buildNotFoundPayload } from "../utils/http.js";
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.use(authRouter);
|
||||
router.use(requireAdminSession);
|
||||
router.use(dashboardRouter);
|
||||
router.use(usersRouter);
|
||||
router.use(auditLogsRouter);
|
||||
router.use(platformConfigRouter);
|
||||
router.use(ordersRouter);
|
||||
router.use(tasksRouter);
|
||||
router.use(inventoryRouter);
|
||||
router.use(messageDeliveriesRouter);
|
||||
router.use(webhookEventsRouter);
|
||||
|
||||
router.use((req, res) => {
|
||||
res.status(404).json(buildNotFoundPayload(req));
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,149 @@
|
||||
import { Router } from "express";
|
||||
|
||||
import {
|
||||
createAdminInventoryItem,
|
||||
importAdminInventoryItems,
|
||||
invalidateAdminInventoryItem,
|
||||
releaseAdminInventoryItem,
|
||||
} from "../../services/admin/write/inventory.js";
|
||||
import {
|
||||
getAdminInventoryItems,
|
||||
getAdminInventorySkuSuggestions,
|
||||
} from "../../services/admin/admin-read-service.js";
|
||||
import { createJsonHandler, requireAdminRoles } from "./shared.js";
|
||||
import type {
|
||||
AdminInventoryCreateRouteBody,
|
||||
AdminInventoryImportRouteBody,
|
||||
AdminInventoryInvalidateRouteBody,
|
||||
AdminInventoryRouteParams,
|
||||
AdminInventoryRouteQuery,
|
||||
AdminInventorySkuSuggestionRouteQuery,
|
||||
} from "../../types/admin-route-inputs.js";
|
||||
import type { AdminInventoryMutationResponse } from "../../types/admin-write-models.js";
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.use("/inventory", requireAdminRoles(["admin", "operator"]));
|
||||
|
||||
router.get(
|
||||
"/inventory",
|
||||
createJsonHandler(
|
||||
(req) =>
|
||||
getAdminInventoryItems(
|
||||
req.query as AdminInventoryRouteQuery,
|
||||
req.adminSession || null
|
||||
),
|
||||
{
|
||||
successMessage: "ok",
|
||||
errorMessage: "读取库存列表失败",
|
||||
scope: "[admin-kuaishou/inventory]",
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
router.get(
|
||||
"/inventory/sku-suggestions",
|
||||
createJsonHandler(
|
||||
(req) =>
|
||||
getAdminInventorySkuSuggestions(
|
||||
req.query as AdminInventorySkuSuggestionRouteQuery,
|
||||
req.adminSession || null
|
||||
),
|
||||
{
|
||||
successMessage: "ok",
|
||||
errorMessage: "读取内部 SKU 建议失败",
|
||||
scope: "[admin-kuaishou/inventory/sku-suggestions]",
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
router.post(
|
||||
"/inventory",
|
||||
createJsonHandler(
|
||||
(req) => createAdminInventoryItem(req.body as AdminInventoryCreateRouteBody),
|
||||
{
|
||||
successMessage: "库存项已新增",
|
||||
errorMessage: "新增库存项失败",
|
||||
scope: "[admin-kuaishou/inventory]",
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
router.post(
|
||||
"/inventory/import",
|
||||
createJsonHandler(
|
||||
(req) => importAdminInventoryItems(req.body as AdminInventoryImportRouteBody),
|
||||
{
|
||||
successMessage: "导入完成",
|
||||
errorMessage: "导入库存凭据失败",
|
||||
scope: "[admin-kuaishou/inventory/import]",
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
router.post(
|
||||
"/inventory/:inventoryItemId/release",
|
||||
requireAdminRoles(["admin"]),
|
||||
createJsonHandler(
|
||||
(req) =>
|
||||
releaseAdminInventoryItem(
|
||||
(req.params as AdminInventoryRouteParams).inventoryItemId
|
||||
),
|
||||
{
|
||||
successMessage: "库存项已释放",
|
||||
errorMessage: "释放库存项失败",
|
||||
scope: "[admin-kuaishou/inventory/:inventoryItemId/release]",
|
||||
audit: (req, data) => {
|
||||
const result = data as AdminInventoryMutationResponse;
|
||||
|
||||
return {
|
||||
action: "inventory_item_released",
|
||||
targetType: "inventory_item",
|
||||
targetId: String(
|
||||
(req.params as AdminInventoryRouteParams).inventoryItemId
|
||||
),
|
||||
data: {
|
||||
inventoryItemId: result.inventoryItem?.inventoryItemId,
|
||||
skuCode: result.inventoryItem?.skuCode,
|
||||
displayValue: result.inventoryItem?.displayValue,
|
||||
},
|
||||
};
|
||||
},
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
router.post(
|
||||
"/inventory/:inventoryItemId/invalidate",
|
||||
requireAdminRoles(["admin"]),
|
||||
createJsonHandler(
|
||||
(req) =>
|
||||
invalidateAdminInventoryItem(
|
||||
(req.params as AdminInventoryRouteParams).inventoryItemId,
|
||||
req.body as AdminInventoryInvalidateRouteBody
|
||||
),
|
||||
{
|
||||
successMessage: "库存项已作废",
|
||||
errorMessage: "作废库存项失败",
|
||||
scope: "[admin-kuaishou/inventory/:inventoryItemId/invalidate]",
|
||||
audit: (req, data) => {
|
||||
const result = data as AdminInventoryMutationResponse;
|
||||
|
||||
return {
|
||||
action: "inventory_item_invalidated",
|
||||
targetType: "inventory_item",
|
||||
targetId: String(
|
||||
(req.params as AdminInventoryRouteParams).inventoryItemId
|
||||
),
|
||||
data: {
|
||||
inventoryItemId: result.inventoryItem?.inventoryItemId,
|
||||
skuCode: result.inventoryItem?.skuCode,
|
||||
invalidReason: result.inventoryItem?.invalidReason,
|
||||
},
|
||||
};
|
||||
},
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,162 @@
|
||||
import type { Request } from "express";
|
||||
import { Router } from "express";
|
||||
|
||||
import {
|
||||
getAdminTaskDetail,
|
||||
getAdminTasks,
|
||||
} from "../../services/admin/admin-read-service.js";
|
||||
import {
|
||||
dispatchAdminTaskKuaishouCloudFulfillment,
|
||||
prepareAdminTaskKuaishouCloudFulfillment,
|
||||
refreshAdminTaskKuaishouCloudRoleInfo,
|
||||
returnNumberAdminTaskKuaishouCloudFulfillment,
|
||||
} from "../../services/admin/write/kuaishou-cloud-actions.js";
|
||||
import {
|
||||
createJsonHandler,
|
||||
requireAdminRoles,
|
||||
} from "./shared.js";
|
||||
import type {
|
||||
AdminTaskKuaishouCloudDispatchRouteBody,
|
||||
AdminTaskRouteParams,
|
||||
AdminTaskRouteQuery,
|
||||
} from "../../types/admin-route-inputs.js";
|
||||
import type { AdminTaskActionResponse } from "../../types/admin-write-models.js";
|
||||
|
||||
const router = Router();
|
||||
|
||||
function getTaskId(req: Request): string | undefined {
|
||||
return (req.params as AdminTaskRouteParams).taskId;
|
||||
}
|
||||
|
||||
router.get(
|
||||
"/tasks",
|
||||
createJsonHandler(
|
||||
(req) =>
|
||||
getAdminTasks(req.query as AdminTaskRouteQuery, req.adminSession || null),
|
||||
{
|
||||
successMessage: "ok",
|
||||
errorMessage: "读取任务列表失败",
|
||||
scope: "[admin-kuaishou/tasks]",
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
router.get(
|
||||
"/tasks/:taskId",
|
||||
createJsonHandler(
|
||||
(req) => getAdminTaskDetail(getTaskId(req), req.adminSession || null),
|
||||
{
|
||||
successMessage: "ok",
|
||||
errorMessage: "读取任务详情失败",
|
||||
scope: "[admin-kuaishou/tasks/:taskId]",
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
router.post(
|
||||
"/tasks/:taskId/kuaishou-cloud/prepare",
|
||||
requireAdminRoles(["admin", "operator"]),
|
||||
createJsonHandler(
|
||||
(req) =>
|
||||
prepareAdminTaskKuaishouCloudFulfillment(
|
||||
getTaskId(req),
|
||||
req.adminSession || null
|
||||
),
|
||||
{
|
||||
successMessage: "绑定资源已准备完成",
|
||||
errorMessage: "准备绑定资源失败",
|
||||
scope: "[admin-kuaishou/tasks/:taskId/kuaishou-cloud/prepare]",
|
||||
audit: (req, data) => buildTaskAudit("task_kuaishou_cloud_prepare", req, data),
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
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-kuaishou/tasks/:taskId/kuaishou-cloud/refresh-role-info]",
|
||||
audit: (req, data) =>
|
||||
buildTaskAudit("task_kuaishou_cloud_role_info_refreshed", req, data),
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
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-kuaishou/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-kuaishou/tasks/:taskId/kuaishou-cloud/return-number]",
|
||||
audit: (req, data) =>
|
||||
buildTaskAudit("task_kuaishou_cloud_return_number", req, data),
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
function buildTaskAudit(action: string, req: Request, data: unknown) {
|
||||
const result = data as AdminTaskActionResponse;
|
||||
|
||||
return {
|
||||
action,
|
||||
targetType: "task",
|
||||
targetId: String(getTaskId(req)),
|
||||
data: {
|
||||
taskId: result.task.taskId,
|
||||
taskNo: result.task.taskNo,
|
||||
status: result.task.status,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default router;
|
||||
@@ -17,9 +17,9 @@ import {
|
||||
} from "../../services/admin/admin-write-service.js";
|
||||
import {
|
||||
getAdminTaskDetail,
|
||||
getAdminTaskScreenshotPath,
|
||||
getAdminTasks,
|
||||
} from "../../services/admin/admin-read-service.js";
|
||||
import { getAdminTaskScreenshotPathWithTencentFallback } from "../../services/admin/admin-task-screenshot-service.js";
|
||||
import {
|
||||
createFileHandler,
|
||||
createJsonHandler,
|
||||
@@ -77,7 +77,7 @@ router.get(
|
||||
"/tasks/:taskId/screenshot",
|
||||
createFileHandler(
|
||||
(req) =>
|
||||
getAdminTaskScreenshotPath(getTaskId(req), req.adminSession || null),
|
||||
getAdminTaskScreenshotPathWithTencentFallback(getTaskId(req), req.adminSession || null),
|
||||
{
|
||||
errorMessage: "读取任务截图失败",
|
||||
scope: "[admin/tasks/:taskId/screenshot]",
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
import { Router } from "express";
|
||||
|
||||
import { replayAdminWebhookEvent } from "../../services/admin/write/webhook-events.js";
|
||||
import {
|
||||
getAdminWebhookEventDetail,
|
||||
getAdminWebhookEvents,
|
||||
} from "../../services/admin/admin-read-service.js";
|
||||
import { createJsonHandler, requireAdminRoles } from "./shared.js";
|
||||
import type {
|
||||
AdminWebhookEventRouteParams,
|
||||
AdminWebhookEventRouteQuery,
|
||||
} from "../../types/admin-route-inputs.js";
|
||||
import type { AdminWebhookReplayResponse } from "../../types/admin-write-models.js";
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.use("/webhook-events", requireAdminRoles(["admin", "operator"]));
|
||||
|
||||
router.get(
|
||||
"/webhook-events",
|
||||
createJsonHandler(
|
||||
(req) => getAdminWebhookEvents(req.query as AdminWebhookEventRouteQuery),
|
||||
{
|
||||
successMessage: "ok",
|
||||
errorMessage: "读取 webhook 列表失败",
|
||||
scope: "[admin-kuaishou/webhook-events]",
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
router.get(
|
||||
"/webhook-events/:eventId",
|
||||
createJsonHandler(
|
||||
(req) =>
|
||||
getAdminWebhookEventDetail(
|
||||
(req.params as AdminWebhookEventRouteParams).eventId
|
||||
),
|
||||
{
|
||||
successMessage: "ok",
|
||||
errorMessage: "读取 webhook 详情失败",
|
||||
scope: "[admin-kuaishou/webhook-events/:eventId]",
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
router.post(
|
||||
"/webhook-events/:eventId/replay",
|
||||
requireAdminRoles(["admin"]),
|
||||
createJsonHandler(
|
||||
(req) =>
|
||||
replayAdminWebhookEvent(
|
||||
(req.params as AdminWebhookEventRouteParams).eventId
|
||||
),
|
||||
{
|
||||
successMessage: "Webhook 已重放",
|
||||
errorMessage: "重放 webhook 失败",
|
||||
scope: "[admin-kuaishou/webhook-events/:eventId/replay]",
|
||||
audit: (req, data) => {
|
||||
const result = data as AdminWebhookReplayResponse;
|
||||
|
||||
return {
|
||||
action: "webhook_replayed",
|
||||
targetType: "webhook_event",
|
||||
targetId: String(
|
||||
(req.params as AdminWebhookEventRouteParams).eventId
|
||||
),
|
||||
data: {
|
||||
eventId: result.eventId,
|
||||
replayed: result.replayed,
|
||||
},
|
||||
};
|
||||
},
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,71 @@
|
||||
import { Router } from "express";
|
||||
|
||||
import {
|
||||
confirmKuaishouCloudClaimRole,
|
||||
getKuaishouCloudClaimDetail,
|
||||
getKuaishouCloudClaimGuideAssetPath,
|
||||
redeemKuaishouCloudClaim,
|
||||
verifyKuaishouCloudClaimTicket,
|
||||
} from "../services/claim/kuaishou-cloud-claim-service.js";
|
||||
import {
|
||||
buildNotFoundPayload,
|
||||
createRouteFileHandler,
|
||||
createRouteHandler,
|
||||
} from "../utils/http.js";
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.get(
|
||||
"/:token",
|
||||
createRouteHandler((req) => getKuaishouCloudClaimDetail(req.params.token), {
|
||||
errorMessage: "查询快手领取详情失败",
|
||||
scope: "[claims-kuaishou/:token]",
|
||||
})
|
||||
);
|
||||
|
||||
router.post(
|
||||
"/:token/kuaishou-cloud/verify-ticket",
|
||||
createRouteHandler(
|
||||
(req) => verifyKuaishouCloudClaimTicket(req.params.token, req.body),
|
||||
{
|
||||
successMessage: "核销码校验成功",
|
||||
errorMessage: "校验核销码失败",
|
||||
scope: "[claims-kuaishou/:token/kuaishou-cloud/verify-ticket]",
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
router.post(
|
||||
"/:token/kuaishou-cloud/confirm-role",
|
||||
createRouteHandler((req) => confirmKuaishouCloudClaimRole(req.params.token), {
|
||||
successMessage: "角色已确认",
|
||||
errorMessage: "确认角色失败",
|
||||
scope: "[claims-kuaishou/:token/kuaishou-cloud/confirm-role]",
|
||||
})
|
||||
);
|
||||
|
||||
router.post(
|
||||
"/:token/kuaishou-cloud/redeem",
|
||||
createRouteHandler((req) => redeemKuaishouCloudClaim(req.params.token), {
|
||||
successMessage: "兑换请求已提交",
|
||||
errorMessage: "兑换失败",
|
||||
scope: "[claims-kuaishou/:token/kuaishou-cloud/redeem]",
|
||||
})
|
||||
);
|
||||
|
||||
router.get(
|
||||
"/assets/kuaishou-cloud/:filename",
|
||||
createRouteFileHandler(
|
||||
(req) => getKuaishouCloudClaimGuideAssetPath(req.params.filename),
|
||||
{
|
||||
errorMessage: "读取指引图片失败",
|
||||
scope: "[claims-kuaishou/assets/kuaishou-cloud/:filename]",
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
router.use((req, res) => {
|
||||
res.status(404).json(buildNotFoundPayload(req));
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -13,7 +13,6 @@ import { listTaskEventsByTaskId } from '../../repositories/task-event-repo.js'
|
||||
import { getWebhookEventById, listWebhookEvents, listWebhookEventsByOrderId } from '../../repositories/webhook-event-repo.js'
|
||||
import { createHttpError } from '../../utils/http.js'
|
||||
import { formatFenToAmount, normalizeFen } from '../../utils/money.js'
|
||||
import { getTencentBrowserSessionReviewScreenshotPath } from '../session/session.js'
|
||||
import { normalizeDateQuery, normalizePage, normalizePageSize, safeParseJson } from './admin-query-utils.js'
|
||||
import {
|
||||
canViewerCloseTask,
|
||||
@@ -322,10 +321,6 @@ export async function getAdminTaskScreenshotPath(
|
||||
return task.screenshot_path
|
||||
}
|
||||
|
||||
if (task.browser_session_id) {
|
||||
return getTencentBrowserSessionReviewScreenshotPath(task.browser_session_id)
|
||||
}
|
||||
|
||||
throw createHttpError('当前任务还没有可查看截图', {
|
||||
statusCode: 404,
|
||||
errorCode: 'admin_task_screenshot_not_found',
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { getTencentBrowserSessionReviewScreenshotPath } from '../session/session.js'
|
||||
import { createHttpError } from '../../utils/http.js'
|
||||
import { getRequiredTask } from './admin-task-read-helpers.js'
|
||||
|
||||
import type {
|
||||
AdminEntityIdInput,
|
||||
AdminViewerSessionInput,
|
||||
} from '../../types/admin-read-inputs.js'
|
||||
|
||||
export async function getAdminTaskScreenshotPathWithTencentFallback(
|
||||
taskId: AdminEntityIdInput,
|
||||
_session: AdminViewerSessionInput | null = null,
|
||||
): Promise<string> {
|
||||
const task = await getRequiredTask(taskId)
|
||||
|
||||
if (task.screenshot_path) {
|
||||
return task.screenshot_path
|
||||
}
|
||||
|
||||
if (task.browser_session_id) {
|
||||
return getTencentBrowserSessionReviewScreenshotPath(task.browser_session_id)
|
||||
}
|
||||
|
||||
throw createHttpError('当前任务还没有可查看截图', {
|
||||
statusCode: 404,
|
||||
errorCode: 'admin_task_screenshot_not_found',
|
||||
})
|
||||
}
|
||||
@@ -18,7 +18,9 @@ import {
|
||||
prepareKuaishouCloudFulfillmentTask,
|
||||
refreshKuaishouCloudTaskRoleInfo,
|
||||
} from '../fulfillment/kuaishou-cloud-task-service.js'
|
||||
import { getClaimContext, getClaimDetail } from './claim-session-service.js'
|
||||
import { buildClaimDetailPayload } from './session/shared.js'
|
||||
import { getClaimContext } from './session/context.js'
|
||||
import { syncKuaishouCloudRoleInfo } from './kuaishou-cloud-sync-service.js'
|
||||
|
||||
const KUAISHOU_CLOUD_GUIDE_DIR = path.resolve(PROJECT_ROOT, '../../tems/imgs')
|
||||
const ALLOWED_GUIDE_FILES = new Set(['1.png', '2.png', '3.png'])
|
||||
@@ -132,7 +134,7 @@ export async function verifyKuaishouCloudClaimTicket(token: unknown, payload: Js
|
||||
goodsTitle: String(detailResult.goods?.itemTitle || '').trim(),
|
||||
}, now)
|
||||
|
||||
return getClaimDetail(token, { includeQrImage: false })
|
||||
return getKuaishouCloudClaimDetail(token)
|
||||
}
|
||||
|
||||
export async function getKuaishouCloudClaimGuideAssetPath(filename) {
|
||||
@@ -155,6 +157,23 @@ export async function getKuaishouCloudClaimGuideAssetPath(filename) {
|
||||
return filePath
|
||||
}
|
||||
|
||||
export async function getKuaishouCloudClaimDetail(token: unknown) {
|
||||
const context = await getClaimContext(token)
|
||||
let task = context.task
|
||||
|
||||
if (String(task.executor_key || '').trim() === 'kuaishou_ct_assisted') {
|
||||
task = await syncKuaishouCloudRoleInfo(task)
|
||||
}
|
||||
|
||||
return buildClaimDetailPayload({
|
||||
claimToken: context.claimToken,
|
||||
task,
|
||||
order: context.order,
|
||||
orderItem: context.orderItem,
|
||||
session: null,
|
||||
})
|
||||
}
|
||||
|
||||
function parseTaskContext(task) {
|
||||
const rawValue = task?.context_json
|
||||
if (!rawValue) {
|
||||
@@ -184,7 +203,7 @@ export async function confirmKuaishouCloudClaimRole(token) {
|
||||
}
|
||||
|
||||
if (['role_confirmed', 'dispatched_pending_return', 'completed'].includes(String(context.task.task_status || '').trim())) {
|
||||
return getClaimDetail(token, { includeQrImage: false })
|
||||
return getKuaishouCloudClaimDetail(token)
|
||||
}
|
||||
|
||||
const refreshed = await refreshKuaishouCloudTaskRoleInfo(context.task, {
|
||||
@@ -225,7 +244,7 @@ export async function confirmKuaishouCloudClaimRole(token) {
|
||||
roleId: flow.binding.roleId,
|
||||
}, now)
|
||||
|
||||
return getClaimDetail(token, { includeQrImage: false })
|
||||
return getKuaishouCloudClaimDetail(token)
|
||||
}
|
||||
|
||||
export async function redeemKuaishouCloudClaim(token) {
|
||||
@@ -252,5 +271,5 @@ export async function redeemKuaishouCloudClaim(token) {
|
||||
autoFinalize: true,
|
||||
})
|
||||
|
||||
return getClaimDetail(token, { includeQrImage: false })
|
||||
return getKuaishouCloudClaimDetail(token)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
import { runDatabaseMigrations } from "../db/migrate.js";
|
||||
import { ensureAdminUsersBootstrapped } from "../services/admin/admin-auth-service.js";
|
||||
import { ensureFulfillmentCatalogBootstrapped } from "../services/bootstrap/fulfillment-bootstrap-service.js";
|
||||
import { startScheduledJobs } from "../services/scheduler/scheduler-service.js";
|
||||
import { logError, logInfo } from "../utils/logger.js";
|
||||
import { formatStartupError, type StartupState } from "./state.js";
|
||||
|
||||
const CORE_BOOT_RETRY_DELAY_MS = 5_000;
|
||||
|
||||
export async function bootstrapKuaishouCoreServices(
|
||||
startupState: StartupState,
|
||||
isShutdownStarted: () => boolean
|
||||
) {
|
||||
if (startupState.core.running || isShutdownStarted()) {
|
||||
return;
|
||||
}
|
||||
|
||||
startupState.core.running = true;
|
||||
startupState.browser.status = "skipped";
|
||||
startupState.browser.message = "kuaishou lite mode";
|
||||
startupState.ocr.status = "skipped";
|
||||
startupState.ocr.message = "kuaishou lite mode";
|
||||
|
||||
while (!isShutdownStarted() && !startupState.core.ready) {
|
||||
startupState.phase =
|
||||
startupState.core.attemptCount === 0 ? "starting" : "retrying";
|
||||
startupState.core.attemptCount += 1;
|
||||
startupState.core.lastAttemptAt = new Date().toISOString();
|
||||
|
||||
try {
|
||||
logInfo("[startup:kuaishou]", "开始执行快手轻量核心启动步骤", {
|
||||
attempt: startupState.core.attemptCount,
|
||||
});
|
||||
|
||||
await runDatabaseMigrations();
|
||||
await ensureFulfillmentCatalogBootstrapped();
|
||||
await ensureAdminUsersBootstrapped();
|
||||
|
||||
startupState.core.ready = true;
|
||||
startupState.core.lastError = "";
|
||||
startupState.phase = "ready";
|
||||
startupState.core.readyAt = new Date().toISOString();
|
||||
|
||||
logInfo("[startup:kuaishou]", "快手轻量核心启动步骤完成,服务已就绪", {
|
||||
attempt: startupState.core.attemptCount,
|
||||
});
|
||||
|
||||
startScheduledJobs();
|
||||
break;
|
||||
} catch (error) {
|
||||
const message = formatStartupError(error);
|
||||
startupState.phase = "retrying";
|
||||
startupState.core.lastError = message;
|
||||
logError("[startup:kuaishou]", "快手轻量核心启动步骤失败,将自动重试", {
|
||||
attempt: startupState.core.attemptCount,
|
||||
retryDelayMs: CORE_BOOT_RETRY_DELAY_MS,
|
||||
error,
|
||||
});
|
||||
|
||||
await sleep(CORE_BOOT_RETRY_DELAY_MS);
|
||||
}
|
||||
}
|
||||
|
||||
startupState.core.running = false;
|
||||
}
|
||||
|
||||
function sleep(ms: number) {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(resolve, ms);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import type { Server } from "node:http";
|
||||
|
||||
import { stopScheduledJobs } from "../services/scheduler/scheduler-service.js";
|
||||
import { logError, logInfo } from "../utils/logger.js";
|
||||
import type { StartupState } from "./state.js";
|
||||
|
||||
export function createKuaishouShutdownController(
|
||||
server: Server,
|
||||
startupState: StartupState
|
||||
) {
|
||||
let shutdownStarted = false;
|
||||
|
||||
async function shutdown(signal: string) {
|
||||
if (shutdownStarted) {
|
||||
return;
|
||||
}
|
||||
|
||||
shutdownStarted = true;
|
||||
startupState.phase = "shutting_down";
|
||||
logInfo("[shutdown:kuaishou]", `received ${signal}, closing HTTP server`);
|
||||
|
||||
stopScheduledJobs();
|
||||
|
||||
if (!server.listening) {
|
||||
return;
|
||||
}
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
server.close((error) => {
|
||||
if (error) {
|
||||
logError("[shutdown:kuaishou]", "failed to close HTTP server", error);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
isShutdownStarted: () => shutdownStarted,
|
||||
shutdown,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"extends": "./tsconfig.kuaishou.json",
|
||||
"compilerOptions": {
|
||||
"noEmit": false,
|
||||
"outDir": "dist",
|
||||
"rootDir": "src",
|
||||
"declaration": false,
|
||||
"sourceMap": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"files": [
|
||||
"src/index-kuaishou.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"data",
|
||||
"dist",
|
||||
"src/**/*.test.js",
|
||||
"src/**/*.test.ts"
|
||||
]
|
||||
}
|
||||
@@ -1,12 +1,14 @@
|
||||
import { createRouter, createWebHashHistory } from 'vue-router'
|
||||
import { getAdminRole, hasAdminSession } from '@/utils/admin-auth'
|
||||
|
||||
const isKuaishouMode = import.meta.env.VITE_APP_MODE === 'kuaishou'
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHashHistory(),
|
||||
routes: [
|
||||
{
|
||||
path: '/',
|
||||
redirect: '/tx/browser',
|
||||
redirect: isKuaishouMode ? '/admin/dashboard' : '/tx/browser',
|
||||
},
|
||||
{
|
||||
path: '/tx',
|
||||
|
||||
Reference in New Issue
Block a user