From 2412df8b541126ca90ea54fecb8d1419f9927475 Mon Sep 17 00:00:00 2001 From: yml2213 Date: Sat, 1 Aug 2026 18:14:37 +0800 Subject: [PATCH] =?UTF-8?q?=E8=B5=84=E6=96=99=E5=AD=97=E6=AE=B5=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E4=B8=8B=E6=8B=89=E9=80=89=E9=A1=B9=E4=B8=8E=E4=B9=B0?= =?UTF-8?q?=E5=AE=B6=E6=88=AA=E5=9B=BE=EF=BC=8C=E5=A4=A7=E5=8E=85=E6=8C=89?= =?UTF-8?q?=20VIP=20=E7=AD=89=E7=BA=A7=E5=BB=B6=E8=BF=9F=E5=8F=AF=E8=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 资料字段: - CollectField 支持 type(text/select) 与 options,规则字段语法 key:名称#选项1,选项2 - 默认字段改为游戏编号/游戏昵称/系统(安卓,苹果)/区服(QQ区,微信区),旧默认模板字段自动替换为新版 - 采集页与管理端补资料支持单选下拉与买家主页截图上传(新增公开上传接口) - 管理端工单详情展示买家截图 VIP 延迟可见: - 等级权限新增 visibleDelaySeconds(默认 VIP1=20s~VIP5=0s,可后台配置) - 大厅按等级延迟过滤新发布工单,页面提示延迟规则 --- .../src/repositories/worker-platform/types.ts | 1 + .../worker-platform/work-order-repo.ts | 7 ++ apps/backend/src/routes/collect.ts | 20 ++++ .../services/worker-platform/admin-service.ts | 5 +- .../src/services/worker-platform/mappers.ts | 92 ++++++++++++++++--- .../worker-platform/worker-service.ts | 13 ++- .../src/components/files/ImageUpload.tsx | 2 +- .../src/pages/admin/panels/LevelsPanel.tsx | 17 ++++ .../pages/admin/panels/ProductRulesPanel.tsx | 8 +- .../pages/admin/panels/WorkOrdersPanel.tsx | 38 +++++++- .../src/pages/admin/panels/shared.tsx | 20 ++++ .../src/pages/collect/CollectPage.tsx | 34 ++++++- .../src/pages/worker/WorkerHallPage.tsx | 9 ++ .../src/services/admin/worker-platform.ts | 5 +- apps/frontend/src/services/files.ts | 6 +- apps/frontend/src/services/worker.ts | 1 + apps/frontend/src/types/worker-platform.ts | 5 + 17 files changed, 252 insertions(+), 31 deletions(-) diff --git a/apps/backend/src/repositories/worker-platform/types.ts b/apps/backend/src/repositories/worker-platform/types.ts index 45004683..592986ef 100644 --- a/apps/backend/src/repositories/worker-platform/types.ts +++ b/apps/backend/src/repositories/worker-platform/types.ts @@ -190,6 +190,7 @@ export type ListInput = { workerId?: number categoryId?: number workerSharingId?: number + visibleAfterIso?: string } export type ProductRuleListInput = { diff --git a/apps/backend/src/repositories/worker-platform/work-order-repo.ts b/apps/backend/src/repositories/worker-platform/work-order-repo.ts index e2bd8fd4..5086980f 100644 --- a/apps/backend/src/repositories/worker-platform/work-order-repo.ts +++ b/apps/backend/src/repositories/worker-platform/work-order-repo.ts @@ -565,6 +565,7 @@ export async function listWorkOrders({ workerId = 0, categoryId = 0, workerSharingId = 0, + visibleAfterIso = '', }: ListInput = {}): Promise<{ items: WorkOrderRow[]; total: number }> { const { whereClause, params } = buildWorkOrderWhere({ status, @@ -572,6 +573,7 @@ export async function listWorkOrders({ workerId, categoryId, workerSharingId, + visibleAfterIso, }) const totalResult = await query<{ total: number }>( ` @@ -1326,6 +1328,7 @@ function buildWorkOrderWhere({ workerId = 0, categoryId = 0, workerSharingId = 0, + visibleAfterIso = '', }: ListInput) { const filters: string[] = [] const params: unknown[] = [] @@ -1357,6 +1360,10 @@ function buildWorkOrderWhere({ ))`, ) } + if (visibleAfterIso) { + params.push(visibleAfterIso) + filters.push(`(wo.published_at IS NULL OR wo.published_at <= $${params.length})`) + } return { whereClause: filters.length > 0 ? `WHERE ${filters.join(' AND ')}` : '', params, diff --git a/apps/backend/src/routes/collect.ts b/apps/backend/src/routes/collect.ts index 578c9ad5..4ca99619 100644 --- a/apps/backend/src/routes/collect.ts +++ b/apps/backend/src/routes/collect.ts @@ -5,7 +5,9 @@ import { collectLookupWorkOrder, collectSubmitWorkOrder, } 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() @@ -36,6 +38,24 @@ router.post( }), ) +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.use((req, res) => { res.status(404).json(buildNotFoundPayload(req)) }) diff --git a/apps/backend/src/services/worker-platform/admin-service.ts b/apps/backend/src/services/worker-platform/admin-service.ts index 55708b81..a9a9c909 100644 --- a/apps/backend/src/services/worker-platform/admin-service.ts +++ b/apps/backend/src/services/worker-platform/admin-service.ts @@ -83,7 +83,7 @@ import { saveWorkerFinanceConfig, } from './worker-finance-config-service.js' -import { DEFAULT_CATEGORY_KEY, DEFAULT_DEPOSIT_THRESHOLD_AMOUNT, DEFAULT_LEVEL_KEY, DEFAULT_LEVEL_NAME, mapFinanceRequest, mapWallet, mapWorkCategory, mapWorkOrderAdmin, mapWorkOrderShare, mapWorkProductRule, mapWorkerLevel, mapWorkerUser, normalizeAdminFinanceReviewStatus, normalizeAmountFen, normalizeBoolean, normalizeEnabledStatus, normalizeFinanceRequestStatus, normalizeFinanceRequestType, normalizeInteger, normalizeMatchType, normalizeOptionalId, normalizePositiveInteger, normalizeProblemResolutionAction, normalizeRequirementFields, normalizeRequirementFieldsFromPayload, normalizeReviewStatus, normalizeSessionVersion, normalizeSlugKey, normalizeSubmittedFields, resolveMatchingProductRule, resolveRequirementFields } from './mappers.js' +import { DEFAULT_CATEGORY_KEY, DEFAULT_DEPOSIT_THRESHOLD_AMOUNT, DEFAULT_LEVEL_KEY, DEFAULT_LEVEL_NAME, mapFinanceRequest, mapWallet, mapWorkCategory, mapWorkOrderAdmin, mapWorkOrderShare, mapWorkProductRule, mapWorkerLevel, mapWorkerUser, normalizeAdminFinanceReviewStatus, normalizeAmountFen, normalizeBoolean, normalizeEnabledStatus, normalizeFinanceRequestStatus, normalizeFinanceRequestType, normalizeInteger, normalizeMatchType, normalizeOptionalId, normalizePositiveInteger, normalizeProblemResolutionAction, normalizeRequirementFields, normalizeRequirementFieldsFromPayload, normalizeReviewStatus, normalizeSessionVersion, normalizeSlugKey, normalizeSubmittedFields, normalizeUploadedFiles, resolveMatchingProductRule, resolveRequirementFields } from './mappers.js' import { ensureWorkerPlatformDefaults, getRequiredWorkOrder, getRequiredWorker } from './worker-service.js' export async function listAdminWorkerLevels() { @@ -146,6 +146,7 @@ export async function saveAdminWorkerLevel(payload: JsonObject = {}) { ), maxActiveOrders: normalizePositiveInteger(payload.maxActiveOrders, 1), upgradeThreshold: normalizeInteger(payload.upgradeThreshold, 0), + visibleDelaySeconds: normalizeInteger(payload.visibleDelaySeconds, -1), } const level = await upsertWorkerLevel({ levelKey, @@ -562,10 +563,12 @@ export async function submitAdminWorkOrderMaterial( (field) => !field.required || String(submittedFields[field.key] || '').trim(), ) const now = nowIso() + const screenshots = normalizeUploadedFiles(payload.screenshots || payload.files) const material = { ...safeParseJson(workOrder.material_json), collect: { fields: submittedFields, + screenshots, submittedAt: now, complete, source: 'admin', diff --git a/apps/backend/src/services/worker-platform/mappers.ts b/apps/backend/src/services/worker-platform/mappers.ts index 860783ee..075f7101 100644 --- a/apps/backend/src/services/worker-platform/mappers.ts +++ b/apps/backend/src/services/worker-platform/mappers.ts @@ -157,9 +157,26 @@ export function resolveWorkerPermissions(worker: WorkerUserRow) { depositFreeAmount: normalizeInteger(permission.depositFreeAmount, 0), maxActiveOrders: normalizePositiveInteger(permission.maxActiveOrders, 1), upgradeThreshold: normalizeInteger(permission.upgradeThreshold, 0), + visibleDelaySeconds: resolveVisibleDelaySeconds( + worker.level_key || '', + permission.visibleDelaySeconds, + ), } } +export function resolveVisibleDelaySeconds(levelKey: string, configured?: unknown): number { + const configuredValue = normalizeInteger(configured, -1) + if (configuredValue >= 0) return configuredValue + const defaultDelays: Record = { + vip1: 20, + vip2: 15, + vip3: 10, + vip4: 5, + vip5: 0, + } + return defaultDelays[String(levelKey || '').toLowerCase()] ?? 0 +} + export function resolveFreezeDepositAmount( workOrder: WorkOrderRow, permissions: { depositFreeAmount: number }, @@ -182,6 +199,10 @@ export function mapWorkerLevel(level: WorkerLevelRow | null | undefined) { depositFreeAmount: normalizeInteger(permission.depositFreeAmount, 0), maxActiveOrders: normalizePositiveInteger(permission.maxActiveOrders, 1), upgradeThreshold: normalizeInteger(permission.upgradeThreshold, 0), + visibleDelaySeconds: resolveVisibleDelaySeconds( + level.level_key, + permission.visibleDelaySeconds, + ), createdAt: level.created_at, updatedAt: level.updated_at, } @@ -434,6 +455,15 @@ export function resolveRequirementFields(workOrder: WorkOrderRow) { return normalizeRequirementFields(safeParseJson(workOrder.requirement_json).fields) } +export type RequirementField = { + key: string + label: string + required: boolean + type: 'text' | 'select' + options: string[] + mockValue: string +} + export function normalizeRequirementFields(value: unknown) { const rawFields = Array.isArray(value) ? value : [] const fields = rawFields @@ -442,24 +472,37 @@ export function normalizeRequirementFields(value: unknown) { item && typeof item === 'object' && !Array.isArray(item) ? (item as JsonObject) : {} const key = String(source.key || '').trim() if (!key) return null + const type = source.type === 'select' ? 'select' : 'text' + const options = Array.isArray(source.options) + ? source.options.map((option) => String(option || '').trim()).filter(Boolean) + : [] return { key, label: String(source.label || key).trim(), required: source.required !== false, + type, + options, mockValue: String(source.mockValue || '').trim(), } }) - .filter((item): item is { key: string; label: string; required: boolean; mockValue: string } => - Boolean(item), - ) + .filter((item): item is RequirementField => Boolean(item)) - return fields.length > 0 - ? fields - : [ - { key: 'gameAccount', label: '游戏账号', required: true, mockValue: 'test_account' }, - { key: 'serverName', label: '区服', required: true, mockValue: '安卓一区' }, - { key: 'roleName', label: '角色名', required: true, mockValue: '测试角色' }, - ] + if (fields.length > 0 && !isDefaultTemplateRequirementFields(fields)) { + return fields + } + return [ + { key: 'gameId', label: '游戏编号', required: true, type: 'text', options: [], mockValue: 'test_uid' }, + { key: 'gameNickname', label: '游戏昵称', required: true, type: 'text', options: [], mockValue: '测试角色' }, + { key: 'system', label: '系统', required: true, type: 'select', options: ['安卓', '苹果'], mockValue: '安卓' }, + { key: 'serverZone', label: '区服', required: true, type: 'select', options: ['QQ区', '微信区'], mockValue: 'QQ区' }, + ] +} + +function isDefaultTemplateRequirementFields(fields: RequirementField[]): boolean { + if (fields.length === 0) return true + const defaultKeys = new Set(['gameId', 'gameNickname', 'system', 'serverZone']) + const legacyKeys = new Set(['gameAccount', 'serverName', 'roleName']) + return fields.every((field) => defaultKeys.has(field.key) || legacyKeys.has(field.key)) } export function normalizeRequirementFieldsFromPayload(payload: JsonObject) { @@ -494,23 +537,42 @@ export function normalizeRequirementFieldsText(value: unknown) { const [rawKey, ...labelParts] = text.split(delimiter) const key = String(rawKey || '').trim() if (!key) return null + const labelWithOptions = labelParts.join(delimiter).trim() || key + const [label, rawOptions] = splitFieldOptions(labelWithOptions) return { key, - label: labelParts.join(delimiter).trim() || key, + label: label || key, required: true, + type: rawOptions.length > 0 ? 'select' : 'text', + options: rawOptions, mockValue: '', } } + const [plainLabel, plainOptions] = splitFieldOptions(text) return { key: `field${index + 1}`, - label: text, + label: plainLabel, required: true, + type: plainOptions.length > 0 ? 'select' : 'text', + options: plainOptions, mockValue: '', } }) - .filter((item): item is { key: string; label: string; required: boolean; mockValue: string } => - Boolean(item), - ) + .filter((item): item is RequirementField => Boolean(item)) +} + +function splitFieldOptions(value: string): [string, string[]] { + const text = String(value || '').trim() + if (!text.includes('#')) { + return [text, []] + } + const [label, ...optionParts] = text.split('#') + const options = optionParts + .join('#') + .split(/[,,、]/) + .map((option) => String(option || '').trim()) + .filter(Boolean) + return [String(label || '').trim(), options] } export function resolveMatchingProductRule( diff --git a/apps/backend/src/services/worker-platform/worker-service.ts b/apps/backend/src/services/worker-platform/worker-service.ts index 1af3956b..2b510bb5 100644 --- a/apps/backend/src/services/worker-platform/worker-service.ts +++ b/apps/backend/src/services/worker-platform/worker-service.ts @@ -641,15 +641,21 @@ export async function listWorkerHallOrders(query: JsonObject = {}, session: Work const page = normalizePage(query.page) const pageSize = normalizePageSize(query.pageSize) const categoryId = normalizeOptionalId(query.categoryId) + const worker = await getRequiredWorker(session.workerId) + const permissions = resolveWorkerPermissions(worker) + const visibleDelaySeconds = Number(permissions.visibleDelaySeconds || 0) + const visibleAfterIso = + visibleDelaySeconds > 0 + ? new Date(Date.now() - visibleDelaySeconds * 1000).toISOString() + : '' const { items, total } = await listWorkOrders({ page, pageSize, status: WORK_ORDER_STATUS.OPEN, keyword: String(query.keyword || '').trim(), categoryId: categoryId || 0, + visibleAfterIso, }) - const worker = await getRequiredWorker(session.workerId) - const permissions = resolveWorkerPermissions(worker) const categories = (await listWorkCategories()).map(mapWorkCategory) const myShares = await listWorkerSharesByWorker(worker.id) const myShareByOrderId = new Map( @@ -663,6 +669,7 @@ export async function listWorkerHallOrders(query: JsonObject = {}, session: Work ), pagination: { page, pageSize, total }, categories, + visibleDelaySeconds, } } @@ -996,10 +1003,12 @@ export async function collectSubmitWorkOrder(payload: JsonObject = {}) { (field) => !field.required || String(submittedFields[field.key] || '').trim(), ) const now = nowIso() + const screenshots = normalizeUploadedFiles(payload.screenshots || payload.files) const material = { ...safeParseJson(workOrder.material_json), collect: { fields: submittedFields, + screenshots, submittedAt: now, complete, }, diff --git a/apps/frontend/src/components/files/ImageUpload.tsx b/apps/frontend/src/components/files/ImageUpload.tsx index 742f6bad..eea6cf56 100644 --- a/apps/frontend/src/components/files/ImageUpload.tsx +++ b/apps/frontend/src/components/files/ImageUpload.tsx @@ -9,7 +9,7 @@ type ImageUploadProps = { value?: UploadedFile[] onChange?: (files: UploadedFile[]) => void scene: string - scope: 'admin' | 'worker' + scope: 'admin' | 'worker' | 'collect' maxCount?: number } diff --git a/apps/frontend/src/pages/admin/panels/LevelsPanel.tsx b/apps/frontend/src/pages/admin/panels/LevelsPanel.tsx index 8d8e178a..2ae119e2 100644 --- a/apps/frontend/src/pages/admin/panels/LevelsPanel.tsx +++ b/apps/frontend/src/pages/admin/panels/LevelsPanel.tsx @@ -107,6 +107,7 @@ export default function LevelsPanel() { depositFreeAmount: level.depositFreeAmount, maxActiveOrders: level.maxActiveOrders, upgradeThreshold: level.upgradeThreshold, + visibleDelaySeconds: level.visibleDelaySeconds, }) } @@ -121,6 +122,7 @@ export default function LevelsPanel() { depositFreeAmount?: number maxActiveOrders?: number upgradeThreshold?: number + visibleDelaySeconds?: number }) { try { await saveAdminWorkerLevel(values) @@ -140,6 +142,7 @@ export default function LevelsPanel() { depositFreeAmount: level.depositFreeAmount, maxActiveOrders: level.maxActiveOrders, upgradeThreshold: level.upgradeThreshold, + visibleDelaySeconds: level.visibleDelaySeconds, status: level.status === 'active' ? 'disabled' : 'active', }) message.success(level.status === 'active' ? '等级已停用' : '等级已启用') @@ -174,6 +177,12 @@ export default function LevelsPanel() { }, { title: '最大同时接单', dataIndex: 'maxActiveOrders' }, { title: '升级所需累计完成', dataIndex: 'upgradeThreshold' }, + { + title: '可见延迟', + width: 110, + render: (_, row) => + row.visibleDelaySeconds > 0 ? `${row.visibleDelaySeconds} 秒` : '立即', + }, { title: '状态', width: 110, @@ -217,6 +226,7 @@ export default function LevelsPanel() { depositFreeAmount: 0, maxActiveOrders: 1, upgradeThreshold: 0, + visibleDelaySeconds: 20, }} onFinish={saveLevel} > @@ -247,6 +257,13 @@ export default function LevelsPanel() { > + + +