拆分快手轻量后端入口

This commit is contained in:
yml
2026-05-25 20:42:27 +08:00
parent d58d483944
commit db22ff691b
23 changed files with 1146 additions and 13 deletions
+4
View File
@@ -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": {
+22
View File
@@ -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"
}
}
+103
View File
@@ -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;
}
+71
View File
@@ -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);
});
+34
View File
@@ -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;
+2 -2
View File
@@ -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,
};
}
+10
View File
@@ -0,0 +1,10 @@
{
"extends": "./tsconfig.kuaishou.json",
"compilerOptions": {
"noEmit": false,
"outDir": "dist",
"rootDir": "src",
"declaration": false,
"sourceMap": true
}
}
+13
View File
@@ -0,0 +1,13 @@
{
"extends": "./tsconfig.json",
"files": [
"src/index-kuaishou.ts"
],
"exclude": [
"node_modules",
"data",
"dist",
"src/**/*.test.js",
"src/**/*.test.ts"
]
}
+3 -1
View File
@@ -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',