开启后端严格空值检查

This commit is contained in:
yml2213
2026-05-26 08:45:06 +08:00
parent 49ac82698f
commit 7aef1a6d50
21 changed files with 101 additions and 38 deletions
+1 -1
View File
@@ -19,7 +19,7 @@ router.get('/orders', createJsonHandler(
)) ))
router.get('/orders/:orderId', createJsonHandler( router.get('/orders/:orderId', createJsonHandler(
(req) => getAdminOrderDetail((req.params as AdminOrderRouteParams).orderId), (req) => getAdminOrderDetail(String((req.params as AdminOrderRouteParams).orderId || '')),
{ {
successMessage: 'ok', successMessage: 'ok',
errorMessage: '读取订单详情失败', errorMessage: '读取订单详情失败',
@@ -23,7 +23,7 @@ router.get(
router.post( router.post(
"/ninetyone/orders/:id/retry", "/ninetyone/orders/:id/retry",
createJsonHandler( createJsonHandler(
(req) => retryAdminNinetyoneOrder((req.params as AdminEntityRouteParams).id), (req) => retryAdminNinetyoneOrder(String((req.params as AdminEntityRouteParams).id || "")),
{ {
successMessage: "91卡券订单已重试", successMessage: "91卡券订单已重试",
errorMessage: "重试 91卡券订单失败", errorMessage: "重试 91卡券订单失败",
@@ -49,7 +49,7 @@ router.post(
createJsonHandler( createJsonHandler(
(req) => (req) =>
failAdminNinetyoneOrder( failAdminNinetyoneOrder(
(req.params as AdminEntityRouteParams).id, String((req.params as AdminEntityRouteParams).id || ""),
req.body as { reason?: string } req.body as { reason?: string }
), ),
{ {
+11
View File
@@ -73,6 +73,17 @@ export function requireAdminRoles(allowedRoles: string[]) {
} }
} }
export function getRequiredAdminSession(req: Request): AdminSession {
if (req.adminSession) {
return req.adminSession
}
throw createHttpError('未登录或登录已失效', {
statusCode: 401,
errorCode: 'admin_auth_required',
})
}
async function recordAdminAudit( async function recordAdminAudit(
session: AdminSession | null | undefined, session: AdminSession | null | undefined,
req: Request, req: Request,
+2 -2
View File
@@ -24,8 +24,8 @@ import type { AdminTaskActionResponse } from "../../types/admin-write-models.js"
const router = Router(); const router = Router();
function getTaskId(req: Request): string | undefined { function getTaskId(req: Request): string {
return (req.params as AdminTaskRouteParams).taskId; return String((req.params as AdminTaskRouteParams).taskId || "");
} }
router.get( router.get(
+4 -4
View File
@@ -7,7 +7,7 @@ import {
updateManagedAdminUserRole, updateManagedAdminUserRole,
updateManagedAdminUserStatus, updateManagedAdminUserStatus,
} from '../../services/admin/admin-auth-service.js' } from '../../services/admin/admin-auth-service.js'
import { createJsonHandler, requireAdminRoles } from './shared.js' import { createJsonHandler, getRequiredAdminSession, requireAdminRoles } from './shared.js'
type AdminUserMutationResult = { type AdminUserMutationResult = {
user: { user: {
@@ -54,7 +54,7 @@ router.post('/users', createJsonHandler(
)) ))
router.post('/users/:userId/role', createJsonHandler( router.post('/users/:userId/role', createJsonHandler(
(req) => updateManagedAdminUserRole(String(req.params.userId), req.body, req.adminSession), (req) => updateManagedAdminUserRole(String(req.params.userId || ''), req.body, getRequiredAdminSession(req)),
{ {
successMessage: '用户角色已更新', successMessage: '用户角色已更新',
errorMessage: '更新用户角色失败', errorMessage: '更新用户角色失败',
@@ -75,7 +75,7 @@ router.post('/users/:userId/role', createJsonHandler(
)) ))
router.post('/users/:userId/status', createJsonHandler( router.post('/users/:userId/status', createJsonHandler(
(req) => updateManagedAdminUserStatus(String(req.params.userId), req.body, req.adminSession), (req) => updateManagedAdminUserStatus(String(req.params.userId || ''), req.body, getRequiredAdminSession(req)),
{ {
successMessage: '用户状态已更新', successMessage: '用户状态已更新',
errorMessage: '更新用户状态失败', errorMessage: '更新用户状态失败',
@@ -96,7 +96,7 @@ router.post('/users/:userId/status', createJsonHandler(
)) ))
router.post('/users/:userId/reset-password', createJsonHandler( router.post('/users/:userId/reset-password', createJsonHandler(
(req) => resetManagedAdminUserPassword(String(req.params.userId), req.body), (req) => resetManagedAdminUserPassword(String(req.params.userId || ''), req.body),
{ {
successMessage: '用户密码已重置', successMessage: '用户密码已重置',
errorMessage: '重置用户密码失败', errorMessage: '重置用户密码失败',
@@ -171,8 +171,8 @@ export async function getAdminSessionSummary(token: unknown): Promise<JsonObject
} }
} }
export function requireAdminRole(session: { role?: string }, allowedRoles: string[]): void { export function requireAdminRole(session: { role?: string } | null | undefined, allowedRoles: string[]): void {
if (allowedRoles.includes(session.role)) { if (session && allowedRoles.includes(session.role || '')) {
return return
} }
@@ -236,6 +236,12 @@ export async function createManagedAdminUser(payload: JsonObject = {}): Promise<
createdAt: now, createdAt: now,
updatedAt: now, updatedAt: now,
}) })
if (!created) {
throw createHttpError('后台用户创建失败', {
statusCode: 500,
errorCode: 'admin_user_create_failed',
})
}
return { return {
user: mapAdminUser(created), user: mapAdminUser(created),
@@ -262,6 +268,12 @@ export async function updateManagedAdminUserRole(
session_version: nextAdminSessionVersion(user), session_version: nextAdminSessionVersion(user),
updated_at: nowIso(), updated_at: nowIso(),
}) })
if (!updated) {
throw createHttpError('后台用户更新失败', {
statusCode: 500,
errorCode: 'admin_user_update_failed',
})
}
return { return {
user: mapAdminUser(updated), user: mapAdminUser(updated),
@@ -288,6 +300,12 @@ export async function updateManagedAdminUserStatus(
session_version: nextAdminSessionVersion(user), session_version: nextAdminSessionVersion(user),
updated_at: nowIso(), updated_at: nowIso(),
}) })
if (!updated) {
throw createHttpError('后台用户更新失败', {
statusCode: 500,
errorCode: 'admin_user_update_failed',
})
}
return { return {
user: mapAdminUser(updated), user: mapAdminUser(updated),
@@ -311,6 +329,12 @@ export async function resetManagedAdminUserPassword(userId: number | string, pay
session_version: nextAdminSessionVersion(user), session_version: nextAdminSessionVersion(user),
updated_at: nowIso(), updated_at: nowIso(),
}) })
if (!updated) {
throw createHttpError('后台用户更新失败', {
statusCode: 500,
errorCode: 'admin_user_update_failed',
})
}
return { return {
user: mapAdminUser(updated), user: mapAdminUser(updated),
@@ -490,7 +514,7 @@ async function ensureAdminUserChangeAllowed(
} }
} }
function mapAdminUser(user: AdminUserRow | null): JsonObject { function mapAdminUser(user: AdminUserRow): JsonObject {
return { return {
userId: Number(user.id), userId: Number(user.id),
username: String(user.username || ''), username: String(user.username || ''),
@@ -158,9 +158,10 @@ export async function getAdminTaskDetail(
}) })
} }
const primaryClaimTokenId = getTaskPrimaryClaimTokenId(task)
const [order, claimToken, taskEvents] = await Promise.all([ const [order, claimToken, taskEvents] = await Promise.all([
getOrderById(task.order_id), getOrderById(task.order_id),
getTaskPrimaryClaimTokenId(task) ? getClaimTokenById(getTaskPrimaryClaimTokenId(task)) : Promise.resolve(null), primaryClaimTokenId ? getClaimTokenById(primaryClaimTokenId) : Promise.resolve(null),
listTaskEventsByTaskId(task.id), listTaskEventsByTaskId(task.id),
]) ])
const orderItems = order ? await listOrderItemsByOrderId(order.id) : [] const orderItems = order ? await listOrderItemsByOrderId(order.id) : []
@@ -269,7 +269,7 @@ export function canRegenerateClaimLinkForViewer(task: TaskLike, viewerContext: A
return false return false
} }
const baseAllowed = ['link_generated', 'claimed', 'role_confirmed', 'retry_pending', 'manual_review'].includes(task.task_status) const baseAllowed = ['link_generated', 'claimed', 'role_confirmed', 'retry_pending', 'manual_review'].includes(String(task.task_status || '').trim())
if (!baseAllowed) { if (!baseAllowed) {
return false return false
@@ -134,7 +134,7 @@ export function updateAdminCloudtentaclesSourceConfig(
}); });
const saved = saveCloudtentaclesSourceByKey(sourceKey, normalized); const saved = saveCloudtentaclesSourceByKey(sourceKey, normalized);
const shouldClearSession = hasCloudtentaclesCredentialContextChanged( const shouldClearSession = hasCloudtentaclesCredentialContextChanged(
current, current || undefined,
saved saved
); );
const session = shouldClearSession const session = shouldClearSession
@@ -146,7 +146,7 @@ export function updateAdminCloudtentaclesSourceConfig(
filePath: getCloudtentaclesSourcesFilePath(), filePath: getCloudtentaclesSourcesFilePath(),
sessionFilePath: getCloudtentaclesSessionFilePath(), sessionFilePath: getCloudtentaclesSessionFilePath(),
source: mapAdminCloudtentaclesSourceConfig(saved), source: mapAdminCloudtentaclesSourceConfig(saved),
session: mapAdminCloudtentaclesSession(session), session: mapAdminCloudtentaclesSession(session || undefined),
}; };
} }
@@ -241,7 +241,7 @@ async function expireClaimContext(claimToken: ClaimTokenRow, task: TaskRow) {
user_action_status: 'expired', user_action_status: 'expired',
last_error: '领取链接已过期', last_error: '领取链接已过期',
updated_at: now, updated_at: now,
}) }) || task
} }
return { return {
@@ -162,7 +162,7 @@ export async function getKuaishouCloudClaimDetail(token: unknown) {
let task = context.task let task = context.task
if (String(task.executor_key || '').trim() === 'kuaishou_ct_assisted') { if (String(task.executor_key || '').trim() === 'kuaishou_ct_assisted') {
task = await syncKuaishouCloudRoleInfo(task) task = await syncKuaishouCloudRoleInfo(task) || task
} }
return buildClaimDetailPayload({ return buildClaimDetailPayload({
@@ -30,14 +30,14 @@ export async function syncKuaishouCloudRoleInfo(task: TaskRow) {
source: 'claim_page_polling_bind_url_probe', source: 'claim_page_polling_bind_url_probe',
actor: { source: 'system' }, actor: { source: 'system' },
}) })
task = probed.task task = probed.task || task
const result = await refreshKuaishouCloudTaskRoleInfo(task, { const result = await refreshKuaishouCloudTaskRoleInfo(task, {
source: 'claim_page_polling', source: 'claim_page_polling',
actor: { source: 'system' }, actor: { source: 'system' },
recordEvent: false, recordEvent: false,
}) })
return result.task return result.task || task
} catch { } catch {
return task return task
} }
@@ -118,13 +118,13 @@ export async function prepareKuaishouCloudFulfillmentTask(
let readyTask = task; let readyTask = task;
if (String(task.task_status || "").trim() !== "waiting_binding") { if (String(task.task_status || "").trim() !== "waiting_binding") {
readyTask = await updateTask(task.id, { readyTask = (await updateTask(task.id, {
task_status: "waiting_binding", task_status: "waiting_binding",
claim_token: claimLinkState.token || task.claim_token || "", claim_token: claimLinkState.token || task.claim_token || "",
claim_expires_at: claim_expires_at:
claimLinkState.expiredAt || getTaskClaimExpiresAt(task), claimLinkState.expiredAt || getTaskClaimExpiresAt(task),
updated_at: now, updated_at: now,
}); })) || task;
} }
return { return {
@@ -69,7 +69,15 @@ export function resolvePersistedCloudtentaclesContextWithFallback(
continue; continue;
} }
const token = String(session?.token || "").trim(); if (!session) {
lastError = createHttpError(
`cloudtentacles 账号 ${sourceKey} 没有可用 token`,
{ statusCode: 409, errorCode: "kuaishou_cloud_missing_cloud_token" }
);
continue;
}
const token = String(session.token || "").trim();
if (!token) { if (!token) {
lastError = createHttpError( lastError = createHttpError(
`cloudtentacles 账号 ${sourceKey} 没有可用 token`, `cloudtentacles 账号 ${sourceKey} 没有可用 token`,
@@ -116,6 +116,12 @@ export async function dispatchKuaishouCloudFulfillmentTask(
context_json: JSON.stringify(nextContext), context_json: JSON.stringify(nextContext),
updated_at: now, updated_at: now,
}); });
if (!updatedTask) {
throw createHttpError("快手 Cloud 发货状态更新失败", {
statusCode: 500,
errorCode: "kuaishou_cloud_dispatch_update_failed",
});
}
await createTaskEvent( await createTaskEvent(
task.id, task.id,
@@ -69,7 +69,7 @@ type TaskContext = {
export async function syncDeliveryTasksForOrder( export async function syncDeliveryTasksForOrder(
order: OrderRow, order: OrderRow,
orderItems: OrderItemRow[], orderItems: OrderItemRow[],
): Promise<Array<TaskRow | null>> { ): Promise<TaskRow[]> {
return syncDeliveryTasksForOrderWithDeps(order, orderItems) return syncDeliveryTasksForOrderWithDeps(order, orderItems)
} }
@@ -77,7 +77,7 @@ export async function syncDeliveryTasksForOrderWithDeps(
order: OrderRow, order: OrderRow,
orderItems: OrderItemRow[], orderItems: OrderItemRow[],
deps: DeliveryTaskDeps = {}, deps: DeliveryTaskDeps = {},
): Promise<Array<TaskRow | null>> { ): Promise<TaskRow[]> {
const { const {
createTask: createDeliveryTask = createTask, createTask: createDeliveryTask = createTask,
listTasksByOrderId: listTasks = listTasksByOrderId, listTasksByOrderId: listTasks = listTasksByOrderId,
@@ -104,11 +104,12 @@ export async function syncDeliveryTasksForOrderWithDeps(
} }
const itemMap = new Map(orderItems.map((item) => [item.id, item])) const itemMap = new Map(orderItems.map((item) => [item.id, item]))
return Promise.all(existingTasks.map((task) => preparePaidTask({ const preparedTasks = await Promise.all(existingTasks.map((task) => preparePaidTask({
...task, ...task,
skuCode: itemMap.get(task.order_item_id)?.sku_code || '', skuCode: itemMap.get(task.order_item_id)?.sku_code || '',
skuName: itemMap.get(task.order_item_id)?.sku_name || '', skuName: itemMap.get(task.order_item_id)?.sku_name || '',
}, runtimeDeps))) }, runtimeDeps)))
return preparedTasks.filter(isTaskRow)
} }
const tasks: DeliveryTaskRow[] = [] const tasks: DeliveryTaskRow[] = []
@@ -243,6 +244,7 @@ export async function syncDeliveryTasksForOrderWithDeps(
updatedAt: createdAt, updatedAt: createdAt,
}) })
if (task) {
tasks.push({ tasks.push({
...task, ...task,
skuCode: item.sku_code, skuCode: item.sku_code,
@@ -250,12 +252,14 @@ export async function syncDeliveryTasksForOrderWithDeps(
}) })
} }
} }
}
if (order.pay_status !== 'paid') { if (order.pay_status !== 'paid') {
return tasks return tasks
} }
return Promise.all(tasks.map((task) => preparePaidTask(task, runtimeDeps))) const preparedTasks = await Promise.all(tasks.map((task) => preparePaidTask(task, runtimeDeps)))
return preparedTasks.filter(isTaskRow)
} }
async function preparePaidTask( async function preparePaidTask(
@@ -380,6 +384,10 @@ function parseJsonObject(value: unknown): JsonObject {
} }
} }
function isTaskRow(task: TaskRow | DeliveryTaskRow | null | undefined): task is TaskRow {
return Boolean(task && Number(task.id || 0) > 0)
}
function isKuaishouCloudExecutor(value: unknown): boolean { function isKuaishouCloudExecutor(value: unknown): boolean {
return String(value || '').trim() === 'kuaishou_ct_assisted' return String(value || '').trim() === 'kuaishou_ct_assisted'
} }
@@ -181,8 +181,10 @@ async function runFlowStep<T>(
runner: () => Promise<T>, runner: () => Promise<T>,
options: FlowStepOptions = {}, options: FlowStepOptions = {},
) { ) {
const retries = Number.isInteger(options.retries) && options.retries > 0 ? options.retries : 0 const rawRetries = Number(options.retries)
const retryDelayMs = Number.isFinite(options.retryDelayMs) && options.retryDelayMs > 0 ? options.retryDelayMs : 1000 const rawRetryDelayMs = Number(options.retryDelayMs)
const retries = Number.isInteger(rawRetries) && rawRetries > 0 ? rawRetries : 0
const retryDelayMs = Number.isFinite(rawRetryDelayMs) && rawRetryDelayMs > 0 ? rawRetryDelayMs : 1000
for (let attempt = 0; attempt <= retries; attempt += 1) { for (let attempt = 0; attempt <= retries; attempt += 1) {
try { try {
@@ -100,7 +100,7 @@ function normalizeKuaishouEticketSourceConfig(rawValue: unknown): KuaishouEticke
baseUrl: String(source.baseUrl || DEFAULT_BASE_URL).trim() || DEFAULT_BASE_URL, baseUrl: String(source.baseUrl || DEFAULT_BASE_URL).trim() || DEFAULT_BASE_URL,
shops: shops shops: shops
.map((item) => normalizeKuaishouEticketShopConfig(item)) .map((item) => normalizeKuaishouEticketShopConfig(item))
.filter(Boolean), .filter((item): item is KuaishouEticketShopConfig => Boolean(item)),
} }
} }
@@ -37,7 +37,9 @@ function loadScheduledJobsConfigFromFile() {
export function normalizeScheduledJobsConfig(rawValue: unknown) { export function normalizeScheduledJobsConfig(rawValue: unknown) {
const source = isPlainObject(rawValue) ? rawValue : {} const source = isPlainObject(rawValue) ? rawValue : {}
const rawJobs = Array.isArray(source.jobs) ? source.jobs : [] const rawJobs = Array.isArray(source.jobs) ? source.jobs : []
const jobs = rawJobs.map((item) => normalizeScheduledJob(item)).filter(Boolean) const jobs = rawJobs
.map((item) => normalizeScheduledJob(item))
.filter((item): item is ReturnType<typeof normalizeCloudtentaclesHealthJob> => Boolean(item))
const hasCloudtentaclesHealth = jobs.some((item) => item.id === CLOUDTENTACLES_HEALTH_JOB_ID) const hasCloudtentaclesHealth = jobs.some((item) => item.id === CLOUDTENTACLES_HEALTH_JOB_ID)
if (!hasCloudtentaclesHealth) { if (!hasCloudtentaclesHealth) {
+2 -2
View File
@@ -45,8 +45,8 @@ export function buildErrorPayload(error: unknown, fallbackMessage: string) {
export function sendRouteError( export function sendRouteError(
res: Response, res: Response,
error: unknown, error: unknown,
fallbackMessage: string, fallbackMessage = "服务内部错误",
scope: string scope = "[route]"
): void { ): void {
const classification = classifyRouteError(error); const classification = classifyRouteError(error);
const logger = classification.statusCode >= 500 ? logError : logWarn; const logger = classification.statusCode >= 500 ? logError : logWarn;
+1
View File
@@ -8,6 +8,7 @@
"noEmit": true, "noEmit": true,
"strict": false, "strict": false,
"noImplicitAny": true, "noImplicitAny": true,
"strictNullChecks": true,
"skipLibCheck": true, "skipLibCheck": true,
"types": ["node"], "types": ["node"],
"lib": ["ES2022"] "lib": ["ES2022"]