fix(order-service): 修复 Mock 工单未绑定自动发布配置

This commit is contained in:
yml2213
2026-11-04 12:58:34 +08:00
parent 3f7a3ab5d1
commit 0638582205
3 changed files with 125 additions and 4 deletions
@@ -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;
@@ -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<Record<string, string>>((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<string, string> = {
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 与真实订单保持相同的关键字段约束,避免生成无法追溯的测试工单。 */
@@ -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****')