开启后端严格空值检查
This commit is contained in:
@@ -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 }
|
||||
),
|
||||
{
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
"noEmit": true,
|
||||
"strict": false,
|
||||
"noImplicitAny": true,
|
||||
"strictNullChecks": true,
|
||||
"skipLibCheck": true,
|
||||
"types": ["node"],
|
||||
"lib": ["ES2022"]
|
||||
|
||||
Reference in New Issue
Block a user