统一前后端代码格式化配置

This commit is contained in:
yml2213
2026-08-16 17:27:12 +08:00
parent 8705f6a6a1
commit 5c7c3e14e3
278 changed files with 6386 additions and 5500 deletions
+36 -36
View File
@@ -1,98 +1,98 @@
import fs from "node:fs";
import process from "node:process";
import fs from 'node:fs'
import process from 'node:process'
export function loadEnvFiles(filePaths: string[]): void {
for (const filePath of filePaths) {
loadEnvFile(filePath);
loadEnvFile(filePath)
}
}
function loadEnvFile(filePath: string): void {
if (!fs.existsSync(filePath)) {
return;
return
}
const rawText = fs.readFileSync(filePath, "utf8");
const lines = rawText.split(/\r?\n/);
const rawText = fs.readFileSync(filePath, 'utf8')
const lines = rawText.split(/\r?\n/)
for (const rawLine of lines) {
const line = rawLine.trim();
const line = rawLine.trim()
if (!line || line.startsWith("#")) {
continue;
if (!line || line.startsWith('#')) {
continue
}
const separatorIndex = line.indexOf("=");
const separatorIndex = line.indexOf('=')
if (separatorIndex <= 0) {
continue;
continue
}
const key = line.slice(0, separatorIndex).trim();
const key = line.slice(0, separatorIndex).trim()
if (!key || key in process.env) {
continue;
continue
}
process.env[key] = parseEnvValue(line.slice(separatorIndex + 1));
process.env[key] = parseEnvValue(line.slice(separatorIndex + 1))
}
}
function parseEnvValue(rawValue: string): string {
const value = String(rawValue || "").trim();
const value = String(rawValue || '').trim()
if (!value) {
return "";
return ''
}
const quote = value[0];
const quote = value[0]
if ((quote === '"' || quote === "'") && value.endsWith(quote)) {
return value.slice(1, -1);
return value.slice(1, -1)
}
return value;
return value
}
export function parseBoolean(rawValue: unknown): boolean | null {
const normalized = String(rawValue || "")
const normalized = String(rawValue || '')
.trim()
.toLowerCase();
.toLowerCase()
if (!normalized) {
return null;
return null
}
if (["1", "true", "yes", "on"].includes(normalized)) {
return true;
if (['1', 'true', 'yes', 'on'].includes(normalized)) {
return true
}
if (["0", "false", "no", "off"].includes(normalized)) {
return false;
if (['0', 'false', 'no', 'off'].includes(normalized)) {
return false
}
return null;
return null
}
export function parseInteger(rawValue: unknown): number | null {
const normalized = String(rawValue || "").trim();
const normalized = String(rawValue || '').trim()
if (!normalized) {
return null;
return null
}
const parsed = Number(normalized);
return Number.isFinite(parsed) ? parsed : null;
const parsed = Number(normalized)
return Number.isFinite(parsed) ? parsed : null
}
export function parseJsonArray<T = unknown>(rawValue: unknown): T[] | null {
const normalized = String(rawValue || "").trim();
const normalized = String(rawValue || '').trim()
if (!normalized) {
return null;
return null
}
try {
const parsed = JSON.parse(normalized);
return Array.isArray(parsed) ? (parsed as T[]) : null;
const parsed = JSON.parse(normalized)
return Array.isArray(parsed) ? (parsed as T[]) : null
} catch {
return null;
return null
}
}
+10 -13
View File
@@ -1,17 +1,14 @@
import path from "node:path";
import { fileURLToPath } from "node:url";
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import { createDefaultRuntimeConfig } from "./defaults.js";
import { applyEnvOverrides } from "./env-overrides.js";
import { loadEnvFiles } from "./runtime-env.js";
import { createDefaultRuntimeConfig } from './defaults.js'
import { applyEnvOverrides } from './env-overrides.js'
import { loadEnvFiles } from './runtime-env.js'
const CURRENT_DIR = path.dirname(fileURLToPath(import.meta.url));
export const PROJECT_ROOT = path.resolve(CURRENT_DIR, "../..");
const WORKSPACE_ROOT = path.resolve(PROJECT_ROOT, "../..");
const CURRENT_DIR = path.dirname(fileURLToPath(import.meta.url))
export const PROJECT_ROOT = path.resolve(CURRENT_DIR, '../..')
const WORKSPACE_ROOT = path.resolve(PROJECT_ROOT, '../..')
loadEnvFiles([
path.join(WORKSPACE_ROOT, ".env"),
path.join(PROJECT_ROOT, ".env"),
]);
loadEnvFiles([path.join(WORKSPACE_ROOT, '.env'), path.join(PROJECT_ROOT, '.env')])
export const runtimeConfig = applyEnvOverrides(createDefaultRuntimeConfig(PROJECT_ROOT));
export const runtimeConfig = applyEnvOverrides(createDefaultRuntimeConfig(PROJECT_ROOT))
+1 -3
View File
@@ -76,9 +76,7 @@ function assertMigrationFilesOrdered() {
const invalid = files.filter((name) => !MIGRATION_FILE_PATTERN.test(name))
if (invalid.length > 0) {
throw new Error(
`迁移文件命名必须为 NNN_name.sql(三位序号),非法文件: ${invalid.join(', ')}`,
)
throw new Error(`迁移文件命名必须为 NNN_name.sql(三位序号),非法文件: ${invalid.join(', ')}`)
}
const versions = files.map((name) => Number(name.slice(0, 3)))
+12 -3
View File
@@ -11,11 +11,20 @@ import {
} from './task-status.js'
test('任务状态机声明 kuaishou-lewan 主流程跳转', () => {
assert.equal(canTaskTransition(TASK_STATUS.PENDING_BINDING_PREPARE, TASK_STATUS.WAITING_BINDING), true)
assert.equal(
canTaskTransition(TASK_STATUS.PENDING_BINDING_PREPARE, TASK_STATUS.WAITING_BINDING),
true,
)
assert.equal(canTaskTransition(TASK_STATUS.WAITING_BINDING, TASK_STATUS.ROLE_CONFIRMED), true)
assert.equal(canTaskTransition(TASK_STATUS.ROLE_CONFIRMED, TASK_STATUS.REDEEMING), true)
assert.equal(canTaskTransition(TASK_STATUS.REDEEMING, TASK_STATUS.DISPATCHED_PENDING_RETURN), true)
assert.equal(canTaskTransition(TASK_STATUS.DISPATCHED_PENDING_RETURN, TASK_STATUS.COMPLETED), true)
assert.equal(
canTaskTransition(TASK_STATUS.REDEEMING, TASK_STATUS.DISPATCHED_PENDING_RETURN),
true,
)
assert.equal(
canTaskTransition(TASK_STATUS.DISPATCHED_PENDING_RETURN, TASK_STATUS.COMPLETED),
true,
)
})
test('任务状态机收敛领取和 91 查询的业务判断', () => {
+1 -4
View File
@@ -259,10 +259,7 @@ export function isKuaishouCloudRedeemSettledStatus(status: unknown): boolean {
export function canRedeemKuaishouCloudClaimStatus(status: unknown): boolean {
const normalized = normalizeTaskStatus(status)
// 允许 WAITING_BINDINGUID 匹配后可一键确认+兑换
return (
normalized === TASK_STATUS.ROLE_CONFIRMED ||
normalized === TASK_STATUS.WAITING_BINDING
)
return normalized === TASK_STATUS.ROLE_CONFIRMED || normalized === TASK_STATUS.WAITING_BINDING
}
export function canRegenerateClaimLinkStatus(status: unknown): boolean {
+3 -1
View File
@@ -9,7 +9,9 @@ export const WORK_ORDER_STATUS = {
CANCELLED: 'cancelled',
} as const
export type WorkOrderStatus = (typeof WORK_ORDER_STATUS)[keyof typeof WORK_ORDER_STATUS] | (string & {})
export type WorkOrderStatus =
| (typeof WORK_ORDER_STATUS)[keyof typeof WORK_ORDER_STATUS]
| (string & {})
const FINAL_STATUSES = new Set<WorkOrderStatus>([
WORK_ORDER_STATUS.ACCEPTED,
+37 -43
View File
@@ -1,69 +1,63 @@
import process from "node:process";
import process from 'node:process'
import { createApp } from "./app.js";
import { runtimeConfig } from "./config/runtime.js";
import { assertRuntimeConfigValid } from "./config/runtime-validation.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";
import { createApp } from './app.js'
import { runtimeConfig } from './config/runtime.js'
import { assertRuntimeConfigValid } from './config/runtime-validation.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 port = Number(runtimeConfig.server.port || 3000);
const host = "0.0.0.0";
const startupState = createStartupState();
const port = Number(runtimeConfig.server.port || 3000)
const host = '0.0.0.0'
const startupState = createStartupState()
let shutdownController: ReturnType<typeof createShutdownController> | null = null;
let shutdownController: ReturnType<typeof createShutdownController> | null = null
try {
assertRuntimeConfigValid(runtimeConfig);
assertRuntimeConfigValid(runtimeConfig)
} catch (error) {
logError("[startup]", "运行时配置校验失败,服务停止启动", error);
process.exit(1);
logError('[startup]', '运行时配置校验失败,服务停止启动', error)
process.exit(1)
}
const app = createApp({
startupState,
isShutdownStarted: () => shutdownController?.isShutdownStarted() || false,
config: runtimeConfig,
});
})
const server = app.listen(port, host, () => {
logInfo(
"[startup]",
`order-site-backend kuaishou-lite listening on http://${host}:${port}`
);
void bootstrapCoreServices(
startupState,
() => shutdownController?.isShutdownStarted() || false
);
});
logInfo('[startup]', `order-site-backend kuaishou-lite listening on http://${host}:${port}`)
void bootstrapCoreServices(startupState, () => shutdownController?.isShutdownStarted() || false)
})
shutdownController = createShutdownController(server, startupState);
shutdownController = createShutdownController(server, startupState)
server.on("error", (error) => {
logError("[startup]", "HTTP server failed", error);
});
server.on('error', (error) => {
logError('[startup]', 'HTTP server failed', error)
})
process.on("SIGINT", () => {
void shutdownController?.shutdown("SIGINT");
});
process.on('SIGINT', () => {
void shutdownController?.shutdown('SIGINT')
})
process.on("SIGTERM", () => {
void shutdownController?.shutdown("SIGTERM");
});
process.on('SIGTERM', () => {
void shutdownController?.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)
})
+24 -31
View File
@@ -1,51 +1,44 @@
import type { Request, Response, NextFunction } from "express";
import { createRequestId, logInfo } from "../utils/logger.js";
import type { Request, Response, NextFunction } from 'express'
import { createRequestId, logInfo } from '../utils/logger.js'
export function accessLogMiddleware(
req: Request,
res: Response,
next: NextFunction
): void {
const startedAt = Date.now();
const requestId = resolveRequestId(req);
export function accessLogMiddleware(req: Request, res: Response, next: NextFunction): void {
const startedAt = Date.now()
const requestId = resolveRequestId(req)
req.requestId = requestId;
res.setHeader("X-Request-Id", requestId);
req.requestId = requestId
res.setHeader('X-Request-Id', requestId)
res.on("finish", () => {
res.on('finish', () => {
if (shouldSkipAccessLog(req.originalUrl, res.statusCode)) {
return;
return
}
logInfo("[http/access]", "request completed", {
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 || "",
});
});
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();
next()
}
function resolveRequestId(req: Request): string {
const fromHeader = String(req.headers["x-request-id"] || "").trim();
return fromHeader || createRequestId("req");
const fromHeader = String(req.headers['x-request-id'] || '').trim()
return fromHeader || createRequestId('req')
}
function shouldSkipAccessLog(originalUrl: string, statusCode: number): boolean {
const pathname = String(originalUrl || "").split("?")[0] || "";
return (
["/health", "/health/live", "/health/ready"].includes(pathname) &&
Number(statusCode) < 400
);
const pathname = String(originalUrl || '').split('?')[0] || ''
return ['/health', '/health/live', '/health/ready'].includes(pathname) && Number(statusCode) < 400
}
+18 -29
View File
@@ -1,43 +1,32 @@
import type { Request, Response, NextFunction } from "express";
import type { RuntimeConfig } from "../types/runtime-config.js";
import type { Request, Response, NextFunction } from 'express'
import type { RuntimeConfig } from '../types/runtime-config.js'
export function createCorsMiddleware(
config: RuntimeConfig
config: RuntimeConfig,
): (req: Request, res: Response, next: NextFunction) => void {
const allowedOrigins = config.cors.allowedOrigins;
const isWildcard =
allowedOrigins.length === 1 && allowedOrigins[0] === "*";
const allowedOrigins = config.cors.allowedOrigins
const isWildcard = allowedOrigins.length === 1 && allowedOrigins[0] === '*'
return function corsMiddleware(
req: Request,
res: Response,
next: NextFunction
): void {
return function corsMiddleware(req: Request, res: Response, next: NextFunction): void {
if (isWildcard) {
res.setHeader("Access-Control-Allow-Origin", "*");
res.setHeader('Access-Control-Allow-Origin', '*')
} else {
const origin = req.headers.origin;
const origin = req.headers.origin
if (origin && allowedOrigins.includes(origin)) {
res.setHeader("Access-Control-Allow-Origin", origin);
res.setHeader("Access-Control-Allow-Credentials", "true");
res.setHeader('Access-Control-Allow-Origin', origin)
res.setHeader('Access-Control-Allow-Credentials', 'true')
}
}
res.setHeader(
"Access-Control-Allow-Headers",
"Content-Type, Authorization"
);
res.setHeader(
"Access-Control-Allow-Methods",
"GET, POST, PUT, PATCH, DELETE, OPTIONS"
);
res.setHeader("Access-Control-Max-Age", "86400");
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization')
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, PATCH, DELETE, OPTIONS')
res.setHeader('Access-Control-Max-Age', '86400')
if (req.method === "OPTIONS") {
res.sendStatus(204);
return;
if (req.method === 'OPTIONS') {
res.sendStatus(204)
return
}
next();
};
next()
}
}
+64 -67
View File
@@ -1,116 +1,113 @@
import test from "node:test";
import assert from "node:assert/strict";
import type { NextFunction, Request, Response } from "express";
import test from 'node:test'
import assert from 'node:assert/strict'
import type { NextFunction, Request, Response } from 'express'
import {
createRateLimitMiddleware,
resetRateLimitBucketsForTest,
} from "./rate-limit.js";
import { createRateLimitMiddleware, resetRateLimitBucketsForTest } from './rate-limit.js'
test("createRateLimitMiddleware allows requests within the window", () => {
resetRateLimitBucketsForTest();
test('createRateLimitMiddleware allows requests within the window', () => {
resetRateLimitBucketsForTest()
const limiter = createRateLimitMiddleware({
scope: "test:allow",
scope: 'test:allow',
windowMs: 60_000,
max: 2,
});
const req = createMockRequest();
const res = createMockResponse();
let nextCount = 0;
})
const req = createMockRequest()
const res = createMockResponse()
let nextCount = 0
const next: NextFunction = () => {
nextCount += 1;
};
nextCount += 1
}
limiter(req, res, next);
limiter(req, res, next);
limiter(req, res, next)
limiter(req, res, next)
assert.equal(nextCount, 2);
assert.equal(res.statusCode, 200);
});
assert.equal(nextCount, 2)
assert.equal(res.statusCode, 200)
})
test("createRateLimitMiddleware blocks requests over the limit", () => {
resetRateLimitBucketsForTest();
test('createRateLimitMiddleware blocks requests over the limit', () => {
resetRateLimitBucketsForTest()
const limiter = createRateLimitMiddleware({
scope: "test:block",
scope: 'test:block',
windowMs: 60_000,
max: 1,
});
const req = createMockRequest();
const res = createMockResponse();
let nextCount = 0;
})
const req = createMockRequest()
const res = createMockResponse()
let nextCount = 0
const next: NextFunction = () => {
nextCount += 1;
};
nextCount += 1
}
limiter(req, res, next);
limiter(req, res, next);
limiter(req, res, next)
limiter(req, res, next)
assert.equal(nextCount, 1);
assert.equal(res.statusCode, 429);
assert.equal(res.headers["Retry-After"], "60");
assert.equal(res.body.errorCode, "rate_limited");
});
assert.equal(nextCount, 1)
assert.equal(res.statusCode, 429)
assert.equal(res.headers['Retry-After'], '60')
assert.equal(res.body.errorCode, 'rate_limited')
})
test("createRateLimitMiddleware supports custom limit responses", () => {
resetRateLimitBucketsForTest();
test('createRateLimitMiddleware supports custom limit responses', () => {
resetRateLimitBucketsForTest()
const limiter = createRateLimitMiddleware({
scope: "test:custom",
scope: 'test:custom',
windowMs: 60_000,
max: 1,
onLimit: (_req, res) => {
res.status(200).json({ code: 429, message: "limited" });
res.status(200).json({ code: 429, message: 'limited' })
},
});
const req = createMockRequest();
const res = createMockResponse();
})
const req = createMockRequest()
const res = createMockResponse()
limiter(req, res, () => {});
limiter(req, res, () => {});
limiter(req, res, () => {})
limiter(req, res, () => {})
assert.equal(res.statusCode, 200);
assert.deepEqual(res.body, { code: 429, message: "limited" });
});
assert.equal(res.statusCode, 200)
assert.deepEqual(res.body, { code: 429, message: 'limited' })
})
function createMockRequest(): Request {
return {
ip: "127.0.0.1",
originalUrl: "/test",
ip: '127.0.0.1',
originalUrl: '/test',
headers: {},
socket: {
remoteAddress: "127.0.0.1",
remoteAddress: '127.0.0.1',
},
} as Request;
} as Request
}
function createMockResponse(): Response & {
statusCode: number;
headers: Record<string, string>;
body: any;
statusCode: number
headers: Record<string, string>
body: any
} {
const res = {
statusCode: 200,
headers: {} as Record<string, string>,
body: null as any,
setHeader(name: string, value: string) {
this.headers[name] = value;
return this;
this.headers[name] = value
return this
},
status(statusCode: number) {
this.statusCode = statusCode;
return this;
this.statusCode = statusCode
return this
},
json(body: any) {
this.body = body;
return this;
this.body = body
return this
},
};
}
return res as Response & {
statusCode: number;
headers: Record<string, string>;
body: any;
};
statusCode: number
headers: Record<string, string>
body: any
}
}
+56 -55
View File
@@ -1,30 +1,30 @@
import type { NextFunction, Request, Response } from "express";
import type { NextFunction, Request, Response } from 'express'
import { buildErrorPayload, createHttpError } from "../utils/http.js";
import { logWarn } from "../utils/logger.js";
import { buildErrorPayload, createHttpError } from '../utils/http.js'
import { logWarn } from '../utils/logger.js'
type RateLimitKeyResolver = (req: Request) => string;
type RateLimitKeyResolver = (req: Request) => string
type RateLimitExceededContext = {
scope: string;
retryAfterSeconds: number;
};
scope: string
retryAfterSeconds: number
}
type RateLimitOptions = {
scope: string;
windowMs: number;
max: number;
key?: RateLimitKeyResolver;
onLimit?: (req: Request, res: Response, context: RateLimitExceededContext) => void;
};
scope: string
windowMs: number
max: number
key?: RateLimitKeyResolver
onLimit?: (req: Request, res: Response, context: RateLimitExceededContext) => void
}
type RateLimitBucket = {
count: number;
resetAt: number;
};
count: number
resetAt: number
}
const buckets = new Map<string, RateLimitBucket>();
let lastCleanupAt = 0;
const buckets = new Map<string, RateLimitBucket>()
let lastCleanupAt = 0
export function createRateLimitMiddleware({
scope,
@@ -33,90 +33,91 @@ export function createRateLimitMiddleware({
key = defaultRateLimitKey,
onLimit,
}: RateLimitOptions) {
const normalizedScope = String(scope || "default").trim() || "default";
const normalizedWindowMs = Math.max(1000, Number(windowMs || 0));
const normalizedMax = Math.max(1, Number(max || 0));
const normalizedScope = String(scope || 'default').trim() || 'default'
const normalizedWindowMs = Math.max(1000, Number(windowMs || 0))
const normalizedMax = Math.max(1, Number(max || 0))
return (req: Request, res: Response, next: NextFunction): void => {
const now = Date.now();
cleanupExpiredBuckets(now);
const now = Date.now()
cleanupExpiredBuckets(now)
const bucketKey = `${normalizedScope}:${key(req)}`;
const current = buckets.get(bucketKey);
const bucket = current && current.resetAt > now
? current
: { count: 0, resetAt: now + normalizedWindowMs };
const bucketKey = `${normalizedScope}:${key(req)}`
const current = buckets.get(bucketKey)
const bucket =
current && current.resetAt > now ? current : { count: 0, resetAt: now + normalizedWindowMs }
bucket.count += 1;
buckets.set(bucketKey, bucket);
bucket.count += 1
buckets.set(bucketKey, bucket)
if (bucket.count <= normalizedMax) {
next();
return;
next()
return
}
const retryAfterSeconds = Math.max(1, Math.ceil((bucket.resetAt - now) / 1000));
res.setHeader("Retry-After", String(retryAfterSeconds));
const retryAfterSeconds = Math.max(1, Math.ceil((bucket.resetAt - now) / 1000))
res.setHeader('Retry-After', String(retryAfterSeconds))
logWarn("[rate-limit]", "请求触发限流", {
logWarn('[rate-limit]', '请求触发限流', {
scope: normalizedScope,
ip: req.ip,
originalUrl: req.originalUrl,
retryAfterSeconds,
});
})
if (onLimit) {
onLimit(req, res, {
scope: normalizedScope,
retryAfterSeconds,
});
return;
})
return
}
const error = createHttpError("请求过于频繁,请稍后再试", {
const error = createHttpError('请求过于频繁,请稍后再试', {
statusCode: 429,
errorCode: "rate_limited",
});
res.status(429).json(buildErrorPayload(error, "请求过于频繁,请稍后再试"));
};
errorCode: 'rate_limited',
})
res.status(429).json(buildErrorPayload(error, '请求过于频繁,请稍后再试'))
}
}
export function resetRateLimitBucketsForTest(): void {
buckets.clear();
lastCleanupAt = 0;
buckets.clear()
lastCleanupAt = 0
}
export function getBodyFieldRateLimitKey(fieldName: string): RateLimitKeyResolver {
return (req) => [defaultRateLimitKey(req), normalizeKeyPart(req.body?.[fieldName])].join(":");
return (req) => [defaultRateLimitKey(req), normalizeKeyPart(req.body?.[fieldName])].join(':')
}
export function getParamRateLimitKey(paramName: string): RateLimitKeyResolver {
return (req) => [defaultRateLimitKey(req), normalizeKeyPart(req.params?.[paramName])].join(":");
return (req) => [defaultRateLimitKey(req), normalizeKeyPart(req.params?.[paramName])].join(':')
}
function defaultRateLimitKey(req: Request): string {
return normalizeKeyPart(
req.ip ||
String(req.headers["x-forwarded-for"] || "").split(",")[0] ||
String(req.headers['x-forwarded-for'] || '').split(',')[0] ||
req.socket.remoteAddress ||
"unknown",
);
'unknown',
)
}
function normalizeKeyPart(value: unknown): string {
const normalized = String(value || "").trim().toLowerCase();
return normalized || "unknown";
const normalized = String(value || '')
.trim()
.toLowerCase()
return normalized || 'unknown'
}
function cleanupExpiredBuckets(now: number): void {
if (now - lastCleanupAt < 60_000) {
return;
return
}
lastCleanupAt = now;
lastCleanupAt = now
for (const [key, bucket] of buckets.entries()) {
if (bucket.resetAt <= now) {
buckets.delete(key);
buckets.delete(key)
}
}
}
@@ -39,7 +39,9 @@ type AdminAuditLogListResult = {
total: number
}
export async function createAdminAuditLog(input: AdminAuditLogCreateInput): Promise<AdminAuditLogRow | null> {
export async function createAdminAuditLog(
input: AdminAuditLogCreateInput,
): Promise<AdminAuditLogRow | null> {
const result = await query<AdminAuditLogRow>(
`
INSERT INTO admin_audit_logs (
@@ -69,12 +71,19 @@ export async function createAdminAuditLog(input: AdminAuditLogCreateInput): Prom
return result.rows[0] || null
}
export async function getAdminAuditLogById(logId: number | string): Promise<AdminAuditLogRow | null> {
const result = await query<AdminAuditLogRow>('SELECT * FROM admin_audit_logs WHERE id = $1 LIMIT 1', [Number(logId)])
export async function getAdminAuditLogById(
logId: number | string,
): Promise<AdminAuditLogRow | null> {
const result = await query<AdminAuditLogRow>(
'SELECT * FROM admin_audit_logs WHERE id = $1 LIMIT 1',
[Number(logId)],
)
return result.rows[0] || null
}
export async function listAdminAuditLogs(queryInput: AdminAuditLogListInput = {}): Promise<AdminAuditLogListResult> {
export async function listAdminAuditLogs(
queryInput: AdminAuditLogListInput = {},
): Promise<AdminAuditLogListResult> {
const conditions: string[] = []
const params: unknown[] = []
@@ -108,7 +117,7 @@ export async function listAdminAuditLogs(queryInput: AdminAuditLogListInput = {}
const pageSize = Number(queryInput.pageSize) || 20
const offset = (page - 1) * pageSize
const totalResult = await query<{ [column: string]: unknown, total: number }>(
const totalResult = await query<{ [column: string]: unknown; total: number }>(
`SELECT COUNT(*)::int AS total FROM admin_audit_logs ${whereClause}`,
params,
)
@@ -20,10 +20,12 @@ type AdminUserCreateInput = {
updatedAt: string
}
type AdminUserPatch = Partial<Pick<
AdminUserRow,
'username' | 'password_hash' | 'role' | 'status' | 'session_version' | 'updated_at'
>>
type AdminUserPatch = Partial<
Pick<
AdminUserRow,
'username' | 'password_hash' | 'role' | 'status' | 'session_version' | 'updated_at'
>
>
type AdminUserListInput = {
page?: number
@@ -44,23 +46,23 @@ const ADMIN_USER_SELECT = `
`
export async function getAdminUserById(userId: number | string): Promise<AdminUserRow | null> {
const result = await query<AdminUserRow>(
`${ADMIN_USER_SELECT} WHERE au.id = $1 LIMIT 1`,
[Number(userId)],
)
const result = await query<AdminUserRow>(`${ADMIN_USER_SELECT} WHERE au.id = $1 LIMIT 1`, [
Number(userId),
])
return result.rows[0] || null
}
export async function getAdminUserByUsername(username: string): Promise<AdminUserRow | null> {
const result = await query<AdminUserRow>(
`${ADMIN_USER_SELECT} WHERE au.username = $1 LIMIT 1`,
[String(username || '').trim().toLowerCase()],
)
const result = await query<AdminUserRow>(`${ADMIN_USER_SELECT} WHERE au.username = $1 LIMIT 1`, [
String(username || '')
.trim()
.toLowerCase(),
])
return result.rows[0] || null
}
export async function createAdminUser(input: AdminUserCreateInput): Promise<AdminUserRow | null> {
const result = await query<{ [column: string]: unknown, id: number }>(
const result = await query<{ [column: string]: unknown; id: number }>(
`
INSERT INTO admin_users (
username,
@@ -97,7 +99,7 @@ export async function updateAdminUser(
}
const next = { ...current, ...patch }
const result = await query<{ [column: string]: unknown, id: number }>(
const result = await query<{ [column: string]: unknown; id: number }>(
`
UPDATE admin_users
SET
@@ -151,7 +153,7 @@ export async function listAdminUsers({
}
const whereClause = filters.length > 0 ? `WHERE ${filters.join(' AND ')}` : ''
const totalResult = await query<{ [column: string]: unknown, total: number }>(
const totalResult = await query<{ [column: string]: unknown; total: number }>(
`SELECT COUNT(*)::int AS total FROM admin_users ${whereClause}`,
params,
)
@@ -175,7 +177,7 @@ export async function listAdminUsers({
}
export async function countActiveAdminUsers(): Promise<number> {
const result = await query<{ [column: string]: unknown, total: number }>(
const result = await query<{ [column: string]: unknown; total: number }>(
`SELECT COUNT(*)::int AS total FROM admin_users WHERE role = 'admin' AND status = 'active'`,
)
return Number(result.rows[0]?.total || 0)
@@ -26,12 +26,16 @@ type ClaimTokenCreateInput = {
updatedAt: string
}
type ClaimTokenPatch = Partial<Pick<
ClaimTokenRow,
'status' | 'expired_at' | 'used_at' | 'max_use_count' | 'used_count' | 'updated_at'
>>
type ClaimTokenPatch = Partial<
Pick<
ClaimTokenRow,
'status' | 'expired_at' | 'used_at' | 'max_use_count' | 'used_count' | 'updated_at'
>
>
export async function createClaimToken(input: ClaimTokenCreateInput): Promise<ClaimTokenRow | null> {
export async function createClaimToken(
input: ClaimTokenCreateInput,
): Promise<ClaimTokenRow | null> {
const result = await query<ClaimTokenRow>(
`
INSERT INTO claim_tokens (
@@ -74,16 +78,23 @@ export async function createClaimToken(input: ClaimTokenCreateInput): Promise<Cl
}
export async function getClaimTokenById(tokenId: number | string): Promise<ClaimTokenRow | null> {
const result = await query<ClaimTokenRow>('SELECT * FROM claim_tokens WHERE id = $1 LIMIT 1', [Number(tokenId)])
const result = await query<ClaimTokenRow>('SELECT * FROM claim_tokens WHERE id = $1 LIMIT 1', [
Number(tokenId),
])
return result.rows[0] || null
}
export async function findClaimTokenByToken(token: string): Promise<ClaimTokenRow | null> {
const result = await query<ClaimTokenRow>('SELECT * FROM claim_tokens WHERE token = $1 LIMIT 1', [String(token || '').trim()])
const result = await query<ClaimTokenRow>('SELECT * FROM claim_tokens WHERE token = $1 LIMIT 1', [
String(token || '').trim(),
])
return result.rows[0] || null
}
export async function updateClaimToken(tokenId: number | string, patch: ClaimTokenPatch): Promise<ClaimTokenRow | null> {
export async function updateClaimToken(
tokenId: number | string,
patch: ClaimTokenPatch,
): Promise<ClaimTokenRow | null> {
const current = await getClaimTokenById(tokenId)
if (!current) {
return null
@@ -47,7 +47,9 @@ export type FulfillmentProfileRequirementInput = {
configJson?: string | Record<string, unknown>
}
export async function getFulfillmentProfileByKey(profileKey: string): Promise<FulfillmentProfileRow | null> {
export async function getFulfillmentProfileByKey(
profileKey: string,
): Promise<FulfillmentProfileRow | null> {
const result = await query<FulfillmentProfileRow>(
'SELECT * FROM fulfillment_profiles WHERE profile_key = $1 LIMIT 1',
[String(profileKey || '').trim()],
@@ -104,10 +106,9 @@ export async function replaceFulfillmentProfileRequirements(
timestamp: string,
): Promise<void> {
await withTransaction(async (client) => {
await client.query(
'DELETE FROM fulfillment_profile_requirements WHERE profile_id = $1',
[Number(profileId)],
)
await client.query('DELETE FROM fulfillment_profile_requirements WHERE profile_id = $1', [
Number(profileId),
])
for (const requirement of requirements) {
await client.query(
@@ -10,32 +10,35 @@ test('syncKuaishouCloudTaskStateForTask 将 Date 更新时间规范化为 ISO
return { rows: [] }
}
await syncKuaishouCloudTaskStateForTask({
taskId: 9,
executorKey: 'kuaishou_ct_assisted',
contextJson: {
kuaishouCloudFulfillment: {
ticket: {
status: 'verified',
code: '597172E91CF18417',
},
binding: {
bindUrl: 'https://example.test/bind',
vnPhone: '14062706492',
roleName: '测试角色',
roleId: '4212063544',
resolvedSourceKey: 'account_test',
},
dispatch: {
status: 'pending',
},
consume: {
status: 'pending',
await syncKuaishouCloudTaskStateForTask(
{
taskId: 9,
executorKey: 'kuaishou_ct_assisted',
contextJson: {
kuaishouCloudFulfillment: {
ticket: {
status: 'verified',
code: '597172E91CF18417',
},
binding: {
bindUrl: 'https://example.test/bind',
vnPhone: '14062706492',
roleName: '测试角色',
roleId: '4212063544',
resolvedSourceKey: 'account_test',
},
dispatch: {
status: 'pending',
},
consume: {
status: 'pending',
},
},
},
updatedAt: new Date('2026-05-29T09:05:46.658Z'),
},
updatedAt: new Date('2026-05-29T09:05:46.658Z'),
}, executor)
executor,
)
assert.equal(calls.length, 1)
assert.match(calls[0].text, /INSERT INTO kuaishou_cloud_task_states/)
@@ -64,7 +64,10 @@ test('resolveOrderItemSyncPlan matches by identity before falling back to positi
const plan = resolveOrderItemSyncPlan(existingItems, nextItems)
assert.deepEqual(plan.updates.map((item) => item.orderItemId), [22, 21])
assert.deepEqual(
plan.updates.map((item) => item.orderItemId),
[22, 21],
)
assert.deepEqual(plan.creates, [])
assert.deepEqual(plan.deletes, [])
})
@@ -88,7 +91,10 @@ test('resolveOrderItemSyncPlan keeps surplus existing ids for conditional cleanu
const plan = resolveOrderItemSyncPlan(existingItems, nextItems)
assert.deepEqual(plan.updates.map((item) => item.orderItemId), [31])
assert.deepEqual(
plan.updates.map((item) => item.orderItemId),
[31],
)
assert.deepEqual(plan.creates, [])
assert.deepEqual(plan.deletes, [32])
})
@@ -204,6 +204,8 @@ async function listOrderItemsByOrderIdWithExecutor(
}
export async function getOrderItemById(orderItemId: number | string): Promise<OrderItemRow | null> {
const result = await query<OrderItemRow>('SELECT * FROM order_items WHERE id = $1 LIMIT 1', [Number(orderItemId)])
const result = await query<OrderItemRow>('SELECT * FROM order_items WHERE id = $1 LIMIT 1', [
Number(orderItemId),
])
return result.rows[0] || null
}
@@ -29,12 +29,7 @@ export async function createTaskEvent(
) VALUES ($1, $2, $3::jsonb, $4)
RETURNING *
`,
[
Number(taskId),
String(eventType || '').trim(),
JSON.stringify(payload || {}),
createdAt,
],
[Number(taskId), String(eventType || '').trim(), JSON.stringify(payload || {}), createdAt],
)
return result.rows[0] || null
+65 -34
View File
@@ -9,10 +9,7 @@ import type {
TaskRuntimeContextPatch,
TaskUpdatePatch,
} from '../types/repository/inputs.js'
import type {
TaskListQueryResult,
TaskRow,
} from '../types/repository/rows.js'
import type { TaskListQueryResult, TaskRow } from '../types/repository/rows.js'
type TaskQueryExecutor = (text: string, params?: unknown[]) => Promise<{ rows: unknown[] }>
const KUAISHOU_FEIFEI_EXECUTOR_KEY = 'kuaishou_feifei'
@@ -150,21 +147,32 @@ export async function createTask(input: TaskCreateInput): Promise<TaskRow | null
const taskId = Number(taskResult.rows[0]?.id || 0)
if (input.runtimeContext) {
await upsertTaskRuntimeContextWithClient(client, taskId, input.runtimeContext, input.createdAt)
await upsertTaskRuntimeContextWithClient(
client,
taskId,
input.runtimeContext,
input.createdAt,
)
}
await syncKuaishouCloudTaskStateForTask({
taskId,
executorKey: input.executorKey,
contextJson: input.contextJson || '{}',
updatedAt: input.updatedAt,
}, client.query.bind(client) as TaskQueryExecutor)
await syncKuaishouCloudTaskStateForTask(
{
taskId,
executorKey: input.executorKey,
contextJson: input.contextJson || '{}',
updatedAt: input.updatedAt,
},
client.query.bind(client) as TaskQueryExecutor,
)
return getTaskByIdWithExecutor(client.query.bind(client) as TaskQueryExecutor, taskId)
})
}
export async function updateTask(taskId: number | string, patch: TaskUpdatePatch): Promise<TaskRow | null> {
export async function updateTask(
taskId: number | string,
patch: TaskUpdatePatch,
): Promise<TaskRow | null> {
const current = await getTaskById(taskId)
if (!current) {
return null
@@ -218,26 +226,38 @@ export async function updateTask(taskId: number | string, patch: TaskUpdatePatch
if (containsRuntimeContextPatch(patch)) {
const runtimeContextPatch: TaskRuntimeContextPatch = {}
if (patch.runtime_session_id !== undefined) runtimeContextPatch.runtimeSessionId = patch.runtime_session_id
if (patch.runtime_session_id !== undefined)
runtimeContextPatch.runtimeSessionId = patch.runtime_session_id
if (patch.login_type !== undefined) runtimeContextPatch.loginType = patch.login_type
if (patch.nickname !== undefined) runtimeContextPatch.nickname = patch.nickname
if (patch.role_id !== undefined) runtimeContextPatch.roleId = patch.role_id
if (patch.role_name !== undefined) runtimeContextPatch.roleName = patch.role_name
if (patch.area !== undefined) runtimeContextPatch.area = patch.area
if (patch.partition_name !== undefined) runtimeContextPatch.partitionName = patch.partition_name
if (patch.screenshot_path !== undefined) runtimeContextPatch.screenshotPath = patch.screenshot_path
if (patch.artifacts_json !== undefined) runtimeContextPatch.artifactsJson = patch.artifacts_json
if (patch.partition_name !== undefined)
runtimeContextPatch.partitionName = patch.partition_name
if (patch.screenshot_path !== undefined)
runtimeContextPatch.screenshotPath = patch.screenshot_path
if (patch.artifacts_json !== undefined)
runtimeContextPatch.artifactsJson = patch.artifacts_json
if (patch.state_json !== undefined) runtimeContextPatch.stateJson = patch.state_json
await upsertTaskRuntimeContextWithClient(client, Number(taskId), runtimeContextPatch, patch.updated_at || current.updated_at)
await upsertTaskRuntimeContextWithClient(
client,
Number(taskId),
runtimeContextPatch,
patch.updated_at || current.updated_at,
)
}
await syncKuaishouCloudTaskStateForTask({
taskId,
executorKey: next.executor_key,
contextJson: next.context_json,
updatedAt: next.updated_at,
}, client.query.bind(client) as TaskQueryExecutor)
await syncKuaishouCloudTaskStateForTask(
{
taskId,
executorKey: next.executor_key,
contextJson: next.context_json,
updatedAt: next.updated_at,
},
client.query.bind(client) as TaskQueryExecutor,
)
return getTaskByIdWithExecutor(client.query.bind(client) as TaskQueryExecutor, taskId)
})
@@ -296,12 +316,15 @@ export async function updateTaskStatusIfCurrent(
return null
}
await syncKuaishouCloudTaskStateForTask({
taskId: updated.id,
executorKey: updated.executor_key,
contextJson: updated.context_json,
updatedAt: updated.updated_at,
}, client.query.bind(client) as TaskQueryExecutor)
await syncKuaishouCloudTaskStateForTask(
{
taskId: updated.id,
executorKey: updated.executor_key,
contextJson: updated.context_json,
updatedAt: updated.updated_at,
},
client.query.bind(client) as TaskQueryExecutor,
)
return getTaskByIdWithExecutor(client.query.bind(client) as TaskQueryExecutor, taskId)
})
@@ -311,7 +334,9 @@ export async function getTaskById(taskId: number | string): Promise<TaskRow | nu
return getTaskByIdWithExecutor(query, taskId)
}
export async function findTaskByClaimTokenId(claimTokenId: number | string): Promise<TaskRow | null> {
export async function findTaskByClaimTokenId(
claimTokenId: number | string,
): Promise<TaskRow | null> {
const result = await query<TaskRow>(
`${buildTaskSelect()}
JOIN claim_tokens ctf ON ctf.task_id = ft.id
@@ -441,7 +466,7 @@ export async function listTasks({
}
const whereClause = filters.length > 0 ? `WHERE ${filters.join(' AND ')}` : ''
const totalResult = await query<{ [column: string]: unknown, total: number }>(
const totalResult = await query<{ [column: string]: unknown; total: number }>(
`
SELECT COUNT(*)::int AS total
FROM fulfillment_tasks ft
@@ -525,8 +550,12 @@ async function upsertTaskRuntimeContextWithClient(
next.area,
next.partition_name,
next.screenshot_path,
typeof next.artifacts_json === 'string' ? next.artifacts_json : JSON.stringify(next.artifacts_json || {}),
typeof next.state_json === 'string' ? next.state_json : JSON.stringify(next.state_json || {}),
typeof next.artifacts_json === 'string'
? next.artifacts_json
: JSON.stringify(next.artifacts_json || {}),
typeof next.state_json === 'string'
? next.state_json
: JSON.stringify(next.state_json || {}),
timestamp,
timestamp,
],
@@ -560,7 +589,9 @@ async function upsertTaskRuntimeContextWithClient(
next.area,
next.partition_name,
next.screenshot_path,
typeof next.artifacts_json === 'string' ? next.artifacts_json : JSON.stringify(next.artifacts_json || {}),
typeof next.artifacts_json === 'string'
? next.artifacts_json
: JSON.stringify(next.artifacts_json || {}),
typeof next.state_json === 'string' ? next.state_json : JSON.stringify(next.state_json || {}),
timestamp,
Number(taskId),
@@ -2,7 +2,6 @@ import type { PoolClient } from 'pg'
import type { WorkerWalletRow } from './types.js'
export async function ensureWorkerWalletWithClient(
client: PoolClient,
workerId: number,
@@ -1,5 +1,3 @@
export type WorkerLevelRow = {
id: number
level_key: string
@@ -1,10 +1,25 @@
import { query, withTransaction } from '../../db/client.js'
import { ensureWorkerWalletWithClient, getWorkerWalletWithClient, toJsonString, toPositiveInteger } from './shared.js'
import type { CreateWorkOrderInput, GrabWorkOrderResult, ListInput, ProblemWorkOrderResolutionAction, ProductRuleListInput, WorkCategoryRow, WorkerDepositUnfreezeRow, WorkOrderRow, WorkOrderShareRow, WorkProductRuleRow } from './types.js'
import {
ensureWorkerWalletWithClient,
getWorkerWalletWithClient,
toJsonString,
toPositiveInteger,
} from './shared.js'
import type {
CreateWorkOrderInput,
GrabWorkOrderResult,
ListInput,
ProblemWorkOrderResolutionAction,
ProductRuleListInput,
WorkCategoryRow,
WorkerDepositUnfreezeRow,
WorkOrderRow,
WorkOrderShareRow,
WorkProductRuleRow,
} from './types.js'
import type { PoolClient } from 'pg'
import { maybeUpgradeWorkerLevelWithClient } from './worker-repo.js'
const WORK_ORDER_SELECT = `
SELECT
wo.*,
@@ -248,7 +263,10 @@ export async function submitWorkOrderShareAcceptance(input: {
workerId: number
acceptanceJson: string
now: string
}): Promise<{ share: WorkOrderShareRow | null; failureReason: 'share_not_found' | 'share_status_invalid' | null }> {
}): Promise<{
share: WorkOrderShareRow | null
failureReason: 'share_not_found' | 'share_status_invalid' | null
}> {
return withTransaction(async (client) => {
const lockResult = await client.query<{ id: number }>(
`
@@ -436,7 +454,9 @@ export async function getWorkCategoryByKey(categoryKey: string): Promise<WorkCat
return result.rows[0] || null
}
export async function getWorkCategoryById(categoryId: number | string): Promise<WorkCategoryRow | null> {
export async function getWorkCategoryById(
categoryId: number | string,
): Promise<WorkCategoryRow | null> {
const result = await query<WorkCategoryRow>(
'SELECT * FROM work_categories WHERE id = $1 LIMIT 1',
[Number(categoryId)],
@@ -747,8 +767,7 @@ export async function listWorkOrders({
pinnedFirst = false,
sort = 'id_desc',
}: ListInput = {}): Promise<{ items: WorkOrderRow[]; total: number }> {
const effectiveStatuses =
statuses.length > 0 ? statuses : status.trim() ? [status.trim()] : []
const effectiveStatuses = statuses.length > 0 ? statuses : status.trim() ? [status.trim()] : []
const { whereClause, params } = buildWorkOrderWhere({
statuses: effectiveStatuses,
keyword,
@@ -787,9 +806,7 @@ export async function listWorkOrders({
) DESC NULLS LAST, wo.id DESC`
})()
: 'wo.id DESC'
const effectiveOrderBy = pinnedFirst
? `wo.pinned_at DESC NULLS LAST, ${orderBy}`
: orderBy
const effectiveOrderBy = pinnedFirst ? `wo.pinned_at DESC NULLS LAST, ${orderBy}` : orderBy
params.push(pageSize, offset)
const itemsResult = await query<WorkOrderRow>(
`${WORK_ORDER_SELECT}
@@ -911,14 +928,8 @@ export async function updateWorkOrderBasic(
String((patch.productName ?? current.product_name) || '').trim(),
patch.categoryId === undefined ? current.category_id : patch.categoryId,
toPositiveInteger(patch.rewardAmount ?? current.reward_amount, 0),
toPositiveInteger(
patch.requiredDepositAmount ?? current.required_deposit_amount,
0,
),
toPositiveInteger(
patch.depositThresholdAmount ?? current.deposit_threshold_amount,
0,
),
toPositiveInteger(patch.requiredDepositAmount ?? current.required_deposit_amount, 0),
toPositiveInteger(patch.depositThresholdAmount ?? current.deposit_threshold_amount, 0),
toJsonString(patch.requirementJson ?? current.requirement_json),
toPositiveInteger(patch.timeoutMinutes ?? current.timeout_minutes, 0),
String((patch.timeoutPolicy ?? current.timeout_policy) || 'reopen').trim(),
@@ -1196,10 +1207,7 @@ export async function cancelWorkerWorkOrder(input: {
if (releaseAmount > 0) {
const nextAvailable = Number(wallet?.available_amount || 0) + releaseAmount
const nextFrozen = Math.max(
0,
Number(wallet?.frozen_deposit_amount || 0) - releaseAmount,
)
const nextFrozen = Math.max(0, Number(wallet?.frozen_deposit_amount || 0) - releaseAmount)
await client.query(
`
UPDATE worker_wallets
@@ -1237,10 +1245,7 @@ export async function cancelWorkerWorkOrder(input: {
})
}
export async function unassignWorkOrder(input: {
workOrderId: number
now: string
}): Promise<{
export async function unassignWorkOrder(input: { workOrderId: number; now: string }): Promise<{
order: WorkOrderRow | null
failureReason: 'work_order_not_in_progress' | 'work_order_no_worker' | null
}> {
@@ -1286,10 +1291,7 @@ export async function unassignWorkOrder(input: {
if (releaseAmount > 0) {
const nextAvailable = Number(wallet?.available_amount || 0) + releaseAmount
const nextFrozen = Math.max(
0,
Number(wallet?.frozen_deposit_amount || 0) - releaseAmount,
)
const nextFrozen = Math.max(0, Number(wallet?.frozen_deposit_amount || 0) - releaseAmount)
await client.query(
`
UPDATE worker_wallets
@@ -1378,10 +1380,7 @@ export async function returnAssignedWorkOrderToHall(input: {
if (releaseAmount > 0) {
const nextAvailable = Number(wallet?.available_amount || 0) + releaseAmount
const nextFrozen = Math.max(
0,
Number(wallet?.frozen_deposit_amount || 0) - releaseAmount,
)
const nextFrozen = Math.max(0, Number(wallet?.frozen_deposit_amount || 0) - releaseAmount)
await client.query(
`
UPDATE worker_wallets
@@ -1474,10 +1473,7 @@ export async function cancelAssignedWorkOrder(input: {
if (releaseAmount > 0) {
const nextAvailable = Number(wallet?.available_amount || 0) + releaseAmount
const nextFrozen = Math.max(
0,
Number(wallet?.frozen_deposit_amount || 0) - releaseAmount,
)
const nextFrozen = Math.max(0, Number(wallet?.frozen_deposit_amount || 0) - releaseAmount)
await client.query(
`
UPDATE worker_wallets
@@ -1608,10 +1604,7 @@ export async function acceptWorkOrderAndSettle(input: {
Number(wallet?.available_amount || 0) +
shareReward +
(shouldDelayUnfreeze ? 0 : releaseAmount)
const nextFrozen = Math.max(
0,
Number(wallet?.frozen_deposit_amount || 0) - releaseAmount,
)
const nextFrozen = Math.max(0, Number(wallet?.frozen_deposit_amount || 0) - releaseAmount)
await client.query(
`
@@ -1829,7 +1822,10 @@ export async function acceptWorkOrderAndSettle(input: {
await maybeUpgradeWorkerLevelWithClient(client, workerId, input.now)
}
return { order: await getWorkOrderByIdWithClient(client, input.workOrderId), failureReason: null }
return {
order: await getWorkOrderByIdWithClient(client, input.workOrderId),
failureReason: null,
}
})
}
@@ -2263,7 +2259,10 @@ export async function listDueDepositUnfreezes({
export async function releaseDepositUnfreeze({
unfreezeId,
now,
}: { unfreezeId: number; now: string }): Promise<WorkerDepositUnfreezeRow | null> {
}: {
unfreezeId: number
now: string
}): Promise<WorkerDepositUnfreezeRow | null> {
return withTransaction(async (client) => {
const currentResult = await client.query<WorkerDepositUnfreezeRow>(
`
@@ -2295,10 +2294,7 @@ export async function releaseDepositUnfreeze({
if (amount > 0) {
const nextAvailable = Number(wallet?.available_amount || 0) + amount
const nextPending = Math.max(
0,
Number(wallet?.pending_unfreeze_amount || 0) - amount,
)
const nextPending = Math.max(0, Number(wallet?.pending_unfreeze_amount || 0) - amount)
await client.query(
`
UPDATE worker_wallets
@@ -2337,7 +2333,13 @@ export async function releaseDepositUnfreeze({
async function enqueueDepositUnfreezeWithClient(
client: PoolClient,
input: { workerId: number; workOrderId: number; amount: number; unfreezeDays: number; now: string },
input: {
workerId: number
workOrderId: number
amount: number
unfreezeDays: number
now: string
},
) {
if (input.amount <= 0) return
const unfreezeAt = new Date(
@@ -2375,7 +2377,9 @@ export async function countWorkerActiveOrders(workerId: number | string): Promis
return Number(result.rows[0]?.total || 0)
}
export async function countTimeoutEventsByWorkerIds(workerIds: number[]): Promise<Map<number, number>> {
export async function countTimeoutEventsByWorkerIds(
workerIds: number[],
): Promise<Map<number, number>> {
const counts = new Map<number, number>()
const uniqueIds = [...new Set(workerIds.map((id) => Number(id)).filter((id) => id > 0))]
if (uniqueIds.length === 0) return counts
@@ -2481,7 +2485,10 @@ async function getOutstandingDepositAmountWithClient(
return resolveOutstandingDepositAmount(result.rows[0])
}
async function countWorkerActiveOrdersWithClient(client: PoolClient, workerId: number): Promise<number> {
async function countWorkerActiveOrdersWithClient(
client: PoolClient,
workerId: number,
): Promise<number> {
const result = await client.query<{ total: number }>(
`
SELECT (
@@ -2542,11 +2549,7 @@ function buildWorkOrderWhere({
const filters: string[] = []
const params: unknown[] = []
const normalizedStatuses = [
...new Set(
statuses
.map((item) => String(item || '').trim())
.filter(Boolean),
),
...new Set(statuses.map((item) => String(item || '').trim()).filter(Boolean)),
]
if (normalizedStatuses.length > 0) {
params.push(normalizedStatuses)
@@ -1,9 +1,18 @@
import { query, withTransaction } from '../../db/client.js'
import { ensureWorkerWalletWithClient, getWorkerWalletWithClient } from './shared.js'
import type { CreateWorkerInput, FinanceRequestListInput, ListInput, WalletLedgerListInput, WorkerFinanceRequestRow, WorkerLevelRow, WorkerUserRow, WorkerWalletLedgerRow, WorkerWalletRow } from './types.js'
import type {
CreateWorkerInput,
FinanceRequestListInput,
ListInput,
WalletLedgerListInput,
WorkerFinanceRequestRow,
WorkerLevelRow,
WorkerUserRow,
WorkerWalletLedgerRow,
WorkerWalletRow,
} from './types.js'
import type { PoolClient } from 'pg'
const WORKER_USER_SELECT = `
SELECT
wu.*,
@@ -119,10 +128,9 @@ export async function getWorkerUserByUsername(username: string): Promise<WorkerU
}
export async function getWorkerUserByPhone(phone: string): Promise<WorkerUserRow | null> {
const result = await query<WorkerUserRow>(
`${WORKER_USER_SELECT} WHERE wu.phone = $1 LIMIT 1`,
[String(phone || '').trim()],
)
const result = await query<WorkerUserRow>(`${WORKER_USER_SELECT} WHERE wu.phone = $1 LIMIT 1`, [
String(phone || '').trim(),
])
return result.rows[0] || null
}
@@ -136,12 +144,14 @@ export async function getWorkerUserByDisplayName(
return result.rows[0] || null
}
export async function getWorkerUserByInviteCode(
inviteCode: string,
): Promise<WorkerUserRow | null> {
export async function getWorkerUserByInviteCode(inviteCode: string): Promise<WorkerUserRow | null> {
const result = await query<WorkerUserRow>(
`${WORKER_USER_SELECT} WHERE wu.invite_code = $1 LIMIT 1`,
[String(inviteCode || '').trim().toUpperCase()],
[
String(inviteCode || '')
.trim()
.toUpperCase(),
],
)
return result.rows[0] || null
}
@@ -827,23 +837,18 @@ export async function maybeUpgradeWorkerLevelWithClient(
return null
}
await client.query(
'UPDATE worker_users SET level_id = $1, updated_at = $2 WHERE id = $3',
[nextLevelId, now, workerId],
)
await client.query('UPDATE worker_users SET level_id = $1, updated_at = $2 WHERE id = $3', [
nextLevelId,
now,
workerId,
])
await client.query(
`
INSERT INTO worker_level_logs (
worker_id, from_level_id, to_level_id, reason, payload_json, created_at
) VALUES ($1, $2, $3, 'auto', $4::jsonb, $5)
`,
[
workerId,
currentRow.level_id || null,
nextLevelId,
JSON.stringify({ acceptedCount }),
now,
],
[workerId, currentRow.level_id || null, nextLevelId, JSON.stringify({ acceptedCount }), now],
)
return getWorkerUserByIdWithClient(client, workerId)
}
@@ -903,10 +908,7 @@ function buildWorkerUserWhere({
}
}
function buildWorkerWalletLedgerWhere({
workerId = 0,
ledgerType = '',
}: WalletLedgerListInput) {
function buildWorkerWalletLedgerWhere({ workerId = 0, ledgerType = '' }: WalletLedgerListInput) {
const filters: string[] = []
const params: unknown[] = []
if (workerId) {
+5 -5
View File
@@ -10,13 +10,13 @@ const router = Router()
router.use('/audit-logs', requireAdminRoles(['admin']))
router.get('/audit-logs', createJsonHandler(
(req) => getAdminAuditLogs(req.query as AdminAuditLogRouteQuery),
{
router.get(
'/audit-logs',
createJsonHandler((req) => getAdminAuditLogs(req.query as AdminAuditLogRouteQuery), {
successMessage: 'ok',
errorMessage: '读取操作审计日志失败',
scope: '[admin/audit-logs]',
},
))
}),
)
export default router
+31 -26
View File
@@ -11,40 +11,45 @@ import { createJsonHandler, extractBearerToken } from './session.js'
const router = Router()
router.post('/auth/login', createRateLimitMiddleware({
scope: 'admin:login',
windowMs: 60_000,
max: 10,
key: getBodyFieldRateLimitKey('username'),
}), createJsonHandler(
(req) => loginAdmin(req.body?.username, req.body?.password, {
ip: resolveClientIp(req),
userAgent: resolveUserAgent(req),
location: resolveClientLocation(req),
router.post(
'/auth/login',
createRateLimitMiddleware({
scope: 'admin:login',
windowMs: 60_000,
max: 10,
key: getBodyFieldRateLimitKey('username'),
}),
{
successMessage: '登录成功',
errorMessage: '后台登录失败',
scope: '[admin/auth/login]',
},
))
createJsonHandler(
(req) =>
loginAdmin(req.body?.username, req.body?.password, {
ip: resolveClientIp(req),
userAgent: resolveUserAgent(req),
location: resolveClientLocation(req),
}),
{
successMessage: '登录成功',
errorMessage: '后台登录失败',
scope: '[admin/auth/login]',
},
),
)
router.get('/auth/session', createJsonHandler(
(req) => getAdminSessionSummary(extractBearerToken(req)),
{
router.get(
'/auth/session',
createJsonHandler((req) => getAdminSessionSummary(extractBearerToken(req)), {
successMessage: 'ok',
errorMessage: '读取后台登录态失败',
scope: '[admin/auth/session]',
},
))
}),
)
router.post('/auth/logout', createJsonHandler(
() => ({ success: true }),
{
router.post(
'/auth/logout',
createJsonHandler(() => ({ success: true }), {
successMessage: '已退出登录',
errorMessage: '后台退出失败',
scope: '[admin/auth/logout]',
},
))
}),
)
export default router
@@ -14,7 +14,12 @@ import { createHttpError, sendRouteError } from '../../utils/http.js'
import type { AdminCloudtentaclesDeliveryRecordQueryRouteBody } from '../../types/admin/route-inputs.js'
const router = Router()
const RECORD_IMAGE_CACHE_DIR = path.join(PROJECT_ROOT, 'data', 'cache', 'cloudtentacles-record-images')
const RECORD_IMAGE_CACHE_DIR = path.join(
PROJECT_ROOT,
'data',
'cache',
'cloudtentacles-record-images',
)
router.get(
'/cloudtentacles-records/sources',
@@ -155,7 +160,10 @@ function buildRecordImageCacheMeta(imageUrl: string) {
return {
candidateFilePaths: candidateTypes.map((contentType) => ({
contentType,
filePath: path.join(RECORD_IMAGE_CACHE_DIR, `${digest}.${extensionForContentType(contentType)}`),
filePath: path.join(
RECORD_IMAGE_CACHE_DIR,
`${digest}.${extensionForContentType(contentType)}`,
),
})),
filePathForContentType(contentType: string) {
return path.join(RECORD_IMAGE_CACHE_DIR, `${digest}.${extensionForContentType(contentType)}`)
@@ -164,7 +172,12 @@ function buildRecordImageCacheMeta(imageUrl: string) {
}
function normalizeImageContentType(contentType: string) {
return String(contentType || 'image/jpeg').split(';')[0]?.trim().toLowerCase() || 'image/jpeg'
return (
String(contentType || 'image/jpeg')
.split(';')[0]
?.trim()
.toLowerCase() || 'image/jpeg'
)
}
function extensionForContentType(contentType: string) {
+10 -10
View File
@@ -7,22 +7,22 @@ import { createJsonHandler } from './session.js'
const router = Router()
router.get('/dashboard/summary', createJsonHandler(
() => getAdminDashboardSummary(),
{
router.get(
'/dashboard/summary',
createJsonHandler(() => getAdminDashboardSummary(), {
successMessage: 'ok',
errorMessage: '读取后台概览失败',
scope: '[admin/dashboard/summary]',
},
))
}),
)
router.get('/dashboard/login-logs', createJsonHandler(
(req) => getAdminLoginLogs(req.query as JsonObject),
{
router.get(
'/dashboard/login-logs',
createJsonHandler((req) => getAdminLoginLogs(req.query as JsonObject), {
successMessage: 'ok',
errorMessage: '读取登录记录失败',
scope: '[admin/dashboard/login-logs]',
},
))
}),
)
export default router
+1 -5
View File
@@ -15,11 +15,7 @@ import { createJsonHandler, requireAdminRoles } from './session.js'
const router = Router()
function requireDevMockEnabled() {
return (
_req: unknown,
_res: unknown,
next: (error?: unknown) => void,
) => {
return (_req: unknown, _res: unknown, next: (error?: unknown) => void) => {
if (!isDevMockEnabled()) {
next(
createHttpError('开发 Mock 仅在非 production 环境可用(或设置 ENABLE_DEV_MOCK=1', {
@@ -57,7 +57,8 @@ router.post(
successMessage: '售后单列表已查询',
errorMessage: '查询快手售后单列表失败',
scope: '[admin/kuaishou-industry/refunds/list]',
audit: (req, data) => buildKuaishouIndustryAudit('kuaishou_industry_refund_list', req.body, data),
audit: (req, data) =>
buildKuaishouIndustryAudit('kuaishou_industry_refund_list', req.body, data),
},
),
)
@@ -72,7 +73,8 @@ router.post(
successMessage: '同意退款接口已执行',
errorMessage: '执行同意退款失败',
scope: '[admin/kuaishou-industry/refunds/approve]',
audit: (req, data) => buildKuaishouIndustryAudit('kuaishou_industry_refund_approve', req.body, data),
audit: (req, data) =>
buildKuaishouIndustryAudit('kuaishou_industry_refund_approve', req.body, data),
},
),
)
@@ -87,7 +89,8 @@ router.post(
successMessage: '不同意退款接口已执行',
errorMessage: '执行不同意退款失败',
scope: '[admin/kuaishou-industry/refunds/disagree]',
audit: (req, data) => buildKuaishouIndustryAudit('kuaishou_industry_refund_disagree', req.body, data),
audit: (req, data) =>
buildKuaishouIndustryAudit('kuaishou_industry_refund_disagree', req.body, data),
},
),
)
@@ -104,7 +107,8 @@ router.post(
successMessage: '电子凭证有效性已检查',
errorMessage: '检查电子凭证有效性失败',
scope: '[admin/kuaishou-industry/vouchers/check-available]',
audit: (req, data) => buildKuaishouIndustryAudit('kuaishou_industry_voucher_check_available', req.body, data),
audit: (req, data) =>
buildKuaishouIndustryAudit('kuaishou_industry_voucher_check_available', req.body, data),
},
),
)
@@ -119,7 +123,8 @@ router.post(
successMessage: '电子凭证冲正回调已执行',
errorMessage: '执行电子凭证冲正失败',
scope: '[admin/kuaishou-industry/vouchers/reverse]',
audit: (req, data) => buildKuaishouIndustryAudit('kuaishou_industry_voucher_reverse', req.body, data),
audit: (req, data) =>
buildKuaishouIndustryAudit('kuaishou_industry_voucher_reverse', req.body, data),
},
),
)
@@ -136,7 +141,8 @@ router.post(
successMessage: '电子凭证已手动核销',
errorMessage: '手动核销电子凭证失败',
scope: '[admin/kuaishou-industry/vouchers/consume]',
audit: (req, data) => buildKuaishouIndustryAudit('kuaishou_industry_voucher_consume', req.body, data),
audit: (req, data) =>
buildKuaishouIndustryAudit('kuaishou_industry_voucher_consume', req.body, data),
},
),
)
@@ -153,7 +159,8 @@ router.post(
successMessage: '电子凭证发码回调已重发',
errorMessage: '重发电子凭证发码回调失败',
scope: '[admin/kuaishou-industry/vouchers/resend-code]',
audit: (req, data) => buildKuaishouIndustryAudit('kuaishou_industry_voucher_resend_code', req.body, data),
audit: (req, data) =>
buildKuaishouIndustryAudit('kuaishou_industry_voucher_resend_code', req.body, data),
},
),
)
@@ -170,14 +177,15 @@ router.post(
successMessage: '电子凭证已手动销毁',
errorMessage: '手动销毁电子凭证失败',
scope: '[admin/kuaishou-industry/vouchers/destroy]',
audit: (req, data) => buildKuaishouIndustryAudit('kuaishou_industry_voucher_destroy', req.body, data),
audit: (req, data) =>
buildKuaishouIndustryAudit('kuaishou_industry_voucher_destroy', req.body, data),
},
),
)
function buildKuaishouIndustryAudit(action: string, body: unknown, data: unknown) {
const payload = body && typeof body === 'object' ? body as Record<string, unknown> : {}
const result = data && typeof data === 'object' ? data as Record<string, unknown> : {}
const payload = body && typeof body === 'object' ? (body as Record<string, unknown>) : {}
const result = data && typeof data === 'object' ? (data as Record<string, unknown>) : {}
return {
action,
+22 -22
View File
@@ -3,34 +3,34 @@ import { Router } from 'express'
import { getAdminOrderDetail, getAdminOrders } from '../../services/admin/admin-read-service.js'
import { resendAdminOrderKuaishouIndustryVoucherCodes } from '../../services/admin/write/kuaishou-cloud-actions.js'
import { createJsonHandler, requireAdminRoles } from './session.js'
import type {
AdminOrderRouteParams,
AdminOrderRouteQuery,
} from '../../types/admin/route-inputs.js'
import type { AdminOrderRouteParams, AdminOrderRouteQuery } from '../../types/admin/route-inputs.js'
const router = Router()
router.get('/orders', createJsonHandler(
(req) => getAdminOrders(req.query as AdminOrderRouteQuery),
{
router.get(
'/orders',
createJsonHandler((req) => getAdminOrders(req.query as AdminOrderRouteQuery), {
successMessage: 'ok',
errorMessage: '读取订单列表失败',
scope: '[admin/orders]',
},
))
}),
)
router.get('/orders/:orderId', createJsonHandler(
(req) =>
getAdminOrderDetail(
String((req.params as AdminOrderRouteParams).orderId || ''),
req.adminSession || null,
),
{
successMessage: 'ok',
errorMessage: '读取订单详情失败',
scope: '[admin/orders/:orderId]',
},
))
router.get(
'/orders/:orderId',
createJsonHandler(
(req) =>
getAdminOrderDetail(
String((req.params as AdminOrderRouteParams).orderId || ''),
req.adminSession || null,
),
{
successMessage: 'ok',
errorMessage: '读取订单详情失败',
scope: '[admin/orders/:orderId]',
},
),
)
router.post(
'/orders/:orderId/kuaishou-industry/resend-code',
@@ -49,7 +49,7 @@ router.post(
action: 'order_kuaishou_industry_resend_code',
targetType: 'order',
targetId: String((req.params as AdminOrderRouteParams).orderId || ''),
data: data && typeof data === 'object' ? data as Record<string, unknown> : {},
data: data && typeof data === 'object' ? (data as Record<string, unknown>) : {},
}),
},
),
@@ -1,23 +1,23 @@
import { Router } from "express";
import { Router } from 'express'
import { requireAdminRoles } from "./session.js";
import cloudtentaclesRouter from "./platform-config/cloudtentacles.js";
import kuaishouIndustryRouter from "./platform-config/kuaishou-industry.js";
import kuaishouFeifeiRouter from "./platform-config/kuaishou-feifei.js";
import ninetyoneRouter from "./platform-config/ninetyone.js";
import notificationsRouter from "./platform-config/notifications.js";
import fulfillmentRoutingRouter from "./platform-config/fulfillment-routing.js";
import affiliateDashRouter from "./platform-config/affiliate-dash.js";
import { requireAdminRoles } from './session.js'
import cloudtentaclesRouter from './platform-config/cloudtentacles.js'
import kuaishouIndustryRouter from './platform-config/kuaishou-industry.js'
import kuaishouFeifeiRouter from './platform-config/kuaishou-feifei.js'
import ninetyoneRouter from './platform-config/ninetyone.js'
import notificationsRouter from './platform-config/notifications.js'
import fulfillmentRoutingRouter from './platform-config/fulfillment-routing.js'
import affiliateDashRouter from './platform-config/affiliate-dash.js'
const router = Router();
const router = Router()
router.use("/platform-config", kuaishouIndustryRouter);
router.use("/platform-config", requireAdminRoles(["admin"]));
router.use("/platform-config", notificationsRouter);
router.use("/platform-config", kuaishouFeifeiRouter);
router.use("/platform-config", ninetyoneRouter);
router.use("/platform-config", fulfillmentRoutingRouter);
router.use("/platform-config", cloudtentaclesRouter);
router.use("/platform-config", affiliateDashRouter);
router.use('/platform-config', kuaishouIndustryRouter)
router.use('/platform-config', requireAdminRoles(['admin']))
router.use('/platform-config', notificationsRouter)
router.use('/platform-config', kuaishouFeifeiRouter)
router.use('/platform-config', ninetyoneRouter)
router.use('/platform-config', fulfillmentRoutingRouter)
router.use('/platform-config', cloudtentaclesRouter)
router.use('/platform-config', affiliateDashRouter)
export default router;
export default router
@@ -1,4 +1,4 @@
import { Router } from "express";
import { Router } from 'express'
import {
getAdminAffiliateDashConfig,
@@ -6,84 +6,77 @@ import {
listAdminAffiliateDashProducts,
matchAdminAffiliateDashSku,
updateAdminAffiliateDashConfig,
} from "../../../services/admin/platform-config/affiliate-dash-service.js";
import { createJsonHandler } from "../session.js";
import type { JsonRecord } from "../../../types/json.js";
} from '../../../services/admin/platform-config/affiliate-dash-service.js'
import { createJsonHandler } from '../session.js'
import type { JsonRecord } from '../../../types/json.js'
const router = Router();
const router = Router()
router.get(
"/affiliate-dash",
'/affiliate-dash',
createJsonHandler(() => getAdminAffiliateDashConfig(), {
successMessage: "ok",
errorMessage: "读取 affiliate-dash 配置失败",
scope: "[admin/platform-config/affiliate-dash]",
})
);
successMessage: 'ok',
errorMessage: '读取 affiliate-dash 配置失败',
scope: '[admin/platform-config/affiliate-dash]',
}),
)
router.post(
"/affiliate-dash",
createJsonHandler(
(req) => updateAdminAffiliateDashConfig(req.body as JsonRecord),
{
successMessage: "affiliate-dash 配置已保存",
errorMessage: "保存 affiliate-dash 配置失败",
scope: "[admin/platform-config/affiliate-dash]",
audit: (_req, data) => {
const result = data as JsonRecord;
const source = result.source as JsonRecord | undefined;
const effective = result.effective as JsonRecord | undefined;
'/affiliate-dash',
createJsonHandler((req) => updateAdminAffiliateDashConfig(req.body as JsonRecord), {
successMessage: 'affiliate-dash 配置已保存',
errorMessage: '保存 affiliate-dash 配置失败',
scope: '[admin/platform-config/affiliate-dash]',
audit: (_req, data) => {
const result = data as JsonRecord
const source = result.source as JsonRecord | undefined
const effective = result.effective as JsonRecord | undefined
return {
action: "platform_affiliate_dash_config_updated",
targetType: "platform_config",
targetId: "affiliate_dash",
data: {
enabled: effective?.enabled !== false,
hasAppKey: Boolean(effective?.hasAppKey),
hasAppSecret: Boolean(effective?.hasAppSecret),
hasCallbackSecret: Boolean(effective?.hasCallbackSecret),
skuMappingCount: Number(source?.skuMapping && typeof source.skuMapping === "object"
return {
action: 'platform_affiliate_dash_config_updated',
targetType: 'platform_config',
targetId: 'affiliate_dash',
data: {
enabled: effective?.enabled !== false,
hasAppKey: Boolean(effective?.hasAppKey),
hasAppSecret: Boolean(effective?.hasAppSecret),
hasCallbackSecret: Boolean(effective?.hasCallbackSecret),
skuMappingCount: Number(
source?.skuMapping && typeof source.skuMapping === 'object'
? Object.keys(source.skuMapping).length
: 0),
},
};
},
}
)
);
: 0,
),
},
}
},
}),
)
router.post(
"/affiliate-dash/match",
createJsonHandler(
(req) => matchAdminAffiliateDashSku(req.body as JsonRecord),
{
successMessage: "ok",
errorMessage: "匹配 affiliate-dash sku 失败",
scope: "[admin/platform-config/affiliate-dash/match]",
}
)
);
'/affiliate-dash/match',
createJsonHandler((req) => matchAdminAffiliateDashSku(req.body as JsonRecord), {
successMessage: 'ok',
errorMessage: '匹配 affiliate-dash sku 失败',
scope: '[admin/platform-config/affiliate-dash/match]',
}),
)
router.post(
"/affiliate-dash/products",
createJsonHandler(
(req) => listAdminAffiliateDashProducts(req.body as JsonRecord),
{
successMessage: "ok",
errorMessage: "查询 affiliate-dash 商品失败",
scope: "[admin/platform-config/affiliate-dash/products]",
}
)
);
'/affiliate-dash/products',
createJsonHandler((req) => listAdminAffiliateDashProducts(req.body as JsonRecord), {
successMessage: 'ok',
errorMessage: '查询 affiliate-dash 商品失败',
scope: '[admin/platform-config/affiliate-dash/products]',
}),
)
router.post(
"/affiliate-dash/wallet",
'/affiliate-dash/wallet',
createJsonHandler(() => getAdminAffiliateDashWallet(), {
successMessage: "ok",
errorMessage: "查询 affiliate-dash 钱包失败",
scope: "[admin/platform-config/affiliate-dash/wallet]",
})
);
successMessage: 'ok',
errorMessage: '查询 affiliate-dash 钱包失败',
scope: '[admin/platform-config/affiliate-dash/wallet]',
}),
)
export default router;
export default router
@@ -1,4 +1,4 @@
import { Router } from "express";
import { Router } from 'express'
import {
appointAdminCloudtentaclesVirtualNumber,
@@ -23,7 +23,7 @@ import {
useAdminCloudtentaclesSku,
validateAdminCloudtentaclesSession,
verifyAdminCloudtentaclesLoginCode,
} from "../../../services/admin/platform-config/cloudtentacles/index.js";
} from '../../../services/admin/platform-config/cloudtentacles/index.js'
import type {
AdminCloudtentaclesCatalogQueryRouteBody,
AdminCloudtentaclesFullFlowRouteBody,
@@ -35,398 +35,353 @@ import type {
AdminCloudtentaclesTestLoginRouteBody,
AdminCloudtentaclesValidateSessionRouteBody,
AdminCloudtentaclesVirtualNumberRouteBody,
} from "../../../types/admin/route-inputs.js";
import { createJsonHandler } from "../session.js";
import type { JsonRecord } from "../../../types/json.js";
} from '../../../types/admin/route-inputs.js'
import { createJsonHandler } from '../session.js'
import type { JsonRecord } from '../../../types/json.js'
const router = Router();
const router = Router()
router.get(
"/cloudtentacles-source",
'/cloudtentacles-source',
createJsonHandler(() => listAdminCloudtentaclesSources(), {
successMessage: "ok",
errorMessage: "读取 cloudtentacles 履约平台配置失败",
scope: "[admin/platform-config/cloudtentacles-source]",
})
);
successMessage: 'ok',
errorMessage: '读取 cloudtentacles 履约平台配置失败',
scope: '[admin/platform-config/cloudtentacles-source]',
}),
)
router.delete(
"/cloudtentacles-source/:sourceKey",
'/cloudtentacles-source/:sourceKey',
createJsonHandler(
(req) =>
deleteAdminCloudtentaclesSource(
String(req.params.sourceKey || "").trim()
),
(req) => deleteAdminCloudtentaclesSource(String(req.params.sourceKey || '').trim()),
{
successMessage: "cloudtentacles 履约平台配置已删除",
errorMessage: "删除 cloudtentacles 履约平台配置失败",
scope: "[admin/platform-config/cloudtentacles-source/:sourceKey]",
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(),
action: 'platform_cloudtentacles_source_deleted',
targetType: 'platform_config',
targetId: String(req.params.sourceKey || '').trim(),
data: {},
}),
}
)
);
},
),
)
router.post(
"/cloudtentacles-source",
'/cloudtentacles-source',
createJsonHandler(
(req) =>
updateAdminCloudtentaclesSourceConfig(
req.body as AdminCloudtentaclesSourceConfigRouteBody
),
updateAdminCloudtentaclesSourceConfig(req.body as AdminCloudtentaclesSourceConfigRouteBody),
{
successMessage: "cloudtentacles 履约平台配置已保存",
errorMessage: "保存 cloudtentacles 履约平台配置失败",
scope: "[admin/platform-config/cloudtentacles-source]",
successMessage: 'cloudtentacles 履约平台配置已保存',
errorMessage: '保存 cloudtentacles 履约平台配置失败',
scope: '[admin/platform-config/cloudtentacles-source]',
audit: (_req, data) => {
const result = data as JsonRecord;
const result = data as JsonRecord
return {
action: "platform_cloudtentacles_source_updated",
targetType: "platform_config",
targetId: "cloudtentacles_source",
action: 'platform_cloudtentacles_source_updated',
targetType: 'platform_config',
targetId: 'cloudtentacles_source',
data: {
filePath: String(result.filePath || "").trim(),
username: String(result.source?.username || "").trim(),
filePath: String(result.filePath || '').trim(),
username: String(result.source?.username || '').trim(),
enabled: Boolean(result.source?.enabled),
},
};
}
},
}
)
);
},
),
)
router.get(
"/cloudtentacles/override-rules",
'/cloudtentacles/override-rules',
createJsonHandler(() => getAdminCloudtentaclesOverrideRules(), {
successMessage: "ok",
errorMessage: "读取 cloudtentacles 覆盖规则失败",
scope: "[admin/platform-config/cloudtentacles/override-rules]",
})
);
successMessage: 'ok',
errorMessage: '读取 cloudtentacles 覆盖规则失败',
scope: '[admin/platform-config/cloudtentacles/override-rules]',
}),
)
router.post(
"/cloudtentacles/override-rules",
'/cloudtentacles/override-rules',
createJsonHandler(
(req) =>
updateAdminCloudtentaclesOverrideRules(
req.body as AdminCloudtentaclesOverrideRuleConfigRouteBody
req.body as AdminCloudtentaclesOverrideRuleConfigRouteBody,
),
{
successMessage: "cloudtentacles 覆盖规则已保存",
errorMessage: "保存 cloudtentacles 覆盖规则失败",
scope: "[admin/platform-config/cloudtentacles/override-rules]",
successMessage: 'cloudtentacles 覆盖规则已保存',
errorMessage: '保存 cloudtentacles 覆盖规则失败',
scope: '[admin/platform-config/cloudtentacles/override-rules]',
audit: (_req, data) => {
const result = data as JsonRecord;
const result = data as JsonRecord
return {
action: "platform_cloudtentacles_override_rules_updated",
targetType: "platform_config",
targetId: "cloudtentacles_override_rules",
action: 'platform_cloudtentacles_override_rules_updated',
targetType: 'platform_config',
targetId: 'cloudtentacles_override_rules',
data: {
filePath: String(result.filePath || "").trim(),
filePath: String(result.filePath || '').trim(),
enabled: result.enabled !== false,
ruleCount: Array.isArray(result.rules) ? result.rules.length : 0,
},
};
}
},
}
)
);
},
),
)
router.post(
"/cloudtentacles/send-sms-code",
'/cloudtentacles/send-sms-code',
createJsonHandler(
(req) =>
sendAdminCloudtentaclesSmsCode(
req.body as AdminCloudtentaclesSendSmsCodeRouteBody
),
(req) => sendAdminCloudtentaclesSmsCode(req.body as AdminCloudtentaclesSendSmsCodeRouteBody),
{
successMessage: "cloudtentacles 短信验证码已发送",
errorMessage: "cloudtentacles 发送短信验证码失败",
scope: "[admin/platform-config/cloudtentacles/send-sms-code]",
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;
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",
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 || "",
baseUrl: result.baseUrl || String(body.baseUrl || '').trim(),
phoneMasked: result.phoneMasked || '',
},
};
}
},
}
)
);
},
),
)
router.post(
"/cloudtentacles/test-login",
'/cloudtentacles/test-login',
createJsonHandler(
(req) =>
testAdminCloudtentaclesLogin(
req.body as AdminCloudtentaclesTestLoginRouteBody
),
(req) => testAdminCloudtentaclesLogin(req.body as AdminCloudtentaclesTestLoginRouteBody),
{
successMessage: "cloudtentacles 登录测试成功",
errorMessage: "cloudtentacles 登录测试失败",
scope: "[admin/platform-config/cloudtentacles/test-login]",
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;
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",
action: 'platform_cloudtentacles_test_login',
targetType: 'platform_config',
targetId: String(result.username || body.username || '').trim() || 'cloudtentacles',
data: {
baseUrl: result.baseUrl || String(body.baseUrl || "").trim(),
baseUrl: result.baseUrl || String(body.baseUrl || '').trim(),
permissionCount: Number(result.session?.permissionCount || 0),
},
};
}
},
}
)
);
},
),
)
router.post(
"/cloudtentacles/validate-session",
'/cloudtentacles/validate-session',
createJsonHandler(
(req) =>
validateAdminCloudtentaclesSession(
req.body as AdminCloudtentaclesValidateSessionRouteBody
),
validateAdminCloudtentaclesSession(req.body as AdminCloudtentaclesValidateSessionRouteBody),
{
successMessage: "cloudtentacles 会话校验成功",
errorMessage: "cloudtentacles 会话校验失败",
scope: "[admin/platform-config/cloudtentacles/validate-session]",
successMessage: 'cloudtentacles 会话校验成功',
errorMessage: 'cloudtentacles 会话校验失败',
scope: '[admin/platform-config/cloudtentacles/validate-session]',
audit: (_req, data) => {
const result = data as JsonRecord;
const result = data as JsonRecord
return {
action: "platform_cloudtentacles_validate_session",
targetType: "platform_config",
targetId: "cloudtentacles_session",
action: 'platform_cloudtentacles_validate_session',
targetType: 'platform_config',
targetId: 'cloudtentacles_session',
data: {
baseUrl: String(result.baseUrl || "").trim(),
baseUrl: String(result.baseUrl || '').trim(),
permissionCount: Number(result.session?.permissionCount || 0),
},
};
}
},
}
)
);
},
),
)
router.post(
"/cloudtentacles/asset",
'/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) =>
getAdminCloudtentaclesAsset(
req.body as AdminCloudtentaclesCatalogQueryRouteBody
),
listAdminCloudtentaclesVirtualNumbers(req.body as AdminCloudtentaclesVirtualNumberRouteBody),
{
successMessage: "cloudtentacles 余额查询成功",
errorMessage: "cloudtentacles 余额查询失败",
scope: "[admin/platform-config/cloudtentacles/asset]",
}
)
);
successMessage: 'cloudtentacles 虚拟号列表查询成功',
errorMessage: 'cloudtentacles 虚拟号列表查询失败',
scope: '[admin/platform-config/cloudtentacles/vn/list]',
},
),
)
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",
'/cloudtentacles/vn/appoint',
createJsonHandler(
(req) =>
appointAdminCloudtentaclesVirtualNumber(
req.body as AdminCloudtentaclesVirtualNumberRouteBody
req.body as AdminCloudtentaclesVirtualNumberRouteBody,
),
{
successMessage: "cloudtentacles 虚拟号申请成功",
errorMessage: "cloudtentacles 虚拟号申请失败",
scope: "[admin/platform-config/cloudtentacles/vn/appoint]",
}
)
);
successMessage: 'cloudtentacles 虚拟号申请成功',
errorMessage: 'cloudtentacles 虚拟号申请失败',
scope: '[admin/platform-config/cloudtentacles/vn/appoint]',
},
),
)
router.post(
"/cloudtentacles/vn/generate-login-code",
'/cloudtentacles/vn/generate-login-code',
createJsonHandler(
(req) =>
generateAdminCloudtentaclesLoginCode(
req.body as AdminCloudtentaclesVirtualNumberRouteBody
),
generateAdminCloudtentaclesLoginCode(req.body as AdminCloudtentaclesVirtualNumberRouteBody),
{
successMessage: "cloudtentacles 登录码生成成功",
errorMessage: "cloudtentacles 登录码生成失败",
scope: "[admin/platform-config/cloudtentacles/vn/generate-login-code]",
}
)
);
successMessage: 'cloudtentacles 登录码生成成功',
errorMessage: 'cloudtentacles 登录码生成失败',
scope: '[admin/platform-config/cloudtentacles/vn/generate-login-code]',
},
),
)
router.post(
"/cloudtentacles/vn/fetch-code",
'/cloudtentacles/vn/fetch-code',
createJsonHandler(
(req) =>
fetchAdminCloudtentaclesVirtualNumberCode(
req.body as AdminCloudtentaclesVirtualNumberRouteBody
req.body as AdminCloudtentaclesVirtualNumberRouteBody,
),
{
successMessage: "cloudtentacles 验证码获取成功",
errorMessage: "cloudtentacles 验证码获取失败",
scope: "[admin/platform-config/cloudtentacles/vn/fetch-code]",
}
)
);
successMessage: 'cloudtentacles 验证码获取成功',
errorMessage: 'cloudtentacles 验证码获取失败',
scope: '[admin/platform-config/cloudtentacles/vn/fetch-code]',
},
),
)
router.post(
"/cloudtentacles/vn/verify-code",
'/cloudtentacles/vn/verify-code',
createJsonHandler(
(req) =>
verifyAdminCloudtentaclesLoginCode(
req.body as AdminCloudtentaclesVirtualNumberRouteBody
),
verifyAdminCloudtentaclesLoginCode(req.body as AdminCloudtentaclesVirtualNumberRouteBody),
{
successMessage: "cloudtentacles 登录码校验成功",
errorMessage: "cloudtentacles 登录码校验失败",
scope: "[admin/platform-config/cloudtentacles/vn/verify-code]",
}
)
);
successMessage: 'cloudtentacles 登录码校验成功',
errorMessage: 'cloudtentacles 登录码校验失败',
scope: '[admin/platform-config/cloudtentacles/vn/verify-code]',
},
),
)
router.post(
"/cloudtentacles/vn/bind-url",
'/cloudtentacles/vn/bind-url',
createJsonHandler(
(req) =>
getAdminCloudtentaclesBindUrl(
req.body as AdminCloudtentaclesVirtualNumberRouteBody
),
(req) => getAdminCloudtentaclesBindUrl(req.body as AdminCloudtentaclesVirtualNumberRouteBody),
{
successMessage: "cloudtentacles 兑换链接获取成功",
errorMessage: "cloudtentacles 兑换链接获取失败",
scope: "[admin/platform-config/cloudtentacles/vn/bind-url]",
}
)
);
successMessage: 'cloudtentacles 兑换链接获取成功',
errorMessage: 'cloudtentacles 兑换链接获取失败',
scope: '[admin/platform-config/cloudtentacles/vn/bind-url]',
},
),
)
router.post(
"/cloudtentacles/vn/back",
'/cloudtentacles/vn/back',
createJsonHandler(
(req) =>
backAdminCloudtentaclesVirtualNumber(
req.body as AdminCloudtentaclesVirtualNumberRouteBody
),
backAdminCloudtentaclesVirtualNumber(req.body as AdminCloudtentaclesVirtualNumberRouteBody),
{
successMessage: "cloudtentacles 号码退还成功",
errorMessage: "cloudtentacles 号码退还失败",
scope: "[admin/platform-config/cloudtentacles/vn/back]",
}
)
);
successMessage: 'cloudtentacles 号码退还成功',
errorMessage: 'cloudtentacles 号码退还失败',
scope: '[admin/platform-config/cloudtentacles/vn/back]',
},
),
)
router.post(
"/cloudtentacles/debug/full-flow",
'/cloudtentacles/debug/full-flow',
createJsonHandler(
(req) =>
runAdminCloudtentaclesFullFlow(
req.body as AdminCloudtentaclesFullFlowRouteBody
),
(req) => runAdminCloudtentaclesFullFlow(req.body as AdminCloudtentaclesFullFlowRouteBody),
{
successMessage: "cloudtentacles 完整调试流程执行成功",
errorMessage: "cloudtentacles 完整调试流程执行失败",
scope: "[admin/platform-config/cloudtentacles/debug/full-flow]",
}
)
);
successMessage: 'cloudtentacles 完整调试流程执行成功',
errorMessage: 'cloudtentacles 完整调试流程执行失败',
scope: '[admin/platform-config/cloudtentacles/debug/full-flow]',
},
),
)
export default router;
export default router
@@ -21,39 +21,33 @@ router.get(
router.post(
'/fulfillment-routing',
createJsonHandler(
(req) => updateAdminFulfillmentRoutingConfig(req.body as JsonRecord),
{
successMessage: '履约路由配置已保存',
errorMessage: '保存履约路由配置失败',
scope: '[admin/platform-config/fulfillment-routing]',
audit: (_req, data) => {
const result = data as JsonRecord
return {
action: 'platform_fulfillment_routing_updated',
targetType: 'platform_config',
targetId: 'fulfillment_routing',
data: {
filePath: String(result.filePath || '').trim(),
enabled: result.enabled !== false,
ruleCount: Array.isArray(result.rules) ? result.rules.length : 0,
},
}
},
createJsonHandler((req) => updateAdminFulfillmentRoutingConfig(req.body as JsonRecord), {
successMessage: '履约路由配置已保存',
errorMessage: '保存履约路由配置失败',
scope: '[admin/platform-config/fulfillment-routing]',
audit: (_req, data) => {
const result = data as JsonRecord
return {
action: 'platform_fulfillment_routing_updated',
targetType: 'platform_config',
targetId: 'fulfillment_routing',
data: {
filePath: String(result.filePath || '').trim(),
enabled: result.enabled !== false,
ruleCount: Array.isArray(result.rules) ? result.rules.length : 0,
},
}
},
),
}),
)
router.post(
'/fulfillment-routing/preview',
createJsonHandler(
(req) => previewAdminFulfillmentRouting(req.body as JsonRecord),
{
successMessage: 'ok',
errorMessage: '预览履约路由失败',
scope: '[admin/platform-config/fulfillment-routing/preview]',
},
),
createJsonHandler((req) => previewAdminFulfillmentRouting(req.body as JsonRecord), {
successMessage: 'ok',
errorMessage: '预览履约路由失败',
scope: '[admin/platform-config/fulfillment-routing/preview]',
}),
)
export default router
@@ -1,4 +1,4 @@
import { Router } from "express";
import { Router } from 'express'
import {
createAdminKuaishouFeifeiTestOrder,
@@ -8,137 +8,117 @@ import {
queryAdminKuaishouFeifeiTestOrder,
syncAdminKuaishouFeifeiProductRules,
updateAdminKuaishouFeifeiConfig,
} from "../../../services/admin/platform-config/kuaishou-feifei-service.js";
import { createJsonHandler } from "../session.js";
import type { JsonRecord } from "../../../types/json.js";
} from '../../../services/admin/platform-config/kuaishou-feifei-service.js'
import { createJsonHandler } from '../session.js'
import type { JsonRecord } from '../../../types/json.js'
const router = Router();
const router = Router()
router.get(
"/kuaishou-feifei",
'/kuaishou-feifei',
createJsonHandler(() => getAdminKuaishouFeifeiConfig(), {
successMessage: "ok",
errorMessage: "读取 kuaishou-feifei 配置失败",
scope: "[admin/platform-config/kuaishou-feifei]",
})
);
successMessage: 'ok',
errorMessage: '读取 kuaishou-feifei 配置失败',
scope: '[admin/platform-config/kuaishou-feifei]',
}),
)
router.post(
"/kuaishou-feifei",
createJsonHandler(
(req) => updateAdminKuaishouFeifeiConfig(req.body as JsonRecord),
{
successMessage: "kuaishou-feifei 配置已保存",
errorMessage: "保存 kuaishou-feifei 配置失败",
scope: "[admin/platform-config/kuaishou-feifei]",
audit: (_req, data) => {
const result = data as JsonRecord;
const source = result.source as JsonRecord | undefined;
return {
action: "platform_kuaishou_feifei_config_updated",
targetType: "platform_config",
targetId: "kuaishou_feifei",
data: {
filePath: String(result.filePath || "").trim(),
enabled: source?.enabled !== false,
ruleCount: Array.isArray(source?.productRules)
? source.productRules.length
: 0,
},
};
},
}
)
);
'/kuaishou-feifei',
createJsonHandler((req) => updateAdminKuaishouFeifeiConfig(req.body as JsonRecord), {
successMessage: 'kuaishou-feifei 配置已保存',
errorMessage: '保存 kuaishou-feifei 配置失败',
scope: '[admin/platform-config/kuaishou-feifei]',
audit: (_req, data) => {
const result = data as JsonRecord
const source = result.source as JsonRecord | undefined
return {
action: 'platform_kuaishou_feifei_config_updated',
targetType: 'platform_config',
targetId: 'kuaishou_feifei',
data: {
filePath: String(result.filePath || '').trim(),
enabled: source?.enabled !== false,
ruleCount: Array.isArray(source?.productRules) ? source.productRules.length : 0,
},
}
},
}),
)
router.post(
"/kuaishou-feifei/match",
createJsonHandler(
(req) => matchAdminKuaishouFeifeiProduct(req.body as JsonRecord),
{
successMessage: "ok",
errorMessage: "匹配 kuaishou-feifei 商品失败",
scope: "[admin/platform-config/kuaishou-feifei/match]",
}
)
);
'/kuaishou-feifei/match',
createJsonHandler((req) => matchAdminKuaishouFeifeiProduct(req.body as JsonRecord), {
successMessage: 'ok',
errorMessage: '匹配 kuaishou-feifei 商品失败',
scope: '[admin/platform-config/kuaishou-feifei/match]',
}),
)
router.post(
"/kuaishou-feifei/products",
createJsonHandler(
(req) => listAdminKuaishouFeifeiProducts(req.body as JsonRecord),
{
successMessage: "ok",
errorMessage: "查询 kuaishou-feifei 商品失败",
scope: "[admin/platform-config/kuaishou-feifei/products]",
}
)
);
'/kuaishou-feifei/products',
createJsonHandler((req) => listAdminKuaishouFeifeiProducts(req.body as JsonRecord), {
successMessage: 'ok',
errorMessage: '查询 kuaishou-feifei 商品失败',
scope: '[admin/platform-config/kuaishou-feifei/products]',
}),
)
router.post(
"/kuaishou-feifei/sync-products",
createJsonHandler(
(req) => syncAdminKuaishouFeifeiProductRules(req.body as JsonRecord),
{
successMessage: "kuaishou-feifei 商品映射已同步",
errorMessage: "同步 kuaishou-feifei 商品映射失败",
scope: "[admin/platform-config/kuaishou-feifei/sync-products]",
audit: (_req, data) => {
const result = data as JsonRecord;
const sync = result.sync as JsonRecord | undefined;
'/kuaishou-feifei/sync-products',
createJsonHandler((req) => syncAdminKuaishouFeifeiProductRules(req.body as JsonRecord), {
successMessage: 'kuaishou-feifei 商品映射已同步',
errorMessage: '同步 kuaishou-feifei 商品映射失败',
scope: '[admin/platform-config/kuaishou-feifei/sync-products]',
audit: (_req, data) => {
const result = data as JsonRecord
const sync = result.sync as JsonRecord | undefined
return {
action: "platform_kuaishou_feifei_products_synced",
targetType: "platform_config",
targetId: "kuaishou_feifei",
data: {
productCount: Number(sync?.productCount || 0) || 0,
ruleCount: Number(sync?.ruleCount || 0) || 0,
status: String(sync?.status || "").trim(),
},
};
},
}
)
);
return {
action: 'platform_kuaishou_feifei_products_synced',
targetType: 'platform_config',
targetId: 'kuaishou_feifei',
data: {
productCount: Number(sync?.productCount || 0) || 0,
ruleCount: Number(sync?.ruleCount || 0) || 0,
status: String(sync?.status || '').trim(),
},
}
},
}),
)
router.post(
"/kuaishou-feifei/test-order",
createJsonHandler(
(req) => createAdminKuaishouFeifeiTestOrder(req.body as JsonRecord),
{
successMessage: "kuaishou-feifei 测试单已创建",
errorMessage: "创建 kuaishou-feifei 测试单失败",
scope: "[admin/platform-config/kuaishou-feifei/test-order]",
audit: (_req, data) => {
const result = data as JsonRecord;
'/kuaishou-feifei/test-order',
createJsonHandler((req) => createAdminKuaishouFeifeiTestOrder(req.body as JsonRecord), {
successMessage: 'kuaishou-feifei 测试单已创建',
errorMessage: '创建 kuaishou-feifei 测试单失败',
scope: '[admin/platform-config/kuaishou-feifei/test-order]',
audit: (_req, data) => {
const result = data as JsonRecord
return {
action: "platform_kuaishou_feifei_test_order_created",
targetType: "platform_config",
targetId: "kuaishou_feifei",
data: {
orderNo: String(result.orderNo || "").trim(),
platformOrderNo: String(result.platformOrderNo || "").trim(),
productCode: String(result.productCode || "").trim(),
rechargeStatus: Number(result.rechargeStatus || 0) || 0,
},
};
},
}
)
);
return {
action: 'platform_kuaishou_feifei_test_order_created',
targetType: 'platform_config',
targetId: 'kuaishou_feifei',
data: {
orderNo: String(result.orderNo || '').trim(),
platformOrderNo: String(result.platformOrderNo || '').trim(),
productCode: String(result.productCode || '').trim(),
rechargeStatus: Number(result.rechargeStatus || 0) || 0,
},
}
},
}),
)
router.post(
"/kuaishou-feifei/query-order",
createJsonHandler(
(req) => queryAdminKuaishouFeifeiTestOrder(req.body as JsonRecord),
{
successMessage: "ok",
errorMessage: "查询 kuaishou-feifei 订单失败",
scope: "[admin/platform-config/kuaishou-feifei/query-order]",
}
)
);
'/kuaishou-feifei/query-order',
createJsonHandler((req) => queryAdminKuaishouFeifeiTestOrder(req.body as JsonRecord), {
successMessage: 'ok',
errorMessage: '查询 kuaishou-feifei 订单失败',
scope: '[admin/platform-config/kuaishou-feifei/query-order]',
}),
)
export default router;
export default router
@@ -1,77 +1,75 @@
import { Router } from "express";
import { Router } from 'express'
import {
failAdminNinetyoneOrder,
getAdminNinetyoneOrders,
retryAdminNinetyoneOrder,
} from "../../../services/admin/platform-config/ninetyone-service.js";
import type { AdminEntityRouteParams } from "../../../types/admin/route-inputs.js";
import { createJsonHandler } from "../session.js";
import type { JsonRecord } from "../../../types/json.js";
} from '../../../services/admin/platform-config/ninetyone-service.js'
import type { AdminEntityRouteParams } from '../../../types/admin/route-inputs.js'
import { createJsonHandler } from '../session.js'
import type { JsonRecord } from '../../../types/json.js'
const router = Router();
const router = Router()
router.get(
"/ninetyone/orders",
'/ninetyone/orders',
createJsonHandler((req) => getAdminNinetyoneOrders(req.query), {
successMessage: "ok",
errorMessage: "读取 91卡券订单失败",
scope: "[admin/platform-config/ninetyone/orders]",
})
);
successMessage: 'ok',
errorMessage: '读取 91卡券订单失败',
scope: '[admin/platform-config/ninetyone/orders]',
}),
)
router.post(
"/ninetyone/orders/:id/retry",
'/ninetyone/orders/:id/retry',
createJsonHandler(
(req) => retryAdminNinetyoneOrder(String((req.params as AdminEntityRouteParams).id || "")),
(req) => retryAdminNinetyoneOrder(String((req.params as AdminEntityRouteParams).id || '')),
{
successMessage: "91卡券订单已重试",
errorMessage: "重试 91卡券订单失败",
scope: "[admin/platform-config/ninetyone/orders/:id/retry]",
successMessage: '91卡券订单已重试',
errorMessage: '重试 91卡券订单失败',
scope: '[admin/platform-config/ninetyone/orders/:id/retry]',
audit: (_req, data) => {
const result = data as JsonRecord;
const result = data as JsonRecord
return {
action: "platform_ninetyone_order_retried",
targetType: "order",
targetId: String(result.orderId || "").trim(),
action: 'platform_ninetyone_order_retried',
targetType: 'order',
targetId: String(result.orderId || '').trim(),
data: {
orderNo: String(result.orderNo || "").trim(),
orderNo: String(result.orderNo || '').trim(),
taskCount: Number(result.taskCount || 0),
},
};
}
},
}
)
);
},
),
)
router.post(
"/ninetyone/orders/:id/fail",
'/ninetyone/orders/:id/fail',
createJsonHandler(
(req) =>
failAdminNinetyoneOrder(
String((req.params as AdminEntityRouteParams).id || ""),
req.body as { reason?: string }
String((req.params as AdminEntityRouteParams).id || ''),
req.body as { reason?: string },
),
{
successMessage: "91卡券订单已标记失败",
errorMessage: "标记 91卡券订单失败",
scope: "[admin/platform-config/ninetyone/orders/:id/fail]",
successMessage: '91卡券订单已标记失败',
errorMessage: '标记 91卡券订单失败',
scope: '[admin/platform-config/ninetyone/orders/:id/fail]',
audit: (req, data) => {
const result = data as JsonRecord;
const result = data as JsonRecord
return {
action: "platform_ninetyone_order_failed",
targetType: "order",
targetId: String(result.orderId || "").trim(),
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(),
orderNo: String(result.orderNo || '').trim(),
reason: String((req.body as { reason?: string }).reason || '').trim(),
},
};
}
},
}
)
);
},
),
)
export default router;
export default router
@@ -1,4 +1,4 @@
import { Router } from "express";
import { Router } from 'express'
import {
getAdminNotificationConfig,
@@ -7,152 +7,132 @@ import {
testAdminNotification,
updateAdminNotificationConfig,
updateAdminScheduledJobsConfig,
} from "../../../services/admin/platform-config/notification-service.js";
} from '../../../services/admin/platform-config/notification-service.js'
import type {
AdminEntityRouteParams,
AdminNotificationConfigRouteBody,
AdminNotificationTestRouteBody,
AdminScheduledJobsConfigRouteBody,
} from "../../../types/admin/route-inputs.js";
import { createJsonHandler } from "../session.js";
import type { JsonRecord } from "../../../types/json.js";
} from '../../../types/admin/route-inputs.js'
import { createJsonHandler } from '../session.js'
import type { JsonRecord } from '../../../types/json.js'
const router = Router();
const router = Router()
router.get(
"/notifications",
'/notifications',
createJsonHandler(() => getAdminNotificationConfig(), {
successMessage: "ok",
errorMessage: "读取内部通知配置失败",
scope: "[admin/platform-config/notifications]",
})
);
successMessage: 'ok',
errorMessage: '读取内部通知配置失败',
scope: '[admin/platform-config/notifications]',
}),
)
router.post(
"/notifications",
'/notifications',
createJsonHandler(
(req) =>
updateAdminNotificationConfig(
req.body as AdminNotificationConfigRouteBody
),
(req) => updateAdminNotificationConfig(req.body as AdminNotificationConfigRouteBody),
{
successMessage: "内部通知配置已保存",
errorMessage: "保存内部通知配置失败",
scope: "[admin/platform-config/notifications]",
successMessage: '内部通知配置已保存',
errorMessage: '保存内部通知配置失败',
scope: '[admin/platform-config/notifications]',
audit: (_req, data) => {
const result = data as JsonRecord;
const result = data as JsonRecord
return {
action: "platform_notification_config_updated",
targetType: "platform_config",
targetId: "notifications",
action: 'platform_notification_config_updated',
targetType: 'platform_config',
targetId: 'notifications',
data: {
filePath: String(result.filePath || "").trim(),
filePath: String(result.filePath || '').trim(),
enabled: Boolean(result.source?.enabled),
barkRecipientCount: Array.isArray(
result.source?.channels?.bark?.recipients
)
barkRecipientCount: Array.isArray(result.source?.channels?.bark?.recipients)
? result.source.channels.bark.recipients.length
: 0,
wpushRecipientCount: Array.isArray(
result.source?.channels?.wpush?.recipients
)
wpushRecipientCount: Array.isArray(result.source?.channels?.wpush?.recipients)
? result.source.channels.wpush.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),
},
};
},
}
)
);
'/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",
'/scheduled-jobs',
createJsonHandler(() => getAdminScheduledJobsConfig(), {
successMessage: "ok",
errorMessage: "读取定时任务配置失败",
scope: "[admin/platform-config/scheduled-jobs]",
})
);
successMessage: 'ok',
errorMessage: '读取定时任务配置失败',
scope: '[admin/platform-config/scheduled-jobs]',
}),
)
router.post(
"/scheduled-jobs",
'/scheduled-jobs',
createJsonHandler(
(req) =>
updateAdminScheduledJobsConfig(
req.body as AdminScheduledJobsConfigRouteBody
),
(req) => updateAdminScheduledJobsConfig(req.body as AdminScheduledJobsConfigRouteBody),
{
successMessage: "定时任务配置已保存",
errorMessage: "保存定时任务配置失败",
scope: "[admin/platform-config/scheduled-jobs]",
successMessage: '定时任务配置已保存',
errorMessage: '保存定时任务配置失败',
scope: '[admin/platform-config/scheduled-jobs]',
audit: (_req, data) => {
const result = data as JsonRecord;
const result = data as JsonRecord
return {
action: "platform_scheduled_jobs_updated",
targetType: "platform_config",
targetId: "scheduled_jobs",
action: 'platform_scheduled_jobs_updated',
targetType: 'platform_config',
targetId: 'scheduled_jobs',
data: {
filePath: String(result.filePath || "").trim(),
filePath: String(result.filePath || '').trim(),
enabled: Boolean(result.source?.enabled),
jobCount: Array.isArray(result.source?.jobs)
? result.source.jobs.length
: 0,
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(),
},
};
},
}
)
);
'/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;
export default router
+9 -2
View File
@@ -1,5 +1,8 @@
import type { Request, Response, NextFunction } from 'express'
import { requireAdminRole, verifyAdminSessionToken } from '../../services/admin/admin-auth-service.js'
import {
requireAdminRole,
verifyAdminSessionToken,
} from '../../services/admin/admin-auth-service.js'
import { writeAdminAuditLog } from '../../services/admin/admin-audit-service.js'
import { buildSuccessPayload, createHttpError, sendRouteError } from '../../utils/http.js'
import { logWarn } from '../../utils/logger.js'
@@ -53,7 +56,11 @@ export function createFileHandler(
}
}
export async function requireAdminSession(req: Request, res: Response, next: NextFunction): Promise<void> {
export async function requireAdminSession(
req: Request,
res: Response,
next: NextFunction,
): Promise<void> {
try {
req.adminSession = await verifyAdminSessionToken(extractBearerToken(req))
next()
+2 -7
View File
@@ -105,11 +105,7 @@ router.post(
'/tasks/:taskId/kuaishou-cloud/dispatch',
requireAdminRoles(['admin', 'operator']),
createJsonHandler(
(req) =>
dispatchAdminTaskKuaishouCloudFulfillment(
getTaskId(req),
req.adminSession || null,
),
(req) => dispatchAdminTaskKuaishouCloudFulfillment(getTaskId(req), req.adminSession || null),
{
successMessage: '已完成绑定确认并发货',
errorMessage: '执行发货失败',
@@ -152,8 +148,7 @@ router.post(
'/tasks/:taskId/kuaishou-industry/resend-code',
requireAdminRoles(['admin', 'operator', 'support']),
createJsonHandler(
(req) =>
resendAdminTaskKuaishouIndustryVoucherCode(getTaskId(req), req.adminSession || null),
(req) => resendAdminTaskKuaishouIndustryVoucherCode(getTaskId(req), req.adminSession || null),
{
successMessage: '电子凭证发码回调已重发',
errorMessage: '重发电子凭证发码回调失败',
+85 -66
View File
@@ -22,18 +22,18 @@ const router = Router()
router.use('/users', requireAdminRoles(['admin']))
router.get('/users', createJsonHandler(
(req) => getAdminUserList(req.query),
{
router.get(
'/users',
createJsonHandler((req) => getAdminUserList(req.query), {
successMessage: 'ok',
errorMessage: '读取后台用户列表失败',
scope: '[admin/users]',
},
))
}),
)
router.post('/users', createJsonHandler(
(req) => createManagedAdminUser(req.body),
{
router.post(
'/users',
createJsonHandler((req) => createManagedAdminUser(req.body), {
successMessage: '后台用户已创建',
errorMessage: '创建后台用户失败',
scope: '[admin/users:create]',
@@ -50,69 +50,88 @@ router.post('/users', createJsonHandler(
},
}
},
},
))
}),
)
router.post('/users/:userId/role', createJsonHandler(
(req) => updateManagedAdminUserRole(String(req.params.userId || ''), req.body, getRequiredAdminSession(req)),
{
successMessage: '用户角色已更新',
errorMessage: '更新用户角色失败',
scope: '[admin/users/:userId/role]',
audit: (_req, data) => {
const result = data as AdminUserMutationResult
return {
action: 'admin_user_role_updated',
targetType: 'admin_user',
targetId: String(result.user.userId),
data: {
username: result.user.username,
role: result.user.role,
},
}
router.post(
'/users/:userId/role',
createJsonHandler(
(req) =>
updateManagedAdminUserRole(
String(req.params.userId || ''),
req.body,
getRequiredAdminSession(req),
),
{
successMessage: '用户角色已更新',
errorMessage: '更新用户角色失败',
scope: '[admin/users/:userId/role]',
audit: (_req, data) => {
const result = data as AdminUserMutationResult
return {
action: 'admin_user_role_updated',
targetType: 'admin_user',
targetId: String(result.user.userId),
data: {
username: result.user.username,
role: result.user.role,
},
}
},
},
},
))
),
)
router.post('/users/:userId/status', createJsonHandler(
(req) => updateManagedAdminUserStatus(String(req.params.userId || ''), req.body, getRequiredAdminSession(req)),
{
successMessage: '用户状态已更新',
errorMessage: '更新用户状态失败',
scope: '[admin/users/:userId/status]',
audit: (_req, data) => {
const result = data as AdminUserMutationResult
return {
action: 'admin_user_status_updated',
targetType: 'admin_user',
targetId: String(result.user.userId),
data: {
username: result.user.username,
status: result.user.status,
},
}
router.post(
'/users/:userId/status',
createJsonHandler(
(req) =>
updateManagedAdminUserStatus(
String(req.params.userId || ''),
req.body,
getRequiredAdminSession(req),
),
{
successMessage: '用户状态已更新',
errorMessage: '更新用户状态失败',
scope: '[admin/users/:userId/status]',
audit: (_req, data) => {
const result = data as AdminUserMutationResult
return {
action: 'admin_user_status_updated',
targetType: 'admin_user',
targetId: String(result.user.userId),
data: {
username: result.user.username,
status: result.user.status,
},
}
},
},
},
))
),
)
router.post('/users/:userId/reset-password', createJsonHandler(
(req) => resetManagedAdminUserPassword(String(req.params.userId || ''), req.body),
{
successMessage: '用户密码已重置',
errorMessage: '重置用户密码失败',
scope: '[admin/users/:userId/reset-password]',
audit: (_req, data) => {
const result = data as AdminUserMutationResult
return {
action: 'admin_user_password_reset',
targetType: 'admin_user',
targetId: String(result.user.userId),
data: {
username: result.user.username,
},
}
router.post(
'/users/:userId/reset-password',
createJsonHandler(
(req) => resetManagedAdminUserPassword(String(req.params.userId || ''), req.body),
{
successMessage: '用户密码已重置',
errorMessage: '重置用户密码失败',
scope: '[admin/users/:userId/reset-password]',
audit: (_req, data) => {
const result = data as AdminUserMutationResult
return {
action: 'admin_user_password_reset',
targetType: 'admin_user',
targetId: String(result.user.userId),
data: {
username: result.user.username,
},
}
},
},
},
))
),
)
export default router
@@ -266,8 +266,7 @@ router.post(
'/worker-platform/finance-requests/:requestId/review',
requireAdminRoles(['admin', 'operator']),
createJsonHandler(
(req) =>
reviewAdminWorkerFinanceRequest(String(req.params.requestId || ''), req.body || {}),
(req) => reviewAdminWorkerFinanceRequest(String(req.params.requestId || ''), req.body || {}),
{
successMessage: '资金申请已处理',
errorMessage: '处理资金申请失败',
@@ -387,20 +386,23 @@ router.post(
router.post(
'/worker-platform/orders/:workOrderId/cancel',
requireAdminRoles(['admin', 'operator', 'support']),
createJsonHandler((req) => cancelAdminWorkOrder(String(req.params.workOrderId || ''), req.body || {}), {
successMessage: '撤单处理完成,打手额度已恢复',
errorMessage: '撤单失败',
scope: '[admin/worker-platform/orders/:workOrderId/cancel]',
audit: (req, data) => ({
action:
String(req.body?.action || '').trim() === 'return_to_hall'
? 'work_order_returned_to_hall'
: 'work_order_cancelled',
targetType: 'work_order',
targetId: String(req.params.workOrderId || ''),
data: data && typeof data === 'object' ? (data as Record<string, unknown>) : {},
}),
}),
createJsonHandler(
(req) => cancelAdminWorkOrder(String(req.params.workOrderId || ''), req.body || {}),
{
successMessage: '撤单处理完成,打手额度已恢复',
errorMessage: '撤单失败',
scope: '[admin/worker-platform/orders/:workOrderId/cancel]',
audit: (req, data) => ({
action:
String(req.body?.action || '').trim() === 'return_to_hall'
? 'work_order_returned_to_hall'
: 'work_order_cancelled',
targetType: 'work_order',
targetId: String(req.params.workOrderId || ''),
data: data && typeof data === 'object' ? (data as Record<string, unknown>) : {},
}),
},
),
)
router.delete(
@@ -576,10 +578,7 @@ router.post(
requireAdminRoles(['admin', 'operator']),
createJsonHandler(
(req) =>
deductAdminWorkOrderPendingDeposit(
String(req.params.workOrderId || ''),
req.body || {},
),
deductAdminWorkOrderPendingDeposit(String(req.params.workOrderId || ''), req.body || {}),
{
successMessage: '待解冻押金已扣减',
errorMessage: '扣减待解冻押金失败',
+10 -5
View File
@@ -44,11 +44,16 @@ router.post('/', notifyRateLimit, async (req, res) => {
})
res.status(200).json({ code: 0 })
} catch (error) {
logIntegration('[affiliate-dash/notify]', '订单回调处理失败', {
requestId,
durationMs: Date.now() - startedAt,
error,
}, { level: 'error' })
logIntegration(
'[affiliate-dash/notify]',
'订单回调处理失败',
{
requestId,
durationMs: Date.now() - startedAt,
error,
},
{ level: 'error' },
)
sendRouteError(res, error, 'affiliate-dash 回调处理失败', '[affiliate-dash/notify]')
}
})
+15 -12
View File
@@ -42,18 +42,21 @@ router.post(
'/upload',
collectRateLimit,
uploadSingleFile,
createRouteHandler((req) => {
return uploadFileAsset({
file: req.file,
scene: req.body?.scene || 'collect-material',
uploaderType: 'collect',
uploaderId: String(req.body?.orderNo || 'anonymous'),
})
}, {
successMessage: '文件已上传',
errorMessage: '文件上传失败',
scope: '[collect/upload]',
}),
createRouteHandler(
(req) => {
return uploadFileAsset({
file: req.file,
scene: req.body?.scene || 'collect-material',
uploaderType: 'collect',
uploaderId: String(req.body?.orderNo || 'anonymous'),
})
},
{
successMessage: '文件已上传',
errorMessage: '文件上传失败',
scope: '[collect/upload]',
},
),
)
router.use((req, res) => {
+10 -5
View File
@@ -45,11 +45,16 @@ router.post('/notify', notifyRateLimit, async (req, res) => {
})
res.status(200).json({ code: 0 })
} catch (error) {
logIntegration('[kuaishou-feifei/notify]', '订单通知处理失败', {
requestId,
durationMs: Date.now() - startedAt,
error,
}, { level: 'error' })
logIntegration(
'[kuaishou-feifei/notify]',
'订单通知处理失败',
{
requestId,
durationMs: Date.now() - startedAt,
error,
},
{ level: 'error' },
)
sendRouteError(res, error, 'kuaishou-feifei 通知处理失败', '[kuaishou-feifei/notify]')
}
})
+40 -20
View File
@@ -45,11 +45,16 @@ router.post('/send-code', industryRateLimit, async (req, res) => {
res.status(200).json(result)
} catch (error) {
const message = error instanceof Error ? error.message : '系统异常'
logIntegration('[kuaishou-industry/send-code]', '发码处理失败', {
requestId,
durationMs: Date.now() - startedAt,
error,
}, { level: 'error' })
logIntegration(
'[kuaishou-industry/send-code]',
'发码处理失败',
{
requestId,
durationMs: Date.now() - startedAt,
error,
},
{ level: 'error' },
)
res.status(200).json(buildIndustryErrorResponse(4010003, message))
}
})
@@ -78,11 +83,16 @@ router.post('/destroy-code', industryRateLimit, async (req, res) => {
res.status(200).json(result)
} catch (error) {
const message = error instanceof Error ? error.message : '系统异常'
logIntegration('[kuaishou-industry/destroy-code]', '销毁处理失败', {
requestId,
durationMs: Date.now() - startedAt,
error,
}, { level: 'error' })
logIntegration(
'[kuaishou-industry/destroy-code]',
'销毁处理失败',
{
requestId,
durationMs: Date.now() - startedAt,
error,
},
{ level: 'error' },
)
res.status(200).json(buildIndustryErrorResponse(4010003, message))
}
})
@@ -111,11 +121,16 @@ router.post('/query-code', industryRateLimit, async (req, res) => {
res.status(200).json(result)
} catch (error) {
const message = error instanceof Error ? error.message : '系统异常'
logIntegration('[kuaishou-industry/query-code]', '查询处理失败', {
requestId,
durationMs: Date.now() - startedAt,
error,
}, { level: 'error' })
logIntegration(
'[kuaishou-industry/query-code]',
'查询处理失败',
{
requestId,
durationMs: Date.now() - startedAt,
error,
},
{ level: 'error' },
)
res.status(200).json(buildIndustryErrorResponse(4010003, message))
}
})
@@ -144,11 +159,16 @@ router.post('/consume-code', industryRateLimit, async (req, res) => {
res.status(200).json(result)
} catch (error) {
const message = error instanceof Error ? error.message : '系统异常'
logIntegration('[kuaishou-industry/consume-code]', '核销处理失败', {
requestId,
durationMs: Date.now() - startedAt,
error,
}, { level: 'error' })
logIntegration(
'[kuaishou-industry/consume-code]',
'核销处理失败',
{
requestId,
durationMs: Date.now() - startedAt,
error,
},
{ level: 'error' },
)
res.status(200).json(buildIndustryErrorResponse(4010003, message))
}
})
+20 -10
View File
@@ -47,11 +47,16 @@ router.post('/orders/create', open91RateLimit, async (req, res) => {
} catch (error) {
const message = error instanceof Error ? error.message : '系统错误'
const code = resolveErrorStatusCode(error) >= 500 ? 500 : 400
logIntegration('[open-91/create]', '91卡券异步下单处理失败', {
requestId,
durationMs: Date.now() - startedAt,
error,
}, { level: 'error' })
logIntegration(
'[open-91/create]',
'91卡券异步下单处理失败',
{
requestId,
durationMs: Date.now() - startedAt,
error,
},
{ level: 'error' },
)
res.status(200).json(buildOpen91ErrorResponse(message, code))
}
})
@@ -79,11 +84,16 @@ router.post('/orders/query', open91RateLimit, async (req, res) => {
} catch (error) {
const message = error instanceof Error ? error.message : '系统错误'
const code = resolveErrorStatusCode(error) >= 500 ? 500 : 400
logIntegration('[open-91/query]', '91卡券订单查询处理失败', {
requestId,
durationMs: Date.now() - startedAt,
error,
}, { level: 'error' })
logIntegration(
'[open-91/query]',
'91卡券订单查询处理失败',
{
requestId,
durationMs: Date.now() - startedAt,
error,
},
{ level: 'error' },
)
res.status(200).json(buildOpen91ErrorResponse(message, code))
}
})
+11 -6
View File
@@ -25,12 +25,17 @@ router.get('/:code', async (req, res) => {
})
res.redirect(302, row.target_url)
} catch (error) {
logIntegration('[short-link]', '短链跳转失败', {
requestId,
code: req.params.code,
durationMs: Date.now() - startedAt,
error,
}, { level: 'warn' })
logIntegration(
'[short-link]',
'短链跳转失败',
{
requestId,
code: req.params.code,
durationMs: Date.now() - startedAt,
error,
},
{ level: 'warn' },
)
sendRouteError(res, error, '短链不可用', '[short-link]')
}
})
+32 -20
View File
@@ -111,40 +111,52 @@ router.get(
router.get(
'/profile/wallet-ledgers',
createRouteHandler((req) => listWorkerProfileWalletLedgers(req.query, getRequiredWorkerSession(req)), {
successMessage: 'ok',
errorMessage: '读取钱包流水失败',
scope: '[worker/profile/wallet-ledgers]',
}),
createRouteHandler(
(req) => listWorkerProfileWalletLedgers(req.query, getRequiredWorkerSession(req)),
{
successMessage: 'ok',
errorMessage: '读取钱包流水失败',
scope: '[worker/profile/wallet-ledgers]',
},
),
)
router.get(
'/profile/finance-requests',
createRouteHandler((req) => listWorkerProfileFinanceRequests(req.query, getRequiredWorkerSession(req)), {
successMessage: 'ok',
errorMessage: '读取资金申请记录失败',
scope: '[worker/profile/finance-requests]',
}),
createRouteHandler(
(req) => listWorkerProfileFinanceRequests(req.query, getRequiredWorkerSession(req)),
{
successMessage: 'ok',
errorMessage: '读取资金申请记录失败',
scope: '[worker/profile/finance-requests]',
},
),
)
router.post(
'/profile/recharge-requests',
requireActiveWorker,
createRouteHandler((req) => createWorkerRechargeRequest(req.body || {}, getRequiredWorkerSession(req)), {
successMessage: '充值申请已提交',
errorMessage: '提交充值申请失败',
scope: '[worker/profile/recharge-requests]',
}),
createRouteHandler(
(req) => createWorkerRechargeRequest(req.body || {}, getRequiredWorkerSession(req)),
{
successMessage: '充值申请已提交',
errorMessage: '提交充值申请失败',
scope: '[worker/profile/recharge-requests]',
},
),
)
router.post(
'/profile/withdraw-requests',
requireActiveWorker,
createRouteHandler((req) => createWorkerWithdrawRequest(req.body || {}, getRequiredWorkerSession(req)), {
successMessage: '提现申请已提交',
errorMessage: '提交提现申请失败',
scope: '[worker/profile/withdraw-requests]',
}),
createRouteHandler(
(req) => createWorkerWithdrawRequest(req.body || {}, getRequiredWorkerSession(req)),
{
successMessage: '提现申请已提交',
errorMessage: '提交提现申请失败',
scope: '[worker/profile/withdraw-requests]',
},
),
)
router.post(
+5 -1
View File
@@ -7,7 +7,11 @@ import {
} from '../../services/worker-platform/index.js'
import { createHttpError, sendRouteError } from '../../utils/http.js'
export async function requireWorkerSession(req: Request, res: Response, next: NextFunction): Promise<void> {
export async function requireWorkerSession(
req: Request,
res: Response,
next: NextFunction,
): Promise<void> {
try {
req.workerSession = await verifyWorkerSessionToken(extractWorkerBearerToken(req))
next()
@@ -1,9 +1,17 @@
import { createAdminAuditLog, listAdminAuditLogs } from '../../repositories/admin-audit-log-repo.js'
import { nowIso } from '../../utils/time.js'
import { normalizeDateQuery, normalizePage, normalizePageSize, safeParseJson } from './admin-query-utils.js'
import {
normalizeDateQuery,
normalizePage,
normalizePageSize,
safeParseJson,
} from './admin-query-utils.js'
import type { JsonObject } from '../../types/json.js'
export async function writeAdminAuditLog(session: JsonObject | null | undefined, payload: JsonObject = {}) {
export async function writeAdminAuditLog(
session: JsonObject | null | undefined,
payload: JsonObject = {},
) {
if (!session?.userId) {
return null
}
@@ -32,10 +32,14 @@ type AdminUserStatus = 'active' | 'disabled'
export async function ensureAdminUsersBootstrapped(): Promise<void> {
ensureAdminAuthConfigured()
const configuredUsers = Array.isArray(runtimeConfig.admin?.defaultUsers) ? runtimeConfig.admin.defaultUsers : []
const configuredUsers = Array.isArray(runtimeConfig.admin?.defaultUsers)
? runtimeConfig.admin.defaultUsers
: []
for (const configuredUser of configuredUsers) {
const username = String(configuredUser?.username || '').trim().toLowerCase()
const username = String(configuredUser?.username || '')
.trim()
.toLowerCase()
const password = String(configuredUser?.password || '').trim()
const role = normalizeAdminRole(configuredUser?.role)
@@ -70,7 +74,9 @@ export async function loginAdmin(
): Promise<JsonObject> {
ensureAdminAuthConfigured()
const normalizedUsername = String(username || '').trim().toLowerCase()
const normalizedUsername = String(username || '')
.trim()
.toLowerCase()
const normalizedPassword = String(password || '').trim()
if (!normalizedUsername || !normalizedPassword) {
@@ -87,7 +93,11 @@ export async function loginAdmin(
}
const user = await getAdminUserByUsername(normalizedUsername)
if (!user || user.status !== 'active' || !verifyAdminPassword(normalizedPassword, user.password_hash)) {
if (
!user ||
user.status !== 'active' ||
!verifyAdminPassword(normalizedPassword, user.password_hash)
) {
await recordAdminLoginLog({
userId: user ? Number(user.id) : null,
username: normalizedUsername,
@@ -201,7 +211,10 @@ export async function getAdminSessionSummary(token: unknown): Promise<JsonObject
}
}
export function requireAdminRole(session: { role?: string } | null | undefined, allowedRoles: string[]): void {
export function requireAdminRole(
session: { role?: string } | null | undefined,
allowedRoles: string[],
): void {
if (session && allowedRoles.includes(session.role || '')) {
return
}
@@ -342,7 +355,10 @@ export async function updateManagedAdminUserStatus(
}
}
export async function resetManagedAdminUserPassword(userId: number | string, payload: JsonObject = {}): Promise<JsonObject> {
export async function resetManagedAdminUserPassword(
userId: number | string,
payload: JsonObject = {},
): Promise<JsonObject> {
const user = await getRequiredAdminUser(userId)
const password = normalizePassword(payload.password)
@@ -373,7 +389,9 @@ export async function resetManagedAdminUserPassword(userId: number | string, pay
export function ensureAdminAuthConfigured(): void {
const sessionSecret = String(runtimeConfig.admin?.sessionSecret || '').trim()
const configuredUsers = Array.isArray(runtimeConfig.admin?.defaultUsers) ? runtimeConfig.admin.defaultUsers : []
const configuredUsers = Array.isArray(runtimeConfig.admin?.defaultUsers)
? runtimeConfig.admin.defaultUsers
: []
if (sessionSecret && configuredUsers.length > 0) {
return
@@ -435,7 +453,9 @@ function signPayload(encodedPayload: string): string {
}
export function normalizeAdminRole(role: unknown): AdminRole {
const normalized = String(role || '').trim().toLowerCase()
const normalized = String(role || '')
.trim()
.toLowerCase()
if (normalized === 'admin') {
return 'admin'
@@ -449,7 +469,11 @@ export function normalizeAdminRole(role: unknown): AdminRole {
}
export function normalizeAdminUserStatus(status: unknown): AdminUserStatus {
return String(status || '').trim().toLowerCase() === 'disabled' ? 'disabled' : 'active'
return String(status || '')
.trim()
.toLowerCase() === 'disabled'
? 'disabled'
: 'active'
}
function safeCompare(input: unknown, expected: unknown): boolean {
@@ -464,17 +488,23 @@ function safeCompare(input: unknown, expected: unknown): boolean {
}
function normalizeRoleQuery(role: unknown): string {
const normalized = String(role || '').trim().toLowerCase()
const normalized = String(role || '')
.trim()
.toLowerCase()
return ['admin', 'operator', 'support'].includes(normalized) ? normalized : ''
}
function normalizeStatusQuery(status: unknown): string {
const normalized = String(status || '').trim().toLowerCase()
const normalized = String(status || '')
.trim()
.toLowerCase()
return ['active', 'disabled'].includes(normalized) ? normalized : ''
}
function normalizeUsername(username: unknown): string {
return String(username || '').trim().toLowerCase()
return String(username || '')
.trim()
.toLowerCase()
}
function normalizePassword(password: unknown): string {
@@ -523,7 +553,7 @@ async function getRequiredAdminUser(userId: number | string): Promise<AdminUserR
async function ensureAdminUserChangeAllowed(
user: AdminUserRow,
options: { nextRole?: AdminRole, nextStatus?: AdminUserStatus } = {},
options: { nextRole?: AdminRole; nextStatus?: AdminUserStatus } = {},
session: AdminSession,
): Promise<void> {
const nextRole = options.nextRole || user.role
@@ -536,7 +566,11 @@ async function ensureAdminUserChangeAllowed(
})
}
if (user.role === 'admin' && (nextRole !== 'admin' || nextStatus !== 'active') && await countActiveAdminUsers() <= 1) {
if (
user.role === 'admin' &&
(nextRole !== 'admin' || nextStatus !== 'active') &&
(await countActiveAdminUsers()) <= 1
) {
throw createHttpError('至少保留一个启用中的管理员账号', {
statusCode: 409,
errorCode: 'admin_user_last_admin_not_allowed',
@@ -555,11 +589,13 @@ function mapAdminUser(user: AdminUserRow): JsonObject {
}
}
function pickLoginMeta(meta: {
ip?: string
userAgent?: string
location?: string
} = {}) {
function pickLoginMeta(
meta: {
ip?: string
userAgent?: string
location?: string
} = {},
) {
const result: {
ip?: string
userAgent?: string
@@ -11,15 +11,8 @@ export async function getAdminDashboardSummary() {
TASK_STATUS.PENDING_BINDING_PREPARE,
TASK_STATUS.WAITING_BINDING,
]
const claimingStatuses = [
TASK_STATUS.CLAIMED,
TASK_STATUS.ROLE_CONFIRMED,
TASK_STATUS.REDEEMING,
]
const abnormalStatuses = [
TASK_STATUS.RETRY_PENDING,
TASK_STATUS.MANUAL_REVIEW,
]
const claimingStatuses = [TASK_STATUS.CLAIMED, TASK_STATUS.ROLE_CONFIRMED, TASK_STATUS.REDEEMING]
const abnormalStatuses = [TASK_STATUS.RETRY_PENDING, TASK_STATUS.MANUAL_REVIEW]
const result = await query(
`
SELECT
@@ -1,17 +1,10 @@
import type { Request } from 'express'
import type { JsonObject } from '../../types/json.js'
import {
createAdminLoginLog,
listAdminLoginLogs,
} from '../../repositories/admin-login-log-repo.js'
import { createAdminLoginLog, listAdminLoginLogs } from '../../repositories/admin-login-log-repo.js'
import { logWarn } from '../../utils/logger.js'
import { nowIso } from '../../utils/time.js'
import {
normalizeDateQuery,
normalizePage,
normalizePageSize,
} from './admin-query-utils.js'
import { normalizeDateQuery, normalizePage, normalizePageSize } from './admin-query-utils.js'
type RecordAdminLoginInput = {
userId?: number | null
@@ -108,16 +101,8 @@ export function resolveClientLocation(req: Request) {
'cloudfront-viewer-country',
'x-country-code',
])
const city = firstHeader(req, [
'cf-ipcity',
'x-vercel-ip-city',
'x-city',
])
const region = firstHeader(req, [
'cf-region',
'x-vercel-ip-country-region',
'x-region',
])
const city = firstHeader(req, ['cf-ipcity', 'x-vercel-ip-city', 'x-city'])
const region = firstHeader(req, ['cf-region', 'x-vercel-ip-country-region', 'x-region'])
return [country, region, city].filter(Boolean).join(' · ')
}
@@ -34,7 +34,10 @@ export async function mapAdminOrderListItem(item: OrderListRow): Promise<AdminOr
createdAt: item.created_at,
updatedAt: item.updated_at,
itemCount: orderItems.length,
totalQuantity: orderItems.reduce((sum, orderItem) => sum + Math.max(1, Number(orderItem.quantity || 1)), 0),
totalQuantity: orderItems.reduce(
(sum, orderItem) => sum + Math.max(1, Number(orderItem.quantity || 1)),
0,
),
itemSummary,
taskCount: Number(item.task_count || tasks.length || 0),
resourceStatus: fulfillmentProgress.resourceStatus,
@@ -52,8 +55,9 @@ export function summarizeOrderItems(items: OrderItemRow[] | null | undefined): s
}
const [firstItem] = normalizedItems
const firstLabel = resolveOrderItemTitle(firstItem)
|| String(firstItem?.sku_name || firstItem?.sku_code || '').trim()
const firstLabel =
resolveOrderItemTitle(firstItem) ||
String(firstItem?.sku_name || firstItem?.sku_code || '').trim()
if (normalizedItems.length === 1) {
return firstLabel
@@ -66,10 +66,7 @@ import type {
AdminTaskListQueryInput,
AdminViewerSessionInput,
} from '../../types/admin/read-inputs.js'
import type {
KuaishouIndustryVoucherRow,
TaskRow,
} from '../../types/repository/rows.js'
import type { KuaishouIndustryVoucherRow, TaskRow } from '../../types/repository/rows.js'
export async function getAdminOrders(
query: AdminOrderListQueryInput = {},
@@ -229,15 +226,16 @@ export async function getAdminTaskDetail(
}
const primaryClaimTokenId = getTaskPrimaryClaimTokenId(task)
const [order, claimToken, taskEvents, taskKuaishouIndustryVouchers, oidKuaishouIndustryVouchers] = await Promise.all([
getOrderById(task.order_id),
primaryClaimTokenId ? getClaimTokenById(primaryClaimTokenId) : Promise.resolve(null),
listTaskEventsByTaskId(task.id),
listKuaishouIndustryVouchersByTaskId(task.id),
task.platform_order_id
? listKuaishouIndustryVouchersByOid(task.platform_order_id)
: Promise.resolve([]),
])
const [order, claimToken, taskEvents, taskKuaishouIndustryVouchers, oidKuaishouIndustryVouchers] =
await Promise.all([
getOrderById(task.order_id),
primaryClaimTokenId ? getClaimTokenById(primaryClaimTokenId) : Promise.resolve(null),
listTaskEventsByTaskId(task.id),
listKuaishouIndustryVouchersByTaskId(task.id),
task.platform_order_id
? listKuaishouIndustryVouchersByOid(task.platform_order_id)
: Promise.resolve([]),
])
const orderItems = order ? await listOrderItemsByOrderId(order.id) : []
const orderItem = orderItems.find((item) => item.id === task.order_item_id) || null
const taskContext = parseTaskContext(task)
@@ -252,10 +250,7 @@ export async function getAdminTaskDetail(
cloudSourceLabelMap,
}) || mapKuaishouCloudTaskStateProjection(task, { cloudSourceLabelMap })
const claimIdentity = buildClaimIdentityAdminSummary(taskContext, {
flowLike:
taskContext.kuaishouCloudFulfillment ||
taskContext.kuaishouFeifei ||
null,
flowLike: taskContext.kuaishouCloudFulfillment || taskContext.kuaishouFeifei || null,
taskRoleId: task.role_id,
taskRoleName: task.role_name,
})
@@ -265,17 +260,20 @@ export async function getAdminTaskDetail(
taskKuaishouIndustryVouchers,
oidKuaishouIndustryVouchers,
)
const kuaishouIndustryVoucher =
firstKuaishouIndustryVoucher
? mapAdminKuaishouIndustryVoucher(firstKuaishouIndustryVoucher)
: mapKuaishouIndustryVoucherContext(taskContext.kuaishouIndustryVoucher)
const kuaishouIndustryVoucher = firstKuaishouIndustryVoucher
? mapAdminKuaishouIndustryVoucher(firstKuaishouIndustryVoucher)
: mapKuaishouIndustryVoucherContext(taskContext.kuaishouIndustryVoucher)
const hasKuaishouIndustryVoucher = Boolean(
kuaishouIndustryVoucher && String(kuaishouIndustryVoucher.voucherCode || '').trim(),
)
const kuaishouIndustryVoucherStatus =
String(kuaishouIndustryVoucher?.status || '').trim().toUpperCase()
const kuaishouIndustryVoucherSendCallbackStatus =
String(kuaishouIndustryVoucher?.sendCallbackStatus || '').trim().toLowerCase()
const kuaishouIndustryVoucherStatus = String(kuaishouIndustryVoucher?.status || '')
.trim()
.toUpperCase()
const kuaishouIndustryVoucherSendCallbackStatus = String(
kuaishouIndustryVoucher?.sendCallbackStatus || '',
)
.trim()
.toLowerCase()
return {
task: mapAdminTaskListItem(
@@ -435,7 +433,7 @@ function resolveKuaishouIndustryVoucherForTask(
taskContext.kuaishouIndustryVoucher &&
typeof taskContext.kuaishouIndustryVoucher === 'object' &&
!Array.isArray(taskContext.kuaishouIndustryVoucher)
? taskContext.kuaishouIndustryVoucher as JsonRecord
? (taskContext.kuaishouIndustryVoucher as JsonRecord)
: {}
const voucherCode = String(voucherContext.voucherCode || voucherContext.eticketId || '').trim()
if (voucherCode) {
@@ -457,7 +455,7 @@ function resolveKuaishouIndustryVoucherForTask(
}
}
return oidVouchers.length === 1 ? (oidVouchers[0] || null) : null
return oidVouchers.length === 1 ? oidVouchers[0] || null : null
}
function buildCloudSourceLabelMap() {
@@ -15,7 +15,10 @@ test('resolveAdminTaskScreenshotUrl lets support view final redeemed screenshot'
runtime_session_id: '',
}
const screenshotUrl = await resolveAdminTaskScreenshotUrl(task, createAdminViewerContext({ role: 'support' }))
const screenshotUrl = await resolveAdminTaskScreenshotUrl(
task,
createAdminViewerContext({ role: 'support' }),
)
assert.equal(screenshotUrl, '/api/v1/admin/tasks/12/screenshot')
})
@@ -27,7 +30,10 @@ test('resolveAdminTaskScreenshotUrl falls back to review screenshot when runtime
runtime_session_id: 'runtime-session-13',
}
const screenshotUrl = await resolveAdminTaskScreenshotUrl(task, createAdminViewerContext({ role: 'support' }))
const screenshotUrl = await resolveAdminTaskScreenshotUrl(
task,
createAdminViewerContext({ role: 'support' }),
)
assert.equal(screenshotUrl, '/api/v1/admin/tasks/13/screenshot')
})
@@ -1,6 +1,6 @@
import { safeParseJson } from './admin-query-utils.js'
import { normalizeAdminRole } from './admin-auth-service.js'
import { asJsonObject, type JsonObject, JsonRecord } from '../../types/json.js'
import { asJsonObject, type JsonObject, JsonRecord } from '../../types/json.js'
import {
parseTaskContext as parseTaskContextValue,
parseTaskState as parseTaskStateValue,
@@ -79,8 +79,7 @@ export function mapKuaishouCloudFulfillmentContext(
const role = asJsonObject(record.role)
const purchase = asJsonObject(record.purchase)
const dispatch = asJsonObject(record.dispatch)
const returnNumber =
asJsonObject(record.returnNumber)
const returnNumber = asJsonObject(record.returnNumber)
const consume = asJsonObject(record.consume)
const cloudSourceKeys = Array.isArray(binding.cloudSourceKeys)
? binding.cloudSourceKeys.map((value: unknown) => String(value || '').trim()).filter(Boolean)
@@ -373,7 +372,8 @@ export function createAdminViewerContext(
canViewSensitiveTaskData: role === 'admin' || role === 'operator',
canManageTaskLifecycle: role === 'admin' || role === 'operator',
canOperateAssistedTask: role === 'admin' || role === 'operator' || role === 'support',
canOperateKuaishouIndustryVoucher: role === 'admin' || role === 'operator' || role === 'support',
canOperateKuaishouIndustryVoucher:
role === 'admin' || role === 'operator' || role === 'support',
}
}
@@ -1,7 +1,7 @@
import { getTaskById } from '../../repositories/task-repo.js'
import { createHttpError } from '../../utils/http.js'
import { safeParseJson } from './admin-query-utils.js'
import { asJsonObject, type JsonObject, JsonRecord } from '../../types/json.js'
import { asJsonObject, type JsonObject, JsonRecord } from '../../types/json.js'
import {
TASK_STATUS,
isTaskFulfillmentCompletedStatus,
@@ -17,10 +17,7 @@ import type {
AdminTaskListItem,
} from '../../types/admin/read-models.js'
import type { AdminTaskActionPayload } from '../../types/admin/write-models.js'
import type {
TaskEventRow,
TaskRow,
} from '../../types/repository/rows.js'
import type { TaskEventRow, TaskRow } from '../../types/repository/rows.js'
import type { AdminViewerContext } from './admin-read-shared-helpers.js'
type TaskFulfillmentState = {
@@ -28,9 +25,7 @@ type TaskFulfillmentState = {
customerStatus: string
}
export function mapAdminTaskSummary(
task: TaskRow,
): JsonRecord {
export function mapAdminTaskSummary(task: TaskRow): JsonRecord {
const fulfillment = buildTaskFulfillmentState(task)
return {
@@ -111,8 +106,10 @@ export function mapAdminTaskListItem(
lastError: task.last_error,
createdAt: task.created_at,
updatedAt: task.updated_at,
claimToken: viewerContext.canViewSensitiveTaskData ? (task.primary_claim_token || task.claim_token || '') : '',
screenshotPath: viewerContext.role === 'support' ? '' : (task.screenshot_path || ''),
claimToken: viewerContext.canViewSensitiveTaskData
? task.primary_claim_token || task.claim_token || ''
: '',
screenshotPath: viewerContext.role === 'support' ? '' : task.screenshot_path || '',
}
}
@@ -134,7 +131,9 @@ export function buildOrderFulfillmentProgress(
const totalTaskCount = normalizedTasks.length
const taskFulfillments = normalizedTasks.map((task) => buildTaskFulfillmentState(task))
const preparedTaskCount = normalizedTasks.filter((task) => isTaskResourcePrepared(task)).length
const completedTaskCount = normalizedTasks.filter((task) => isTaskFulfillmentCompleted(task)).length
const completedTaskCount = normalizedTasks.filter((task) =>
isTaskFulfillmentCompleted(task),
).length
let resourceStatus = 'pending_prepare'
let customerStatus = 'not_started'
@@ -152,7 +151,11 @@ export function buildOrderFulfillmentProgress(
if (normalizedTasks.every((task) => isTaskFulfillmentCompleted(task))) {
resourceStatus = 'resource_ready'
customerStatus = 'customer_completed'
} else if (taskFulfillments.some((item) => ['customer_processing', 'customer_confirmed', 'link_opened'].includes(item.customerStatus))) {
} else if (
taskFulfillments.some((item) =>
['customer_processing', 'customer_confirmed', 'link_opened'].includes(item.customerStatus),
)
) {
resourceStatus = preparedTaskCount > 0 ? 'resource_ready' : 'pending_prepare'
customerStatus = 'customer_processing'
} else if (taskFulfillments.some((item) => item.customerStatus === 'waiting_customer')) {
@@ -163,7 +166,11 @@ export function buildOrderFulfillmentProgress(
customerStatus = 'customer_exception'
} else if (preparedTaskCount > 0) {
resourceStatus = 'resource_ready'
} else if (taskFulfillments.some((item) => ['manual_review', 'retry_pending'].includes(item.resourceStatus))) {
} else if (
taskFulfillments.some((item) =>
['manual_review', 'retry_pending'].includes(item.resourceStatus),
)
) {
resourceStatus = 'resource_exception'
}
@@ -1,8 +1,6 @@
import { getAffiliateDashConfig } from '../../platforms/affiliate-dash/config.js'
import type { JsonObject } from '../../../types/json.js'
import {
getAffiliateDashWallet,
} from '../../platforms/affiliate-dash/order-service.js'
import { getAffiliateDashWallet } from '../../platforms/affiliate-dash/order-service.js'
import { listAffiliateDashProducts } from '../../platforms/affiliate-dash/product-service.js'
import {
getAffiliateDashSourceConfig,
@@ -35,9 +33,7 @@ export async function updateAdminAffiliateDashConfig(payload: JsonObject = {}) {
export function matchAdminAffiliateDashSku(payload: JsonObject = {}) {
const productNo = String(payload.productNo || payload.product_no || '').trim()
const source = getAdminEditableAffiliateDashConfig()
const sku = productNo
? String(source.skuMapping[productNo] || '').trim()
: ''
const sku = productNo ? String(source.skuMapping[productNo] || '').trim() : ''
return {
productNo,
@@ -52,7 +52,13 @@ test('resolveCloudtentaclesAdminContext merges payload source and persisted sess
test('hasCloudtentaclesCredentialContextChanged detects normalized credential changes', () => {
assert.equal(
hasCloudtentaclesCredentialContextChanged(
{ baseUrl: ' https://a ', username: 'u1', phone: '13812345678', deviceId: 'd1', deviceType: 1 },
{
baseUrl: ' https://a ',
username: 'u1',
phone: '13812345678',
deviceId: 'd1',
deviceType: 1,
},
{ baseUrl: 'https://a', username: 'u1', phone: '13812345678', deviceId: 'd1', deviceType: 1 },
),
false,
@@ -1,70 +1,57 @@
export function pickFirstNonEmpty(values: unknown[]) {
for (const value of values) {
const normalized = String(value || "").trim();
const normalized = String(value || '').trim()
if (normalized) {
return normalized;
return normalized
}
}
return "";
return ''
}
export function isPlainObject(value: unknown): value is JsonObject {
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
return Boolean(value) && typeof value === 'object' && !Array.isArray(value)
}
import { getCloudtentaclesSourceByKey } from "../../../platforms/cloudtentacles/source-config-service.js";
import { getCloudtentaclesSessionStateByKey } from "../../../platforms/cloudtentacles/session-state-service.js";
import { getCloudtentaclesSourceByKey } from '../../../platforms/cloudtentacles/source-config-service.js'
import { getCloudtentaclesSessionStateByKey } from '../../../platforms/cloudtentacles/session-state-service.js'
import type { JsonObject } from '../../../../types/json.js'
import {
normalizeCloudtentaclesDeviceId,
normalizeCloudtentaclesDeviceType,
} from "../../../platforms/cloudtentacles/defaults.js";
} from '../../../platforms/cloudtentacles/defaults.js'
export function resolveCloudtentaclesAdminContext(payload: JsonObject = {}, options: JsonObject = {}) {
const sourceKey = String(
payload.sourceKey || options.sourceKey || "default"
).trim();
const savedSource = options.savedSource || getCloudtentaclesSourceByKey(sourceKey) || {};
const persistedSession = options.persistedSession ||
getCloudtentaclesSessionStateByKey(sourceKey) ||
{};
const defaultBaseUrl = String(
options.defaultBaseUrl || "https://123.207.217.176"
).trim();
export function resolveCloudtentaclesAdminContext(
payload: JsonObject = {},
options: JsonObject = {},
) {
const sourceKey = String(payload.sourceKey || options.sourceKey || 'default').trim()
const savedSource = options.savedSource || getCloudtentaclesSourceByKey(sourceKey) || {}
const persistedSession =
options.persistedSession || getCloudtentaclesSessionStateByKey(sourceKey) || {}
const defaultBaseUrl = String(options.defaultBaseUrl || 'https://123.207.217.176').trim()
return {
sourceKey,
baseUrl: pickFirstNonEmpty([
payload.baseUrl,
savedSource.baseUrl,
defaultBaseUrl,
]),
baseUrl: pickFirstNonEmpty([payload.baseUrl, savedSource.baseUrl, defaultBaseUrl]),
token: pickFirstNonEmpty([payload.token, persistedSession.token]),
deviceId: normalizeCloudtentaclesDeviceId(
pickFirstNonEmpty([
payload.deviceId,
savedSource.deviceId,
persistedSession.deviceId,
])
pickFirstNonEmpty([payload.deviceId, savedSource.deviceId, persistedSession.deviceId]),
),
deviceType: normalizeCloudtentaclesDeviceType(
payload.deviceType ?? savedSource.deviceType ?? persistedSession.deviceType
payload.deviceType ?? savedSource.deviceType ?? persistedSession.deviceType,
),
};
}
}
export function hasCloudtentaclesCredentialContextChanged(
current: JsonObject = {},
next: JsonObject = {}
next: JsonObject = {},
) {
return (
String(current.baseUrl || "").trim() !==
String(next.baseUrl || "").trim() ||
String(current.username || "").trim() !==
String(next.username || "").trim() ||
String(current.phone || "").trim() !== String(next.phone || "").trim() ||
String(current.deviceId || "").trim() !==
String(next.deviceId || "").trim() ||
String(current.baseUrl || '').trim() !== String(next.baseUrl || '').trim() ||
String(current.username || '').trim() !== String(next.username || '').trim() ||
String(current.phone || '').trim() !== String(next.phone || '').trim() ||
String(current.deviceId || '').trim() !== String(next.deviceId || '').trim() ||
Number(current.deviceType || 0) !== Number(next.deviceType || 0)
);
)
}
File diff suppressed because it is too large Load Diff
@@ -1,11 +1,7 @@
import test from 'node:test'
import assert from 'node:assert/strict'
import {
mapAdminCloudtentaclesSession,
maskPhone,
maskSecret,
} from './mappers.js'
import { mapAdminCloudtentaclesSession, maskPhone, maskSecret } from './mappers.js'
test('maskSecret preserves edges while hiding middle characters', () => {
assert.equal(maskSecret('abcdef1234567890'), 'abcdef****567890')
@@ -2,44 +2,44 @@ import type { JsonObject } from '../../../../types/json.js'
import {
maskPhone as maskPhoneValue,
maskSecret as maskSecretValue,
} from "../../../../utils/masking.js";
} from '../../../../utils/masking.js'
import {
normalizeCloudtentaclesDeviceId,
normalizeCloudtentaclesDeviceType,
} from "../../../platforms/cloudtentacles/defaults.js";
} from '../../../platforms/cloudtentacles/defaults.js'
export function maskSecret(value: unknown) {
return maskSecretValue(value);
return maskSecretValue(value)
}
export function maskPhone(value: unknown) {
return maskPhoneValue(value, { maskShort: false });
return maskPhoneValue(value, { maskShort: false })
}
export function mapAdminCloudtentaclesSourceConfig(config: JsonObject = {}) {
return {
key: String(config.key || "").trim(),
label: String(config.label || "").trim(),
key: String(config.key || '').trim(),
label: String(config.label || '').trim(),
enabled: config.enabled !== false,
baseUrl: String(config.baseUrl || "").trim(),
username: String(config.username || "").trim(),
password: String(config.password || "").trim(),
phone: String(config.phone || "").trim(),
baseUrl: String(config.baseUrl || '').trim(),
username: String(config.username || '').trim(),
password: String(config.password || '').trim(),
phone: String(config.phone || '').trim(),
deviceId: normalizeCloudtentaclesDeviceId(config.deviceId),
deviceType: normalizeCloudtentaclesDeviceType(config.deviceType),
};
}
}
export function mapAdminCloudtentaclesSession(session: JsonObject = {}) {
return {
token: String(session.token || "").trim(),
token: String(session.token || '').trim(),
tokenMasked: maskSecret(session.token),
baseUrl: String(session.baseUrl || "").trim(),
username: String(session.username || "").trim(),
baseUrl: String(session.baseUrl || '').trim(),
username: String(session.username || '').trim(),
phoneMasked: maskPhone(session.phone),
loggedInAt: String(session.loggedInAt || "").trim(),
loggedInAt: String(session.loggedInAt || '').trim(),
deviceId: normalizeCloudtentaclesDeviceId(session.deviceId),
deviceType: normalizeCloudtentaclesDeviceType(session.deviceType),
hasToken: Boolean(String(session.token || "").trim()),
};
hasToken: Boolean(String(session.token || '').trim()),
}
}
@@ -1,96 +1,80 @@
import { getCloudtentaclesSourceByKey } from "../../../platforms/cloudtentacles/source-config-service.js";
import { getCloudtentaclesSessionStateByKey } from "../../../platforms/cloudtentacles/session-state-service.js";
import { getCloudtentaclesSourceByKey } from '../../../platforms/cloudtentacles/source-config-service.js'
import { getCloudtentaclesSessionStateByKey } from '../../../platforms/cloudtentacles/session-state-service.js'
import type { JsonObject } from '../../../../types/json.js'
import {
normalizeCloudtentaclesDeviceId,
normalizeCloudtentaclesDeviceType,
} from "../../../platforms/cloudtentacles/defaults.js";
import {
pickFirstNonEmpty,
resolveCloudtentaclesAdminContext,
} from "./context.js";
import { maskPhone, maskSecret } from "./mappers.js";
} from '../../../platforms/cloudtentacles/defaults.js'
import { pickFirstNonEmpty, resolveCloudtentaclesAdminContext } from './context.js'
import { maskPhone, maskSecret } from './mappers.js'
const DEFAULT_CLOUDTENTACLES_BASE_URL = "https://123.207.217.176";
const DEFAULT_CLOUDTENTACLES_BASE_URL = 'https://123.207.217.176'
function _resolveSourceKey(payload: JsonObject = {}) {
return String(payload.sourceKey || "").trim() || "default";
return String(payload.sourceKey || '').trim() || 'default'
}
export function resolveAdminCloudtentaclesCredentialPayload(
payload: JsonObject = {},
options: JsonObject = {}
options: JsonObject = {},
) {
const sourceKey = _resolveSourceKey(payload);
const savedSource = options.savedSource || getCloudtentaclesSourceByKey(sourceKey) || {};
const defaultBaseUrl = String(
options.defaultBaseUrl || DEFAULT_CLOUDTENTACLES_BASE_URL
).trim();
const sourceKey = _resolveSourceKey(payload)
const savedSource = options.savedSource || getCloudtentaclesSourceByKey(sourceKey) || {}
const defaultBaseUrl = String(options.defaultBaseUrl || DEFAULT_CLOUDTENTACLES_BASE_URL).trim()
return {
sourceKey,
baseUrl: pickFirstNonEmpty([
payload.baseUrl,
savedSource.baseUrl,
defaultBaseUrl,
]),
baseUrl: pickFirstNonEmpty([payload.baseUrl, savedSource.baseUrl, defaultBaseUrl]),
username: pickFirstNonEmpty([payload.username, savedSource.username]),
password: pickFirstNonEmpty([payload.password, savedSource.password]),
phone: pickFirstNonEmpty([payload.phone, savedSource.phone]),
deviceId: normalizeCloudtentaclesDeviceId(
pickFirstNonEmpty([payload.deviceId, savedSource.deviceId])
pickFirstNonEmpty([payload.deviceId, savedSource.deviceId]),
),
deviceType: normalizeCloudtentaclesDeviceType(
payload.deviceType ?? savedSource.deviceType
),
};
deviceType: normalizeCloudtentaclesDeviceType(payload.deviceType ?? savedSource.deviceType),
}
}
export function resolveAdminCloudtentaclesSessionPayload(
payload: JsonObject = {},
options: JsonObject = {}
options: JsonObject = {},
) {
const sourceKey = _resolveSourceKey(payload);
const savedSource = options.savedSource || getCloudtentaclesSourceByKey(sourceKey) || {};
const persistedSession = options.persistedSession ||
getCloudtentaclesSessionStateByKey(sourceKey) ||
{};
const sourceKey = _resolveSourceKey(payload)
const savedSource = options.savedSource || getCloudtentaclesSourceByKey(sourceKey) || {}
const persistedSession =
options.persistedSession || getCloudtentaclesSessionStateByKey(sourceKey) || {}
return resolveCloudtentaclesAdminContext(payload, {
savedSource,
persistedSession,
sourceKey,
defaultBaseUrl: options.defaultBaseUrl || DEFAULT_CLOUDTENTACLES_BASE_URL,
});
})
}
export function buildAdminCloudtentaclesPersistedSessionPayload(
session: JsonObject = {},
options: JsonObject = {}
options: JsonObject = {},
) {
return {
token: String(session.token || "").trim(),
baseUrl: String(session.baseUrl || "").trim(),
token: String(session.token || '').trim(),
baseUrl: String(session.baseUrl || '').trim(),
username: pickFirstNonEmpty([options.username, session.username]),
phone: pickFirstNonEmpty([options.phone, session.phone]),
loggedInAt: String(session.loggedInAt || "").trim(),
loggedInAt: String(session.loggedInAt || '').trim(),
deviceId: normalizeCloudtentaclesDeviceId(
pickFirstNonEmpty([options.deviceId, session.deviceId])
pickFirstNonEmpty([options.deviceId, session.deviceId]),
),
deviceType: normalizeCloudtentaclesDeviceType(
options.deviceType ?? session.deviceType
),
};
deviceType: normalizeCloudtentaclesDeviceType(options.deviceType ?? session.deviceType),
}
}
export function buildAdminCloudtentaclesSessionSummary(
session: JsonObject = {},
savedSession: JsonObject = {},
sourceKey = "default"
sourceKey = 'default',
) {
const permissions = Array.isArray(session.permissions)
? session.permissions
: [];
const permissions = Array.isArray(session.permissions) ? session.permissions : []
return {
sourceKey,
@@ -98,47 +82,39 @@ export function buildAdminCloudtentaclesSessionSummary(
permissionCount: permissions.length,
permissions,
persisted: Boolean(savedSession.token),
};
}
}
export function buildAdminCloudtentaclesLoginResult(
session: JsonObject = {},
savedSession: JsonObject = {},
sourceKey = "default"
sourceKey = 'default',
) {
return {
sourceKey,
baseUrl: String(session.baseUrl || "").trim(),
username: String(session.username || "").trim(),
baseUrl: String(session.baseUrl || '').trim(),
username: String(session.username || '').trim(),
phoneMasked: maskPhone(session.phone),
loggedInAt: String(session.loggedInAt || "").trim(),
responseMessage: String(session.responseMessage || "").trim(),
token: String(session.token || "").trim(),
session: buildAdminCloudtentaclesSessionSummary(
session,
savedSession,
sourceKey
),
loggedInAt: String(session.loggedInAt || '').trim(),
responseMessage: String(session.responseMessage || '').trim(),
token: String(session.token || '').trim(),
session: buildAdminCloudtentaclesSessionSummary(session, savedSession, sourceKey),
userInfo: session.userInfo,
asset: session.asset,
};
}
}
export function buildAdminCloudtentaclesValidateResult(
session: JsonObject = {},
savedSession: JsonObject = {},
sourceKey = "default"
sourceKey = 'default',
) {
return {
sourceKey,
baseUrl: String(session.baseUrl || "").trim(),
loggedInAt: String(session.loggedInAt || "").trim(),
session: buildAdminCloudtentaclesSessionSummary(
session,
savedSession,
sourceKey
),
baseUrl: String(session.baseUrl || '').trim(),
loggedInAt: String(session.loggedInAt || '').trim(),
session: buildAdminCloudtentaclesSessionSummary(session, savedSession, sourceKey),
userInfo: session.userInfo,
asset: session.asset,
};
}
}
@@ -2,22 +2,19 @@ import type { JsonObject } from '../../../../types/json.js'
import {
normalizeCloudtentaclesDeviceId,
normalizeCloudtentaclesDeviceType,
} from "../../../platforms/cloudtentacles/defaults.js";
} from '../../../platforms/cloudtentacles/defaults.js'
export function normalizeAdminCloudtentaclesSourceConfigPayload(
payload: JsonObject = {}
) {
const sourceKey =
String(payload.sourceKey || payload.key || "").trim() || "default";
export function normalizeAdminCloudtentaclesSourceConfigPayload(payload: JsonObject = {}) {
const sourceKey = String(payload.sourceKey || payload.key || '').trim() || 'default'
return {
key: sourceKey,
label: String(payload.label || "").trim(),
label: String(payload.label || '').trim(),
enabled: payload.enabled !== false,
baseUrl: String(payload.baseUrl || "").trim() || "https://123.207.217.176",
username: String(payload.username || "").trim(),
password: String(payload.password || "").trim(),
phone: String(payload.phone || "").trim(),
baseUrl: String(payload.baseUrl || '').trim() || 'https://123.207.217.176',
username: String(payload.username || '').trim(),
password: String(payload.password || '').trim(),
phone: String(payload.phone || '').trim(),
deviceId: normalizeCloudtentaclesDeviceId(payload.deviceId),
deviceType: normalizeCloudtentaclesDeviceType(payload.deviceType),
};
}
}
@@ -60,14 +60,18 @@ export async function listAdminKuaishouFeifeiProducts(payload: JsonObject = {})
page: Number(payload.page || 1) || 1,
perPage: Number(payload.perPage || payload.per_page || 20) || 20,
status: String(payload.status || 'on_sale').trim(),
supplyProductName: String(payload.supplyProductName || payload.supply_product_name || '').trim(),
supplyProductName: String(
payload.supplyProductName || payload.supply_product_name || '',
).trim(),
})
}
export async function syncAdminKuaishouFeifeiProductRules(payload: JsonObject = {}) {
const source = getAdminEditableKuaishouFeifeiConfig()
const status = String(payload.status || 'on_sale').trim() || 'on_sale'
const supplyProductName = String(payload.supplyProductName || payload.supply_product_name || '').trim()
const supplyProductName = String(
payload.supplyProductName || payload.supply_product_name || '',
).trim()
const products = await listAllKuaishouFeifeiProducts({
status,
supplyProductName,
@@ -185,10 +189,7 @@ function mapEffectiveKuaishouFeifeiConfig(config: ReturnType<typeof getKuaishouF
}
}
async function listAllKuaishouFeifeiProducts(input: {
status: string
supplyProductName: string
}) {
async function listAllKuaishouFeifeiProducts(input: { status: string; supplyProductName: string }) {
const perPage = 100
const firstPage = await listKuaishouFeifeiProducts({
page: 1,
@@ -14,16 +14,9 @@ import {
refreshKuaishouIndustryAccessToken,
} from '../../platforms/kuaishou-industry/token-service.js'
const SECRET_FIELDS = [
'appSecret',
'signSecret',
'messageSecret',
] as const
const SECRET_FIELDS = ['appSecret', 'signSecret', 'messageSecret'] as const
const SHOP_SECRET_FIELDS = [
'accessToken',
'refreshToken',
] as const
const SHOP_SECRET_FIELDS = ['accessToken', 'refreshToken'] as const
export function getAdminKuaishouIndustrySourceConfig() {
const config = getKuaishouIndustrySourceConfig()
@@ -38,15 +31,23 @@ export async function updateAdminKuaishouIndustrySourceConfig(payload: JsonObjec
const current = getKuaishouIndustrySourceConfig()
const saved = await saveKuaishouIndustrySourceConfig({
...current,
enabled: hasPayloadField(payload, 'enabled') ? payload.enabled !== false : current.enabled !== false,
enabled: hasPayloadField(payload, 'enabled')
? payload.enabled !== false
: current.enabled !== false,
baseUrl: readConfigString(payload, 'baseUrl', current.baseUrl),
authBaseUrl: readConfigString(payload, 'authBaseUrl', current.authBaseUrl),
redirectUri: readConfigString(payload, 'redirectUri', current.redirectUri, { allowBlank: true }),
scopes: normalizeScopeText(readConfigString(payload, 'scopes', current.scopes, { allowBlank: true })),
redirectUri: readConfigString(payload, 'redirectUri', current.redirectUri, {
allowBlank: true,
}),
scopes: normalizeScopeText(
readConfigString(payload, 'scopes', current.scopes, { allowBlank: true }),
),
authState: readConfigString(payload, 'authState', current.authState, { allowBlank: true }),
appKey: readConfigString(payload, 'appKey', current.appKey, { allowBlank: true }),
openId: readConfigString(payload, 'openId', current.openId, { allowBlank: true }),
grantedScopes: normalizeScopeText(readConfigString(payload, 'grantedScopes', current.grantedScopes, { allowBlank: true })),
grantedScopes: normalizeScopeText(
readConfigString(payload, 'grantedScopes', current.grantedScopes, { allowBlank: true }),
),
sellerId: readConfigString(payload, 'sellerId', current.sellerId, { allowBlank: true }),
provider: readConfigString(payload, 'provider', current.provider),
platform: readConfigString(payload, 'platform', current.platform),
@@ -245,33 +246,77 @@ function normalizeShopConfigPayload(
const payload = rawShop as JsonObject
const current = resolveCurrentShopConfig(payload, index, currentShops)
const sellerId = readConfigString(payload, 'sellerId', current?.sellerId || '', { allowBlank: true })
const shopId = readConfigString(payload, 'shopId', current?.shopId || sellerId, { allowBlank: true }) || sellerId
const shopName = readConfigString(payload, 'shopName', current?.shopName || '', { allowBlank: true })
const customShopName = readConfigString(payload, 'customShopName', current?.customShopName || '', { allowBlank: true })
const sellerId = readConfigString(payload, 'sellerId', current?.sellerId || '', {
allowBlank: true,
})
const shopId =
readConfigString(payload, 'shopId', current?.shopId || sellerId, { allowBlank: true }) ||
sellerId
const shopName = readConfigString(payload, 'shopName', current?.shopName || '', {
allowBlank: true,
})
const customShopName = readConfigString(
payload,
'customShopName',
current?.customShopName || '',
{ allowBlank: true },
)
const accessToken = readSecretString(payload, 'accessToken', current?.accessToken || '')
const refreshToken = readSecretString(payload, 'refreshToken', current?.refreshToken || '')
const openId = readConfigString(payload, 'openId', current?.openId || '', { allowBlank: true })
if (!sellerId && !shopId && !shopName && !customShopName && !accessToken && !refreshToken && !openId) {
if (
!sellerId &&
!shopId &&
!shopName &&
!customShopName &&
!accessToken &&
!refreshToken &&
!openId
) {
return null
}
return {
enabled: hasPayloadField(payload, 'enabled') ? payload.enabled !== false : current?.enabled !== false,
enabled: hasPayloadField(payload, 'enabled')
? payload.enabled !== false
: current?.enabled !== false,
sellerId,
shopId,
shopName,
customShopName,
authState: readConfigString(payload, 'authState', current?.authState || '', { allowBlank: true }),
authState: readConfigString(payload, 'authState', current?.authState || '', {
allowBlank: true,
}),
accessToken,
refreshToken,
accessTokenExpiresAt: readConfigString(payload, 'accessTokenExpiresAt', current?.accessTokenExpiresAt || '', { allowBlank: true }),
refreshTokenExpiresAt: readConfigString(payload, 'refreshTokenExpiresAt', current?.refreshTokenExpiresAt || '', { allowBlank: true }),
accessTokenExpiresAt: readConfigString(
payload,
'accessTokenExpiresAt',
current?.accessTokenExpiresAt || '',
{ allowBlank: true },
),
refreshTokenExpiresAt: readConfigString(
payload,
'refreshTokenExpiresAt',
current?.refreshTokenExpiresAt || '',
{ allowBlank: true },
),
openId,
grantedScopes: normalizeScopeText(readConfigString(payload, 'grantedScopes', current?.grantedScopes || '', { allowBlank: true })),
lastRefreshedAt: readConfigString(payload, 'lastRefreshedAt', current?.lastRefreshedAt || '', { allowBlank: true }),
lastRefreshError: readConfigString(payload, 'lastRefreshError', current?.lastRefreshError || '', { allowBlank: true }),
grantedScopes: normalizeScopeText(
readConfigString(payload, 'grantedScopes', current?.grantedScopes || '', {
allowBlank: true,
}),
),
lastRefreshedAt: readConfigString(payload, 'lastRefreshedAt', current?.lastRefreshedAt || '', {
allowBlank: true,
}),
lastRefreshError: readConfigString(
payload,
'lastRefreshError',
current?.lastRefreshError || '',
{ allowBlank: true },
),
...resolveShopSecretPatch(payload, current),
}
}
@@ -305,16 +350,14 @@ function resolveShopSecretPatch(
return patch
}
function readSecretString(
payload: JsonObject,
field: string,
fallback: string,
): string {
function readSecretString(payload: JsonObject, field: string, fallback: string): string {
const text = String(payload[field] || '').trim()
return text || fallback
}
function resolveAccessTokenStatus(config: Pick<KuaishouIndustrySourceConfig, 'accessToken' | 'accessTokenExpiresAt'>) {
function resolveAccessTokenStatus(
config: Pick<KuaishouIndustrySourceConfig, 'accessToken' | 'accessTokenExpiresAt'>,
) {
if (!config.accessToken) {
return {
status: 'missing',
@@ -94,23 +94,27 @@ function mapAdminNotificationConfig(config: JsonObject = {}) {
bark: {
enabled: bark.enabled !== false,
serverUrl: String(bark.serverUrl || 'https://api.day.app').trim(),
recipients: (Array.isArray(bark.recipients) ? bark.recipients : []).map((item: JsonObject) => ({
id: String(item.id || '').trim(),
name: String(item.name || '').trim(),
deviceKey: String(item.deviceKey || '').trim(),
deviceKeyMasked: maskSecret(item.deviceKey),
enabled: item.enabled !== false,
})),
recipients: (Array.isArray(bark.recipients) ? bark.recipients : []).map(
(item: JsonObject) => ({
id: String(item.id || '').trim(),
name: String(item.name || '').trim(),
deviceKey: String(item.deviceKey || '').trim(),
deviceKeyMasked: maskSecret(item.deviceKey),
enabled: item.enabled !== false,
}),
),
},
wpush: {
enabled: wpush.enabled !== false,
recipients: (Array.isArray(wpush.recipients) ? wpush.recipients : []).map((item: JsonObject) => ({
id: String(item.id || '').trim(),
name: String(item.name || '').trim(),
apiKey: String(item.apiKey || item.apikey || '').trim(),
apiKeyMasked: maskSecret(item.apiKey || item.apikey),
enabled: item.enabled !== false,
})),
recipients: (Array.isArray(wpush.recipients) ? wpush.recipients : []).map(
(item: JsonObject) => ({
id: String(item.id || '').trim(),
name: String(item.name || '').trim(),
apiKey: String(item.apiKey || item.apikey || '').trim(),
apiKeyMasked: maskSecret(item.apiKey || item.apikey),
enabled: item.enabled !== false,
}),
),
},
},
}
@@ -118,8 +122,7 @@ function mapAdminNotificationConfig(config: JsonObject = {}) {
function mapAdminScheduledJobsConfig(config: JsonObject = {}) {
const cloudtentaclesAccountMap = new Map(
listAdminCloudtentaclesMonitorAccounts()
.map((item) => [item.sourceKey, item]),
listAdminCloudtentaclesMonitorAccounts().map((item) => [item.sourceKey, item]),
)
return {
@@ -147,21 +150,23 @@ function listAdminCloudtentaclesMonitorAccounts() {
const sessionsConfig = getAllCloudtentaclesSessionStates()
const sessions: Record<string, JsonObject> = sessionsConfig.sessions || {}
return (Array.isArray(sourcesConfig.sources) ? sourcesConfig.sources : []).map((source) => {
const sourceKey = String(source.key || '').trim()
const session = sessions[sourceKey] || {}
const label = String(source.label || source.username || sourceKey).trim() || sourceKey
return (Array.isArray(sourcesConfig.sources) ? sourcesConfig.sources : [])
.map((source) => {
const sourceKey = String(source.key || '').trim()
const session = sessions[sourceKey] || {}
const label = String(source.label || source.username || sourceKey).trim() || sourceKey
return {
sourceKey,
label,
enabled: source.enabled !== false,
username: String(source.username || '').trim(),
phoneMasked: maskPhone(source.phone || session.phone),
hasToken: Boolean(String(session.token || '').trim()),
loggedInAt: String(session.loggedInAt || '').trim(),
}
}).filter((item) => item.sourceKey)
return {
sourceKey,
label,
enabled: source.enabled !== false,
username: String(source.username || '').trim(),
phoneMasked: maskPhone(source.phone || session.phone),
hasToken: Boolean(String(session.token || '').trim()),
loggedInAt: String(session.loggedInAt || '').trim(),
}
})
.filter((item) => item.sourceKey)
}
function mapScheduledJobCloudtentaclesAccounts(
@@ -193,30 +198,35 @@ function mapScheduledJobCloudtentaclesAccounts(
},
] as const
})
.filter(Boolean) as Array<readonly [string, {
sourceKey: string
label: string
enabled: boolean
assetThreshold: number
}]>,
.filter(Boolean) as Array<
readonly [
string,
{
sourceKey: string
label: string
enabled: boolean
assetThreshold: number
},
]
>,
)
// 读配置时合并全部 kuaishou-lewan 账号,避免前端/任务只看到历史 default。
const merged = Array.from(cloudtentaclesAccountMap.values()).map((option) => {
const sourceKey = String(option.sourceKey || '').trim()
const configured = configuredMap.get(sourceKey)
return {
sourceKey,
label: String(configured?.label || option.label || sourceKey).trim(),
enabled: configured
? configured.enabled !== false
: option.enabled !== false,
assetThreshold: normalizeNonNegativeNumber(
configured?.assetThreshold,
defaultAssetThreshold,
),
}
}).filter((item) => item.sourceKey)
const merged = Array.from(cloudtentaclesAccountMap.values())
.map((option) => {
const sourceKey = String(option.sourceKey || '').trim()
const configured = configuredMap.get(sourceKey)
return {
sourceKey,
label: String(configured?.label || option.label || sourceKey).trim(),
enabled: configured ? configured.enabled !== false : option.enabled !== false,
assetThreshold: normalizeNonNegativeNumber(
configured?.assetThreshold,
defaultAssetThreshold,
),
}
})
.filter((item) => item.sourceKey)
const mergedKeys = new Set(merged.map((item) => item.sourceKey))
for (const [sourceKey, configured] of configuredMap.entries()) {
@@ -1,8 +1,5 @@
import { buildClaimUrl, createTaskClaimToken } from '../claim/claim-service.js'
import {
maskCode as maskCodeValue,
maskPhone as maskPhoneValue,
} from '../../utils/masking.js'
import { maskCode as maskCodeValue, maskPhone as maskPhoneValue } from '../../utils/masking.js'
import type { TaskRow } from '../../types/repository/rows.js'
@@ -31,7 +28,9 @@ export function isRecoverableTaskSessionCloseError(error: ErrorLike | null | und
}
export function normalizeManualDispatchOutcome(value: unknown): 'delivered' | 'failed' {
const normalized = String(value || '').trim().toLowerCase()
const normalized = String(value || '')
.trim()
.toLowerCase()
if (normalized === 'failed') {
return 'failed'
@@ -25,18 +25,13 @@ import {
refreshFulfillmentRole,
returnFulfillmentNumber,
} from '../../fulfillment/executors/registry.js'
import {
isKuaishouCloudTask,
normalizeKuaishouCloudFlow,
} from './kuaishou-cloud-helpers.js'
import { isKuaishouCloudTask, normalizeKuaishouCloudFlow } from './kuaishou-cloud-helpers.js'
import type {
AdminEntityIdInput,
AdminViewerSessionInput,
} from '../../../types/admin/read-inputs.js'
import type {
AdminTaskKuaishouIndustryConsumeInput,
} from '../../../types/admin/write-inputs.js'
import type { AdminTaskKuaishouIndustryConsumeInput } from '../../../types/admin/write-inputs.js'
import type { AdminTaskActionResponse } from '../../../types/admin/write-models.js'
import type { KuaishouIndustryVoucherRow, TaskRow } from '../../../types/repository/rows.js'
@@ -443,8 +438,8 @@ export async function consumeAdminTaskKuaishouIndustryVoucher(
const taskContext = parseTaskContext(consumeTargetTask)
const hasCloudFulfillmentContext = Boolean(
taskContext.kuaishouCloudFulfillment &&
typeof taskContext.kuaishouCloudFulfillment === 'object' &&
!Array.isArray(taskContext.kuaishouCloudFulfillment),
typeof taskContext.kuaishouCloudFulfillment === 'object' &&
!Array.isArray(taskContext.kuaishouCloudFulfillment),
)
const flow = hasCloudFulfillmentContext
? normalizeKuaishouCloudFlow(taskContext.kuaishouCloudFulfillment)
@@ -491,7 +486,9 @@ export async function consumeAdminTaskKuaishouIndustryVoucher(
return { task: mapTaskActionPayload(updatedTask || consumeTargetTask) }
}
async function getRequiredIndustryVoucherForTask(task: TaskRow): Promise<KuaishouIndustryVoucherRow> {
async function getRequiredIndustryVoucherForTask(
task: TaskRow,
): Promise<KuaishouIndustryVoucherRow> {
const vouchers = await listKuaishouIndustryVouchersByTaskId(task.id)
const firstVoucher = vouchers[0]
if (firstVoucher) {
@@ -499,7 +496,9 @@ async function getRequiredIndustryVoucherForTask(task: TaskRow): Promise<Kuaisho
}
const taskContext = parseTaskContext(task)
const voucherContext = normalizeAdminTaskIndustryVoucherContext(taskContext.kuaishouIndustryVoucher)
const voucherContext = normalizeAdminTaskIndustryVoucherContext(
taskContext.kuaishouIndustryVoucher,
)
const voucherCode = String(voucherContext.voucherCode || voucherContext.eticketId || '').trim()
if (voucherCode) {
const voucher = await findKuaishouIndustryVoucherByCode(voucherCode, task.platform_order_id)
@@ -539,12 +538,12 @@ async function getRequiredIndustryVoucherForTask(task: TaskRow): Promise<Kuaisho
}
function normalizeAdminTaskIndustryVoucherContext(value: unknown): JsonObject {
return value && typeof value === 'object' && !Array.isArray(value)
? value as JsonObject
: {}
return value && typeof value === 'object' && !Array.isArray(value) ? (value as JsonObject) : {}
}
async function resolveIndustryVouchersForOrder(platformOrderId: string): Promise<KuaishouIndustryVoucherRow[]> {
async function resolveIndustryVouchersForOrder(
platformOrderId: string,
): Promise<KuaishouIndustryVoucherRow[]> {
const normalizedOid = String(platformOrderId || '').trim()
if (!normalizedOid) {
return []
@@ -52,7 +52,9 @@ export type PreparedKuaishouCloudBindResource = {
bindUrl: string
}
export function resolvePersistedCloudtentaclesContext(sourceKeys: unknown[]): CloudtentaclesContext {
export function resolvePersistedCloudtentaclesContext(
sourceKeys: unknown[],
): CloudtentaclesContext {
try {
return resolvePersistedCloudtentaclesContextBySourceKeys(sourceKeys)
} catch (error) {
@@ -4,14 +4,8 @@ import { createTaskEvent } from '../../../repositories/task-event-repo.js'
import { createHttpError } from '../../../utils/http.js'
import { nowIso } from '../../../utils/time.js'
import { TASK_STATUS } from '../../../domain/task-status.js'
import {
canViewerCloseTask,
createAdminViewerContext,
} from '../admin-read-shared-helpers.js'
import {
getRequiredTask,
mapTaskActionPayload,
} from '../admin-task-read-helpers.js'
import { canViewerCloseTask, createAdminViewerContext } from '../admin-read-shared-helpers.js'
import { getRequiredTask, mapTaskActionPayload } from '../admin-task-read-helpers.js'
import type {
AdminEntityIdInput,
@@ -62,16 +56,21 @@ export async function closeAdminTask(
})
}
await createTaskEvent(task.id, 'task_closed', {
closedBy: session
? {
userId: session.userId,
username: session.username,
role: session.role,
}
: null,
claimTokenClosed: claimTokenId > 0,
}, now)
await createTaskEvent(
task.id,
'task_closed',
{
closedBy: session
? {
userId: session.userId,
username: session.username,
role: session.role,
}
: null,
claimTokenClosed: claimTokenId > 0,
},
now,
)
return {
task: mapTaskActionPayload(updatedTask),
@@ -69,7 +69,11 @@ export function getClaimIdentityFromTask(task: Partial<TaskRow> | null | undefin
}
export function hasClaimExpectedUid(taskOrContext: unknown): boolean {
if (taskOrContext && typeof taskOrContext === 'object' && 'context_json' in (taskOrContext as object)) {
if (
taskOrContext &&
typeof taskOrContext === 'object' &&
'context_json' in (taskOrContext as object)
) {
return Boolean(getClaimIdentityFromTask(taskOrContext as TaskRow).expectedUid)
}
return Boolean(getClaimIdentityFromContext(taskOrContext).expectedUid)
@@ -158,16 +162,11 @@ export function buildClaimIdentityAdminSummary(
role.name || binding.roleName || options.taskRoleName || '',
).trim()
const boundUid = useShipped ? shippedUid : liveBoundUid
const boundRoleName = useShipped
? shippedRoleName || liveBoundRoleName
: liveBoundRoleName
const boundRoleName = useShipped ? shippedRoleName || liveBoundRoleName : liveBoundRoleName
const ready = Boolean(identity.expectedUid)
const uidMatched = ready && boundUid ? isClaimUidMatched(identity.expectedUid, boundUid) : null
const liveMismatched =
useShipped &&
liveBoundUid &&
shippedUid &&
!isClaimUidMatched(shippedUid, liveBoundUid)
useShipped && liveBoundUid && shippedUid && !isClaimUidMatched(shippedUid, liveBoundUid)
let note = '旧单或未提交 UID:用户须先打开领取页填写 UID,否则禁止自动发货'
if (ready) {
@@ -90,8 +90,5 @@ test('assertBoundUidMatchesExpected 在不匹配时抛错', () => {
})
test('assertClaimExpectedUidReady 要求 claimIdentity', () => {
assert.throws(
() => assertClaimExpectedUidReady({ context_json: '{}' }),
/填写游戏 UID/,
)
assert.throws(() => assertClaimExpectedUidReady({ context_json: '{}' }), /填写游戏 UID/)
})
@@ -42,9 +42,7 @@ export function resolveClaimTokenExpiration(
tokenTtlHours: unknown,
): string | null {
const ttlHours = Number(tokenTtlHours)
return Number.isFinite(ttlHours) && ttlHours > 0
? addHours(createdAt, ttlHours)
: null
return Number.isFinite(ttlHours) && ttlHours > 0 ? addHours(createdAt, ttlHours) : null
}
export function buildClaimUrl(token: string): string {
@@ -12,7 +12,7 @@ import { buildClaimIdentityPayload, getClaimIdentityFromContext } from './claim-
import { resolveKuaishouFeifeiH5UrlWithUid } from '../fulfillment/kuaishou-feifei/index.js'
import { normalizeAffiliateDashFlow } from '../fulfillment/affiliate-dash/index.js'
import type { ClaimTokenRow, OrderItemRow, OrderRow, TaskRow } from '../../types/repository/rows.js'
import { asJsonObject, type JsonObject } from '../../types/json.js'
import { asJsonObject, type JsonObject } from '../../types/json.js'
export const CLAIM_TERMINAL_STATUSES = new Set([TASK_STATUS.EXPIRED, TASK_STATUS.CLOSED])
type ClaimContext = {
@@ -148,16 +148,21 @@ export function buildClaimDetailPayload({ claimToken, task, order, orderItem }:
kuaishouFeifei: null as null,
result: task.redeemed_at
? {
resultCode: String(task.result_code || ''),
resultMessage: String(task.result_message || ''),
screenshotReady: false,
screenshotUrl: '',
}
resultCode: String(task.result_code || ''),
resultMessage: String(task.result_message || ''),
screenshotReady: false,
screenshotUrl: '',
}
: null,
}
}
function buildAffiliateDashClaimDetailPayload({ claimToken, task, order, orderItem }: ClaimContext) {
function buildAffiliateDashClaimDetailPayload({
claimToken,
task,
order,
orderItem,
}: ClaimContext) {
const context = parseTaskContext(task)
const claimIdentity = buildClaimIdentityPayload(context.claimIdentity)
const flow = normalizeAffiliateDashFlow(context.affiliateDash)
@@ -169,11 +174,13 @@ function buildAffiliateDashClaimDetailPayload({ claimToken, task, order, orderIt
skuCode: String(orderItem.sku_code || '').trim(),
quantity: Math.max(1, Number(orderItem.quantity || 1) || 1),
isBundle: false,
items: [{
cloudSkuId: 0,
name: productTitle,
quantity: Math.max(1, Number(orderItem.quantity || 1) || 1),
}],
items: [
{
cloudSkuId: 0,
name: productTitle,
quantity: Math.max(1, Number(orderItem.quantity || 1) || 1),
},
],
}
return {
@@ -218,16 +225,21 @@ function buildAffiliateDashClaimDetailPayload({ claimToken, task, order, orderIt
affiliateDash: flow,
result: task.redeemed_at
? {
resultCode: String(task.result_code || ''),
resultMessage: String(task.result_message || ''),
screenshotReady: false,
screenshotUrl: '',
}
resultCode: String(task.result_code || ''),
resultMessage: String(task.result_message || ''),
screenshotReady: false,
screenshotUrl: '',
}
: null,
}
}
function buildKuaishouFeifeiClaimDetailPayload({ claimToken, task, order, orderItem }: ClaimContext) {
function buildKuaishouFeifeiClaimDetailPayload({
claimToken,
task,
order,
orderItem,
}: ClaimContext) {
const context = parseTaskContext(task)
const claimIdentity = buildClaimIdentityPayload(context.claimIdentity)
const expectedUid = getClaimIdentityFromContext(context).expectedUid
@@ -237,11 +249,13 @@ function buildKuaishouFeifeiClaimDetailPayload({ claimToken, task, order, orderI
skuCode: String(orderItem.sku_code || '').trim(),
quantity: Math.max(1, Number(orderItem.quantity || 1) || 1),
isBundle: false,
items: [{
cloudSkuId: 0,
name: String(flow.productName || orderItem.sku_name || orderItem.sku_code || '').trim(),
quantity: Math.max(1, Number(orderItem.quantity || 1) || 1),
}],
items: [
{
cloudSkuId: 0,
name: String(flow.productName || orderItem.sku_name || orderItem.sku_code || '').trim(),
quantity: Math.max(1, Number(orderItem.quantity || 1) || 1),
},
],
}
return {
@@ -285,11 +299,11 @@ function buildKuaishouFeifeiClaimDetailPayload({ claimToken, task, order, orderI
kuaishouFeifei: flow,
result: task.redeemed_at
? {
resultCode: String(task.result_code || ''),
resultMessage: String(task.result_message || ''),
screenshotReady: false,
screenshotUrl: '',
}
resultCode: String(task.result_code || ''),
resultMessage: String(task.result_message || ''),
screenshotReady: false,
screenshotUrl: '',
}
: null,
}
}
@@ -362,13 +376,16 @@ function buildClaimProductPayload(
quantity: item.quantity,
}))
.filter((item) => item.name)
const normalizedDeliveryItems = deliveryItems.length > 0
? mergeClaimProductItems(deliveryItems)
: [{
cloudSkuId: 0,
name: displaySkuName,
quantity: Math.max(1, Number(orderItem.quantity || 1) || 1),
}]
const normalizedDeliveryItems =
deliveryItems.length > 0
? mergeClaimProductItems(deliveryItems)
: [
{
cloudSkuId: 0,
name: displaySkuName,
quantity: Math.max(1, Number(orderItem.quantity || 1) || 1),
},
]
return {
title: displaySkuName,
@@ -452,7 +469,9 @@ export function mapClaimKuaishouCloudFulfillment(task: TaskRow, order: OrderRow)
binding: {
prepareStatus: String(binding.prepareStatus || 'pending').trim() || 'pending',
cloudSourceKeys: Array.isArray(binding.cloudSourceKeys)
? binding.cloudSourceKeys.map((value: unknown) => String(value || '').trim()).filter(Boolean)
? binding.cloudSourceKeys
.map((value: unknown) => String(value || '').trim())
.filter(Boolean)
: [],
resolvedSourceKey: String(binding.resolvedSourceKey || '').trim(),
skuId: Number(binding.skuId || 0) || 0,
@@ -566,7 +585,9 @@ function normalizeClaimDeliveryItems(value: unknown, binding: JsonObject) {
const rawItems = Array.isArray(value) ? value : []
const items = rawItems
.map((item) => normalizeClaimDeliveryItem(item))
.filter((item): item is { cloudSkuId: number; cloudSkuName: string; quantity: number } => Boolean(item))
.filter((item): item is { cloudSkuId: number; cloudSkuName: string; quantity: number } =>
Boolean(item),
)
if (items.length > 0) {
return mergeClaimDeliveryItems(items)
@@ -577,11 +598,13 @@ function normalizeClaimDeliveryItems(value: unknown, binding: JsonObject) {
return []
}
return [{
cloudSkuId,
cloudSkuName: String(binding.skuName || '').trim(),
quantity: 1,
}]
return [
{
cloudSkuId,
cloudSkuName: String(binding.skuName || '').trim(),
quantity: 1,
},
]
}
function normalizeClaimDeliveryItem(value: unknown) {
@@ -633,12 +656,13 @@ async function expireClaimContext(claimToken: ClaimTokenRow, task: TaskRow) {
let nextTask = task
if (!isTaskFinalStatus(task.task_status)) {
nextTask = await updateTask(task.id, {
task_status: TASK_STATUS.EXPIRED,
user_action_status: TASK_STATUS.EXPIRED,
last_error: '领取链接已过期',
updated_at: now,
}) || task
nextTask =
(await updateTask(task.id, {
task_status: TASK_STATUS.EXPIRED,
user_action_status: TASK_STATUS.EXPIRED,
last_error: '领取链接已过期',
updated_at: now,
})) || task
}
return {
@@ -61,7 +61,9 @@ async function requireExecutorAction<T>(
}
function isKuaishouCloudBindingReady(flow: ReturnType<typeof normalizeKuaishouCloudFlow>) {
return flow.binding.prepareStatus === 'ready' && Boolean(String(flow.binding.bindUrl || '').trim())
return (
flow.binding.prepareStatus === 'ready' && Boolean(String(flow.binding.bindUrl || '').trim())
)
}
/**
@@ -160,9 +162,10 @@ async function verifyIndustryVoucherTicket(
shopId: context.order.shop_id,
shopName: context.order.shop_name,
autoConsumeEnabled: true,
consumedAt: voucherContext.status === 'CONSUMED'
? voucherContext.consumedAt || flow.consume.consumedAt || now
: flow.consume.consumedAt,
consumedAt:
voucherContext.status === 'CONSUMED'
? voucherContext.consumedAt || flow.consume.consumedAt || now
: flow.consume.consumedAt,
},
certInfo: {
certExpireType,
@@ -242,8 +245,10 @@ export async function getKuaishouCloudClaimDetail(token: unknown): Promise<Claim
// 已核销也要走 verify:内部会补 prepare 绑定资源
if (flow.consume.status !== 'success' || !isKuaishouCloudBindingReady(flow)) {
const now = nowIso()
const prepared: ClaimDetailPayload | null = await verifyIndustryVoucherTicket(context, now)
.catch(() => null)
const prepared: ClaimDetailPayload | null = await verifyIndustryVoucherTicket(
context,
now,
).catch(() => null)
if (prepared) {
return prepared
}
@@ -255,15 +260,14 @@ export async function getKuaishouCloudClaimDetail(token: unknown): Promise<Claim
const flow = normalizeKuaishouCloudFlow(taskContext.kuaishouCloudFulfillment)
const needsIndustryVoucherPrepare =
hasUsableIndustryVoucher(taskContext) &&
(
flow.ticket.status !== 'verified' ||
!isKuaishouCloudBindingReady(flow)
)
(flow.ticket.status !== 'verified' || !isKuaishouCloudBindingReady(flow))
if (needsIndustryVoucherPrepare) {
const now = nowIso()
const prepared: ClaimDetailPayload | null = await verifyIndustryVoucherTicket(context, now)
.catch(() => null)
const prepared: ClaimDetailPayload | null = await verifyIndustryVoucherTicket(
context,
now,
).catch(() => null)
if (prepared) {
return prepared
}
@@ -273,7 +277,8 @@ export async function getKuaishouCloudClaimDetail(token: unknown): Promise<Claim
if (!isKuaishouCloudMockTask(task)) {
const latestFlow = normalizeKuaishouCloudFlow(parseTaskContext(task).kuaishouCloudFulfillment)
if (
(latestFlow.ticket.status === 'verified' || hasUsableIndustryVoucher(parseTaskContext(task))) &&
(latestFlow.ticket.status === 'verified' ||
hasUsableIndustryVoucher(parseTaskContext(task))) &&
!isKuaishouCloudBindingReady(latestFlow)
) {
task = await ensureKuaishouCloudBindingPrepared(task, {
@@ -816,8 +821,8 @@ async function refreshAffiliateDashBindState(task: TaskRow): Promise<TaskRow | n
// (不依赖平台 mismatch 字段,防止平台未标记但实际账号不一致的情况)。
const localMismatch = Boolean(
boundAccount &&
expectedGameAccount &&
normalizeUid(boundAccount) !== normalizeUid(expectedGameAccount),
expectedGameAccount &&
normalizeUid(boundAccount) !== normalizeUid(expectedGameAccount),
)
const nextFlow = {
...flow,
@@ -923,10 +928,11 @@ function hasUsableIndustryVoucher(context: JsonObject = {}) {
}
function normalizeIndustryVoucherContext(value: unknown): JsonObject {
const source = value && typeof value === 'object' && !Array.isArray(value)
? value as JsonObject
: {}
const status = String(source.status || 'UNUSED').trim().toUpperCase()
const source =
value && typeof value === 'object' && !Array.isArray(value) ? (value as JsonObject) : {}
const status = String(source.status || 'UNUSED')
.trim()
.toUpperCase()
return {
...source,
@@ -1,9 +1,6 @@
import fs from 'node:fs'
import {
listAppConfigEntries,
upsertAppConfigEntry,
} from '../../repositories/app-config-repo.js'
import { listAppConfigEntries, upsertAppConfigEntry } from '../../repositories/app-config-repo.js'
import { readJsonFile } from '../../utils/json-file-store.js'
type NormalizeJsonValue<T> = (value: unknown) => T
@@ -67,8 +67,8 @@ const JSON_CONFIG_MIGRATION_ITEMS: JsonConfigMigrationItem[] = [
]
export async function migrateJsonConfigFilesToDatabase() {
const migrated: Array<{ configKey: string, filePath: string }> = []
const skipped: Array<{ configKey: string, filePath: string, reason: string }> = []
const migrated: Array<{ configKey: string; filePath: string }> = []
const skipped: Array<{ configKey: string; filePath: string; reason: string }> = []
for (const item of JSON_CONFIG_MIGRATION_ITEMS) {
const filePath = path.join(DATA_DIR, item.fileName)
@@ -65,7 +65,11 @@ export function isDevMockEnabled(env: NodeJS.ProcessEnv = process.env): boolean
if (String(env.ENABLE_DEV_MOCK || '').trim() === '1') {
return true
}
if (String(env.ENABLE_DEV_MOCK || '').trim().toLowerCase() === 'true') {
if (
String(env.ENABLE_DEV_MOCK || '')
.trim()
.toLowerCase() === 'true'
) {
return true
}
return !isProductionLike(env)
@@ -111,14 +115,16 @@ export function getDevMockStatus() {
}
}
export async function createLewanMockClaim(input: {
step?: unknown
orderNo?: unknown
productNo?: unknown
uid?: unknown
items?: unknown
frontendBaseUrl?: unknown
} = {}): Promise<DevMockCreateResult> {
export async function createLewanMockClaim(
input: {
step?: unknown
orderNo?: unknown
productNo?: unknown
uid?: unknown
items?: unknown
frontendBaseUrl?: unknown
} = {},
): Promise<DevMockCreateResult> {
assertDevMockEnabled()
const step = normalizeLewanStep(input.step)
@@ -192,7 +198,10 @@ export async function createLewanMockClaim(input: {
})
if (!task) {
throw createHttpError('履约任务创建失败', { statusCode: 500, errorCode: 'dev_mock_task_failed' })
throw createHttpError('履约任务创建失败', {
statusCode: 500,
errorCode: 'dev_mock_task_failed',
})
}
const claimToken = await createTaskClaimToken(task.id)
@@ -223,15 +232,17 @@ export async function createLewanMockClaim(input: {
})
}
export async function createFeifeiMockClaim(input: {
step?: unknown
orderNo?: unknown
productName?: unknown
productCode?: unknown
uid?: unknown
h5Url?: unknown
frontendBaseUrl?: unknown
} = {}): Promise<DevMockCreateResult> {
export async function createFeifeiMockClaim(
input: {
step?: unknown
orderNo?: unknown
productName?: unknown
productCode?: unknown
uid?: unknown
h5Url?: unknown
frontendBaseUrl?: unknown
} = {},
): Promise<DevMockCreateResult> {
assertDevMockEnabled()
const step = normalizeFeifeiStep(input.step)
@@ -296,7 +307,12 @@ export async function createFeifeiMockClaim(input: {
? TASK_STATUS.MANUAL_REVIEW
: TASK_STATUS.LINK_GENERATED,
deliveryStatus: step === 'completed' ? 'delivered' : 'pending',
resultCode: step === 'completed' ? 'kuaishou_feifei_completed' : step === 'failed' ? 'kuaishou_feifei_status_40' : '',
resultCode:
step === 'completed'
? 'kuaishou_feifei_completed'
: step === 'failed'
? 'kuaishou_feifei_status_40'
: '',
resultMessage: rechargeStatusLabel,
claimToken: '',
claimExpiresAt: null,
@@ -349,7 +365,10 @@ export async function createFeifeiMockClaim(input: {
})
if (!task) {
throw createHttpError('履约任务创建失败', { statusCode: 500, errorCode: 'dev_mock_task_failed' })
throw createHttpError('履约任务创建失败', {
statusCode: 500,
errorCode: 'dev_mock_task_failed',
})
}
const claimToken = await createTaskClaimToken(task.id)
@@ -383,14 +402,16 @@ type AffiliateDashMockStep = 'uid' | 'bind' | 'submitted' | 'completed' | 'faile
* 生成 affiliate-dash 领取 mock:不调用真实 affiliate-dash 平台。
* 上下文带 mock 标记,sync/refresh/submit 全部短路,领取页可走完四步。
*/
export async function createAffiliateDashMockClaim(input: {
step?: unknown
orderNo?: unknown
productName?: unknown
productSku?: unknown
uid?: unknown
frontendBaseUrl?: unknown
} = {}): Promise<DevMockCreateResult> {
export async function createAffiliateDashMockClaim(
input: {
step?: unknown
orderNo?: unknown
productName?: unknown
productSku?: unknown
uid?: unknown
frontendBaseUrl?: unknown
} = {},
): Promise<DevMockCreateResult> {
assertDevMockEnabled()
const step = normalizeAffiliateDashStep(input.step)
@@ -457,11 +478,7 @@ export async function createAffiliateDashMockClaim(input: {
? TASK_STATUS.REDEEMING
: TASK_STATUS.LINK_GENERATED,
deliveryStatus:
step === 'completed'
? 'delivered'
: step === 'submitted'
? 'delivering'
: 'pending',
step === 'completed' ? 'delivered' : step === 'submitted' ? 'delivering' : 'pending',
resultCode:
step === 'completed'
? 'affiliate_dash_delivered'
@@ -541,7 +558,10 @@ export async function createAffiliateDashMockClaim(input: {
})
if (!task) {
throw createHttpError('履约任务创建失败', { statusCode: 500, errorCode: 'dev_mock_task_failed' })
throw createHttpError('履约任务创建失败', {
statusCode: 500,
errorCode: 'dev_mock_task_failed',
})
}
const claimToken = await createTaskClaimToken(task.id)
@@ -573,9 +593,11 @@ export async function createAffiliateDashMockClaim(input: {
* 生成电子凭证列表测试数据,不调用快手接口。
* 覆盖未使用、已核销、已销毁和发码失败等运营页面常见状态。
*/
export async function createKuaishouIndustryVoucherMockData(input: {
sellerId?: unknown
} = {}): Promise<DevMockKuaishouIndustryVoucherResult> {
export async function createKuaishouIndustryVoucherMockData(
input: {
sellerId?: unknown
} = {},
): Promise<DevMockKuaishouIndustryVoucherResult> {
assertDevMockEnabled()
const now = nowIso()
@@ -668,11 +690,13 @@ export async function createKuaishouIndustryVoucherMockData(input: {
/**
* 生成 91 查询请求体(带签名),可选对本机发起查询。
*/
export async function buildOpen91QueryMock(input: {
orderNo?: unknown
execute?: unknown
baseUrl?: unknown
} = {}) {
export async function buildOpen91QueryMock(
input: {
orderNo?: unknown
execute?: unknown
baseUrl?: unknown
} = {},
) {
assertDevMockEnabled()
const orderNo = String(input.orderNo || '').trim()
@@ -767,7 +791,10 @@ async function ensureProfile(profileKey: string, name: string, requiresClaim: bo
})
if (!profile) {
throw createHttpError('履约配置创建失败', { statusCode: 500, errorCode: 'dev_mock_profile_failed' })
throw createHttpError('履约配置创建失败', {
statusCode: 500,
errorCode: 'dev_mock_profile_failed',
})
}
return profile
}
@@ -853,19 +880,19 @@ async function createBaseOrderItem(input: {
},
}
: {
source: OPEN_91_PROVIDER,
orderNo: input.orderNo,
productNo: input.productNo,
productName: input.productName,
shopId: input.consumeShopId,
cloudtentacles: {
matchMode: 'mock',
normalizedProductName: input.productName,
cloudSourceKeys: ['mock-cloudtentacles'],
resolvedSourceKey: 'mock-cloudtentacles',
deliveryItems: input.deliveryItems,
},
}
source: OPEN_91_PROVIDER,
orderNo: input.orderNo,
productNo: input.productNo,
productName: input.productName,
shopId: input.consumeShopId,
cloudtentacles: {
matchMode: 'mock',
normalizedProductName: input.productName,
cloudSourceKeys: ['mock-cloudtentacles'],
resolvedSourceKey: 'mock-cloudtentacles',
deliveryItems: input.deliveryItems,
},
}
const [orderItem] = await replaceOrderItems(input.orderId, [
{
@@ -885,7 +912,10 @@ async function createBaseOrderItem(input: {
])
if (!orderItem) {
throw createHttpError('订单商品创建失败', { statusCode: 500, errorCode: 'dev_mock_item_failed' })
throw createHttpError('订单商品创建失败', {
statusCode: 500,
errorCode: 'dev_mock_item_failed',
})
}
return orderItem
}
@@ -931,9 +961,7 @@ function buildLewanMockContext(input: {
configId: `mock:${input.productName}`,
internalSkuCode: input.productName,
internalSkuName: input.productName,
deliveryItems: input.deliveryItems.length
? input.deliveryItems
: [primaryItem],
deliveryItems: input.deliveryItems.length ? input.deliveryItems : [primaryItem],
mock: {
enabled: true,
orderNo: input.orderNo,
@@ -960,9 +988,7 @@ function buildLewanMockContext(input: {
vnKey: '1',
vnId: roleReady ? 900001 : 0,
vnPhone: roleReady ? '13800000000' : '',
bindUrl: roleReady
? `https://example.com/mock-kuaishou-cloud-bind/${input.orderNo}`
: '',
bindUrl: roleReady ? `https://example.com/mock-kuaishou-cloud-bind/${input.orderNo}` : '',
bindPreparedAt: roleReady ? input.timestamp : null,
bindExpiresAt: roleReady ? addHours(input.timestamp, 24) : null,
bindProbeAt: roleReady ? input.timestamp : null,
@@ -1190,7 +1216,10 @@ function buildAffiliateDashTips(step: AffiliateDashMockStep, uid: string) {
return [`打开领取链接,Step1 填 UID${uid}(或自定义)`, '提交后进入绑定步,mock 会给出二维码']
}
if (step === 'bind') {
return [`已预填 UID=${uid},绑定二维码为 mock 生成(扫描无效)`, '正常流程:扫码完成真实绑定后自动进入下一步']
return [
`已预填 UID=${uid},绑定二维码为 mock 生成(扫描无效)`,
'正常流程:扫码完成真实绑定后自动进入下一步',
]
}
if (step === 'submitted') {
return ['已模拟绑定成功,可点「提交发货」(mock 直接模拟发货成功)']
@@ -50,10 +50,7 @@ function buildTask(overrides: Partial<TaskRow> = {}): TaskRow {
test('buildAffiliateDashClientOrderNo uses the platform order number for a single task', () => {
const task = buildTask()
assert.equal(
buildAffiliateDashClientOrderNo(task, [task]),
'2622300001260431',
)
assert.equal(buildAffiliateDashClientOrderNo(task, [task]), '2622300001260431')
})
test('buildAffiliateDashClientOrderNo appends a stable position for split affiliate-dash tasks', () => {
@@ -52,10 +52,13 @@ export async function prepareAffiliateDashTask(task: TaskRow) {
}
if (!flow.sku) {
throw createHttpError('affiliate-dash 商品 sku 缺失,请配置 91 商品 → affiliate_dash sku 映射', {
statusCode: 409,
errorCode: 'affiliate_dash_sku_missing',
})
throw createHttpError(
'affiliate-dash 商品 sku 缺失,请配置 91 商品 → affiliate_dash sku 映射',
{
statusCode: 409,
errorCode: 'affiliate_dash_sku_missing',
},
)
}
const siblingTasks = await listTasksByOrderId(task.order_id)
@@ -97,13 +100,18 @@ export async function prepareAffiliateDashTask(task: TaskRow) {
)
}
if (!consumeResult.ok) {
logIntegration('[affiliate-dash]', '建单成功但电子凭证核销失败,delivered 回调将兜底重试', {
taskId: task.id,
orderNo: order.orderNo,
voucherCount: consumeResult.vouchers.length,
failedCount: consumeResult.failed.length,
errorMessage: consumeResult.failed[0]?.errorMessage || '',
}, { level: 'warn' })
logIntegration(
'[affiliate-dash]',
'建单成功但电子凭证核销失败,delivered 回调将兜底重试',
{
taskId: task.id,
orderNo: order.orderNo,
voucherCount: consumeResult.vouchers.length,
failedCount: consumeResult.failed.length,
errorMessage: consumeResult.failed[0]?.errorMessage || '',
},
{ level: 'warn' },
)
}
} else {
nextFlow.consumeStatus = 'not_required'
@@ -231,11 +239,16 @@ export async function syncAffiliateDashTaskStatus(
resultMessage = consumeResult.failed[0]?.errorMessage || '电子凭证核销失败,请人工处理'
lastError = resultMessage
nextFlow.consumeStatus = 'failed'
logIntegration('[affiliate-dash]', 'affiliate-dash 履约完成但核销失败', {
taskId: task.id,
orderNo: flow.orderNo,
errorMessage: resultMessage,
}, { level: 'warn' })
logIntegration(
'[affiliate-dash]',
'affiliate-dash 履约完成但核销失败',
{
taskId: task.id,
orderNo: flow.orderNo,
errorMessage: resultMessage,
},
{ level: 'warn' },
)
}
break
}
@@ -256,7 +269,12 @@ export async function syncAffiliateDashTaskStatus(
}
const isAlreadyTerminal = (
[TASK_STATUS.REDEEMED, TASK_STATUS.MANUAL_REVIEW, TASK_STATUS.CLOSED, TASK_STATUS.FAILED] as string[]
[
TASK_STATUS.REDEEMED,
TASK_STATUS.MANUAL_REVIEW,
TASK_STATUS.CLOSED,
TASK_STATUS.FAILED,
] as string[]
).includes(task.task_status)
// 终态保护:已收敛成功的任务,非 delivered 状态不允许降级(如轮询时平台详情短暂返回
// delivering/paid 会把 REDEEMED 打回 REDEEMING,导致结果页闪烁/倒退)。
@@ -316,9 +334,8 @@ export type AffiliateDashFlow = {
}
export function normalizeAffiliateDashFlow(value: unknown): AffiliateDashFlow {
const source = value && typeof value === 'object' && !Array.isArray(value)
? value as JsonObject
: {}
const source =
value && typeof value === 'object' && !Array.isArray(value) ? (value as JsonObject) : {}
return {
flowType: 'affiliate_dash',
@@ -397,10 +414,15 @@ async function preflightAffiliateDashWallet(sku: string) {
if ((error as { errorCode?: string })?.errorCode === 'affiliate_dash_wallet_not_enough') {
throw error
}
logIntegration('[affiliate-dash]', '余额预检失败(忽略,继续下单)', {
sku,
error: error instanceof Error ? error.message : String(error),
}, { level: 'warn' })
logIntegration(
'[affiliate-dash]',
'余额预检失败(忽略,继续下单)',
{
sku,
error: error instanceof Error ? error.message : String(error),
},
{ level: 'warn' },
)
}
}
@@ -6,11 +6,13 @@ import { resolveTaskDeliveryLink } from './delivery-link-service.js'
import type { TaskRow } from '../../types/repository/rows.js'
test('resolveTaskDeliveryLink 为 cloud 任务返回内部领取链接', async () => {
const result = await resolveTaskDeliveryLink(createTask({
executor_key: 'kuaishou_ct_assisted',
primary_claim_token: 'cloud-token',
primary_claim_expires_at: '2026-07-09T00:00:00.000Z',
}))
const result = await resolveTaskDeliveryLink(
createTask({
executor_key: 'kuaishou_ct_assisted',
primary_claim_token: 'cloud-token',
primary_claim_expires_at: '2026-07-09T00:00:00.000Z',
}),
)
assert.deepEqual(result, {
claimUrl: buildClaimUrl('cloud-token'),
@@ -19,11 +21,13 @@ test('resolveTaskDeliveryLink 为 cloud 任务返回内部领取链接', async (
})
test('resolveTaskDeliveryLink 兼容历史 kuaishou-industry cloud 任务', async () => {
const result = await resolveTaskDeliveryLink(createTask({
executor_key: 'kuaishou-industry',
claim_token: 'industry-token',
claim_expires_at: '2026-07-09T08:00:00.000Z',
}))
const result = await resolveTaskDeliveryLink(
createTask({
executor_key: 'kuaishou-industry',
claim_token: 'industry-token',
claim_expires_at: '2026-07-09T08:00:00.000Z',
}),
)
assert.deepEqual(result, {
claimUrl: buildClaimUrl('industry-token'),
@@ -32,18 +36,20 @@ test('resolveTaskDeliveryLink 兼容历史 kuaishou-industry cloud 任务', asyn
})
test('resolveTaskDeliveryLink 为 feifei 任务返回本站领取链接', async () => {
const result = await resolveTaskDeliveryLink(createTask({
executor_key: 'kuaishou_feifei',
primary_claim_token: 'feifei-token',
primary_claim_expires_at: '2026-07-09T12:00:00.000Z',
context_json: {
kuaishouFeifei: {
h5: {
rechargeUrl: 'https://feifei.example.com/h5/bind?code=very-long',
const result = await resolveTaskDeliveryLink(
createTask({
executor_key: 'kuaishou_feifei',
primary_claim_token: 'feifei-token',
primary_claim_expires_at: '2026-07-09T12:00:00.000Z',
context_json: {
kuaishouFeifei: {
h5: {
rechargeUrl: 'https://feifei.example.com/h5/bind?code=very-long',
},
},
},
},
}))
}),
)
assert.deepEqual(result, {
claimUrl: buildClaimUrl('feifei-token'),
@@ -52,9 +58,11 @@ test('resolveTaskDeliveryLink 为 feifei 任务返回本站领取链接', async
})
test('resolveTaskDeliveryLink 对人工履约任务返回 null', async () => {
const result = await resolveTaskDeliveryLink(createTask({
executor_key: 'manual_dispatch',
}))
const result = await resolveTaskDeliveryLink(
createTask({
executor_key: 'manual_dispatch',
}),
)
assert.equal(result, null)
})
@@ -4,8 +4,6 @@ import type { TaskRow } from '../../types/repository/rows.js'
export type TaskDeliveryLink = FulfillmentDeliveryLink
export async function resolveTaskDeliveryLink(
task: TaskRow,
): Promise<TaskDeliveryLink | null> {
export async function resolveTaskDeliveryLink(task: TaskRow): Promise<TaskDeliveryLink | null> {
return resolveFulfillmentDeliveryLink(task)
}
@@ -1,8 +1,5 @@
import { buildClaimUrl } from '../../claim/claim-service.js'
import {
shouldEnsureKuaishouCloudClaimLink,
TASK_STATUS,
} from '../../../domain/task-status.js'
import { shouldEnsureKuaishouCloudClaimLink, TASK_STATUS } from '../../../domain/task-status.js'
import {
confirmKuaishouCloudTaskRole,
dispatchKuaishouCloudFulfillmentTask,
@@ -95,9 +92,7 @@ async function prepareBinding(
options: FulfillmentActionOptions = {},
): Promise<FulfillmentActionResult> {
// 后台/显式 force:旧号可被 CT 回收,忽略退号失败并取新号+新绑链
const force =
options.force === true ||
String(options.source || '').includes('admin_')
const force = options.force === true || String(options.source || '').includes('admin_')
const result = await prepareKuaishouCloudFulfillmentTask(task, {
source: options.source || 'executor_prepare_binding',
actor: options.actor,
@@ -78,51 +78,30 @@ async function runExecutorAction(
return handler(task, options)
}
export function prepareFulfillmentBinding(
task: TaskRow,
options: FulfillmentActionOptions = {},
) {
export function prepareFulfillmentBinding(task: TaskRow, options: FulfillmentActionOptions = {}) {
return runExecutorAction(task, 'prepareBinding', options)
}
export function rebindFulfillmentRole(
task: TaskRow,
options: FulfillmentActionOptions = {},
) {
export function rebindFulfillmentRole(task: TaskRow, options: FulfillmentActionOptions = {}) {
return runExecutorAction(task, 'rebindRole', options)
}
export function refreshFulfillmentRole(
task: TaskRow,
options: FulfillmentActionOptions = {},
) {
export function refreshFulfillmentRole(task: TaskRow, options: FulfillmentActionOptions = {}) {
return runExecutorAction(task, 'refreshRole', options)
}
export function confirmFulfillmentRole(
task: TaskRow,
options: FulfillmentActionOptions = {},
) {
export function confirmFulfillmentRole(task: TaskRow, options: FulfillmentActionOptions = {}) {
return runExecutorAction(task, 'confirmRole', options)
}
export function redeemFulfillmentTask(
task: TaskRow,
options: FulfillmentActionOptions = {},
) {
export function redeemFulfillmentTask(task: TaskRow, options: FulfillmentActionOptions = {}) {
return runExecutorAction(task, 'redeemTask', options)
}
export function dispatchFulfillmentTask(
task: TaskRow,
options: FulfillmentActionOptions = {},
) {
export function dispatchFulfillmentTask(task: TaskRow, options: FulfillmentActionOptions = {}) {
return runExecutorAction(task, 'dispatchTask', options)
}
export function returnFulfillmentNumber(
task: TaskRow,
options: FulfillmentActionOptions = {},
) {
export function returnFulfillmentNumber(task: TaskRow, options: FulfillmentActionOptions = {}) {
return runExecutorAction(task, 'returnNumber', options)
}
@@ -10,7 +10,8 @@ export const FULFILLMENT_EXECUTOR_KEYS = {
} as const
export type FulfillmentExecutorKey =
(typeof FULFILLMENT_EXECUTOR_KEYS)[keyof typeof FULFILLMENT_EXECUTOR_KEYS] | (string & {})
| (typeof FULFILLMENT_EXECUTOR_KEYS)[keyof typeof FULFILLMENT_EXECUTOR_KEYS]
| (string & {})
export type FulfillmentDeliveryLink = {
claimUrl: string
@@ -53,10 +54,7 @@ export type FulfillmentActionResult = {
export type FulfillmentExecutor = {
key: FulfillmentExecutorKey
preparePaidTask?: (
task: TaskRow,
deps: FulfillmentPrepareDeps,
) => Promise<TaskRow | null>
preparePaidTask?: (task: TaskRow, deps: FulfillmentPrepareDeps) => Promise<TaskRow | null>
resolveDeliveryLink?: (task: TaskRow) => Promise<FulfillmentDeliveryLink | null>
/** lewan:准备绑定资源(虚拟号 / bindUrl) */
prepareBinding?: (
@@ -104,8 +102,10 @@ export function normalizeExecutorKey(value: unknown): FulfillmentExecutorKey {
export function isKuaishouCloudExecutor(value: unknown): boolean {
const executorKey = normalizeExecutorKey(value)
return executorKey === FULFILLMENT_EXECUTOR_KEYS.KUAISHOU_CLOUD ||
return (
executorKey === FULFILLMENT_EXECUTOR_KEYS.KUAISHOU_CLOUD ||
executorKey === FULFILLMENT_EXECUTOR_KEYS.KUAISHOU_INDUSTRY
)
}
export function isKuaishouFeifeiExecutor(value: unknown): boolean {
@@ -65,7 +65,8 @@ test('selectCloudtentaclesSourceForFulfillment 无号码时忽略残留固定账
},
},
{
resolveContextBySourceKeys: (sourceKeys) => contexts[String(sourceKeys[0]) as keyof typeof contexts],
resolveContextBySourceKeys: (sourceKeys) =>
contexts[String(sourceKeys[0]) as keyof typeof contexts],
listSourceLoadStats: async () => [
{ sourceKey: 'account-a', activeCount: 20, redeemingCount: 0 },
{ sourceKey: 'account-b', activeCount: 1, redeemingCount: 0 },
@@ -86,7 +87,8 @@ test('selectCloudtentaclesSourceForFulfillment 跳过真实占用已达20的账
const selection = await selectCloudtentaclesSourceForFulfillment(
{ binding: { cloudSourceKeys: ['account-a', 'account-b'] } },
{
resolveContextBySourceKeys: (sourceKeys) => contexts[String(sourceKeys[0]) as keyof typeof contexts],
resolveContextBySourceKeys: (sourceKeys) =>
contexts[String(sourceKeys[0]) as keyof typeof contexts],
listSourceLoadStats: async () => [],
listVirtualNumbers: async (payload) => {
const count = payload.sourceKey === 'account-a' ? 20 : 3
@@ -115,12 +117,14 @@ test('selectCloudtentaclesSourceForFulfillment 不把 status=0 的空闲号码
const selection = await selectCloudtentaclesSourceForFulfillment(
{ binding: { cloudSourceKeys: ['account-a', 'account-b'] } },
{
resolveContextBySourceKeys: (sourceKeys) => contexts[String(sourceKeys[0]) as keyof typeof contexts],
resolveContextBySourceKeys: (sourceKeys) =>
contexts[String(sourceKeys[0]) as keyof typeof contexts],
listSourceLoadStats: async () => [],
listVirtualNumbers: async (payload) => {
const items = payload.sourceKey === 'account-a'
? Array.from({ length: 20 }, (_, id) => ({ id: id + 1, status: 0 }))
: Array.from({ length: 3 }, (_, id) => ({ id: id + 1, status: 1 }))
const items =
payload.sourceKey === 'account-a'
? Array.from({ length: 20 }, (_, id) => ({ id: id + 1, status: 0 }))
: Array.from({ length: 3 }, (_, id) => ({ id: id + 1, status: 1 }))
return {
baseUrl: 'https://cloud.example.com',
key: '1',
@@ -145,7 +149,8 @@ test('selectCloudtentaclesSourceForFulfillment 真实占用优先于数据库历
const selection = await selectCloudtentaclesSourceForFulfillment(
{ binding: { cloudSourceKeys: ['account-a', 'account-b'] } },
{
resolveContextBySourceKeys: (sourceKeys) => contexts[String(sourceKeys[0]) as keyof typeof contexts],
resolveContextBySourceKeys: (sourceKeys) =>
contexts[String(sourceKeys[0]) as keyof typeof contexts],
listSourceLoadStats: async () => [
{ sourceKey: 'account-a', activeCount: 99, redeemingCount: 10 },
{ sourceKey: 'account-b', activeCount: 0, redeemingCount: 0 },
@@ -10,14 +10,8 @@ import {
} from './domain.js'
test('isKuaishouCloudDispatchSucceeded 识别 dispatch.success', () => {
assert.equal(
isKuaishouCloudDispatchSucceeded({ dispatch: { status: 'success' } }),
true,
)
assert.equal(
isKuaishouCloudDispatchSucceeded({ dispatch: { status: 'pending' } }),
false,
)
assert.equal(isKuaishouCloudDispatchSucceeded({ dispatch: { status: 'success' } }), true)
assert.equal(isKuaishouCloudDispatchSucceeded({ dispatch: { status: 'pending' } }), false)
})
test('isKuaishouCloudBindingMutationFrozendispatch.success 即使 status=waiting_binding 也冻结', () => {
@@ -1,5 +1,5 @@
import { createHttpError } from "../../../utils/http.js";
import { normalizeProductName } from "../product-resolution-service.js";
import { createHttpError } from '../../../utils/http.js'
import { normalizeProductName } from '../product-resolution-service.js'
import {
appointCloudtentaclesVirtualNumber,
backCloudtentaclesVirtualNumber,
@@ -7,107 +7,95 @@ import {
generateCloudtentaclesLoginCode,
getCloudtentaclesBindUrl,
verifyCloudtentaclesLoginCode,
} from "../../platforms/cloudtentacles/virtual-number-service.js";
import { KUAISHOU_CLOUD_FIXED_VN_KEY, type JsonObject } from "./domain.js";
import { logInfo } from "../../../utils/logger.js";
} from '../../platforms/cloudtentacles/virtual-number-service.js'
import { KUAISHOU_CLOUD_FIXED_VN_KEY, type JsonObject } from './domain.js'
import { logInfo } from '../../../utils/logger.js'
/** 账号号码配额占满后冷却:避免领取页轮询 / open-91 交付每轮都狂打上游 */
const APPOINT_QUOTA_COOLDOWN_MS = 60_000;
const appointQuotaCooldowns = new Map<string, number>();
const APPOINT_QUOTA_COOLDOWN_MS = 60_000
const appointQuotaCooldowns = new Map<string, number>()
export function resolveKuaishouCloudBindingResources(
flow: JsonObject,
{ skuItems = [], knapsackItems = [] }: { skuItems?: unknown[], knapsackItems?: unknown[] } = {}
{ skuItems = [], knapsackItems = [] }: { skuItems?: unknown[]; knapsackItems?: unknown[] } = {},
) {
const normalizedSkuItems = Array.isArray(skuItems)
? skuItems.filter(isCloudSkuLikeItem)
: [];
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 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;
? normalizedSkuItems.find((item) => Number(item.id || 0) === currentSkuId) || null
: null
const knapsackItemById =
currentSkuId > 0
? normalizedKnapsackItems.find(
(item) => Number(item.id || 0) === currentSkuId
) || null
: null;
? normalizedKnapsackItems.find((item) => Number(item.id || 0) === currentSkuId) || null
: null
if (skuItemById || knapsackItemById) {
const matchedItem = skuItemById || knapsackItemById;
const matchedItem = skuItemById || knapsackItemById
return {
skuId: Number(matchedItem?.id || 0) || 0,
skuName: String(currentSkuName || matchedItem?.name || "").trim(),
skuName: String(currentSkuName || matchedItem?.name || '').trim(),
vnKey: KUAISHOU_CLOUD_FIXED_VN_KEY,
skuItem: skuItemById,
knapsackItem: knapsackItemById,
resolvedByName: false,
};
}
}
const matchedSkuItem = findCloudItemByNames(
normalizedSkuItems,
nameCandidates
);
const matchedSkuItem = findCloudItemByNames(normalizedSkuItems, nameCandidates)
const matchedKnapsackItem = findCloudItemByNames(
normalizedKnapsackItems,
nameCandidates,
matchedSkuItem ? Number(matchedSkuItem.id || 0) : 0
);
const matchedItem = matchedSkuItem || matchedKnapsackItem;
matchedSkuItem ? Number(matchedSkuItem.id || 0) : 0,
)
const matchedItem = matchedSkuItem || matchedKnapsackItem
return {
skuId: Number(matchedItem?.id || 0) || 0,
skuName: String(currentSkuName || matchedItem?.name || "").trim(),
skuName: String(currentSkuName || matchedItem?.name || '').trim(),
vnKey: KUAISHOU_CLOUD_FIXED_VN_KEY,
skuItem: matchedSkuItem,
knapsackItem: matchedKnapsackItem,
resolvedByName: Boolean(matchedItem),
};
}
}
export function resolveKuaishouCloudVnKeyCandidates(_input: JsonObject = {}) {
return [KUAISHOU_CLOUD_FIXED_VN_KEY];
return [KUAISHOU_CLOUD_FIXED_VN_KEY]
}
export async function prepareKuaishouCloudBindResourceWithFallback(input: JsonObject = {}) {
const { cloudContext = {}, vnKeyCandidates = [] } = input;
const candidates = Array.isArray(vnKeyCandidates) ? vnKeyCandidates : [];
const sourceKey = String(cloudContext.resolvedSourceKey || "").trim();
let lastError = null;
const { cloudContext = {}, vnKeyCandidates = [] } = input
const candidates = Array.isArray(vnKeyCandidates) ? vnKeyCandidates : []
const sourceKey = String(cloudContext.resolvedSourceKey || '').trim()
let lastError = null
for (const vnKey of candidates) {
let vnId = 0;
let vnPhone = "";
const cooldownKey = `${sourceKey}|${vnKey}`;
let vnId = 0
let vnPhone = ''
const cooldownKey = `${sourceKey}|${vnKey}`
const cooldownUntil = appointQuotaCooldowns.get(cooldownKey) || 0;
const cooldownUntil = appointQuotaCooldowns.get(cooldownKey) || 0
if (cooldownUntil > Date.now()) {
throw createHttpError(
"账号虚拟号配额已满,请先退回已占用号码或稍后重试",
{
statusCode: 409,
errorCode: "cloudtentacles_vn_quota_cooldown",
}
);
throw createHttpError('账号虚拟号配额已满,请先退回已占用号码或稍后重试', {
statusCode: 409,
errorCode: 'cloudtentacles_vn_quota_cooldown',
})
}
try {
const appointed = await appointCloudtentaclesVirtualNumber({
...cloudContext,
key: vnKey,
});
vnId = Number(appointed.item?.id || 0);
vnPhone = String(appointed.item?.phone || "").trim();
})
vnId = Number(appointed.item?.id || 0)
vnPhone = String(appointed.item?.phone || '').trim()
logInfo('[kuaishou-cloud/binding]', '虚拟号申请成功', {
sourceKey,
@@ -115,51 +103,51 @@ export async function prepareKuaishouCloudBindResourceWithFallback(input: JsonOb
purpose: String(input.purpose || 'prepare_binding'),
vnKey,
vnId,
});
})
if (!vnId || !vnPhone) {
throw createHttpError("申请虚拟号成功但返回数据不完整", {
throw createHttpError('申请虚拟号成功但返回数据不完整', {
statusCode: 502,
errorCode: "kuaishou_cloud_invalid_vn",
});
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(),
};
bindUrl: String(bindUrlResult.bindUrl || '').trim(),
}
} catch (error) {
lastError = error;
lastError = error
if (isAppointQuotaExhaustedError(error)) {
appointQuotaCooldowns.set(cooldownKey, Date.now() + APPOINT_QUOTA_COOLDOWN_MS);
appointQuotaCooldowns.set(cooldownKey, Date.now() + APPOINT_QUOTA_COOLDOWN_MS)
}
if (vnId > 0) {
@@ -168,112 +156,108 @@ export async function prepareKuaishouCloudBindResourceWithFallback(input: JsonOb
...cloudContext,
key: vnKey,
id: vnId,
});
})
} catch {
// 退号失败保留主错误
}
}
if (!isRecoverableKuaishouCloudVnKeyError(error)) {
throw error;
throw error
}
}
}
throw (
lastError ||
createHttpError("没有找到可用的 VN Key", {
createHttpError('没有找到可用的 VN Key', {
statusCode: 409,
errorCode: "kuaishou_cloud_missing_binding_config",
errorCode: 'kuaishou_cloud_missing_binding_config',
})
);
)
}
function collectKuaishouCloudNameCandidates(flow: JsonObject) {
return Array.from(
new Set(
[
String(flow?.binding?.skuName || "").trim(),
String(flow?.internalSkuName || "").trim(),
String(flow?.internalSkuCode || "").trim(),
].filter(Boolean)
)
);
String(flow?.binding?.skuName || '').trim(),
String(flow?.internalSkuName || '').trim(),
String(flow?.internalSkuCode || '').trim(),
].filter(Boolean),
),
)
}
function findCloudItemByNames(items: JsonObject[], nameCandidates: string[], preferredId = 0) {
const normalizedItems = Array.isArray(items) ? items : [];
const normalizedItems = Array.isArray(items) ? items : []
const normalizedNames = nameCandidates
.map((item: string) => ({
raw: String(item || "").trim(),
raw: String(item || '').trim(),
normalized: normalizeProductName(item),
}))
.filter((item) => item.raw && item.normalized);
.filter((item) => item.raw && item.normalized)
if (normalizedNames.length === 0 || normalizedItems.length === 0) {
return preferredId > 0
? normalizedItems.find((item: JsonObject) => Number(item.id || 0) === preferredId) ||
null
: null;
? normalizedItems.find((item: JsonObject) => Number(item.id || 0) === preferredId) || null
: null
}
if (preferredId > 0) {
const preferred =
normalizedItems.find((item: JsonObject) => Number(item.id || 0) === preferredId) ||
null;
normalizedItems.find((item: JsonObject) => Number(item.id || 0) === preferredId) || null
if (preferred) {
return preferred;
return preferred
}
}
const exactMatches = normalizedItems.filter((item: JsonObject) => {
const itemName = normalizeProductName(item.name);
const itemName = normalizeProductName(item.name)
return normalizedNames.some(
(candidate: { normalized: string }) => candidate.normalized === itemName
);
});
(candidate: { normalized: string }) => candidate.normalized === itemName,
)
})
if (exactMatches.length > 0) {
return exactMatches[0];
return exactMatches[0]
}
const partialMatches = normalizedItems.filter((item: JsonObject) => {
const itemName = normalizeProductName(item.name);
const itemName = normalizeProductName(item.name)
return normalizedNames.some(
(candidate: { normalized: string }) =>
itemName.includes(candidate.normalized) ||
candidate.normalized.includes(itemName)
);
});
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];
(left, right) => String(left.name || '').length - String(right.name || '').length,
)[0]
}
return null;
return null
}
function isCloudSkuLikeItem(item: unknown): item is JsonObject {
const current = item && typeof item === "object" ? item as JsonObject : {};
return Number(current.id || 0) > 0;
const current = item && typeof item === 'object' ? (item as JsonObject) : {}
return Number(current.id || 0) > 0
}
function isAppointQuotaExhaustedError(error: unknown) {
const current = error && typeof error === "object" ? error as JsonObject : {};
const current = error && typeof error === 'object' ? (error as JsonObject) : {}
return (
String(current.errorCode || current.code || "").trim() ===
"cloudtentacles_vn_appoint_failed" &&
String(current.message || "").trim().includes("最多同时占用")
);
String(current.errorCode || current.code || '').trim() === 'cloudtentacles_vn_appoint_failed' &&
String(current.message || '')
.trim()
.includes('最多同时占用')
)
}
function isRecoverableKuaishouCloudVnKeyError(error: unknown) {
const current = error && typeof error === "object" ? error as JsonObject : {};
const errorCode = String(current.errorCode || current.code || "").trim();
const errorMessage = String(current.message || "").trim();
const current = error && typeof error === 'object' ? (error as JsonObject) : {}
const errorCode = String(current.errorCode || current.code || '').trim()
const errorMessage = String(current.message || '').trim()
return (
errorCode === "cloudtentacles_vn_bind_url_failed" &&
errorMessage.includes("不支持的游戏类型")
);
errorCode === 'cloudtentacles_vn_bind_url_failed' && errorMessage.includes('不支持的游戏类型')
)
}
@@ -1,11 +1,11 @@
import { createHttpError } from "../../../utils/http.js";
import { createHttpError } from '../../../utils/http.js'
import {
normalizeCloudtentaclesDeviceId,
normalizeCloudtentaclesDeviceType,
} from "../../platforms/cloudtentacles/defaults.js";
import { getCloudtentaclesSourceByKey } from "../../platforms/cloudtentacles/source-config-service.js";
import { getCloudtentaclesSessionStateByKey } from "../../platforms/cloudtentacles/session-state-service.js";
import { normalizeStringArray } from "./domain.js";
} from '../../platforms/cloudtentacles/defaults.js'
import { getCloudtentaclesSourceByKey } from '../../platforms/cloudtentacles/source-config-service.js'
import { getCloudtentaclesSessionStateByKey } from '../../platforms/cloudtentacles/session-state-service.js'
import { normalizeStringArray } from './domain.js'
/**
* 严格模式:只解析列表中第一个账号(调用方均把任务实际取号账号 resolvedSourceKey 放首位),
@@ -14,21 +14,19 @@ import { normalizeStringArray } from "./domain.js";
* 退号、取链、发货等「操作任务已有号码」的场景必须使用此函数。
* 将实际使用的 resolvedSourceKey 也返回,确保后续操作使用同一个 sourceKey。
*/
export function resolvePersistedCloudtentaclesContextBySourceKeys(
sourceKeys: unknown[] = []
) {
const candidates = [...new Set(normalizeStringArray(sourceKeys))];
export function resolvePersistedCloudtentaclesContextBySourceKeys(sourceKeys: unknown[] = []) {
const candidates = [...new Set(normalizeStringArray(sourceKeys))]
if (candidates.length === 0) {
throw createHttpError(
"cloudtentacles 没有可用账号,请先到平台配置完成账号配置",
{ statusCode: 409, errorCode: "kuaishou_cloud_no_source_keys" }
);
throw createHttpError('cloudtentacles 没有可用账号,请先到平台配置完成账号配置', {
statusCode: 409,
errorCode: 'kuaishou_cloud_no_source_keys',
})
}
const sourceKey = String(candidates[0] || "").trim();
const sourceKey = String(candidates[0] || '').trim()
return resolveSingleCloudtentaclesAccountContext(sourceKey);
return resolveSingleCloudtentaclesAccountContext(sourceKey)
}
/**
@@ -36,77 +34,69 @@ export function resolvePersistedCloudtentaclesContextBySourceKeys(
* 仅用于「取新号」场景(新号码归属被选中的账号,不会产生跨账号孤儿),
* 以及 account-selector 单候选解析。严禁用于退号/取链/发货等操作已有号码的场景。
*/
export function resolvePersistedCloudtentaclesContextWithFallback(
sourceKeys: unknown[] = []
) {
const candidates = [...new Set(normalizeStringArray(sourceKeys))];
export function resolvePersistedCloudtentaclesContextWithFallback(sourceKeys: unknown[] = []) {
const candidates = [...new Set(normalizeStringArray(sourceKeys))]
let lastError: unknown = null;
let lastError: unknown = null
for (const sourceKey of candidates) {
try {
return resolveSingleCloudtentaclesAccountContext(sourceKey);
return resolveSingleCloudtentaclesAccountContext(sourceKey)
} catch (error) {
lastError = error;
lastError = error
}
}
throw (
lastError ||
createHttpError(
"所有 cloudtentacles 账号均不可用,请先到平台配置完成登录校验",
{ statusCode: 409, errorCode: "kuaishou_cloud_all_source_keys_exhausted" }
)
);
createHttpError('所有 cloudtentacles 账号均不可用,请先到平台配置完成登录校验', {
statusCode: 409,
errorCode: 'kuaishou_cloud_all_source_keys_exhausted',
})
)
}
function resolveSingleCloudtentaclesAccountContext(sourceKey: string) {
const source = getCloudtentaclesSourceByKey(sourceKey);
const session = getCloudtentaclesSessionStateByKey(sourceKey);
const source = getCloudtentaclesSourceByKey(sourceKey)
const session = getCloudtentaclesSessionStateByKey(sourceKey)
if (!source) {
throw createHttpError(
`cloudtentacles 账号 ${sourceKey} 不存在(取号账号已变更或被删除,无法继续操作其虚拟号)`,
{ statusCode: 409, errorCode: "kuaishou_cloud_source_missing" }
);
{ statusCode: 409, errorCode: 'kuaishou_cloud_source_missing' },
)
}
if (source.enabled === false) {
throw createHttpError(
`cloudtentacles 账号 ${sourceKey} 已停用`,
{ statusCode: 409, errorCode: "kuaishou_cloud_source_disabled" }
);
throw createHttpError(`cloudtentacles 账号 ${sourceKey} 已停用`, {
statusCode: 409,
errorCode: 'kuaishou_cloud_source_disabled',
})
}
if (!session) {
throw createHttpError(
`cloudtentacles 账号 ${sourceKey} 没有可用 token`,
{ statusCode: 409, errorCode: "kuaishou_cloud_missing_cloud_token" }
);
throw createHttpError(`cloudtentacles 账号 ${sourceKey} 没有可用 token`, {
statusCode: 409,
errorCode: 'kuaishou_cloud_missing_cloud_token',
})
}
const token = String(session.token || "").trim();
const token = String(session.token || '').trim()
if (!token) {
throw createHttpError(
`cloudtentacles 账号 ${sourceKey} 没有可用 token`,
{ statusCode: 409, errorCode: "kuaishou_cloud_missing_cloud_token" }
);
throw createHttpError(`cloudtentacles 账号 ${sourceKey} 没有可用 token`, {
statusCode: 409,
errorCode: 'kuaishou_cloud_missing_cloud_token',
})
}
return {
baseUrl:
String(session.baseUrl || source.baseUrl || "").trim() ||
"https://123.207.217.176",
baseUrl: String(session.baseUrl || source.baseUrl || '').trim() || 'https://123.207.217.176',
token,
deviceId: normalizeCloudtentaclesDeviceId(
session.deviceId || source.deviceId
),
deviceType: normalizeCloudtentaclesDeviceType(
session.deviceType ?? source.deviceType
),
deviceId: normalizeCloudtentaclesDeviceId(session.deviceId || source.deviceId),
deviceType: normalizeCloudtentaclesDeviceType(session.deviceType ?? source.deviceType),
// 透传给 http-client / 错误包装,便于定位跨账号问题
sourceKey,
resolvedSourceKey: sourceKey,
accountLabel: String(source.label || sourceKey).trim() || sourceKey,
};
}
}
@@ -47,7 +47,8 @@ export async function confirmKuaishouCloudTaskRole(
}
const expectedUid = assertClaimExpectedUidReady(task)
const mockMode = isKuaishouCloudMockTask(task) || isKuaishouCloudMockContext(parseTaskContext(task))
const mockMode =
isKuaishouCloudMockTask(task) || isKuaishouCloudMockContext(parseTaskContext(task))
const refreshed = mockMode
? { task }
@@ -58,9 +59,7 @@ export async function confirmKuaishouCloudTaskRole(
forceProbe: options.forceProbe !== false,
})
const flow = normalizeKuaishouCloudFlow(
parseTaskContext(refreshed.task).kuaishouCloudFulfillment,
)
const flow = normalizeKuaishouCloudFlow(parseTaskContext(refreshed.task).kuaishouCloudFulfillment)
if (!flow.binding.vnPhone || !flow.binding.roleName || !flow.binding.roleId) {
throw createHttpError('角色信息还未刷新到系统,请完成绑定后稍等片刻再试', {
@@ -68,8 +68,7 @@ export async function dispatchKuaishouCloudFulfillmentTask(
const industryVoucherCode = String(
voucherContext.voucherCode || voucherContext.eticketId || '',
).trim()
const hasIndustryVoucherForDispatch =
Boolean(industryVoucherCode) || isIndustryEVoucherTask(task)
const hasIndustryVoucherForDispatch = Boolean(industryVoucherCode) || isIndustryEVoucherTask(task)
const resolvedTicketCode = persistedTicketCode || industryVoucherCode
if (!resolvedTicketCode && !hasIndustryVoucherForDispatch) {
@@ -190,9 +189,7 @@ export async function dispatchKuaishouCloudFulfillmentTask(
purchaseTriggered: stockResult.purchaseTriggered,
assetBefore: stockResult.assetBefore,
assetAfter: stockResult.assetAfter,
purchaseAt: stockResult.purchaseTriggered
? now
: syncedFlow.purchase.purchaseAt,
purchaseAt: stockResult.purchaseTriggered ? now : syncedFlow.purchase.purchaseAt,
items: stockResult.items,
},
},
@@ -255,9 +252,7 @@ export async function dispatchKuaishouCloudFulfillmentTask(
return {
task: updatedTask,
flow: normalizeKuaishouCloudFlow(
parseTaskContext(updatedTask).kuaishouCloudFulfillment,
),
flow: normalizeKuaishouCloudFlow(parseTaskContext(updatedTask).kuaishouCloudFulfillment),
}
}
@@ -1,75 +1,65 @@
import { createTaskEvent } from "../../../repositories/task-event-repo.js";
import { updateTask } from "../../../repositories/task-repo.js";
import type { TaskRow } from "../../../types/repository/rows.js";
import { createHttpError } from "../../../utils/http.js";
import { getCloudtentaclesBindInfo } from "../../platforms/cloudtentacles/virtual-number-service.js";
import { createTaskEvent } from '../../../repositories/task-event-repo.js'
import { updateTask } from '../../../repositories/task-repo.js'
import type { TaskRow } from '../../../types/repository/rows.js'
import { createHttpError } from '../../../utils/http.js'
import { getCloudtentaclesBindInfo } from '../../platforms/cloudtentacles/virtual-number-service.js'
import {
assertLewanAutoFulfillmentUidReady,
isClaimUidMatched,
normalizeClaimUid,
} from "../../claim/claim-identity.js";
import { asJsonObject } from "../../../types/json.js";
} from '../../claim/claim-identity.js'
import { asJsonObject } from '../../../types/json.js'
import {
normalizeKuaishouCloudFlow,
normalizeKuaishouCloudRoleInfo,
type JsonObject,
} from "./domain.js";
} from './domain.js'
export async function syncKuaishouCloudRoleInfoBeforeDispatch(
task: TaskRow,
input: JsonObject = {}
input: JsonObject = {},
) {
const now = String(input.now || "").trim() || new Date().toISOString();
const source =
String(input.source || "system_before_dispatch").trim() ||
"system_before_dispatch";
const now = String(input.now || '').trim() || new Date().toISOString()
const source = String(input.source || 'system_before_dispatch').trim() || 'system_before_dispatch'
const errorCodePrefix =
String(input.errorCodePrefix || "kuaishou_cloud").trim() ||
"kuaishou_cloud";
const cloudContext =
asJsonObject(input.cloudContext);
const taskContext =
asJsonObject(input.taskContext);
const flow = normalizeKuaishouCloudFlow(
input.flow || taskContext.kuaishouCloudFulfillment
);
String(input.errorCodePrefix || 'kuaishou_cloud').trim() || 'kuaishou_cloud'
const cloudContext = asJsonObject(input.cloudContext)
const taskContext = asJsonObject(input.taskContext)
const flow = normalizeKuaishouCloudFlow(input.flow || taskContext.kuaishouCloudFulfillment)
// 新策略:lewan 自动发货必须有 expectedUid,旧单无 UID 不可静默回落
const expectedUid = assertLewanAutoFulfillmentUidReady(task, {
allowMockSkip: true,
errorCodePrefix,
});
})
if (!flow.binding.vnId || !flow.binding.vnKey) {
throw createHttpError("当前任务缺少可同步角色的虚拟号信息,暂时不能发货", {
throw createHttpError('当前任务缺少可同步角色的虚拟号信息,暂时不能发货', {
statusCode: 409,
errorCode: `${errorCodePrefix}_dispatch_missing_bind_info_context`,
});
})
}
const bindInfoResult = await getCloudtentaclesBindInfo({
...cloudContext,
key: flow.binding.vnKey,
id: flow.binding.vnId,
});
const roleInfo = normalizeKuaishouCloudRoleInfo(bindInfoResult.bindInfo);
})
const roleInfo = normalizeKuaishouCloudRoleInfo(bindInfoResult.bindInfo)
if (!roleInfo.name || !roleInfo.rid) {
throw createHttpError(
"cloudtentacles 还没有同步到客户绑定角色,请稍后刷新角色信息后再发货",
{
statusCode: 409,
errorCode: `${errorCodePrefix}_dispatch_role_not_ready`,
}
);
throw createHttpError('cloudtentacles 还没有同步到客户绑定角色,请稍后刷新角色信息后再发货', {
statusCode: 409,
errorCode: `${errorCodePrefix}_dispatch_role_not_ready`,
})
}
const liveBoundUid = normalizeClaimUid(roleInfo.rid);
const liveBoundUid = normalizeClaimUid(roleInfo.rid)
if (expectedUid && !isClaimUidMatched(expectedUid, liveBoundUid)) {
await createTaskEvent(
task.id,
"kuaishou_cloud_dispatch_uid_mismatch",
'kuaishou_cloud_dispatch_uid_mismatch',
{
source,
vnId: flow.binding.vnId,
@@ -78,16 +68,16 @@ export async function syncKuaishouCloudRoleInfoBeforeDispatch(
cloudtentaclesRoleId: roleInfo.rid,
actor: input.actor || null,
},
now
);
now,
)
throw createHttpError(
`cloudtentacles 当前绑定角色 ID${roleInfo.rid})与用户填写 UID${expectedUid})不一致,不能发货`,
{
statusCode: 409,
errorCode: `${errorCodePrefix}_dispatch_uid_mismatch`,
}
);
},
)
}
const nextFlow = normalizeKuaishouCloudFlow({
@@ -99,36 +89,36 @@ export async function syncKuaishouCloudRoleInfoBeforeDispatch(
},
role: {
...flow.role,
status: "ready",
status: 'ready',
name: roleInfo.name,
rid: roleInfo.rid,
refreshedAt: now,
errorMessage: "",
errorMessage: '',
rawInfo: roleInfo.rawInfo,
},
});
})
const nextContext = {
...taskContext,
kuaishouCloudFulfillment: nextFlow,
};
}
const updatedTask = await updateTask(task.id, {
role_id: roleInfo.rid,
role_name: roleInfo.name,
context_json: JSON.stringify(nextContext),
updated_at: now,
});
})
if (!updatedTask) {
throw createHttpError("发货前角色信息同步失败", {
throw createHttpError('发货前角色信息同步失败', {
statusCode: 500,
errorCode: `${errorCodePrefix}_dispatch_role_update_failed`,
});
})
}
await createTaskEvent(
task.id,
"kuaishou_cloud_role_info_synced_before_dispatch",
'kuaishou_cloud_role_info_synced_before_dispatch',
{
source,
roleName: roleInfo.name,
@@ -136,15 +126,13 @@ export async function syncKuaishouCloudRoleInfoBeforeDispatch(
vnId: flow.binding.vnId,
actor: input.actor || null,
},
now
);
now,
)
return {
task: updatedTask,
taskContext: nextContext,
flow: nextFlow,
roleInfo,
};
}
}
@@ -51,9 +51,8 @@ export function resolveDispatchDeliveryItems(flow: JsonObject): DispatchDelivery
return items
}
const binding = flow.binding && typeof flow.binding === 'object'
? (flow.binding as JsonObject)
: {}
const binding =
flow.binding && typeof flow.binding === 'object' ? (flow.binding as JsonObject) : {}
const fallbackSkuId = Number(binding.skuId || 0) || 0
if (!fallbackSkuId) {
return []
@@ -1,10 +1,6 @@
import { resolveCloudtentaclesConfig } from '../../platforms/cloudtentacles/helpers.js'
import { maskCode as maskCodeValue, maskPhone as maskPhoneValue } from '../../../utils/masking.js'
import {
isTaskFinalStatus,
normalizeTaskStatus,
TASK_STATUS,
} from '../../../domain/task-status.js'
import { isTaskFinalStatus, normalizeTaskStatus, TASK_STATUS } from '../../../domain/task-status.js'
import { asJsonObject, type JsonObject } from '../../../types/json.js'
export type { JsonObject }
@@ -138,8 +134,7 @@ export function normalizeKuaishouCloudFlow(value: unknown): KuaishouCloudFlow {
const role = asJsonObject(source.role)
const purchase = asJsonObject(source.purchase)
const dispatch = asJsonObject(source.dispatch)
const returnNumber =
asJsonObject(source.returnNumber)
const returnNumber = asJsonObject(source.returnNumber)
const consume = asJsonObject(source.consume)
const ticket = asJsonObject(source.ticket)
const rebind = asJsonObject(source.rebind)
@@ -280,7 +275,8 @@ export function normalizeKuaishouCloudShippedSnapshot(
roleName: String(source.roleName || source.name || '').trim(),
vnId,
vnPhone: String(source.vnPhone || '').trim(),
vnKey: String(source.vnKey || KUAISHOU_CLOUD_FIXED_VN_KEY).trim() || KUAISHOU_CLOUD_FIXED_VN_KEY,
vnKey:
String(source.vnKey || KUAISHOU_CLOUD_FIXED_VN_KEY).trim() || KUAISHOU_CLOUD_FIXED_VN_KEY,
dispatchedAt,
}
}
@@ -331,8 +327,7 @@ export function buildKuaishouCloudShippedSnapshot(input: {
roleName: String(input.roleName || '').trim(),
vnId: Number(input.vnId || 0) || 0,
vnPhone: String(input.vnPhone || '').trim(),
vnKey:
String(input.vnKey || KUAISHOU_CLOUD_FIXED_VN_KEY).trim() || KUAISHOU_CLOUD_FIXED_VN_KEY,
vnKey: String(input.vnKey || KUAISHOU_CLOUD_FIXED_VN_KEY).trim() || KUAISHOU_CLOUD_FIXED_VN_KEY,
dispatchedAt: input.dispatchedAt ? String(input.dispatchedAt) : null,
}
}
@@ -33,11 +33,7 @@ import {
} from './account-selector.js'
import { resolvePersistedCloudtentaclesContextBySourceKeys } from './cloudtentacles-context.js'
import { wrapCloudtentaclesOperationError } from './cloudtentacles-errors.js'
import {
getTaskClaimExpiresAt,
normalizeActor,
parseTaskContext,
} from './task-context.js'
import { getTaskClaimExpiresAt, normalizeActor, parseTaskContext } from './task-context.js'
import { resolveKuaishouCloudDeliveryPlan } from './delivery-plan.js'
import { ensureTaskClaimLink } from './ensure-claim-link.js'
import {
@@ -332,11 +328,14 @@ export async function prepareKuaishouCloudFulfillmentTask(task: TaskRow, options
}
function isRetryableCloudtentaclesSourceError(error: unknown) {
const current = error && typeof error === 'object' ? error as JsonObject : {}
const current = error && typeof error === 'object' ? (error as JsonObject) : {}
const code = String(current.errorCode || current.code || '').trim()
return (
code === 'cloudtentacles_vn_quota_cooldown' ||
(code === 'cloudtentacles_vn_appoint_failed' && String(current.message || '').trim().includes('最多同时占用')) ||
(code === 'cloudtentacles_vn_appoint_failed' &&
String(current.message || '')
.trim()
.includes('最多同时占用')) ||
code === 'cloudtentacles_vn_list_failed' ||
code === 'cloudtentacles_sku_list_failed' ||
code === 'cloudtentacles_knapsack_failed' ||
@@ -484,8 +483,9 @@ export async function refreshKuaishouCloudTaskBindUrl(task: TaskRow, options: Js
binding: {
...flow.binding,
prepareStatus: 'ready',
resolvedSourceKey:
String(cloudContext.resolvedSourceKey || flow.binding.resolvedSourceKey || '').trim(),
resolvedSourceKey: String(
cloudContext.resolvedSourceKey || flow.binding.resolvedSourceKey || '',
).trim(),
bindUrl,
bindPreparedAt: now,
bindExpiresAt: resolveKuaishouCloudBindUrlExpiresAt(now),
@@ -667,8 +667,9 @@ export async function refreshKuaishouCloudTaskBindUrl(task: TaskRow, options: Js
binding: {
...flow.binding,
prepareStatus: 'ready',
resolvedSourceKey:
String(cloudContext.resolvedSourceKey || flow.binding.resolvedSourceKey || '').trim(),
resolvedSourceKey: String(
cloudContext.resolvedSourceKey || flow.binding.resolvedSourceKey || '',
).trim(),
vnKey: preparedBinding.vnKey,
vnId: preparedBinding.vnId,
vnPhone: preparedBinding.vnPhone,
@@ -739,7 +740,6 @@ export async function refreshKuaishouCloudTaskBindUrl(task: TaskRow, options: Js
}
}
async function markKuaishouCloudBindUrlRefreshFailed(
task: TaskRow,
{
@@ -855,7 +855,7 @@ async function clearKuaishouCloudStaleBinding(
error: unknown
claimUrl: string
token: string
}
},
) {
const errorMessage = error instanceof Error ? error.message : String(error || '绑定已失效')
const oldVnId = flow.binding.vnId
@@ -912,7 +912,7 @@ async function clearKuaishouCloudStaleBinding(
errorMessage,
actor,
},
now
now,
)
return {
@@ -51,9 +51,7 @@ export async function probeKuaishouCloudTaskBindUrl(task: TaskRow, options: Json
const probeIntervalMs =
Number(resolveCloudtentaclesConfig().bindUrlProbeIntervalSeconds || 30) * 1000
const lastProbeAt = flow.binding.bindProbeAt
? Date.parse(String(flow.binding.bindProbeAt))
: NaN
const lastProbeAt = flow.binding.bindProbeAt ? Date.parse(String(flow.binding.bindProbeAt)) : NaN
if (
!options.force &&
Number.isFinite(lastProbeAt) &&
@@ -187,4 +185,3 @@ async function markKuaishouCloudBindUrlRefreshFailed(
return updatedTask
}

Some files were not shown because too many files have changed in this diff Show More