Files
order_site/apps/backend/src/routes/collect.ts
T
zhaoxing a9e2de443e feat(collect): 补资料查询仅限待完善并优化页面展示
- 公开查询只返回待完善工单,待分配/已验收等状态提示「该订单当前无需补充资料」
- 404 失败查询按 IP 冷却 2 秒,成功查询不计数;前端显示「查询过于频繁」
- 提交成功后本地展示成功页,刷新后按严格模式提示无需补充资料
- 收敛补资料页重复提示,移除状态标签与工单号,商品与订单号合并展示
- 修复 React StrictMode 下刷新重复请求查询接口的问题
2026-11-02 10:38:09 +08:00

172 lines
4.3 KiB
TypeScript

import { Router } from 'express'
import {
createFailureCooldownMiddleware,
createRateLimitMiddleware,
getBodyFieldRateLimitKey,
getParamRateLimitKey,
} from '../middleware/rate-limit.js'
import {
collectLookupWorkOrder,
collectSubmitWorkOrder,
getPublicWorkOrderMaterialLink,
getPublicWorkOrderMaterialLinkUploadContext,
submitPublicWorkOrderMaterialLink,
} from '../services/worker-platform/index.js'
import { uploadFileAsset } from '../services/file-storage/file-storage-service.js'
import { buildNotFoundPayload, createRouteHandler } from '../utils/http.js'
import { uploadSingleFile } from './file-upload.js'
const router = Router()
const collectRateLimit = createRateLimitMiddleware({
scope: 'collect',
windowMs: 60_000,
max: 60,
key: getBodyFieldRateLimitKey('orderNo'),
})
const materialLinkReadRateLimit = createRateLimitMiddleware({
scope: 'collect:link:read',
windowMs: 60_000,
max: 120,
key: getParamRateLimitKey('code'),
})
/** 平台订单号可被枚举,额外加一层只按 IP 的总限流,避免换 code 绕过单链接限流。 */
const materialLinkReadIpRateLimit = createRateLimitMiddleware({
scope: 'collect:link:read:ip',
windowMs: 60_000,
max: 300,
})
/** 查询失败(404)后按 IP 冷却 2 秒;正常查询不计数,避免误伤用户。 */
const materialLinkReadFailureCooldown = createFailureCooldownMiddleware({
scope: 'collect:link:read:fail:ip',
cooldownMs: 2_000,
})
const materialLinkWriteRateLimit = createRateLimitMiddleware({
scope: 'collect:link:write',
windowMs: 60_000,
max: 20,
key: getParamRateLimitKey('code'),
})
const materialLinkWriteIpRateLimit = createRateLimitMiddleware({
scope: 'collect:link:write:ip',
windowMs: 60_000,
max: 40,
})
const materialLinkUploadRateLimit = createRateLimitMiddleware({
scope: 'collect:link:upload',
windowMs: 60_000,
max: 30,
key: getParamRateLimitKey('code'),
})
const materialLinkUploadIpRateLimit = createRateLimitMiddleware({
scope: 'collect:link:upload:ip',
windowMs: 60_000,
max: 60,
})
router.post(
'/lookup',
collectRateLimit,
createRouteHandler((req) => collectLookupWorkOrder(req.body || {}), {
successMessage: 'ok',
errorMessage: '查询待完善订单失败',
scope: '[collect/lookup]',
}),
)
router.post(
'/submit',
collectRateLimit,
createRouteHandler((req) => collectSubmitWorkOrder(req.body || {}), {
successMessage: '资料已提交',
errorMessage: '提交补充资料失败',
scope: '[collect/submit]',
}),
)
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]',
},
),
)
router.get(
'/link/:code',
materialLinkReadFailureCooldown,
materialLinkReadIpRateLimit,
materialLinkReadRateLimit,
createRouteHandler((req) => getPublicWorkOrderMaterialLink(String(req.params.code || '')), {
errorMessage: '读取补资料链接失败',
scope: '[collect/link/:code]',
}),
)
router.post(
'/link/:code/submit',
materialLinkWriteIpRateLimit,
materialLinkWriteRateLimit,
createRouteHandler(
(req) => submitPublicWorkOrderMaterialLink(String(req.params.code || ''), req.body || {}),
{
successMessage: '资料已提交',
errorMessage: '提交补资料失败',
scope: '[collect/link/:code/submit]',
},
),
)
router.post(
'/link/:code/upload',
materialLinkUploadIpRateLimit,
materialLinkUploadRateLimit,
uploadSingleFile,
createRouteHandler(
async (req) => {
const { order } = await getPublicWorkOrderMaterialLinkUploadContext(
String(req.params.code || ''),
req.query.workOrderId,
)
return uploadFileAsset({
file: req.file,
scene: req.body?.scene || 'collect-material',
uploaderType: 'collect',
uploaderId: `work-order-${order.id}`,
})
},
{
successMessage: '文件已上传',
errorMessage: '文件上传失败',
scope: '[collect/link/:code/upload]',
},
),
)
router.use((req, res) => {
res.status(404).json(buildNotFoundPayload(req))
})
export default router