开启后端严格空值检查

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