From 0638582205382c71d0945e2e853f82966a9a0dfc Mon Sep 17 00:00:00 2001 From: yml2213 Date: Wed, 4 Nov 2026 12:58:34 +0800 Subject: [PATCH] =?UTF-8?q?fix(order-service):=20=E4=BF=AE=E5=A4=8D=20Mock?= =?UTF-8?q?=20=E5=B7=A5=E5=8D=95=E6=9C=AA=E7=BB=91=E5=AE=9A=E8=87=AA?= =?UTF-8?q?=E5=8A=A8=E5=8F=91=E5=B8=83=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ..._backfill_mock_order_service_snapshots.sql | 48 +++++++++++++ .../admin-mock-work-order-service.ts | 70 +++++++++++++++++-- .../worker-platform-service.test.ts | 11 +++ 3 files changed, 125 insertions(+), 4 deletions(-) create mode 100644 apps/backend/src/db/migrations/136_backfill_mock_order_service_snapshots.sql diff --git a/apps/backend/src/db/migrations/136_backfill_mock_order_service_snapshots.sql b/apps/backend/src/db/migrations/136_backfill_mock_order_service_snapshots.sql new file mode 100644 index 00000000..ba643dc7 --- /dev/null +++ b/apps/backend/src/db/migrations/136_backfill_mock_order_service_snapshots.sql @@ -0,0 +1,48 @@ +-- 136_backfill_mock_order_service_snapshots.sql +-- 修复历史 Mock 工单未绑定订单服务版本,导致资料提交后无法自动发布的问题。 + +UPDATE work_orders wo +SET service_profile_id = p.id, + service_profile_version_id = v.id, + service_profile_snapshot_json = jsonb_build_object( + 'profileId', p.id, + 'categoryId', p.category_id, + 'versionId', v.id, + 'versionNo', v.version_no, + 'config', v.config_json + ), + requirement_json = jsonb_build_object( + 'fields', COALESCE(v.config_json -> 'fields', '[]'::jsonb) + ), + updated_at = NOW() +FROM order_service_profiles p +JOIN order_service_profile_versions v ON v.id = p.current_version_id +WHERE wo.category_id = p.category_id + AND wo.service_profile_snapshot_json IS NULL + AND wo.status IN ('pending_material', 'unassigned') + AND wo.assigned_worker_id IS NULL + AND wo.material_json -> 'source' ->> 'mock' = 'true' + AND NOT EXISTS ( + SELECT 1 + FROM work_order_shares share + WHERE share.work_order_id = wo.id + AND share.status <> 'cancelled' + ); + +INSERT INTO order_service_auto_publish_jobs (work_order_id, max_attempts) +SELECT + wo.id, + GREATEST( + 1, + COALESCE( + NULLIF(wo.service_profile_snapshot_json -> 'config' -> 'autoPublish' ->> 'retryMax', '')::int, + 3 + ) + 1 + ) +FROM work_orders wo +WHERE wo.material_json -> 'source' ->> 'mock' = 'true' + AND wo.status = 'unassigned' + AND wo.assigned_worker_id IS NULL + AND wo.material_json -> 'collect' ->> 'complete' = 'true' + AND wo.service_profile_snapshot_json -> 'config' -> 'autoPublish' ->> 'enabled' = 'true' +ON CONFLICT DO NOTHING; diff --git a/apps/backend/src/services/worker-platform/admin-mock-work-order-service.ts b/apps/backend/src/services/worker-platform/admin-mock-work-order-service.ts index 25fc980d..18ded96e 100644 --- a/apps/backend/src/services/worker-platform/admin-mock-work-order-service.ts +++ b/apps/backend/src/services/worker-platform/admin-mock-work-order-service.ts @@ -29,6 +29,14 @@ import { } from './mappers.js' import { rememberAdminMockCustomShop } from './admin-mock-custom-shop-service.js' import { resolveMockWorkOrderProductRule } from './mock-work-order-rule-match.js' +import { + getPublishedOrderServiceProfileSnapshotByCategory, + isOrderServiceMaterialRequired, +} from './order-service-profile-service.js' +import { + enqueueOrderServiceAutoPublish, + processDueOrderServiceAutoPublishJobs, +} from './order-service-auto-publish-service.js' import { ensureWorkerPlatformDefaults } from './worker-platform-defaults.js' import { normalizeWorkOrderTimeoutPolicy } from './work-order-timeout-policy.js' @@ -63,8 +71,17 @@ export async function createAdminMockWorkOrder(payload: JsonObject = {}) { } categoryId = Number(category.id) } - const materialComplete = payload.materialComplete === true || payload.materialComplete === 'true' - const fields = normalizeRequirementFields(payload.fields) + const serviceProfile = categoryId + ? await getPublishedOrderServiceProfileSnapshotByCategory(categoryId) + : null + const requestedMaterialComplete = + payload.materialComplete === true || payload.materialComplete === 'true' + const materialComplete = + requestedMaterialComplete || + (serviceProfile ? !isOrderServiceMaterialRequired(serviceProfile.config) : false) + const fields = serviceProfile + ? normalizeRequirementFields(serviceProfile.config.fields) + : normalizeRequirementFields(payload.fields) const { platformOrderId, productName, shop } = input // Mock 工单同样要挂到接单模板上:后台「同步模板价格」按 product_rule_id 精确匹配, // 不绑定的话改模板价永远命中不到 Mock 单(真实单在 sync-worker-orders-service 已绑定)。 @@ -93,7 +110,7 @@ export async function createAdminMockWorkOrder(payload: JsonObject = {}) { complete: true, submittedAt: now, fields: fields.reduce>((result, field) => { - result[field.key] = String(field.mockValue || `${field.label}测试值`) + result[field.key] = resolveMockMaterialFieldValue(field) return result }, {}), }, @@ -129,6 +146,17 @@ export async function createAdminMockWorkOrder(payload: JsonObject = {}) { giftCooldownMinutes: normalizeWorkOrderGiftCooldownMinutes(payload.giftCooldownMinutes), materialJson: JSON.stringify(material), requirementJson: JSON.stringify({ fields }), + serviceProfileId: serviceProfile?.profileId || null, + serviceProfileVersionId: serviceProfile?.versionId || null, + serviceProfileSnapshotJson: serviceProfile + ? JSON.stringify({ + profileId: serviceProfile.profileId, + categoryId: serviceProfile.categoryId, + versionId: serviceProfile.versionId, + versionNo: serviceProfile.versionNo, + config: serviceProfile.config, + }) + : null, now, }) if (!workOrder) { @@ -157,10 +185,44 @@ export async function createAdminMockWorkOrder(payload: JsonObject = {}) { productRuleId: productRuleMatch.rule ? Number(productRuleMatch.rule.id) : 0, productRuleKey: productRuleMatch.rule ? String(productRuleMatch.rule.rule_key || '') : '', productRuleMatch: productRuleMatch.reason, + serviceProfileId: serviceProfile?.profileId || 0, + serviceProfileVersionId: serviceProfile?.versionId || 0, }), now, }) - return { order: mapWorkOrderAdmin(workOrder) } + let autoPublishQueued = false + if (materialComplete && serviceProfile?.config.autoPublish.enabled) { + const job = await enqueueOrderServiceAutoPublish({ + workOrderId: Number(workOrder.id), + maxAttempts: serviceProfile.config.autoPublish.retryMax + 1, + }) + autoPublishQueued = Boolean(job) + if (autoPublishQueued) { + void processDueOrderServiceAutoPublishJobs(1).catch(() => undefined) + } + } + return { order: mapWorkOrderAdmin(workOrder), autoPublishQueued } +} + +const MOCK_FIELD_DEFAULT_VALUES: Record = { + gameId: '10000001', + gameNickname: '测试角色', + serverZone: '微信区', + system: '安卓', + gameAccount: 'test_account', + roleLevel: '100', + contactPhone: '13800138000', + orderNote: 'Mock 订单备注', +} + +function resolveMockMaterialFieldValue(field: { + key: string + label: string + validation?: 'none' | 'digits' + mockValue?: string +}) { + if (field.validation === 'digits') return '10000001' + return String(field.mockValue || MOCK_FIELD_DEFAULT_VALUES[field.key] || `${field.label}测试值`) } /** 本地 Mock 与真实订单保持相同的关键字段约束,避免生成无法追溯的测试工单。 */ diff --git a/apps/backend/src/services/worker-platform/worker-platform-service.test.ts b/apps/backend/src/services/worker-platform/worker-platform-service.test.ts index 4b2fb302..2585ce56 100644 --- a/apps/backend/src/services/worker-platform/worker-platform-service.test.ts +++ b/apps/backend/src/services/worker-platform/worker-platform-service.test.ts @@ -893,6 +893,17 @@ test('本地 Mock 工单要求订单、店铺、商品与两个金额完整有 assert.deepEqual(resolveAdminMockWorkOrderInput(baseInput, shops).rewardAmount, 2500) }) +test('Mock 建单必须绑定已发布订单服务快照,并在资料完整时进入自动发布队列', async () => { + const { readFile } = await import('node:fs/promises') + const source = await readFile( + new URL('./admin-mock-work-order-service.ts', import.meta.url), + 'utf8', + ) + assert.match(source, /getPublishedOrderServiceProfileSnapshotByCategory/) + assert.match(source, /serviceProfileSnapshotJson/) + assert.match(source, /enqueueOrderServiceAutoPublish/) +}) + test('排行榜打手名称仅展示首字符', () => { assert.equal(maskWorkerLeaderboardDisplayName('张三'), '张****') assert.equal(maskWorkerLeaderboardDisplayName('Alice'), 'A****')