refactor backend route and fulfillment modules

This commit is contained in:
yml
2026-05-21 18:38:04 +08:00
parent 1a9c832c7a
commit efa41ba976
22 changed files with 2441 additions and 2246 deletions
+88
View File
@@ -0,0 +1,88 @@
import express from "express";
import process from "node:process";
import adminRouter from "./routes/admin.js";
import claimsRouter from "./routes/claims.js";
import open91Router from "./routes/open-91.js";
import tencentRouter from "./routes/tencent.js";
import webhooksRouter from "./routes/webhooks.js";
import { buildSuccessPayload } from "./utils/http.js";
import { accessLogMiddleware } from "./middleware/access-log.js";
import { corsMiddleware } from "./middleware/cors.js";
import { buildHealthPayload, type StartupState } from "./startup/state.js";
type CreateAppOptions = {
startupState: StartupState;
isShutdownStarted: () => boolean;
};
export function createApp({ startupState, isShutdownStarted }: CreateAppOptions) {
const app = express();
app.use(accessLogMiddleware);
app.use(corsMiddleware);
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,
})
);
});
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/tencent", tencentRouter);
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);
return app;
}
+39 -397
View File
@@ -1,415 +1,57 @@
import express from 'express'
import process from 'node:process'
import process from "node:process";
import { runtimeConfig } from './config/runtime.js'
import { runDatabaseMigrations } from './db/migrate.js'
import adminRouter from './routes/admin.js'
import claimsRouter from './routes/claims.js'
import open91Router from './routes/open-91.js'
import webhooksRouter from './routes/webhooks.js'
import { ensureAdminUsersBootstrapped } from './services/admin/admin-auth-service.js'
import { ensureFulfillmentCatalogBootstrapped } from './services/bootstrap/fulfillment-bootstrap-service.js'
import { startScheduledJobs, stopScheduledJobs } from './services/scheduler/scheduler-service.js'
import { closeOcrService, warmupOcrService } from './services/session/ocr.js'
import { closeAllTencentBrowserSessions, warmupTencentBrowser } from './services/session/session.js'
import { buildSuccessPayload } from './utils/http.js'
import { createRequestId, logError, logInfo, logWarn } from './utils/logger.js'
import tencentRouter from './routes/tencent.js'
import { createApp } from "./app.js";
import { runtimeConfig } from "./config/runtime.js";
import { bootstrapCoreServices } from "./startup/bootstrap.js";
import { createShutdownController } from "./startup/shutdown.js";
import { createStartupState, formatStartupError } from "./startup/state.js";
import { logError, logInfo } from "./utils/logger.js";
const CORE_BOOT_RETRY_DELAY_MS = 5_000
const port = Number(runtimeConfig.server.port || 3000);
const host = "0.0.0.0";
const startupState = createStartupState();
const app = express()
const port = Number(runtimeConfig.server.port || 3000)
const host = '0.0.0.0'
const startupState = createStartupState()
let shutdownController: ReturnType<typeof createShutdownController> | null = null;
app.use((req, res, next) => {
const startedAt = Date.now()
const requestId = resolveRequestId(req)
req.requestId = requestId
res.setHeader('X-Request-Id', requestId)
res.on('finish', () => {
if (shouldSkipAccessLog(req.originalUrl, res.statusCode)) {
return
}
logInfo('[http/access]', 'request completed', {
requestId,
method: req.method,
originalUrl: req.originalUrl,
statusCode: res.statusCode,
durationMs: Date.now() - startedAt,
ip: req.ip,
forwardedFor: String(req.headers['x-forwarded-for'] || ''),
userAgent: String(req.headers['user-agent'] || ''),
referer: String(req.headers.referer || ''),
contentLength: Number(res.getHeader('content-length') || 0),
actorUserId: req.adminSession?.userId || '',
actorUsername: req.adminSession?.username || '',
actorRole: req.adminSession?.role || '',
})
})
next()
})
app.use((req, res, next) => {
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')
if (req.method === 'OPTIONS') {
res.sendStatus(204)
return
}
next()
})
app.use(express.json({ limit: '2mb' }))
app.use(express.urlencoded({ extended: true }))
app.get('/health', (_req, res) => {
res.json(buildSuccessPayload(buildHealthPayload(), startupState.core.ready ? 'ready' : 'starting'))
})
app.get('/health/live', (_req, res) => {
res.json(buildSuccessPayload({
status: shutdownStarted ? 'shutting_down' : 'alive',
pid: process.pid,
}))
})
app.get('/health/ready', (_req, res) => {
if (startupState.core.ready) {
res.json(buildSuccessPayload(buildHealthPayload(), 'ready'))
return
}
res.status(503).json({
code: 1,
msg: startupState.core.lastError || '服务启动中,请稍后重试',
errorCode: 'service_not_ready',
time: Math.floor(Date.now() / 1000),
data: buildHealthPayload(),
})
})
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/tencent', tencentRouter)
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)
const app = createApp({
startupState,
isShutdownStarted: () => shutdownController?.isShutdownStarted() || false,
});
const server = app.listen(port, host, () => {
logInfo('[startup]', `order-site-backend listening on http://${host}:${port}`)
void bootstrapCoreServices()
})
logInfo("[startup]", `order-site-backend listening on http://${host}:${port}`);
void bootstrapCoreServices(
startupState,
() => shutdownController?.isShutdownStarted() || false
);
});
server.on('error', (error) => {
logError('[startup]', 'HTTP server failed', error)
})
shutdownController = createShutdownController(server, startupState);
async function bootstrapCoreServices() {
if (startupState.core.running || shutdownStarted) {
return
}
server.on("error", (error) => {
logError("[startup]", "HTTP server failed", error);
});
startupState.core.running = true
process.on("SIGINT", () => {
void shutdownController?.shutdown("SIGINT");
});
while (!shutdownStarted && !startupState.core.ready) {
startupState.phase = startupState.core.attemptCount === 0 ? 'starting' : 'retrying'
startupState.core.attemptCount += 1
startupState.core.lastAttemptAt = new Date().toISOString()
process.on("SIGTERM", () => {
void shutdownController?.shutdown("SIGTERM");
});
try {
logInfo('[startup]', '开始执行核心启动步骤', {
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]', '核心启动步骤完成,服务已就绪', {
attempt: startupState.core.attemptCount,
})
startScheduledJobs()
void bootstrapBrowser()
void bootstrapOcr()
break
} catch (error) {
const message = formatStartupError(error)
startupState.phase = 'retrying'
startupState.core.lastError = message
logError('[startup]', '核心启动步骤失败,将自动重试', {
attempt: startupState.core.attemptCount,
retryDelayMs: CORE_BOOT_RETRY_DELAY_MS,
error,
})
await sleep(CORE_BOOT_RETRY_DELAY_MS)
}
}
startupState.core.running = false
}
async function bootstrapBrowser() {
startupState.browser.status = 'starting'
startupState.browser.lastAttemptAt = new Date().toISOString()
try {
const result = await warmupTencentBrowser()
if (!result.warmed) {
startupState.browser.status = 'skipped'
startupState.browser.message = 'browser prewarm skipped'
logInfo('[startup]', 'browser prewarm skipped')
return
}
startupState.browser.status = 'ready'
startupState.browser.message = 'browser prewarm ready'
logInfo('[startup]', 'browser prewarm ready')
} catch (error) {
if (isMissingPlaywrightBrowserError(error)) {
const installCommand = resolveBrowserInstallCommand()
startupState.browser.status = 'degraded'
startupState.browser.message = formatErrorMessage(error)
logWarn('[startup]', `browser prewarm skipped: ${formatErrorMessage(error)}`)
logWarn('[startup]', `请先安装浏览器依赖: ${installCommand}`)
if (String(runtimeConfig.browser.chromePath || '').trim()) {
logWarn('[startup]', '当前设置了 CHROME_PATH,也请确认该路径指向的浏览器可执行文件真实存在')
}
return
}
startupState.browser.status = 'degraded'
startupState.browser.message = formatStartupError(error)
logError('[startup]', 'browser prewarm failed', error)
}
}
async function bootstrapOcr() {
startupState.ocr.status = 'starting'
startupState.ocr.lastAttemptAt = new Date().toISOString()
try {
await warmupOcrService()
startupState.ocr.status = 'ready'
startupState.ocr.message = 'OCR service ready'
logInfo('[startup]', 'OCR service ready')
} catch (error) {
startupState.ocr.status = 'degraded'
startupState.ocr.message = formatStartupError(error)
logWarn('[startup]', `OCR service skipped: ${formatStartupError(error)}`)
}
}
function buildHealthPayload() {
return {
phase: startupState.phase,
startedAt: startupState.startedAt,
core: {
ready: startupState.core.ready,
running: startupState.core.running,
attemptCount: startupState.core.attemptCount,
readyAt: startupState.core.readyAt,
lastAttemptAt: startupState.core.lastAttemptAt,
lastError: startupState.core.lastError,
},
browser: {
status: startupState.browser.status,
message: startupState.browser.message,
lastAttemptAt: startupState.browser.lastAttemptAt,
},
ocr: {
status: startupState.ocr.status,
message: startupState.ocr.message,
lastAttemptAt: startupState.ocr.lastAttemptAt,
},
process: {
pid: process.pid,
shutdownStarted,
lastUnhandledRejection: startupState.process.lastUnhandledRejection,
lastUncaughtException: startupState.process.lastUncaughtException,
},
}
}
function createStartupState() {
return {
phase: 'starting',
startedAt: new Date().toISOString(),
core: {
ready: false,
running: false,
attemptCount: 0,
readyAt: '',
lastAttemptAt: '',
lastError: '',
},
browser: {
status: 'pending',
message: '',
lastAttemptAt: '',
},
ocr: {
status: 'pending',
message: '',
lastAttemptAt: '',
},
process: {
lastUnhandledRejection: null,
lastUncaughtException: null,
},
}
}
function formatStartupError(error) {
if (error instanceof Error && error.message) {
return error.message.split('\n')[0].trim()
}
return String(error || '未知错误').trim()
}
function isMissingPlaywrightBrowserError(error) {
const message = formatErrorMessage(error).toLowerCase()
return (
message.includes("executable doesn't exist") ||
message.includes('please run the following command to download new browsers') ||
message.includes('browserType.launch'.toLowerCase())
&& message.includes('please run')
)
}
function formatErrorMessage(error) {
if (error instanceof Error && error.message) {
return error.message.split('\n')[0].trim()
}
return String(error || '未知错误').trim()
}
function resolveBrowserInstallCommand() {
if (process.platform === 'linux') {
return 'npm run browser:install:linux'
}
return 'npm run browser:install'
}
function sleep(ms) {
return new Promise((resolve) => {
setTimeout(resolve, ms)
})
}
function resolveRequestId(req) {
const fromHeader = String(req.headers['x-request-id'] || '').trim()
return fromHeader || createRequestId('req')
}
function shouldSkipAccessLog(originalUrl, statusCode) {
const pathname = String(originalUrl || '').split('?')[0]
return ['/health', '/health/live', '/health/ready'].includes(pathname) && Number(statusCode) < 400
}
let shutdownStarted = false
async function shutdown(signal) {
if (shutdownStarted) {
return
}
shutdownStarted = true
startupState.phase = 'shutting_down'
logInfo('[shutdown]', `received ${signal}, closing browser sessions and HTTP server`)
stopScheduledJobs()
try {
await closeAllTencentBrowserSessions({ markClosed: true })
} catch (error) {
logError('[shutdown]', 'failed to close browser sessions', error)
}
try {
await closeOcrService()
} catch (error) {
logError('[shutdown]', 'failed to close OCR service', error)
}
if (!server.listening) {
return
}
await new Promise<void>((resolve) => {
server.close((error) => {
if (error) {
logError('[shutdown]', 'failed to close HTTP server', error)
process.exitCode = 1
}
resolve()
})
})
}
process.on('SIGINT', () => {
void shutdown('SIGINT')
})
process.on('SIGTERM', () => {
void shutdown('SIGTERM')
})
process.on('unhandledRejection', (reason) => {
process.on("unhandledRejection", (reason) => {
startupState.process.lastUnhandledRejection = {
time: new Date().toISOString(),
message: formatStartupError(reason),
}
logError('[process]', 'unhandled promise rejection', reason)
})
};
logError("[process]", "unhandled promise rejection", reason);
});
process.on('uncaughtException', (error) => {
process.on("uncaughtException", (error) => {
startupState.process.lastUncaughtException = {
time: new Date().toISOString(),
message: formatStartupError(error),
}
logError('[process]', 'uncaught exception captured', error)
})
};
logError("[process]", "uncaught exception captured", error);
});
+43
View File
@@ -0,0 +1,43 @@
import { createRequestId, logInfo } from "../utils/logger.js";
export function accessLogMiddleware(req, res, next) {
const startedAt = Date.now();
const requestId = resolveRequestId(req);
req.requestId = requestId;
res.setHeader("X-Request-Id", requestId);
res.on("finish", () => {
if (shouldSkipAccessLog(req.originalUrl, res.statusCode)) {
return;
}
logInfo("[http/access]", "request completed", {
requestId,
method: req.method,
originalUrl: req.originalUrl,
statusCode: res.statusCode,
durationMs: Date.now() - startedAt,
ip: req.ip,
forwardedFor: String(req.headers["x-forwarded-for"] || ""),
userAgent: String(req.headers["user-agent"] || ""),
referer: String(req.headers.referer || ""),
contentLength: Number(res.getHeader("content-length") || 0),
actorUserId: req.adminSession?.userId || "",
actorUsername: req.adminSession?.username || "",
actorRole: req.adminSession?.role || "",
});
});
next();
}
function resolveRequestId(req) {
const fromHeader = String(req.headers["x-request-id"] || "").trim();
return fromHeader || createRequestId("req");
}
function shouldSkipAccessLog(originalUrl, statusCode) {
const pathname = String(originalUrl || "").split("?")[0];
return ["/health", "/health/live", "/health/ready"].includes(pathname) && Number(statusCode) < 400;
}
+12
View File
@@ -0,0 +1,12 @@
export function corsMiddleware(req, res, next) {
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");
if (req.method === "OPTIONS") {
res.sendStatus(204);
return;
}
next();
}
+15 -914
View File
@@ -1,922 +1,23 @@
import { Router } from "express";
import {
appointAdminCloudtentaclesVirtualNumber,
backAdminCloudtentaclesVirtualNumber,
buyAdminCloudtentaclesSku,
fetchAdminCloudtentaclesVirtualNumberCode,
generateAdminCloudtentaclesLoginCode,
getAdminCloudtentaclesAsset,
getAdminCloudtentaclesBindUrl,
getAdminCloudtentaclesCategories,
getAdminNotificationConfig,
getAdminScheduledJobsConfig,
getAdminKuaishouCloudFulfillmentConfig,
getAdminKuaishouEticketSourceConfig,
getAdminCloudtentaclesKnapsack,
listAdminCloudtentaclesSources,
deleteAdminCloudtentaclesSource,
getAdminCloudtentaclesSkuList,
useAdminCloudtentaclesSku,
getAdminAgisoShopConfigs,
getAdminFulfillmentBindingConfigs,
getAdminNinetyoneOrders,
consumeAdminKuaishouEticket,
failAdminNinetyoneOrder,
listAdminCloudtentaclesVirtualNumbers,
lookupAdminFulfillmentBindingOrder,
queryAdminKuaishouEticketShopInfo,
queryAdminKuaishouEticketDetail,
retryAdminNinetyoneOrder,
runAdminCloudtentaclesFullFlow,
runAdminScheduledJobNow,
sendAdminCloudtentaclesSmsCode,
testAdminNotification,
testAdminCloudtentaclesLogin,
updateAdminKuaishouCloudFulfillmentConfig,
updateAdminKuaishouEticketSourceConfig,
updateAdminCloudtentaclesSourceConfig,
updateAdminNotificationConfig,
updateAdminScheduledJobsConfig,
updateAdminAgisoShopConfigs,
updateAdminFulfillmentBindingConfigs,
verifyAdminCloudtentaclesLoginCode,
validateAdminCloudtentaclesSession,
} from "../../services/admin/platform-config/service.js";
import { createJsonHandler, requireAdminRoles } from "./shared.js";
import type { AdminAgisoShopConfigSaveResponse } from "../../types/admin-write-models.js";
import type {
AdminAgisoShopConfigRouteBody,
AdminCloudtentaclesCatalogQueryRouteBody,
AdminCloudtentaclesFullFlowRouteBody,
AdminCloudtentaclesSendSmsCodeRouteBody,
AdminCloudtentaclesSkuBuyRouteBody,
AdminCloudtentaclesSkuUseRouteBody,
AdminCloudtentaclesSourceConfigRouteBody,
AdminCloudtentaclesTestLoginRouteBody,
AdminCloudtentaclesValidateSessionRouteBody,
AdminCloudtentaclesVirtualNumberRouteBody,
AdminEntityRouteParams,
AdminFulfillmentBindingConfigRouteBody,
AdminFulfillmentBindingLookupRouteBody,
AdminKuaishouCloudFulfillmentConfigRouteBody,
AdminKuaishouEticketConsumeRouteBody,
AdminKuaishouEticketDetailQueryRouteBody,
AdminKuaishouEticketShopInfoRouteBody,
AdminKuaishouEticketSourceConfigRouteBody,
AdminNotificationConfigRouteBody,
AdminNotificationTestRouteBody,
AdminScheduledJobsConfigRouteBody,
} from "../../types/admin-route-inputs.js";
import { requireAdminRoles } from "./shared.js";
import agisoRouter from "./platform-config/agiso.js";
import cloudtentaclesRouter from "./platform-config/cloudtentacles.js";
import fulfillmentBindingsRouter from "./platform-config/fulfillment-bindings.js";
import kuaishouCloudFulfillmentRouter from "./platform-config/kuaishou-cloud-fulfillment.js";
import kuaishouEticketRouter from "./platform-config/kuaishou-eticket.js";
import ninetyoneRouter from "./platform-config/ninetyone.js";
import notificationsRouter from "./platform-config/notifications.js";
const router = Router();
type JsonRecord = Record<string, any>;
router.use("/platform-config", requireAdminRoles(["admin"]));
router.get(
"/platform-config/agiso-shops",
createJsonHandler(() => getAdminAgisoShopConfigs(), {
successMessage: "ok",
errorMessage: "读取 Agiso 店铺配置失败",
scope: "[admin/platform-config/agiso-shops]",
})
);
router.post(
"/platform-config/agiso-shops",
createJsonHandler(
(req) =>
updateAdminAgisoShopConfigs(
req.body as AdminAgisoShopConfigRouteBody
),
{
successMessage: "Agiso 店铺配置已保存",
errorMessage: "保存 Agiso 店铺配置失败",
scope: "[admin/platform-config/agiso-shops]",
audit: (_req, data) => {
const result = data as AdminAgisoShopConfigSaveResponse;
return {
action: "platform_shop_config_updated",
targetType: "platform_config",
targetId: "agiso_shops",
data: {
shopCount: result.shops.length,
filePath: result.filePath,
},
};
},
}
)
);
router.get(
"/platform-config/notifications",
createJsonHandler(() => getAdminNotificationConfig(), {
successMessage: "ok",
errorMessage: "读取内部通知配置失败",
scope: "[admin/platform-config/notifications]",
})
);
router.post(
"/platform-config/notifications",
createJsonHandler(
(req) =>
updateAdminNotificationConfig(
req.body as AdminNotificationConfigRouteBody
),
{
successMessage: "内部通知配置已保存",
errorMessage: "保存内部通知配置失败",
scope: "[admin/platform-config/notifications]",
audit: (_req, data) => {
const result = data as JsonRecord;
return {
action: "platform_notification_config_updated",
targetType: "platform_config",
targetId: "notifications",
data: {
filePath: String(result.filePath || "").trim(),
enabled: Boolean(result.source?.enabled),
barkRecipientCount: Array.isArray(
result.source?.channels?.bark?.recipients
)
? result.source.channels.bark.recipients.length
: 0,
},
};
},
}
)
);
router.post(
"/platform-config/notifications/test",
createJsonHandler(
(req) =>
testAdminNotification(
req.body as AdminNotificationTestRouteBody
),
{
successMessage: "内部通知测试已执行",
errorMessage: "内部通知测试失败",
scope: "[admin/platform-config/notifications/test]",
audit: (_req, data) => {
const result = data as JsonRecord;
return {
action: "platform_notification_test_sent",
targetType: "platform_config",
targetId: "notifications",
data: {
channel: String(result.channel || "").trim(),
successCount: Number(result.successCount || 0),
failedCount: Number(result.failedCount || 0),
},
};
},
}
)
);
router.get(
"/platform-config/scheduled-jobs",
createJsonHandler(() => getAdminScheduledJobsConfig(), {
successMessage: "ok",
errorMessage: "读取定时任务配置失败",
scope: "[admin/platform-config/scheduled-jobs]",
})
);
router.post(
"/platform-config/scheduled-jobs",
createJsonHandler(
(req) =>
updateAdminScheduledJobsConfig(
req.body as AdminScheduledJobsConfigRouteBody
),
{
successMessage: "定时任务配置已保存",
errorMessage: "保存定时任务配置失败",
scope: "[admin/platform-config/scheduled-jobs]",
audit: (_req, data) => {
const result = data as JsonRecord;
return {
action: "platform_scheduled_jobs_updated",
targetType: "platform_config",
targetId: "scheduled_jobs",
data: {
filePath: String(result.filePath || "").trim(),
enabled: Boolean(result.source?.enabled),
jobCount: Array.isArray(result.source?.jobs)
? result.source.jobs.length
: 0,
},
};
},
}
)
);
router.post(
"/platform-config/scheduled-jobs/:id/run",
createJsonHandler(
(req) =>
runAdminScheduledJobNow(
(req.params as AdminEntityRouteParams).id
),
{
successMessage: "定时任务已执行",
errorMessage: "执行定时任务失败",
scope: "[admin/platform-config/scheduled-jobs/:id/run]",
audit: (req, data) => {
const result = data as JsonRecord;
return {
action: "platform_scheduled_job_run",
targetType: "platform_config",
targetId: String(
(req.params as AdminEntityRouteParams).id || ""
).trim(),
data: {
status: String(result.result?.status || "").trim(),
message: String(result.result?.message || "").trim(),
},
};
},
}
)
);
router.get(
"/platform-config/kuaishou-eticket-source",
createJsonHandler(() => getAdminKuaishouEticketSourceConfig(), {
successMessage: "ok",
errorMessage: "读取快手小店核销配置失败",
scope: "[admin/platform-config/kuaishou-eticket-source]",
})
);
router.post(
"/platform-config/kuaishou-eticket-source",
createJsonHandler(
(req) =>
updateAdminKuaishouEticketSourceConfig(
req.body as AdminKuaishouEticketSourceConfigRouteBody
),
{
successMessage: "快手小店核销配置已保存",
errorMessage: "保存快手小店核销配置失败",
scope: "[admin/platform-config/kuaishou-eticket-source]",
audit: (_req, data) => {
const result = data as JsonRecord;
return {
action: "platform_kuaishou_eticket_source_updated",
targetType: "platform_config",
targetId: "kuaishou_eticket_source",
data: {
filePath: String(result.filePath || "").trim(),
enabled: Boolean(result.source?.enabled),
shopCount: Array.isArray(result.source?.shops)
? result.source.shops.length
: 0,
},
};
},
}
)
);
router.get(
"/platform-config/ninetyone/orders",
createJsonHandler((req) => getAdminNinetyoneOrders(req.query), {
successMessage: "ok",
errorMessage: "读取 91卡券订单失败",
scope: "[admin/platform-config/ninetyone/orders]",
})
);
router.post(
"/platform-config/ninetyone/orders/:id/retry",
createJsonHandler(
(req) =>
retryAdminNinetyoneOrder(
(req.params as AdminEntityRouteParams).id
),
{
successMessage: "91卡券订单已重试",
errorMessage: "重试 91卡券订单失败",
scope: "[admin/platform-config/ninetyone/orders/:id/retry]",
audit: (_req, data) => {
const result = data as JsonRecord;
return {
action: "platform_ninetyone_order_retried",
targetType: "order",
targetId: String(result.orderId || "").trim(),
data: {
orderNo: String(result.orderNo || "").trim(),
taskCount: Number(result.taskCount || 0),
},
};
},
}
)
);
router.post(
"/platform-config/ninetyone/orders/:id/fail",
createJsonHandler(
(req) =>
failAdminNinetyoneOrder(
(req.params as AdminEntityRouteParams).id,
req.body as { reason?: string }
),
{
successMessage: "91卡券订单已标记失败",
errorMessage: "标记 91卡券订单失败",
scope: "[admin/platform-config/ninetyone/orders/:id/fail]",
audit: (req, data) => {
const result = data as JsonRecord;
return {
action: "platform_ninetyone_order_failed",
targetType: "order",
targetId: String(result.orderId || "").trim(),
data: {
orderNo: String(result.orderNo || "").trim(),
reason: String(
(req.body as { reason?: string }).reason || ""
).trim(),
},
};
},
}
)
);
router.post(
"/platform-config/kuaishou-eticket/query-detail",
createJsonHandler(
(req) =>
queryAdminKuaishouEticketDetail(
req.body as AdminKuaishouEticketDetailQueryRouteBody
),
{
successMessage: "快手小店核销信息查询成功",
errorMessage: "查询快手小店核销信息失败",
scope: "[admin/platform-config/kuaishou-eticket/query-detail]",
audit: (req, data) => {
const body = req.body as AdminKuaishouEticketDetailQueryRouteBody;
const result = data as JsonRecord;
return {
action: "platform_kuaishou_eticket_query_detail",
targetType: "platform_config",
targetId:
String(result.eTicketId || body.eTicketId || "").trim() ||
"kuaishou_eticket",
data: {
result: Number(result.result || 0),
ok: Boolean(result.ok),
alreadyConsumed: Boolean(result.alreadyConsumed),
},
};
},
}
)
);
router.post(
"/platform-config/kuaishou-eticket/query-shop-info",
createJsonHandler(
(req) =>
queryAdminKuaishouEticketShopInfo(
req.body as AdminKuaishouEticketShopInfoRouteBody
),
{
successMessage: "快手小店店铺信息查询成功",
errorMessage: "查询快手小店店铺信息失败",
scope: "[admin/platform-config/kuaishou-eticket/query-shop-info]",
audit: (req, data) => {
const body = req.body as AdminKuaishouEticketShopInfoRouteBody;
const result = data as JsonRecord;
return {
action: "platform_kuaishou_eticket_query_shop_info",
targetType: "platform_config",
targetId:
String(result.shop?.shopId || body.shopId || "").trim() ||
"kuaishou_eticket_shop_info",
data: {
result: Number(result.result || 0),
ok: Boolean(result.ok),
kshopName: String(result.shop?.kshopName || "").trim(),
},
};
},
}
)
);
router.post(
"/platform-config/kuaishou-eticket/consume",
createJsonHandler(
(req) =>
consumeAdminKuaishouEticket(
req.body as AdminKuaishouEticketConsumeRouteBody
),
{
successMessage: "快手小店核销请求已执行",
errorMessage: "执行快手小店核销失败",
scope: "[admin/platform-config/kuaishou-eticket/consume]",
audit: (req, data) => {
const body = req.body as AdminKuaishouEticketConsumeRouteBody;
const result = data as JsonRecord;
return {
action: "platform_kuaishou_eticket_consume",
targetType: "platform_config",
targetId:
String(result.eTicketId || body.eTicketId || "").trim() ||
"kuaishou_eticket",
data: {
result: Number(result.result || 0),
consumed: Boolean(result.consumed),
alreadyConsumed: Boolean(result.alreadyConsumed),
},
};
},
}
)
);
router.get(
"/platform-config/cloudtentacles-source",
createJsonHandler(() => listAdminCloudtentaclesSources(), {
successMessage: "ok",
errorMessage: "读取 cloudtentacles 履约平台配置失败",
scope: "[admin/platform-config/cloudtentacles-source]",
})
);
router.delete(
"/platform-config/cloudtentacles-source/:sourceKey",
createJsonHandler(
(req) =>
deleteAdminCloudtentaclesSource(
String(req.params.sourceKey || "").trim()
),
{
successMessage: "cloudtentacles 履约平台配置已删除",
errorMessage: "删除 cloudtentacles 履约平台配置失败",
scope: "[admin/platform-config/cloudtentacles-source/:sourceKey]",
audit: (req) => ({
action: "platform_cloudtentacles_source_deleted",
targetType: "platform_config",
targetId: String(req.params.sourceKey || "").trim(),
data: {},
}),
}
)
);
router.post(
"/platform-config/cloudtentacles-source",
createJsonHandler(
(req) =>
updateAdminCloudtentaclesSourceConfig(
req.body as AdminCloudtentaclesSourceConfigRouteBody
),
{
successMessage: "cloudtentacles 履约平台配置已保存",
errorMessage: "保存 cloudtentacles 履约平台配置失败",
scope: "[admin/platform-config/cloudtentacles-source]",
audit: (_req, data) => {
const result = data as JsonRecord;
return {
action: "platform_cloudtentacles_source_updated",
targetType: "platform_config",
targetId: "cloudtentacles_source",
data: {
filePath: String(result.filePath || "").trim(),
username: String(result.source?.username || "").trim(),
enabled: Boolean(result.source?.enabled),
},
};
},
}
)
);
router.post(
"/platform-config/cloudtentacles/send-sms-code",
createJsonHandler(
(req) =>
sendAdminCloudtentaclesSmsCode(
req.body as AdminCloudtentaclesSendSmsCodeRouteBody
),
{
successMessage: "cloudtentacles 短信验证码已发送",
errorMessage: "cloudtentacles 发送短信验证码失败",
scope: "[admin/platform-config/cloudtentacles/send-sms-code]",
audit: (req, data) => {
const body = req.body as AdminCloudtentaclesSendSmsCodeRouteBody;
const result = data as JsonRecord;
return {
action: "platform_cloudtentacles_send_sms_code",
targetType: "platform_config",
targetId:
String(result.username || body.username || "").trim() ||
"cloudtentacles",
data: {
baseUrl: result.baseUrl || String(body.baseUrl || "").trim(),
phoneMasked: result.phoneMasked || "",
},
};
},
}
)
);
router.post(
"/platform-config/cloudtentacles/test-login",
createJsonHandler(
(req) =>
testAdminCloudtentaclesLogin(
req.body as AdminCloudtentaclesTestLoginRouteBody
),
{
successMessage: "cloudtentacles 登录测试成功",
errorMessage: "cloudtentacles 登录测试失败",
scope: "[admin/platform-config/cloudtentacles/test-login]",
audit: (req, data) => {
const body = req.body as AdminCloudtentaclesTestLoginRouteBody;
const result = data as JsonRecord;
return {
action: "platform_cloudtentacles_test_login",
targetType: "platform_config",
targetId:
String(result.username || body.username || "").trim() ||
"cloudtentacles",
data: {
baseUrl: result.baseUrl || String(body.baseUrl || "").trim(),
permissionCount: Number(result.session?.permissionCount || 0),
},
};
},
}
)
);
router.post(
"/platform-config/cloudtentacles/validate-session",
createJsonHandler(
(req) =>
validateAdminCloudtentaclesSession(
req.body as AdminCloudtentaclesValidateSessionRouteBody
),
{
successMessage: "cloudtentacles 会话校验成功",
errorMessage: "cloudtentacles 会话校验失败",
scope: "[admin/platform-config/cloudtentacles/validate-session]",
audit: (_req, data) => {
const result = data as JsonRecord;
return {
action: "platform_cloudtentacles_validate_session",
targetType: "platform_config",
targetId: "cloudtentacles_session",
data: {
baseUrl: String(result.baseUrl || "").trim(),
permissionCount: Number(result.session?.permissionCount || 0),
},
};
},
}
)
);
router.post(
"/platform-config/cloudtentacles/asset",
createJsonHandler(
(req) =>
getAdminCloudtentaclesAsset(
req.body as AdminCloudtentaclesCatalogQueryRouteBody
),
{
successMessage: "cloudtentacles 余额查询成功",
errorMessage: "cloudtentacles 余额查询失败",
scope: "[admin/platform-config/cloudtentacles/asset]",
}
)
);
router.post(
"/platform-config/cloudtentacles/categories",
createJsonHandler(
(req) =>
getAdminCloudtentaclesCategories(
req.body as AdminCloudtentaclesCatalogQueryRouteBody
),
{
successMessage: "cloudtentacles 分类查询成功",
errorMessage: "cloudtentacles 分类查询失败",
scope: "[admin/platform-config/cloudtentacles/categories]",
}
)
);
router.post(
"/platform-config/cloudtentacles/sku/list",
createJsonHandler(
(req) =>
getAdminCloudtentaclesSkuList(
req.body as AdminCloudtentaclesCatalogQueryRouteBody
),
{
successMessage: "cloudtentacles SKU 列表查询成功",
errorMessage: "cloudtentacles SKU 列表查询失败",
scope: "[admin/platform-config/cloudtentacles/sku/list]",
}
)
);
router.post(
"/platform-config/cloudtentacles/sku/buy",
createJsonHandler(
(req) =>
buyAdminCloudtentaclesSku(
req.body as AdminCloudtentaclesSkuBuyRouteBody
),
{
successMessage: "cloudtentacles SKU 购买成功",
errorMessage: "cloudtentacles SKU 购买失败",
scope: "[admin/platform-config/cloudtentacles/sku/buy]",
}
)
);
router.post(
"/platform-config/cloudtentacles/sku/use",
createJsonHandler(
(req) =>
useAdminCloudtentaclesSku(
req.body as AdminCloudtentaclesSkuUseRouteBody
),
{
successMessage: "cloudtentacles 发货成功",
errorMessage: "cloudtentacles 发货失败",
scope: "[admin/platform-config/cloudtentacles/sku/use]",
}
)
);
router.post(
"/platform-config/cloudtentacles/knapsack",
createJsonHandler(
(req) =>
getAdminCloudtentaclesKnapsack(
req.body as AdminCloudtentaclesCatalogQueryRouteBody
),
{
successMessage: "cloudtentacles 背包查询成功",
errorMessage: "cloudtentacles 背包查询失败",
scope: "[admin/platform-config/cloudtentacles/knapsack]",
}
)
);
router.post(
"/platform-config/cloudtentacles/vn/list",
createJsonHandler(
(req) =>
listAdminCloudtentaclesVirtualNumbers(
req.body as AdminCloudtentaclesVirtualNumberRouteBody
),
{
successMessage: "cloudtentacles 虚拟号列表查询成功",
errorMessage: "cloudtentacles 虚拟号列表查询失败",
scope: "[admin/platform-config/cloudtentacles/vn/list]",
}
)
);
router.post(
"/platform-config/cloudtentacles/vn/appoint",
createJsonHandler(
(req) =>
appointAdminCloudtentaclesVirtualNumber(
req.body as AdminCloudtentaclesVirtualNumberRouteBody
),
{
successMessage: "cloudtentacles 虚拟号申请成功",
errorMessage: "cloudtentacles 虚拟号申请失败",
scope: "[admin/platform-config/cloudtentacles/vn/appoint]",
}
)
);
router.post(
"/platform-config/cloudtentacles/vn/generate-login-code",
createJsonHandler(
(req) =>
generateAdminCloudtentaclesLoginCode(
req.body as AdminCloudtentaclesVirtualNumberRouteBody
),
{
successMessage: "cloudtentacles 登录码生成成功",
errorMessage: "cloudtentacles 登录码生成失败",
scope: "[admin/platform-config/cloudtentacles/vn/generate-login-code]",
}
)
);
router.post(
"/platform-config/cloudtentacles/vn/fetch-code",
createJsonHandler(
(req) =>
fetchAdminCloudtentaclesVirtualNumberCode(
req.body as AdminCloudtentaclesVirtualNumberRouteBody
),
{
successMessage: "cloudtentacles 验证码获取成功",
errorMessage: "cloudtentacles 验证码获取失败",
scope: "[admin/platform-config/cloudtentacles/vn/fetch-code]",
}
)
);
router.post(
"/platform-config/cloudtentacles/vn/verify-code",
createJsonHandler(
(req) =>
verifyAdminCloudtentaclesLoginCode(
req.body as AdminCloudtentaclesVirtualNumberRouteBody
),
{
successMessage: "cloudtentacles 登录码校验成功",
errorMessage: "cloudtentacles 登录码校验失败",
scope: "[admin/platform-config/cloudtentacles/vn/verify-code]",
}
)
);
router.post(
"/platform-config/cloudtentacles/vn/bind-url",
createJsonHandler(
(req) =>
getAdminCloudtentaclesBindUrl(
req.body as AdminCloudtentaclesVirtualNumberRouteBody
),
{
successMessage: "cloudtentacles 兑换链接获取成功",
errorMessage: "cloudtentacles 兑换链接获取失败",
scope: "[admin/platform-config/cloudtentacles/vn/bind-url]",
}
)
);
router.post(
"/platform-config/cloudtentacles/vn/back",
createJsonHandler(
(req) =>
backAdminCloudtentaclesVirtualNumber(
req.body as AdminCloudtentaclesVirtualNumberRouteBody
),
{
successMessage: "cloudtentacles 号码退还成功",
errorMessage: "cloudtentacles 号码退还失败",
scope: "[admin/platform-config/cloudtentacles/vn/back]",
}
)
);
router.post(
"/platform-config/cloudtentacles/debug/full-flow",
createJsonHandler(
(req) =>
runAdminCloudtentaclesFullFlow(
req.body as AdminCloudtentaclesFullFlowRouteBody
),
{
successMessage: "cloudtentacles 完整调试流程执行成功",
errorMessage: "cloudtentacles 完整调试流程执行失败",
scope: "[admin/platform-config/cloudtentacles/debug/full-flow]",
}
)
);
router.get(
"/platform-config/fulfillment-bindings",
createJsonHandler(() => getAdminFulfillmentBindingConfigs(), {
successMessage: "ok",
errorMessage: "读取履约配置失败",
scope: "[admin/platform-config/fulfillment-bindings]",
})
);
router.post(
"/platform-config/fulfillment-bindings/lookup-order",
createJsonHandler(
(req) =>
lookupAdminFulfillmentBindingOrder(
req.body as AdminFulfillmentBindingLookupRouteBody
),
{
successMessage: "订单商品查询成功",
errorMessage: "手动查询订单商品失败",
scope: "[admin/platform-config/fulfillment-bindings/lookup-order]",
audit: (req, data) => {
const result = data as JsonRecord;
return {
action: "platform_fulfillment_order_lookup",
targetType: "platform_config",
targetId: [
result.order?.provider || "unknown",
result.order?.platform || "unknown",
result.order?.shopId || "unknown",
result.order?.platformOrderId || "unknown",
].join(":"),
data: {
itemCount: Array.isArray(result.items) ? result.items.length : 0,
shopId:
result.order?.shopId ||
String(
(req.body as AdminFulfillmentBindingLookupRouteBody)?.shopId || ""
).trim(),
platformOrderId:
result.order?.platformOrderId ||
String(
(req.body as AdminFulfillmentBindingLookupRouteBody)
?.platformOrderId || ""
).trim(),
},
};
},
}
)
);
router.post(
"/platform-config/fulfillment-bindings",
createJsonHandler(
(req) =>
updateAdminFulfillmentBindingConfigs(
req.body as AdminFulfillmentBindingConfigRouteBody
),
{
successMessage: "履约配置已保存",
errorMessage: "保存履约配置失败",
scope: "[admin/platform-config/fulfillment-bindings]",
audit: (_req, data) => {
const result = data as JsonRecord;
return {
action: "platform_fulfillment_config_updated",
targetType: "platform_config",
targetId: "fulfillment_bindings",
data: {
bindingCount: Array.isArray(result.bindings)
? result.bindings.length
: 0,
filePath: result.filePath || "",
},
};
},
}
)
);
router.get(
"/platform-config/kuaishou-cloud-fulfillment",
createJsonHandler(() => getAdminKuaishouCloudFulfillmentConfig(), {
successMessage: "ok",
errorMessage: "读取新履约配置失败",
scope: "[admin/platform-config/kuaishou-cloud-fulfillment]",
})
);
router.post(
"/platform-config/kuaishou-cloud-fulfillment",
createJsonHandler(
(req) =>
updateAdminKuaishouCloudFulfillmentConfig(
req.body as AdminKuaishouCloudFulfillmentConfigRouteBody
),
{
successMessage: "新履约配置已保存",
errorMessage: "保存新履约配置失败",
scope: "[admin/platform-config/kuaishou-cloud-fulfillment]",
audit: (_req, data) => {
const result = data as JsonRecord;
return {
action: "platform_kuaishou_cloud_fulfillment_updated",
targetType: "platform_config",
targetId: "kuaishou_cloud_fulfillment",
data: {
itemCount: Array.isArray(result.source?.items)
? result.source.items.length
: 0,
filePath: String(result.filePath || "").trim(),
},
};
},
}
)
);
router.use("/platform-config", agisoRouter);
router.use("/platform-config", notificationsRouter);
router.use("/platform-config", kuaishouEticketRouter);
router.use("/platform-config", ninetyoneRouter);
router.use("/platform-config", cloudtentaclesRouter);
router.use("/platform-config", fulfillmentBindingsRouter);
router.use("/platform-config", kuaishouCloudFulfillmentRouter);
export default router;
@@ -0,0 +1,47 @@
import { Router } from "express";
import {
getAdminAgisoShopConfigs,
updateAdminAgisoShopConfigs,
} from "../../../services/admin/platform-config/service.js";
import type { AdminAgisoShopConfigRouteBody } from "../../../types/admin-route-inputs.js";
import type { AdminAgisoShopConfigSaveResponse } from "../../../types/admin-write-models.js";
import { createJsonHandler } from "../shared.js";
const router = Router();
router.get(
"/agiso-shops",
createJsonHandler(() => getAdminAgisoShopConfigs(), {
successMessage: "ok",
errorMessage: "读取 Agiso 店铺配置失败",
scope: "[admin/platform-config/agiso-shops]",
})
);
router.post(
"/agiso-shops",
createJsonHandler(
(req) =>
updateAdminAgisoShopConfigs(req.body as AdminAgisoShopConfigRouteBody),
{
successMessage: "Agiso 店铺配置已保存",
errorMessage: "保存 Agiso 店铺配置失败",
scope: "[admin/platform-config/agiso-shops]",
audit: (_req, data) => {
const result = data as AdminAgisoShopConfigSaveResponse;
return {
action: "platform_shop_config_updated",
targetType: "platform_config",
targetId: "agiso_shops",
data: {
shopCount: result.shops.length,
filePath: result.filePath,
},
};
},
}
)
);
export default router;
@@ -0,0 +1,392 @@
import { Router } from "express";
import {
appointAdminCloudtentaclesVirtualNumber,
backAdminCloudtentaclesVirtualNumber,
buyAdminCloudtentaclesSku,
deleteAdminCloudtentaclesSource,
fetchAdminCloudtentaclesVirtualNumberCode,
generateAdminCloudtentaclesLoginCode,
getAdminCloudtentaclesAsset,
getAdminCloudtentaclesBindUrl,
getAdminCloudtentaclesCategories,
getAdminCloudtentaclesKnapsack,
getAdminCloudtentaclesSkuList,
listAdminCloudtentaclesSources,
listAdminCloudtentaclesVirtualNumbers,
runAdminCloudtentaclesFullFlow,
sendAdminCloudtentaclesSmsCode,
testAdminCloudtentaclesLogin,
updateAdminCloudtentaclesSourceConfig,
useAdminCloudtentaclesSku,
validateAdminCloudtentaclesSession,
verifyAdminCloudtentaclesLoginCode,
} from "../../../services/admin/platform-config/service.js";
import type {
AdminCloudtentaclesCatalogQueryRouteBody,
AdminCloudtentaclesFullFlowRouteBody,
AdminCloudtentaclesSendSmsCodeRouteBody,
AdminCloudtentaclesSkuBuyRouteBody,
AdminCloudtentaclesSkuUseRouteBody,
AdminCloudtentaclesSourceConfigRouteBody,
AdminCloudtentaclesTestLoginRouteBody,
AdminCloudtentaclesValidateSessionRouteBody,
AdminCloudtentaclesVirtualNumberRouteBody,
} from "../../../types/admin-route-inputs.js";
import { createJsonHandler } from "../shared.js";
import type { JsonRecord } from "./shared.js";
const router = Router();
router.get(
"/cloudtentacles-source",
createJsonHandler(() => listAdminCloudtentaclesSources(), {
successMessage: "ok",
errorMessage: "读取 cloudtentacles 履约平台配置失败",
scope: "[admin/platform-config/cloudtentacles-source]",
})
);
router.delete(
"/cloudtentacles-source/:sourceKey",
createJsonHandler(
(req) =>
deleteAdminCloudtentaclesSource(
String(req.params.sourceKey || "").trim()
),
{
successMessage: "cloudtentacles 履约平台配置已删除",
errorMessage: "删除 cloudtentacles 履约平台配置失败",
scope: "[admin/platform-config/cloudtentacles-source/:sourceKey]",
audit: (req) => ({
action: "platform_cloudtentacles_source_deleted",
targetType: "platform_config",
targetId: String(req.params.sourceKey || "").trim(),
data: {},
}),
}
)
);
router.post(
"/cloudtentacles-source",
createJsonHandler(
(req) =>
updateAdminCloudtentaclesSourceConfig(
req.body as AdminCloudtentaclesSourceConfigRouteBody
),
{
successMessage: "cloudtentacles 履约平台配置已保存",
errorMessage: "保存 cloudtentacles 履约平台配置失败",
scope: "[admin/platform-config/cloudtentacles-source]",
audit: (_req, data) => {
const result = data as JsonRecord;
return {
action: "platform_cloudtentacles_source_updated",
targetType: "platform_config",
targetId: "cloudtentacles_source",
data: {
filePath: String(result.filePath || "").trim(),
username: String(result.source?.username || "").trim(),
enabled: Boolean(result.source?.enabled),
},
};
},
}
)
);
router.post(
"/cloudtentacles/send-sms-code",
createJsonHandler(
(req) =>
sendAdminCloudtentaclesSmsCode(
req.body as AdminCloudtentaclesSendSmsCodeRouteBody
),
{
successMessage: "cloudtentacles 短信验证码已发送",
errorMessage: "cloudtentacles 发送短信验证码失败",
scope: "[admin/platform-config/cloudtentacles/send-sms-code]",
audit: (req, data) => {
const body = req.body as AdminCloudtentaclesSendSmsCodeRouteBody;
const result = data as JsonRecord;
return {
action: "platform_cloudtentacles_send_sms_code",
targetType: "platform_config",
targetId:
String(result.username || body.username || "").trim() ||
"cloudtentacles",
data: {
baseUrl: result.baseUrl || String(body.baseUrl || "").trim(),
phoneMasked: result.phoneMasked || "",
},
};
},
}
)
);
router.post(
"/cloudtentacles/test-login",
createJsonHandler(
(req) =>
testAdminCloudtentaclesLogin(
req.body as AdminCloudtentaclesTestLoginRouteBody
),
{
successMessage: "cloudtentacles 登录测试成功",
errorMessage: "cloudtentacles 登录测试失败",
scope: "[admin/platform-config/cloudtentacles/test-login]",
audit: (req, data) => {
const body = req.body as AdminCloudtentaclesTestLoginRouteBody;
const result = data as JsonRecord;
return {
action: "platform_cloudtentacles_test_login",
targetType: "platform_config",
targetId:
String(result.username || body.username || "").trim() ||
"cloudtentacles",
data: {
baseUrl: result.baseUrl || String(body.baseUrl || "").trim(),
permissionCount: Number(result.session?.permissionCount || 0),
},
};
},
}
)
);
router.post(
"/cloudtentacles/validate-session",
createJsonHandler(
(req) =>
validateAdminCloudtentaclesSession(
req.body as AdminCloudtentaclesValidateSessionRouteBody
),
{
successMessage: "cloudtentacles 会话校验成功",
errorMessage: "cloudtentacles 会话校验失败",
scope: "[admin/platform-config/cloudtentacles/validate-session]",
audit: (_req, data) => {
const result = data as JsonRecord;
return {
action: "platform_cloudtentacles_validate_session",
targetType: "platform_config",
targetId: "cloudtentacles_session",
data: {
baseUrl: String(result.baseUrl || "").trim(),
permissionCount: Number(result.session?.permissionCount || 0),
},
};
},
}
)
);
router.post(
"/cloudtentacles/asset",
createJsonHandler(
(req) =>
getAdminCloudtentaclesAsset(
req.body as AdminCloudtentaclesCatalogQueryRouteBody
),
{
successMessage: "cloudtentacles 余额查询成功",
errorMessage: "cloudtentacles 余额查询失败",
scope: "[admin/platform-config/cloudtentacles/asset]",
}
)
);
router.post(
"/cloudtentacles/categories",
createJsonHandler(
(req) =>
getAdminCloudtentaclesCategories(
req.body as AdminCloudtentaclesCatalogQueryRouteBody
),
{
successMessage: "cloudtentacles 分类查询成功",
errorMessage: "cloudtentacles 分类查询失败",
scope: "[admin/platform-config/cloudtentacles/categories]",
}
)
);
router.post(
"/cloudtentacles/sku/list",
createJsonHandler(
(req) =>
getAdminCloudtentaclesSkuList(
req.body as AdminCloudtentaclesCatalogQueryRouteBody
),
{
successMessage: "cloudtentacles SKU 列表查询成功",
errorMessage: "cloudtentacles SKU 列表查询失败",
scope: "[admin/platform-config/cloudtentacles/sku/list]",
}
)
);
router.post(
"/cloudtentacles/sku/buy",
createJsonHandler(
(req) =>
buyAdminCloudtentaclesSku(req.body as AdminCloudtentaclesSkuBuyRouteBody),
{
successMessage: "cloudtentacles SKU 购买成功",
errorMessage: "cloudtentacles SKU 购买失败",
scope: "[admin/platform-config/cloudtentacles/sku/buy]",
}
)
);
router.post(
"/cloudtentacles/sku/use",
createJsonHandler(
(req) =>
useAdminCloudtentaclesSku(req.body as AdminCloudtentaclesSkuUseRouteBody),
{
successMessage: "cloudtentacles 发货成功",
errorMessage: "cloudtentacles 发货失败",
scope: "[admin/platform-config/cloudtentacles/sku/use]",
}
)
);
router.post(
"/cloudtentacles/knapsack",
createJsonHandler(
(req) =>
getAdminCloudtentaclesKnapsack(
req.body as AdminCloudtentaclesCatalogQueryRouteBody
),
{
successMessage: "cloudtentacles 背包查询成功",
errorMessage: "cloudtentacles 背包查询失败",
scope: "[admin/platform-config/cloudtentacles/knapsack]",
}
)
);
router.post(
"/cloudtentacles/vn/list",
createJsonHandler(
(req) =>
listAdminCloudtentaclesVirtualNumbers(
req.body as AdminCloudtentaclesVirtualNumberRouteBody
),
{
successMessage: "cloudtentacles 虚拟号列表查询成功",
errorMessage: "cloudtentacles 虚拟号列表查询失败",
scope: "[admin/platform-config/cloudtentacles/vn/list]",
}
)
);
router.post(
"/cloudtentacles/vn/appoint",
createJsonHandler(
(req) =>
appointAdminCloudtentaclesVirtualNumber(
req.body as AdminCloudtentaclesVirtualNumberRouteBody
),
{
successMessage: "cloudtentacles 虚拟号申请成功",
errorMessage: "cloudtentacles 虚拟号申请失败",
scope: "[admin/platform-config/cloudtentacles/vn/appoint]",
}
)
);
router.post(
"/cloudtentacles/vn/generate-login-code",
createJsonHandler(
(req) =>
generateAdminCloudtentaclesLoginCode(
req.body as AdminCloudtentaclesVirtualNumberRouteBody
),
{
successMessage: "cloudtentacles 登录码生成成功",
errorMessage: "cloudtentacles 登录码生成失败",
scope: "[admin/platform-config/cloudtentacles/vn/generate-login-code]",
}
)
);
router.post(
"/cloudtentacles/vn/fetch-code",
createJsonHandler(
(req) =>
fetchAdminCloudtentaclesVirtualNumberCode(
req.body as AdminCloudtentaclesVirtualNumberRouteBody
),
{
successMessage: "cloudtentacles 验证码获取成功",
errorMessage: "cloudtentacles 验证码获取失败",
scope: "[admin/platform-config/cloudtentacles/vn/fetch-code]",
}
)
);
router.post(
"/cloudtentacles/vn/verify-code",
createJsonHandler(
(req) =>
verifyAdminCloudtentaclesLoginCode(
req.body as AdminCloudtentaclesVirtualNumberRouteBody
),
{
successMessage: "cloudtentacles 登录码校验成功",
errorMessage: "cloudtentacles 登录码校验失败",
scope: "[admin/platform-config/cloudtentacles/vn/verify-code]",
}
)
);
router.post(
"/cloudtentacles/vn/bind-url",
createJsonHandler(
(req) =>
getAdminCloudtentaclesBindUrl(
req.body as AdminCloudtentaclesVirtualNumberRouteBody
),
{
successMessage: "cloudtentacles 兑换链接获取成功",
errorMessage: "cloudtentacles 兑换链接获取失败",
scope: "[admin/platform-config/cloudtentacles/vn/bind-url]",
}
)
);
router.post(
"/cloudtentacles/vn/back",
createJsonHandler(
(req) =>
backAdminCloudtentaclesVirtualNumber(
req.body as AdminCloudtentaclesVirtualNumberRouteBody
),
{
successMessage: "cloudtentacles 号码退还成功",
errorMessage: "cloudtentacles 号码退还失败",
scope: "[admin/platform-config/cloudtentacles/vn/back]",
}
)
);
router.post(
"/cloudtentacles/debug/full-flow",
createJsonHandler(
(req) =>
runAdminCloudtentaclesFullFlow(
req.body as AdminCloudtentaclesFullFlowRouteBody
),
{
successMessage: "cloudtentacles 完整调试流程执行成功",
errorMessage: "cloudtentacles 完整调试流程执行失败",
scope: "[admin/platform-config/cloudtentacles/debug/full-flow]",
}
)
);
export default router;
@@ -0,0 +1,98 @@
import { Router } from "express";
import {
getAdminFulfillmentBindingConfigs,
lookupAdminFulfillmentBindingOrder,
updateAdminFulfillmentBindingConfigs,
} from "../../../services/admin/platform-config/service.js";
import type {
AdminFulfillmentBindingConfigRouteBody,
AdminFulfillmentBindingLookupRouteBody,
} from "../../../types/admin-route-inputs.js";
import { createJsonHandler } from "../shared.js";
import type { JsonRecord } from "./shared.js";
const router = Router();
router.get(
"/fulfillment-bindings",
createJsonHandler(() => getAdminFulfillmentBindingConfigs(), {
successMessage: "ok",
errorMessage: "读取履约配置失败",
scope: "[admin/platform-config/fulfillment-bindings]",
})
);
router.post(
"/fulfillment-bindings/lookup-order",
createJsonHandler(
(req) =>
lookupAdminFulfillmentBindingOrder(
req.body as AdminFulfillmentBindingLookupRouteBody
),
{
successMessage: "订单商品查询成功",
errorMessage: "手动查询订单商品失败",
scope: "[admin/platform-config/fulfillment-bindings/lookup-order]",
audit: (req, data) => {
const result = data as JsonRecord;
return {
action: "platform_fulfillment_order_lookup",
targetType: "platform_config",
targetId: [
result.order?.provider || "unknown",
result.order?.platform || "unknown",
result.order?.shopId || "unknown",
result.order?.platformOrderId || "unknown",
].join(":"),
data: {
itemCount: Array.isArray(result.items) ? result.items.length : 0,
shopId:
result.order?.shopId ||
String(
(req.body as AdminFulfillmentBindingLookupRouteBody)?.shopId ||
""
).trim(),
platformOrderId:
result.order?.platformOrderId ||
String(
(req.body as AdminFulfillmentBindingLookupRouteBody)
?.platformOrderId || ""
).trim(),
},
};
},
}
)
);
router.post(
"/fulfillment-bindings",
createJsonHandler(
(req) =>
updateAdminFulfillmentBindingConfigs(
req.body as AdminFulfillmentBindingConfigRouteBody
),
{
successMessage: "履约配置已保存",
errorMessage: "保存履约配置失败",
scope: "[admin/platform-config/fulfillment-bindings]",
audit: (_req, data) => {
const result = data as JsonRecord;
return {
action: "platform_fulfillment_config_updated",
targetType: "platform_config",
targetId: "fulfillment_bindings",
data: {
bindingCount: Array.isArray(result.bindings)
? result.bindings.length
: 0,
filePath: result.filePath || "",
},
};
},
}
)
);
export default router;
@@ -0,0 +1,51 @@
import { Router } from "express";
import {
getAdminKuaishouCloudFulfillmentConfig,
updateAdminKuaishouCloudFulfillmentConfig,
} from "../../../services/admin/platform-config/service.js";
import type { AdminKuaishouCloudFulfillmentConfigRouteBody } from "../../../types/admin-route-inputs.js";
import { createJsonHandler } from "../shared.js";
import type { JsonRecord } from "./shared.js";
const router = Router();
router.get(
"/kuaishou-cloud-fulfillment",
createJsonHandler(() => getAdminKuaishouCloudFulfillmentConfig(), {
successMessage: "ok",
errorMessage: "读取新履约配置失败",
scope: "[admin/platform-config/kuaishou-cloud-fulfillment]",
})
);
router.post(
"/kuaishou-cloud-fulfillment",
createJsonHandler(
(req) =>
updateAdminKuaishouCloudFulfillmentConfig(
req.body as AdminKuaishouCloudFulfillmentConfigRouteBody
),
{
successMessage: "新履约配置已保存",
errorMessage: "保存新履约配置失败",
scope: "[admin/platform-config/kuaishou-cloud-fulfillment]",
audit: (_req, data) => {
const result = data as JsonRecord;
return {
action: "platform_kuaishou_cloud_fulfillment_updated",
targetType: "platform_config",
targetId: "kuaishou_cloud_fulfillment",
data: {
itemCount: Array.isArray(result.source?.items)
? result.source.items.length
: 0,
filePath: String(result.filePath || "").trim(),
},
};
},
}
)
);
export default router;
@@ -0,0 +1,153 @@
import { Router } from "express";
import {
consumeAdminKuaishouEticket,
getAdminKuaishouEticketSourceConfig,
queryAdminKuaishouEticketDetail,
queryAdminKuaishouEticketShopInfo,
updateAdminKuaishouEticketSourceConfig,
} from "../../../services/admin/platform-config/service.js";
import type {
AdminKuaishouEticketConsumeRouteBody,
AdminKuaishouEticketDetailQueryRouteBody,
AdminKuaishouEticketShopInfoRouteBody,
AdminKuaishouEticketSourceConfigRouteBody,
} from "../../../types/admin-route-inputs.js";
import { createJsonHandler } from "../shared.js";
import type { JsonRecord } from "./shared.js";
const router = Router();
router.get(
"/kuaishou-eticket-source",
createJsonHandler(() => getAdminKuaishouEticketSourceConfig(), {
successMessage: "ok",
errorMessage: "读取快手小店核销配置失败",
scope: "[admin/platform-config/kuaishou-eticket-source]",
})
);
router.post(
"/kuaishou-eticket-source",
createJsonHandler(
(req) =>
updateAdminKuaishouEticketSourceConfig(
req.body as AdminKuaishouEticketSourceConfigRouteBody
),
{
successMessage: "快手小店核销配置已保存",
errorMessage: "保存快手小店核销配置失败",
scope: "[admin/platform-config/kuaishou-eticket-source]",
audit: (_req, data) => {
const result = data as JsonRecord;
return {
action: "platform_kuaishou_eticket_source_updated",
targetType: "platform_config",
targetId: "kuaishou_eticket_source",
data: {
filePath: String(result.filePath || "").trim(),
enabled: Boolean(result.source?.enabled),
shopCount: Array.isArray(result.source?.shops)
? result.source.shops.length
: 0,
},
};
},
}
)
);
router.post(
"/kuaishou-eticket/query-detail",
createJsonHandler(
(req) =>
queryAdminKuaishouEticketDetail(
req.body as AdminKuaishouEticketDetailQueryRouteBody
),
{
successMessage: "快手小店核销信息查询成功",
errorMessage: "查询快手小店核销信息失败",
scope: "[admin/platform-config/kuaishou-eticket/query-detail]",
audit: (req, data) => {
const body = req.body as AdminKuaishouEticketDetailQueryRouteBody;
const result = data as JsonRecord;
return {
action: "platform_kuaishou_eticket_query_detail",
targetType: "platform_config",
targetId:
String(result.eTicketId || body.eTicketId || "").trim() ||
"kuaishou_eticket",
data: {
result: Number(result.result || 0),
ok: Boolean(result.ok),
alreadyConsumed: Boolean(result.alreadyConsumed),
},
};
},
}
)
);
router.post(
"/kuaishou-eticket/query-shop-info",
createJsonHandler(
(req) =>
queryAdminKuaishouEticketShopInfo(
req.body as AdminKuaishouEticketShopInfoRouteBody
),
{
successMessage: "快手小店店铺信息查询成功",
errorMessage: "查询快手小店店铺信息失败",
scope: "[admin/platform-config/kuaishou-eticket/query-shop-info]",
audit: (req, data) => {
const body = req.body as AdminKuaishouEticketShopInfoRouteBody;
const result = data as JsonRecord;
return {
action: "platform_kuaishou_eticket_query_shop_info",
targetType: "platform_config",
targetId:
String(result.shop?.shopId || body.shopId || "").trim() ||
"kuaishou_eticket_shop_info",
data: {
result: Number(result.result || 0),
ok: Boolean(result.ok),
kshopName: String(result.shop?.kshopName || "").trim(),
},
};
},
}
)
);
router.post(
"/kuaishou-eticket/consume",
createJsonHandler(
(req) =>
consumeAdminKuaishouEticket(
req.body as AdminKuaishouEticketConsumeRouteBody
),
{
successMessage: "快手小店核销请求已执行",
errorMessage: "执行快手小店核销失败",
scope: "[admin/platform-config/kuaishou-eticket/consume]",
audit: (req, data) => {
const body = req.body as AdminKuaishouEticketConsumeRouteBody;
const result = data as JsonRecord;
return {
action: "platform_kuaishou_eticket_consume",
targetType: "platform_config",
targetId:
String(result.eTicketId || body.eTicketId || "").trim() ||
"kuaishou_eticket",
data: {
result: Number(result.result || 0),
consumed: Boolean(result.consumed),
alreadyConsumed: Boolean(result.alreadyConsumed),
},
};
},
}
)
);
export default router;
@@ -0,0 +1,77 @@
import { Router } from "express";
import {
failAdminNinetyoneOrder,
getAdminNinetyoneOrders,
retryAdminNinetyoneOrder,
} from "../../../services/admin/platform-config/service.js";
import type { AdminEntityRouteParams } from "../../../types/admin-route-inputs.js";
import { createJsonHandler } from "../shared.js";
import type { JsonRecord } from "./shared.js";
const router = Router();
router.get(
"/ninetyone/orders",
createJsonHandler((req) => getAdminNinetyoneOrders(req.query), {
successMessage: "ok",
errorMessage: "读取 91卡券订单失败",
scope: "[admin/platform-config/ninetyone/orders]",
})
);
router.post(
"/ninetyone/orders/:id/retry",
createJsonHandler(
(req) => retryAdminNinetyoneOrder((req.params as AdminEntityRouteParams).id),
{
successMessage: "91卡券订单已重试",
errorMessage: "重试 91卡券订单失败",
scope: "[admin/platform-config/ninetyone/orders/:id/retry]",
audit: (_req, data) => {
const result = data as JsonRecord;
return {
action: "platform_ninetyone_order_retried",
targetType: "order",
targetId: String(result.orderId || "").trim(),
data: {
orderNo: String(result.orderNo || "").trim(),
taskCount: Number(result.taskCount || 0),
},
};
},
}
)
);
router.post(
"/ninetyone/orders/:id/fail",
createJsonHandler(
(req) =>
failAdminNinetyoneOrder(
(req.params as AdminEntityRouteParams).id,
req.body as { reason?: string }
),
{
successMessage: "91卡券订单已标记失败",
errorMessage: "标记 91卡券订单失败",
scope: "[admin/platform-config/ninetyone/orders/:id/fail]",
audit: (req, data) => {
const result = data as JsonRecord;
return {
action: "platform_ninetyone_order_failed",
targetType: "order",
targetId: String(result.orderId || "").trim(),
data: {
orderNo: String(result.orderNo || "").trim(),
reason: String(
(req.body as { reason?: string }).reason || ""
).trim(),
},
};
},
}
)
);
export default router;
@@ -0,0 +1,153 @@
import { Router } from "express";
import {
getAdminNotificationConfig,
getAdminScheduledJobsConfig,
runAdminScheduledJobNow,
testAdminNotification,
updateAdminNotificationConfig,
updateAdminScheduledJobsConfig,
} from "../../../services/admin/platform-config/service.js";
import type {
AdminEntityRouteParams,
AdminNotificationConfigRouteBody,
AdminNotificationTestRouteBody,
AdminScheduledJobsConfigRouteBody,
} from "../../../types/admin-route-inputs.js";
import { createJsonHandler } from "../shared.js";
import type { JsonRecord } from "./shared.js";
const router = Router();
router.get(
"/notifications",
createJsonHandler(() => getAdminNotificationConfig(), {
successMessage: "ok",
errorMessage: "读取内部通知配置失败",
scope: "[admin/platform-config/notifications]",
})
);
router.post(
"/notifications",
createJsonHandler(
(req) =>
updateAdminNotificationConfig(
req.body as AdminNotificationConfigRouteBody
),
{
successMessage: "内部通知配置已保存",
errorMessage: "保存内部通知配置失败",
scope: "[admin/platform-config/notifications]",
audit: (_req, data) => {
const result = data as JsonRecord;
return {
action: "platform_notification_config_updated",
targetType: "platform_config",
targetId: "notifications",
data: {
filePath: String(result.filePath || "").trim(),
enabled: Boolean(result.source?.enabled),
barkRecipientCount: Array.isArray(
result.source?.channels?.bark?.recipients
)
? result.source.channels.bark.recipients.length
: 0,
},
};
},
}
)
);
router.post(
"/notifications/test",
createJsonHandler(
(req) => testAdminNotification(req.body as AdminNotificationTestRouteBody),
{
successMessage: "内部通知测试已执行",
errorMessage: "内部通知测试失败",
scope: "[admin/platform-config/notifications/test]",
audit: (_req, data) => {
const result = data as JsonRecord;
return {
action: "platform_notification_test_sent",
targetType: "platform_config",
targetId: "notifications",
data: {
channel: String(result.channel || "").trim(),
successCount: Number(result.successCount || 0),
failedCount: Number(result.failedCount || 0),
},
};
},
}
)
);
router.get(
"/scheduled-jobs",
createJsonHandler(() => getAdminScheduledJobsConfig(), {
successMessage: "ok",
errorMessage: "读取定时任务配置失败",
scope: "[admin/platform-config/scheduled-jobs]",
})
);
router.post(
"/scheduled-jobs",
createJsonHandler(
(req) =>
updateAdminScheduledJobsConfig(
req.body as AdminScheduledJobsConfigRouteBody
),
{
successMessage: "定时任务配置已保存",
errorMessage: "保存定时任务配置失败",
scope: "[admin/platform-config/scheduled-jobs]",
audit: (_req, data) => {
const result = data as JsonRecord;
return {
action: "platform_scheduled_jobs_updated",
targetType: "platform_config",
targetId: "scheduled_jobs",
data: {
filePath: String(result.filePath || "").trim(),
enabled: Boolean(result.source?.enabled),
jobCount: Array.isArray(result.source?.jobs)
? result.source.jobs.length
: 0,
},
};
},
}
)
);
router.post(
"/scheduled-jobs/:id/run",
createJsonHandler(
(req) => runAdminScheduledJobNow((req.params as AdminEntityRouteParams).id),
{
successMessage: "定时任务已执行",
errorMessage: "执行定时任务失败",
scope: "[admin/platform-config/scheduled-jobs/:id/run]",
audit: (req, data) => {
const result = data as JsonRecord;
return {
action: "platform_scheduled_job_run",
targetType: "platform_config",
targetId: String(
(req.params as AdminEntityRouteParams).id || ""
).trim(),
data: {
status: String(result.result?.status || "").trim(),
message: String(result.result?.message || "").trim(),
},
};
},
}
)
);
export default router;
@@ -0,0 +1 @@
export type JsonRecord = Record<string, any>
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,238 @@
import { createHttpError } from "../../../utils/http.js";
import { normalizeProductName } from "../../order/product-match-service.js";
import {
appointCloudtentaclesVirtualNumber,
backCloudtentaclesVirtualNumber,
fetchCloudtentaclesVirtualNumberCode,
generateCloudtentaclesLoginCode,
getCloudtentaclesBindUrl,
verifyCloudtentaclesLoginCode,
} from "../../platforms/cloudtentacles/virtual-number-service.js";
import { KUAISHOU_CLOUD_FIXED_VN_KEY, type JsonObject } from "./domain.js";
export function resolveKuaishouCloudBindingResources(
flow,
{ skuItems = [], knapsackItems = [] } = {}
) {
const normalizedSkuItems = Array.isArray(skuItems)
? skuItems.filter(isCloudSkuLikeItem)
: [];
const normalizedKnapsackItems = Array.isArray(knapsackItems)
? knapsackItems.filter(isCloudSkuLikeItem)
: [];
const currentSkuId = Number(flow?.binding?.skuId || 0) || 0;
const currentSkuName = String(flow?.binding?.skuName || "").trim();
const nameCandidates = collectKuaishouCloudNameCandidates(flow);
const skuItemById =
currentSkuId > 0
? normalizedSkuItems.find(
(item) => Number(item.id || 0) === currentSkuId
) || null
: null;
const knapsackItemById =
currentSkuId > 0
? normalizedKnapsackItems.find(
(item) => Number(item.id || 0) === currentSkuId
) || null
: null;
if (skuItemById || knapsackItemById) {
const matchedItem = skuItemById || knapsackItemById;
return {
skuId: Number(matchedItem?.id || 0) || 0,
skuName: String(currentSkuName || matchedItem?.name || "").trim(),
vnKey: KUAISHOU_CLOUD_FIXED_VN_KEY,
skuItem: skuItemById,
knapsackItem: knapsackItemById,
resolvedByName: false,
};
}
const matchedSkuItem = findCloudItemByNames(
normalizedSkuItems,
nameCandidates
);
const matchedKnapsackItem = findCloudItemByNames(
normalizedKnapsackItems,
nameCandidates,
matchedSkuItem ? Number(matchedSkuItem.id || 0) : 0
);
const matchedItem = matchedSkuItem || matchedKnapsackItem;
return {
skuId: Number(matchedItem?.id || 0) || 0,
skuName: String(currentSkuName || matchedItem?.name || "").trim(),
vnKey: KUAISHOU_CLOUD_FIXED_VN_KEY,
skuItem: matchedSkuItem,
knapsackItem: matchedKnapsackItem,
resolvedByName: Boolean(matchedItem),
};
}
export function resolveKuaishouCloudVnKeyCandidates(_input = {}) {
return [KUAISHOU_CLOUD_FIXED_VN_KEY];
}
export async function prepareKuaishouCloudBindResourceWithFallback(input: JsonObject = {}) {
const { cloudContext = {}, vnKeyCandidates = [] } = input;
const candidates = Array.isArray(vnKeyCandidates) ? vnKeyCandidates : [];
let lastError = null;
for (const vnKey of candidates) {
let vnId = 0;
let vnPhone = "";
try {
const appointed = await appointCloudtentaclesVirtualNumber({
...cloudContext,
key: vnKey,
});
vnId = Number(appointed.item?.id || 0);
vnPhone = String(appointed.item?.phone || "").trim();
if (!vnId || !vnPhone) {
throw createHttpError("申请虚拟号成功但返回数据不完整", {
statusCode: 502,
errorCode: "kuaishou_cloud_invalid_vn",
});
}
await generateCloudtentaclesLoginCode({
...cloudContext,
key: vnKey,
id: vnId,
});
const fetchedCode = await fetchCloudtentaclesVirtualNumberCode({
...cloudContext,
key: vnKey,
phone: vnPhone,
});
await verifyCloudtentaclesLoginCode({
...cloudContext,
key: vnKey,
id: vnId,
code: fetchedCode.code,
});
const bindUrlResult = await getCloudtentaclesBindUrl({
...cloudContext,
key: vnKey,
id: vnId,
});
return {
vnKey,
vnId,
vnPhone,
bindUrl: String(bindUrlResult.bindUrl || "").trim(),
};
} catch (error) {
lastError = error;
if (vnId > 0) {
try {
await backCloudtentaclesVirtualNumber({
...cloudContext,
key: vnKey,
id: vnId,
});
} catch {
// 退号失败保留主错误
}
}
if (!isRecoverableKuaishouCloudVnKeyError(error)) {
throw error;
}
}
}
throw (
lastError ||
createHttpError("没有找到可用的 VN Key", {
statusCode: 409,
errorCode: "kuaishou_cloud_missing_binding_config",
})
);
}
function collectKuaishouCloudNameCandidates(flow) {
return Array.from(
new Set(
[
String(flow?.binding?.skuName || "").trim(),
String(flow?.internalSkuName || "").trim(),
String(flow?.internalSkuCode || "").trim(),
].filter(Boolean)
)
);
}
function findCloudItemByNames(items, nameCandidates, preferredId = 0) {
const normalizedItems = Array.isArray(items) ? items : [];
const normalizedNames = nameCandidates
.map((item) => ({
raw: String(item || "").trim(),
normalized: normalizeProductName(item),
}))
.filter((item) => item.raw && item.normalized);
if (normalizedNames.length === 0 || normalizedItems.length === 0) {
return preferredId > 0
? normalizedItems.find((item) => Number(item.id || 0) === preferredId) ||
null
: null;
}
if (preferredId > 0) {
const preferred =
normalizedItems.find((item) => Number(item.id || 0) === preferredId) ||
null;
if (preferred) {
return preferred;
}
}
const exactMatches = normalizedItems.filter((item) => {
const itemName = normalizeProductName(item.name);
return normalizedNames.some(
(candidate) => candidate.normalized === itemName
);
});
if (exactMatches.length > 0) {
return exactMatches[0];
}
const partialMatches = normalizedItems.filter((item) => {
const itemName = normalizeProductName(item.name);
return normalizedNames.some(
(candidate) =>
itemName.includes(candidate.normalized) ||
candidate.normalized.includes(itemName)
);
});
if (partialMatches.length > 0) {
return partialMatches.sort(
(left, right) =>
String(left.name || "").length - String(right.name || "").length
)[0];
}
return null;
}
function isCloudSkuLikeItem(item) {
return Boolean(item) && typeof item === "object" && Number(item.id || 0) > 0;
}
function isRecoverableKuaishouCloudVnKeyError(error) {
const errorCode = String(error?.errorCode || error?.code || "").trim();
const errorMessage = String(error?.message || "").trim();
return (
errorCode === "cloudtentacles_vn_bind_url_failed" &&
errorMessage.includes("不支持的游戏类型")
);
}
@@ -0,0 +1,93 @@
import { createHttpError } from "../../../utils/http.js";
import {
getCloudtentaclesSourceConfig,
getCloudtentaclesSourceByKey,
} from "../../platforms/cloudtentacles/source-config-service.js";
import {
getCloudtentaclesSessionState,
getCloudtentaclesSessionStateByKey,
} from "../../platforms/cloudtentacles/session-state-service.js";
import { normalizeStringArray } from "./domain.js";
export function resolvePersistedCloudtentaclesContext(sourceKey = "default") {
const source =
getCloudtentaclesSourceByKey(sourceKey) || getCloudtentaclesSourceConfig();
const session =
getCloudtentaclesSessionStateByKey(sourceKey) ||
getCloudtentaclesSessionState();
const token = String(session.token || "").trim();
if (!token) {
throw createHttpError(
"当前 cloudtentacles 没有可用 token,请先到平台配置完成登录校验",
{
statusCode: 409,
errorCode: "kuaishou_cloud_missing_cloud_token",
}
);
}
return {
baseUrl:
String(session.baseUrl || source.baseUrl || "").trim() ||
"https://123.207.217.176",
token,
deviceId: String(session.deviceId || source.deviceId || "-").trim() || "-",
deviceType: Number(session.deviceType ?? source.deviceType ?? 0),
};
}
/**
* 按优先级依次尝试 cloudSourceKey 和 fallbacks 列表中的账号,
* 找到第一个有可用 token 的账号返回其 context。
* 将实际使用的 resolvedSourceKey 也返回,确保后续操作(退号、发货等)
* 使用同一个 sourceKey,防止跨账号操作导致数据不一致。
*/
export function resolvePersistedCloudtentaclesContextWithFallback(
primarySourceKey = "default",
fallbacks = []
) {
const candidates = [
String(primarySourceKey || "default").trim() || "default",
...normalizeStringArray(fallbacks),
];
const uniqueCandidates = [...new Set(candidates)];
let lastError = null;
for (const sourceKey of uniqueCandidates) {
const source = getCloudtentaclesSourceByKey(sourceKey);
const session = getCloudtentaclesSessionStateByKey(sourceKey);
if (!source) continue;
const token = String(session?.token || "").trim();
if (!token) {
lastError = createHttpError(
`cloudtentacles 账号 ${sourceKey} 没有可用 token`,
{ statusCode: 409, errorCode: "kuaishou_cloud_missing_cloud_token" }
);
continue;
}
return {
baseUrl:
String(session.baseUrl || source.baseUrl || "").trim() ||
"https://123.207.217.176",
token,
deviceId:
String(session.deviceId || source.deviceId || "-").trim() || "-",
deviceType: Number(session.deviceType ?? source.deviceType ?? 0),
resolvedSourceKey: sourceKey,
};
}
throw (
lastError ||
createHttpError(
"所有 cloudtentacles 备选账号均不可用,请先到平台配置完成登录校验",
{ statusCode: 409, errorCode: "kuaishou_cloud_all_source_keys_exhausted" }
)
);
}
@@ -0,0 +1,223 @@
import { resolveCloudtentaclesConfig } from "../../platforms/cloudtentacles/shared.js";
export const KUAISHOU_CLOUD_FIXED_VN_KEY = "1";
export type JsonObject = Record<string, any>;
export function isKuaishouCloudTask(task) {
return String(task?.executor_key || "").trim() === "kuaishou_ct_assisted";
}
export function normalizeKuaishouCloudFlow(value) {
const source = value && typeof value === "object" ? value : {};
const binding =
source.binding && typeof source.binding === "object" ? source.binding : {};
const role =
source.role && typeof source.role === "object" ? source.role : {};
const purchase =
source.purchase && typeof source.purchase === "object"
? source.purchase
: {};
const dispatch =
source.dispatch && typeof source.dispatch === "object"
? source.dispatch
: {};
const returnNumber =
source.returnNumber && typeof source.returnNumber === "object"
? source.returnNumber
: {};
const consume =
source.consume && typeof source.consume === "object" ? source.consume : {};
const ticket =
source.ticket && typeof source.ticket === "object" ? source.ticket : {};
const roleName = String(role.name || binding.roleName || "").trim();
const roleId = String(role.rid || binding.roleId || "").trim();
const bindPreparedAt = binding.bindPreparedAt || null;
const bindExpiresAt =
binding.bindExpiresAt ||
resolveKuaishouCloudBindUrlExpiresAt(bindPreparedAt);
return {
...source,
configId: String(source.configId || "").trim(),
internalSkuCode: String(source.internalSkuCode || "").trim(),
internalSkuName: String(source.internalSkuName || "").trim(),
ticket: {
code: String(ticket.code || "").trim(),
status: String(ticket.status || "pending").trim() || "pending",
capturedAt: ticket.capturedAt || null,
capturedBy: ticket.capturedBy || null,
verifiedAt: ticket.verifiedAt || null,
oid: String(ticket.oid || "").trim(),
formToken: String(ticket.formToken || "").trim(),
leftCount: Number(ticket.leftCount || 0) || 0,
goodsTitle: String(ticket.goodsTitle || "").trim(),
},
binding: {
prepareStatus:
String(binding.prepareStatus || "pending").trim() || "pending",
cloudSourceKey:
String(binding.cloudSourceKey || "default").trim() || "default",
cloudSourceKeyFallbacks: normalizeStringArray(
binding.cloudSourceKeyFallbacks
),
resolvedSourceKey: String(binding.resolvedSourceKey || "").trim(),
skuId: Number(binding.skuId || 0) || 0,
skuName: String(binding.skuName || "").trim(),
vnKey:
String(binding.vnKey || KUAISHOU_CLOUD_FIXED_VN_KEY).trim() ||
KUAISHOU_CLOUD_FIXED_VN_KEY,
vnId: Number(binding.vnId || 0) || 0,
vnPhone: String(binding.vnPhone || "").trim(),
bindUrl: String(binding.bindUrl || "").trim(),
bindPreparedAt,
bindExpiresAt,
bindProbeAt: binding.bindProbeAt || null,
bindProbeStatus: String(binding.bindProbeStatus || "").trim(),
bindProbeMessage: String(binding.bindProbeMessage || "").trim(),
roleName,
roleId,
},
role: {
status:
String(
role.status || (roleName || roleId ? "ready" : "pending")
).trim() || "pending",
name: roleName,
rid: roleId,
refreshedAt: role.refreshedAt || null,
errorMessage: String(role.errorMessage || "").trim(),
rawInfo:
role.rawInfo && typeof role.rawInfo === "object" ? role.rawInfo : null,
},
purchase: {
autoBuyEnabled: purchase.autoBuyEnabled !== false,
minAssetReserve: Number(purchase.minAssetReserve || 0) || 0,
usedKnapsack: purchase.usedKnapsack === true,
purchaseTriggered: purchase.purchaseTriggered === true,
assetBefore: Number(purchase.assetBefore || 0) || 0,
assetAfter: Number(purchase.assetAfter || 0) || 0,
purchaseAt: purchase.purchaseAt || null,
},
dispatch: {
status: String(dispatch.status || "pending").trim() || "pending",
dispatchAt: dispatch.dispatchAt || null,
dispatchBy: dispatch.dispatchBy || null,
sendType: Number(dispatch.sendType || 0) || 0,
note: String(dispatch.note || "").trim(),
},
returnNumber: {
status: String(returnNumber.status || "pending").trim() || "pending",
returnedAt: returnNumber.returnedAt || null,
returnedBy: returnNumber.returnedBy || null,
autoReturnEnabled: returnNumber.autoReturnEnabled === true,
},
consume: {
status: String(consume.status || "pending").trim() || "pending",
shopId: String(consume.shopId || "").trim(),
shopName: String(consume.shopName || "").trim(),
autoConsumeEnabled: consume.autoConsumeEnabled === true,
consumedAt: consume.consumedAt || null,
errorMessage: String(consume.errorMessage || "").trim(),
},
notes: String(source.notes || "").trim(),
};
}
export function normalizeKuaishouCloudRoleInfo(value) {
const rawInfo = value && typeof value === "object" ? value : null;
const nestedBindInfo =
rawInfo?.sBindInfo && typeof rawInfo.sBindInfo === "object"
? rawInfo.sBindInfo
: null;
const source = nestedBindInfo || rawInfo;
return {
name: String(
source?.name ||
source?.roleName ||
source?.nickname ||
source?.sRoleName ||
""
).trim(),
rid: String(
source?.rid ||
source?.roleId ||
source?.uid ||
source?.sRoleId ||
source?.sUserId ||
""
).trim(),
rawInfo,
};
}
export function maskPhone(value) {
const text = String(value || "").trim();
if (!text) {
return "";
}
if (text.length <= 7) {
return `${text.slice(0, 2)}***${text.slice(-2)}`;
}
return `${text.slice(0, 3)}****${text.slice(-4)}`;
}
export function maskCode(value) {
const text = String(value || "").trim();
if (!text) {
return "";
}
if (text.length <= 8) {
return `${text.slice(0, 2)}***${text.slice(-2)}`;
}
return `${text.slice(0, 4)}****${text.slice(-4)}`;
}
export function resolveKuaishouCloudBindUrlExpiresAt(preparedAt) {
const preparedTime = Date.parse(String(preparedAt || ""));
if (!Number.isFinite(preparedTime)) {
return null;
}
const config = resolveCloudtentaclesConfig();
const ttlSeconds = Number(config.bindUrlTtlSeconds || 600);
return new Date(preparedTime + Math.max(1, ttlSeconds) * 1000).toISOString();
}
export function isKuaishouCloudBindUrlFresh(flow, now = new Date()) {
const normalizedFlow = normalizeKuaishouCloudFlow(flow);
if (!normalizedFlow.binding.bindUrl) {
return false;
}
const expiresAt = normalizedFlow.binding.bindExpiresAt;
if (!expiresAt) {
return false;
}
const expiresTime = Date.parse(String(expiresAt || ""));
if (!Number.isFinite(expiresTime)) {
return false;
}
return expiresTime > now.getTime();
}
export function normalizeStringArray(value) {
if (Array.isArray(value)) {
return value.map((v) => String(v || "").trim()).filter(Boolean);
}
if (typeof value === "string") {
return value
.split(",")
.map((v) => v.trim())
.filter(Boolean);
}
return [];
}
@@ -0,0 +1,52 @@
export function normalizeActor(actor) {
if (!actor || typeof actor !== "object") {
return null;
}
const source = String(actor.source || "").trim();
const userId = Number(actor.userId || 0) || 0;
const username = String(actor.username || "").trim();
const role = String(actor.role || "").trim();
if (!source && !userId && !username && !role) {
return null;
}
return {
source,
userId,
username,
role,
};
}
export function parseTaskContext(task) {
const value = task?.context_json;
if (!value) {
return {};
}
if (typeof value === "object") {
return value;
}
try {
return JSON.parse(String(value || "{}"));
} catch {
return {};
}
}
export function getTaskClaimExpiresAt(task) {
return task?.claim_expires_at || task?.primary_claim_expires_at || null;
}
export function isClaimExpired(expiredAt) {
if (!expiredAt) {
return false;
}
const timestamp = new Date(expiredAt).getTime();
return Number.isFinite(timestamp) && timestamp <= Date.now();
}
@@ -0,0 +1,347 @@
import { getOrderById } from "../../../repositories/order-repo.js";
import { createTaskEvent } from "../../../repositories/task-event-repo.js";
import { updateTask } from "../../../repositories/task-repo.js";
import { createHttpError } from "../../../utils/http.js";
import { nowIso } from "../../../utils/time.js";
import { notifyKuaishouCloudConsumeFailed } from "../../notification/domain-notifications.js";
import { useCloudtentaclesSku } from "../../platforms/cloudtentacles/catalog-service.js";
import { backCloudtentaclesVirtualNumber } from "../../platforms/cloudtentacles/virtual-number-service.js";
import { consumeKuaishouEticket } from "../../platforms/kuaishou-eticket/consume-service.js";
import {
getKuaishouEticketSourceConfig,
resolveKuaishouEticketShopConfig,
} from "../../platforms/kuaishou-eticket/source-config-service.js";
import {
isKuaishouCloudTask,
maskCode,
maskPhone,
normalizeKuaishouCloudFlow,
type JsonObject,
} from "./domain.js";
import { resolvePersistedCloudtentaclesContextWithFallback } from "./cloudtentacles-context.js";
import { normalizeActor, parseTaskContext } from "./task-context.js";
export async function dispatchKuaishouCloudFulfillmentTask(
task,
options: JsonObject = {}
) {
if (!isKuaishouCloudTask(task)) {
throw createHttpError("当前任务不是快手 Cloud 履约任务", {
statusCode: 409,
errorCode: "kuaishou_cloud_task_invalid",
});
}
const actor = normalizeActor(options.actor);
const now = nowIso();
const taskContext = parseTaskContext(task);
const flow = normalizeKuaishouCloudFlow(taskContext.kuaishouCloudFulfillment);
const effectiveSourceKey =
flow.binding.resolvedSourceKey || flow.binding.cloudSourceKey || "default";
const cloudContext = resolvePersistedCloudtentaclesContextWithFallback(
effectiveSourceKey,
flow.binding.cloudSourceKeyFallbacks || []
);
if (!flow.binding.skuId || !flow.binding.vnId || !flow.binding.vnPhone) {
throw createHttpError(
"当前任务还没有准备好绑定资源,请先完成绑定资源准备",
{
statusCode: 409,
errorCode: "kuaishou_cloud_not_prepared",
}
);
}
const ticketCode = String(options.ticketCode || "").trim();
const persistedTicketCode = String(flow.ticket.code || "").trim();
if (!persistedTicketCode && !ticketCode) {
throw createHttpError("客户还没有提交有效核销码,暂时不能继续兑换", {
statusCode: 409,
errorCode: "kuaishou_cloud_missing_ticket_code",
});
}
if (
flow.dispatch.status === "success" &&
String(task.task_status || "").trim() === "dispatched_pending_return"
) {
return { task, flow };
}
const dispatchResult = await useCloudtentaclesSku({
...cloudContext,
id: flow.binding.skuId,
virtualNumberId: flow.binding.vnId,
phone: flow.binding.vnPhone,
});
const nextContext = {
...taskContext,
kuaishouCloudFulfillment: {
...flow,
ticket: {
...flow.ticket,
code: ticketCode || persistedTicketCode,
capturedAt: ticketCode ? now : flow.ticket.capturedAt,
capturedBy: ticketCode && actor ? actor : flow.ticket.capturedBy,
},
dispatch: {
...flow.dispatch,
status: "success",
dispatchAt: now,
dispatchBy: actor,
sendType: Number(dispatchResult.sendType || 0) || 0,
note: String(
dispatchResult.note ||
dispatchResult.responseMessage ||
"cloudtentacles 发货成功"
).trim(),
},
},
};
let updatedTask = await updateTask(task.id, {
task_status: "dispatched_pending_return",
delivery_status: "delivered",
result_code: "kuaishou_cloud_dispatched",
result_message: String(
dispatchResult.responseMessage ||
dispatchResult.note ||
"cloudtentacles 发货成功"
).trim(),
user_action_status: "not_required",
last_error: "",
context_json: JSON.stringify(nextContext),
updated_at: now,
});
await createTaskEvent(
task.id,
"kuaishou_cloud_dispatched",
{
source: String(options.source || "system").trim() || "system",
ticketCodeMasked: maskCode(ticketCode || persistedTicketCode),
skuId: flow.binding.skuId,
vnId: flow.binding.vnId,
vnPhoneMasked: maskPhone(flow.binding.vnPhone),
sendType: dispatchResult.sendType,
note: dispatchResult.note,
actor,
},
now
);
const shouldAutoFinalize =
options.autoFinalize === true &&
normalizeKuaishouCloudFlow(nextContext.kuaishouCloudFulfillment)
.returnNumber.autoReturnEnabled === true;
if (shouldAutoFinalize) {
const finalizeResult = await returnKuaishouCloudFulfillmentTask(
updatedTask,
{
actor,
source: options.source || "system_auto_finalize",
}
);
updatedTask = finalizeResult.task;
}
return {
task: updatedTask,
flow: normalizeKuaishouCloudFlow(
parseTaskContext(updatedTask).kuaishouCloudFulfillment
),
};
}
export async function returnKuaishouCloudFulfillmentTask(
task,
options: JsonObject = {}
) {
if (!isKuaishouCloudTask(task)) {
throw createHttpError("当前任务不是快手 Cloud 履约任务", {
statusCode: 409,
errorCode: "kuaishou_cloud_task_invalid",
});
}
const actor = normalizeActor(options.actor);
const now = nowIso();
const taskContext = parseTaskContext(task);
const flow = normalizeKuaishouCloudFlow(taskContext.kuaishouCloudFulfillment);
const effectiveSourceKey =
flow.binding.resolvedSourceKey || flow.binding.cloudSourceKey || "default";
const cloudContext = resolvePersistedCloudtentaclesContextWithFallback(
effectiveSourceKey,
flow.binding.cloudSourceKeyFallbacks || []
);
if (!flow.binding.vnId || !flow.binding.vnKey) {
throw createHttpError("当前任务缺少可退还的虚拟号信息", {
statusCode: 409,
errorCode: "kuaishou_cloud_missing_return_context",
});
}
if (
flow.returnNumber.status === "success" &&
["completed", "manual_review"].includes(
String(task.task_status || "").trim()
)
) {
return { task, flow };
}
await backCloudtentaclesVirtualNumber({
...cloudContext,
key: flow.binding.vnKey,
id: flow.binding.vnId,
});
const order = await getOrderById(task.order_id);
const ticketCode = String(flow.ticket.code || "").trim();
const shopId = String(flow.consume.shopId || order?.shop_id || "").trim();
const shopName = String(
flow.consume.shopName || order?.shop_name || ""
).trim();
const eticketSource = getKuaishouEticketSourceConfig();
const shopConfig = resolveKuaishouEticketShopConfig({
shopId,
shopName,
});
let consumeStatus = "pending";
let consumeErrorMessage = "";
let consumedAt = null;
let nextTaskStatus = "completed";
let nextResultCode = "kuaishou_cloud_completed";
let nextResultMessage = "cloudtentacles 发货、退号并完成快手核销";
if (!order) {
consumeStatus = "failed";
consumeErrorMessage = "任务关联订单不存在,无法执行快手核销";
} else if (!ticketCode) {
consumeStatus = "failed";
consumeErrorMessage = "客户未提交有效核销码,无法执行快手核销";
} else if (
!shopConfig ||
shopConfig.enabled === false ||
!String(shopConfig.cookie || "").trim()
) {
consumeStatus = "failed";
consumeErrorMessage = "订单对应快手小店缺少可用 Cookie,无法执行快手核销";
} else {
try {
const consumeResult = await consumeKuaishouEticket({
baseUrl: eticketSource.baseUrl,
cookie: shopConfig.cookie,
eTicketId: ticketCode,
oid: String(flow.ticket.oid || "").trim(),
formToken: String(flow.ticket.formToken || "").trim(),
});
if (consumeResult.consumed) {
consumeStatus = "success";
consumedAt = now;
} else {
consumeStatus = "failed";
consumeErrorMessage = String(
consumeResult.errorMessage || "快手核销失败"
).trim();
}
} catch (error) {
consumeStatus = "failed";
consumeErrorMessage =
error instanceof Error ? error.message : "快手核销失败";
}
}
if (consumeStatus !== "success") {
nextTaskStatus = "manual_review";
nextResultCode = "kuaishou_cloud_consume_failed";
nextResultMessage =
consumeErrorMessage || "号码已退还,但快手核销未完成,请人工处理";
}
const nextContext = {
...taskContext,
kuaishouCloudFulfillment: {
...flow,
returnNumber: {
...flow.returnNumber,
status: "success",
returnedAt: now,
returnedBy: actor,
},
consume: {
...flow.consume,
status: consumeStatus,
shopId: shopId || flow.consume.shopId,
shopName,
autoConsumeEnabled: flow.consume.autoConsumeEnabled === true,
consumedAt,
errorMessage: consumeErrorMessage,
},
},
};
const updatedTask = await updateTask(task.id, {
task_status: nextTaskStatus,
delivery_status: "delivered",
result_code: nextResultCode,
result_message: nextResultMessage,
redeemed_at: consumeStatus === "success" ? now : task.redeemed_at,
last_error: consumeErrorMessage,
context_json: JSON.stringify(nextContext),
updated_at: now,
});
await createTaskEvent(
task.id,
"kuaishou_cloud_number_returned",
{
source: String(options.source || "system").trim() || "system",
vnId: flow.binding.vnId,
vnPhoneMasked: maskPhone(flow.binding.vnPhone),
actor,
},
now
);
await createTaskEvent(
task.id,
consumeStatus === "success"
? "kuaishou_cloud_consumed"
: "kuaishou_cloud_consume_failed",
{
source: String(options.source || "system").trim() || "system",
ticketCodeMasked: maskCode(ticketCode),
shopId,
shopName,
consumeStatus,
errorMessage: consumeErrorMessage,
actor,
},
now
);
if (consumeStatus !== "success") {
await notifyKuaishouCloudConsumeFailed({
task: updatedTask,
order,
ticketCodeMasked: maskCode(ticketCode),
shopId,
shopName,
errorMessage: consumeErrorMessage,
});
}
return {
task: updatedTask,
flow: normalizeKuaishouCloudFlow(
parseTaskContext(updatedTask).kuaishouCloudFulfillment
),
};
}
+154
View File
@@ -0,0 +1,154 @@
import process from "node:process";
import { runtimeConfig } from "../config/runtime.js";
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 { warmupOcrService } from "../services/session/ocr.js";
import { warmupTencentBrowser } from "../services/session/session.js";
import { logError, logInfo, logWarn } from "../utils/logger.js";
import { formatStartupError, type StartupState } from "./state.js";
const CORE_BOOT_RETRY_DELAY_MS = 5_000;
export async function bootstrapCoreServices(
startupState: StartupState,
isShutdownStarted: () => boolean
) {
if (startupState.core.running || isShutdownStarted()) {
return;
}
startupState.core.running = true;
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]", "开始执行核心启动步骤", {
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]", "核心启动步骤完成,服务已就绪", {
attempt: startupState.core.attemptCount,
});
startScheduledJobs();
void bootstrapBrowser(startupState);
void bootstrapOcr(startupState);
break;
} catch (error) {
const message = formatStartupError(error);
startupState.phase = "retrying";
startupState.core.lastError = message;
logError("[startup]", "核心启动步骤失败,将自动重试", {
attempt: startupState.core.attemptCount,
retryDelayMs: CORE_BOOT_RETRY_DELAY_MS,
error,
});
await sleep(CORE_BOOT_RETRY_DELAY_MS);
}
}
startupState.core.running = false;
}
async function bootstrapBrowser(startupState: StartupState) {
startupState.browser.status = "starting";
startupState.browser.lastAttemptAt = new Date().toISOString();
try {
const result = await warmupTencentBrowser();
if (!result.warmed) {
startupState.browser.status = "skipped";
startupState.browser.message = "browser prewarm skipped";
logInfo("[startup]", "browser prewarm skipped");
return;
}
startupState.browser.status = "ready";
startupState.browser.message = "browser prewarm ready";
logInfo("[startup]", "browser prewarm ready");
} catch (error) {
if (isMissingPlaywrightBrowserError(error)) {
const installCommand = resolveBrowserInstallCommand();
startupState.browser.status = "degraded";
startupState.browser.message = formatErrorMessage(error);
logWarn("[startup]", `browser prewarm skipped: ${formatErrorMessage(error)}`);
logWarn("[startup]", `请先安装浏览器依赖: ${installCommand}`);
if (String(runtimeConfig.browser.chromePath || "").trim()) {
logWarn("[startup]", "当前设置了 CHROME_PATH,也请确认该路径指向的浏览器可执行文件真实存在");
}
return;
}
startupState.browser.status = "degraded";
startupState.browser.message = formatStartupError(error);
logError("[startup]", "browser prewarm failed", error);
}
}
async function bootstrapOcr(startupState: StartupState) {
startupState.ocr.status = "starting";
startupState.ocr.lastAttemptAt = new Date().toISOString();
try {
await warmupOcrService();
startupState.ocr.status = "ready";
startupState.ocr.message = "OCR service ready";
logInfo("[startup]", "OCR service ready");
} catch (error) {
startupState.ocr.status = "degraded";
startupState.ocr.message = formatStartupError(error);
logWarn("[startup]", `OCR service skipped: ${formatStartupError(error)}`);
}
}
function isMissingPlaywrightBrowserError(error: unknown) {
const message = formatErrorMessage(error).toLowerCase();
return (
message.includes("executable doesn't exist") ||
message.includes("please run the following command to download new browsers") ||
(message.includes("browserType.launch".toLowerCase()) &&
message.includes("please run"))
);
}
function formatErrorMessage(error: unknown) {
if (error instanceof Error && error.message) {
return error.message.split("\n")[0].trim();
}
return String(error || "未知错误").trim();
}
function resolveBrowserInstallCommand() {
if (process.platform === "linux") {
return "npm run browser:install:linux";
}
return "npm run browser:install";
}
function sleep(ms: number) {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
}
+55
View File
@@ -0,0 +1,55 @@
import type { Server } from "node:http";
import { stopScheduledJobs } from "../services/scheduler/scheduler-service.js";
import { closeOcrService } from "../services/session/ocr.js";
import { closeAllTencentBrowserSessions } from "../services/session/session.js";
import { logError, logInfo } from "../utils/logger.js";
import type { StartupState } from "./state.js";
export function createShutdownController(server: Server, startupState: StartupState) {
let shutdownStarted = false;
async function shutdown(signal: string) {
if (shutdownStarted) {
return;
}
shutdownStarted = true;
startupState.phase = "shutting_down";
logInfo("[shutdown]", `received ${signal}, closing browser sessions and HTTP server`);
stopScheduledJobs();
try {
await closeAllTencentBrowserSessions({ markClosed: true });
} catch (error) {
logError("[shutdown]", "failed to close browser sessions", error);
}
try {
await closeOcrService();
} catch (error) {
logError("[shutdown]", "failed to close OCR service", error);
}
if (!server.listening) {
return;
}
await new Promise<void>((resolve) => {
server.close((error) => {
if (error) {
logError("[shutdown]", "failed to close HTTP server", error);
process.exitCode = 1;
}
resolve();
});
});
}
return {
isShutdownStarted: () => shutdownStarted,
shutdown,
};
}
+69
View File
@@ -0,0 +1,69 @@
export type StartupState = ReturnType<typeof createStartupState>;
export function createStartupState() {
return {
phase: "starting",
startedAt: new Date().toISOString(),
core: {
ready: false,
running: false,
attemptCount: 0,
readyAt: "",
lastAttemptAt: "",
lastError: "",
},
browser: {
status: "pending",
message: "",
lastAttemptAt: "",
},
ocr: {
status: "pending",
message: "",
lastAttemptAt: "",
},
process: {
lastUnhandledRejection: null,
lastUncaughtException: null,
},
};
}
export function buildHealthPayload(startupState: StartupState, shutdownStarted: boolean) {
return {
phase: startupState.phase,
startedAt: startupState.startedAt,
core: {
ready: startupState.core.ready,
running: startupState.core.running,
attemptCount: startupState.core.attemptCount,
readyAt: startupState.core.readyAt,
lastAttemptAt: startupState.core.lastAttemptAt,
lastError: startupState.core.lastError,
},
browser: {
status: startupState.browser.status,
message: startupState.browser.message,
lastAttemptAt: startupState.browser.lastAttemptAt,
},
ocr: {
status: startupState.ocr.status,
message: startupState.ocr.message,
lastAttemptAt: startupState.ocr.lastAttemptAt,
},
process: {
pid: process.pid,
shutdownStarted,
lastUnhandledRejection: startupState.process.lastUnhandledRejection,
lastUncaughtException: startupState.process.lastUncaughtException,
},
};
}
export function formatStartupError(error: unknown) {
if (error instanceof Error && error.message) {
return error.message.split("\n")[0].trim();
}
return String(error || "未知错误").trim();
}