From 7ccf3a6cee631e3220228e7317f947fe9d81154e Mon Sep 17 00:00:00 2001 From: yml2213 Date: Tue, 18 Aug 2026 14:37:13 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E6=8C=89=E4=BB=B6=E6=8B=BC?= =?UTF-8?q?=E5=8D=95=E8=87=AA=E5=8A=A8=E8=AE=A1=E7=AE=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../036_worker_product_rule_sharing_auto.sql | 7 ++ .../src/repositories/worker-platform/types.ts | 1 + .../worker-platform/work-order-repo.ts | 13 ++-- .../services/worker-platform/admin-service.ts | 75 +++++++++++++++---- .../src/services/worker-platform/mappers.ts | 27 +++++++ .../worker-platform-service.test.ts | 38 ++++++++++ .../pages/admin/panels/ProductRulesPanel.tsx | 68 ++++++++++++----- .../src/services/admin/worker-platform.ts | 1 + apps/frontend/src/types/worker-platform.ts | 1 + 9 files changed, 193 insertions(+), 38 deletions(-) create mode 100644 apps/backend/src/db/migrations/036_worker_product_rule_sharing_auto.sql diff --git a/apps/backend/src/db/migrations/036_worker_product_rule_sharing_auto.sql b/apps/backend/src/db/migrations/036_worker_product_rule_sharing_auto.sql new file mode 100644 index 00000000..8255f5f4 --- /dev/null +++ b/apps/backend/src/db/migrations/036_worker_product_rule_sharing_auto.sql @@ -0,0 +1,7 @@ +-- 036_worker_product_rule_sharing_auto.sql —— 按件计价拼单可随匹配订单自动计算。 + +ALTER TABLE work_product_rules + ADD COLUMN IF NOT EXISTS sharing_auto_from_order BOOLEAN NOT NULL DEFAULT false; + +COMMENT ON COLUMN work_product_rules.sharing_auto_from_order + IS '按件计价且启用拼单时,是否用匹配 SKU 数量与模板单价自动生成拼单配置;false 表示使用手工配置'; diff --git a/apps/backend/src/repositories/worker-platform/types.ts b/apps/backend/src/repositories/worker-platform/types.ts index 9bbc9c59..17284155 100644 --- a/apps/backend/src/repositories/worker-platform/types.ts +++ b/apps/backend/src/repositories/worker-platform/types.ts @@ -150,6 +150,7 @@ export type WorkProductRuleRow = { sharing_enabled: boolean sharing_total_quantity: number sharing_unit_reward: number + sharing_auto_from_order: boolean timeout_minutes: number timeout_policy: string requirement_json: string | Record 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 b01488a9..620e6351 100644 --- a/apps/backend/src/repositories/worker-platform/work-order-repo.ts +++ b/apps/backend/src/repositories/worker-platform/work-order-repo.ts @@ -718,6 +718,7 @@ export async function upsertWorkProductRule(input: { sharingEnabled?: boolean sharingTotalQuantity?: number sharingUnitReward?: number + sharingAutoFromOrder?: boolean timeoutMinutes?: number timeoutPolicy?: string requirementJson: string @@ -732,7 +733,7 @@ export async function upsertWorkProductRule(input: { match_type, category_id, enabled, auto_create, reward_amount, unit_price_fen, required_deposit_amount, deposit_threshold_amount, - sharing_enabled, sharing_total_quantity, sharing_unit_reward, + sharing_enabled, sharing_total_quantity, sharing_unit_reward, sharing_auto_from_order, timeout_minutes, timeout_policy, requirement_json, match_json, sort_order, created_at, updated_at @@ -741,10 +742,10 @@ export async function upsertWorkProductRule(input: { $7, $8, $9, $10, $11, $12, $13, $14, - $15, $16, $17, - $18, $19, - $20::jsonb, $21::jsonb, - $22, $23, $24 + $15, $16, $17, $18, + $19, $20, + $21::jsonb, $22::jsonb, + $23, $24, $25 ) ON CONFLICT (rule_key) DO UPDATE SET @@ -764,6 +765,7 @@ export async function upsertWorkProductRule(input: { sharing_enabled = EXCLUDED.sharing_enabled, sharing_total_quantity = EXCLUDED.sharing_total_quantity, sharing_unit_reward = EXCLUDED.sharing_unit_reward, + sharing_auto_from_order = EXCLUDED.sharing_auto_from_order, timeout_minutes = EXCLUDED.timeout_minutes, timeout_policy = EXCLUDED.timeout_policy, requirement_json = EXCLUDED.requirement_json, @@ -790,6 +792,7 @@ export async function upsertWorkProductRule(input: { input.sharingEnabled === true, toPositiveInteger(input.sharingTotalQuantity, 1), toPositiveInteger(input.sharingUnitReward, 0), + input.sharingAutoFromOrder === true, toPositiveInteger(input.timeoutMinutes, 0), String(input.timeoutPolicy || 'reopen').trim(), input.requirementJson, diff --git a/apps/backend/src/services/worker-platform/admin-service.ts b/apps/backend/src/services/worker-platform/admin-service.ts index 69098252..b6a5c2fc 100644 --- a/apps/backend/src/services/worker-platform/admin-service.ts +++ b/apps/backend/src/services/worker-platform/admin-service.ts @@ -133,6 +133,7 @@ import { resolveKuaishouWorkProductMatchContext, resolveRequirementFields, resolveSkuNameQuantity, + resolveWorkOrderRulePricing, resolveWorkerPermissions, } from './mappers.js' import { @@ -604,12 +605,47 @@ export async function saveAdminWorkProductRule(payload: JsonObject = {}) { }) } + const ruleId = normalizeOptionalId(payload.ruleId ?? payload.rule_id) + const existingRule = ruleId ? await getWorkProductRuleById(ruleId) : null + if (ruleId && !existingRule) { + throw createHttpError('接单模板不存在', { + statusCode: 404, + errorCode: 'work_product_rule_not_found', + }) + } + const rewardAmount = normalizeAmountFen(payload.rewardAmount ?? payload.rewardAmountYuan, 0) const unitPriceFen = payload.unitPrice === undefined && payload.unitPriceYuan === undefined ? 0 : normalizeAmountFen(payload.unitPrice ?? payload.unitPriceYuan, 0) const sharingEnabled = normalizeBoolean(payload.sharingEnabled ?? payload.sharing_enabled, false) + const hasManualSharingConfig = + hasConfiguredSharingValue(payload.sharingTotalQuantity ?? payload.sharing_total_quantity) || + hasConfiguredSharingValue(payload.sharingUnitReward ?? payload.sharingUnitRewardYuan) || + hasConfiguredSharingValue(payload.sharingTotalAmount ?? payload.sharingTotalAmountYuan) + const sharingAutoFromOrder = + sharingEnabled && + unitPriceFen > 0 && + (normalizeBoolean(payload.sharingAutoFromOrder ?? payload.sharing_auto_from_order, false) || + (!hasManualSharingConfig && existingRule?.sharing_auto_from_order !== false)) + const hasManualSharingQuantity = hasConfiguredSharingValue( + payload.sharingTotalQuantity ?? payload.sharing_total_quantity, + ) + const hasManualSharingUnitReward = hasConfiguredSharingValue( + payload.sharingUnitReward ?? payload.sharingUnitRewardYuan, + ) + if ( + sharingEnabled && + unitPriceFen > 0 && + !sharingAutoFromOrder && + hasManualSharingQuantity !== hasManualSharingUnitReward + ) { + throw createHttpError('按件计价的拼单覆盖值请同时填写拼单数量和拼单单价', { + statusCode: 400, + errorCode: 'work_product_rule_sharing_override_incomplete', + }) + } const sharingUnitReward = normalizeAmountFen( payload.sharingUnitReward ?? payload.sharingUnitRewardYuan, 0, @@ -625,7 +661,7 @@ export async function saveAdminWorkProductRule(payload: JsonObject = {}) { ) const resolvedSharingTotalAmount = sharingTotalAmount > 0 ? sharingTotalAmount : sharingUnitReward * sharingTotalQuantity - if (sharingEnabled) { + if (sharingEnabled && !sharingAutoFromOrder) { if (resolvedSharingTotalAmount <= 0) { throw createHttpError('启用拼单时请填写拼单总价或单价', { statusCode: 400, @@ -633,16 +669,11 @@ export async function saveAdminWorkProductRule(payload: JsonObject = {}) { }) } } - const finalRewardAmount = sharingEnabled ? resolvedSharingTotalAmount : rewardAmount - - const ruleId = normalizeOptionalId(payload.ruleId ?? payload.rule_id) - const existingRule = ruleId ? await getWorkProductRuleById(ruleId) : null - if (ruleId && !existingRule) { - throw createHttpError('接单模板不存在', { - statusCode: 404, - errorCode: 'work_product_rule_not_found', - }) - } + const finalRewardAmount = sharingEnabled + ? sharingAutoFromOrder + ? 0 + : resolvedSharingTotalAmount + : rewardAmount const fields = normalizeRequirementFieldsFromPayload(payload) const ruleKey = existingRule?.rule_key || randomId('rule-') const rule = await upsertWorkProductRule({ @@ -658,7 +689,7 @@ export async function saveAdminWorkProductRule(payload: JsonObject = {}) { defaults.category?.id || null, enabled: normalizeBoolean(payload.enabled, true), - autoCreate: normalizeBoolean(payload.autoCreate ?? payload.auto_create, false), + autoCreate: normalizeBoolean(payload.autoCreate ?? payload.auto_create, true), rewardAmount: finalRewardAmount, unitPriceFen, // 商品规则不配置押金;工单创建后由运营按需手工填写。 @@ -667,6 +698,7 @@ export async function saveAdminWorkProductRule(payload: JsonObject = {}) { sharingEnabled, sharingTotalQuantity, sharingUnitReward, + sharingAutoFromOrder, timeoutMinutes: Math.max(0, normalizeInteger(payload.timeoutMinutes, 0)), timeoutPolicy: normalizeWorkOrderTimeoutPolicy(payload.timeoutPolicy), requirementJson: JSON.stringify({ fields }), @@ -1287,6 +1319,10 @@ function resolveSharingQuantity( return Math.max(1, fallbackQuantity) } +function hasConfiguredSharingValue(value: unknown) { + return value !== undefined && value !== null && String(value).trim() !== '' +} + export async function submitAdminWorkOrderMaterial( workOrderId: number | string, payload: JsonObject = {}, @@ -2070,8 +2106,15 @@ export async function syncWorkerOrdersForSourceOrder( const skuQuantity = resolveSkuNameQuantity(item.sku_name) const unitPriceFen = Number(rule.unit_price_fen || 0) // 按数量计价:单价 × SKU 名称文字中的规格数量(如 指挥官秘钥15个);未启用则用固定接单金额 - const rewardAmount = - unitPriceFen > 0 ? unitPriceFen * skuQuantity : Number(rule.reward_amount || 0) + const { sharingTotalQuantity, sharingUnitReward, rewardAmount } = resolveWorkOrderRulePricing({ + skuQuantity, + unitPriceFen, + fixedRewardAmount: Number(rule.reward_amount || 0), + sharingEnabled: rule.sharing_enabled === true, + sharingAutoFromOrder: rule.sharing_auto_from_order === true, + sharingTotalQuantity: Number(rule.sharing_total_quantity || 1), + sharingUnitReward: Number(rule.sharing_unit_reward || 0), + }) const now = nowIso() const materialComplete = fields.every((field) => !field.required) const workOrder = await createWorkOrder({ @@ -2087,8 +2130,8 @@ export async function syncWorkerOrdersForSourceOrder( requiredDepositAmount: 0, depositThresholdAmount: 0, sharingEnabled: rule.sharing_enabled === true, - sharingTotalQuantity: Number(rule.sharing_total_quantity || 1), - sharingUnitReward: Number(rule.sharing_unit_reward || 0), + sharingTotalQuantity, + sharingUnitReward, timeoutMinutes: Number(rule.timeout_minutes || 0), timeoutPolicy: String(rule.timeout_policy || 'reopen').trim(), materialJson: JSON.stringify({ diff --git a/apps/backend/src/services/worker-platform/mappers.ts b/apps/backend/src/services/worker-platform/mappers.ts index 47b11873..d07d04d4 100644 --- a/apps/backend/src/services/worker-platform/mappers.ts +++ b/apps/backend/src/services/worker-platform/mappers.ts @@ -200,6 +200,7 @@ export function mapWorkProductRule(rule: WorkProductRuleRow | null | undefined) enabled: rule.sharing_enabled === true, totalQuantity: Number(rule.sharing_total_quantity || 1), unitReward: Number(rule.sharing_unit_reward || 0), + autoFromOrder: rule.sharing_auto_from_order === true, }, timeoutMinutes: Number(rule.timeout_minutes || 0), timeoutPolicy: String(rule.timeout_policy || 'reopen').trim(), @@ -990,6 +991,32 @@ export function resolveSkuNameQuantity(productName: unknown): number { return 1 } +export function resolveWorkOrderRulePricing(input: { + skuQuantity: number + unitPriceFen: number + fixedRewardAmount: number + sharingEnabled: boolean + sharingAutoFromOrder: boolean + sharingTotalQuantity: number + sharingUnitReward: number +}) { + const skuQuantity = Math.max(1, Number(input.skuQuantity) || 1) + const unitPriceFen = Math.max(0, Number(input.unitPriceFen) || 0) + const sharingTotalQuantity = input.sharingAutoFromOrder + ? skuQuantity + : Math.max(1, Number(input.sharingTotalQuantity) || 1) + const sharingUnitReward = input.sharingAutoFromOrder + ? unitPriceFen + : Math.max(0, Number(input.sharingUnitReward) || 0) + const rewardAmount = input.sharingEnabled + ? sharingTotalQuantity * sharingUnitReward + : unitPriceFen > 0 + ? unitPriceFen * skuQuantity + : Math.max(0, Number(input.fixedRewardAmount) || 0) + + return { sharingTotalQuantity, sharingUnitReward, rewardAmount } +} + const CHINESE_DIGITS: Record = { 零: 0, 一: 1, 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 6dc84e9f..15d200dc 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 @@ -20,6 +20,7 @@ import { resolveMatchingProductRuleDecision, resolveWorkerPermissions, resolveSkuNameQuantity, + resolveWorkOrderRulePricing, validateWorkerPassword, } from './index.js' @@ -87,6 +88,7 @@ function buildProductRule(overrides: Partial = {}): WorkProd sharing_enabled: false, sharing_total_quantity: 1, sharing_unit_reward: 0, + sharing_auto_from_order: false, timeout_minutes: 0, timeout_policy: 'reopen', requirement_json: {}, @@ -519,6 +521,42 @@ test('resolveSkuNameQuantity falls back to 1 when no quantity is found', () => { assert.equal(resolveSkuNameQuantity('指挥官秘钥礼包'), 1) }) +test('按件拼单未手动覆盖时,使用匹配数量与模板单价', () => { + const pricing = resolveWorkOrderRulePricing({ + skuQuantity: 10, + unitPriceFen: 20, + fixedRewardAmount: 0, + sharingEnabled: true, + sharingAutoFromOrder: true, + sharingTotalQuantity: 1, + sharingUnitReward: 0, + }) + + assert.deepEqual(pricing, { + sharingTotalQuantity: 10, + sharingUnitReward: 20, + rewardAmount: 200, + }) +}) + +test('按件拼单手动覆盖数量和单价时,保留手工配置', () => { + const pricing = resolveWorkOrderRulePricing({ + skuQuantity: 10, + unitPriceFen: 20, + fixedRewardAmount: 0, + sharingEnabled: true, + sharingAutoFromOrder: false, + sharingTotalQuantity: 3, + sharingUnitReward: 50, + }) + + assert.deepEqual(pricing, { + sharingTotalQuantity: 3, + sharingUnitReward: 50, + rewardAmount: 150, + }) +}) + function buildWorkOrderRow(overrides: Partial = {}): WorkOrderRow { return { id: 1, diff --git a/apps/frontend/src/pages/admin/panels/ProductRulesPanel.tsx b/apps/frontend/src/pages/admin/panels/ProductRulesPanel.tsx index 936a95b9..312fb636 100644 --- a/apps/frontend/src/pages/admin/panels/ProductRulesPanel.tsx +++ b/apps/frontend/src/pages/admin/panels/ProductRulesPanel.tsx @@ -106,10 +106,16 @@ export default function ProductRulesPanel() { rewardAmount: rule.rewardAmount / 100, unitPrice: rule.unitPriceFen / 100, sharingEnabled: rule.sharing?.enabled || false, - sharingTotalQuantity: rule.sharing?.totalQuantity || 1, - sharingUnitReward: (rule.sharing?.unitReward || 0) / 100, - sharingTotalAmount: - ((rule.sharing?.totalQuantity || 1) * (rule.sharing?.unitReward || 0)) / 100, + sharingAutoFromOrder: rule.sharing?.autoFromOrder || false, + sharingTotalQuantity: rule.sharing?.autoFromOrder + ? undefined + : rule.sharing?.totalQuantity || 1, + sharingUnitReward: rule.sharing?.autoFromOrder + ? undefined + : (rule.sharing?.unitReward || 0) / 100, + sharingTotalAmount: rule.sharing?.autoFromOrder + ? undefined + : ((rule.sharing?.totalQuantity || 1) * (rule.sharing?.unitReward || 0)) / 100, timeoutMinutes: rule.timeoutMinutes || 0, timeoutPolicy: rule.timeoutPolicy || 'reopen', fieldsText: formatRuleFields(rule.requirement?.fields || []), @@ -142,6 +148,7 @@ export default function ProductRulesPanel() { pricingMode?: string unitPrice?: number sharingEnabled?: boolean + sharingAutoFromOrder?: boolean sharingTotalQuantity?: number sharingUnitReward?: number sharingTotalAmount?: number @@ -154,6 +161,8 @@ export default function ProductRulesPanel() { }) { try { const unitMode = values.pricingMode === 'unit' + const hasManualSharingOverride = + Number(values.sharingTotalQuantity || 0) > 0 || Number(values.sharingUnitReward || 0) > 0 const { pricingMode: _pricingMode, ...payload } = values await saveAdminWorkProductRule({ ...payload, @@ -161,6 +170,8 @@ export default function ProductRulesPanel() { ruleKey: editingRule?.ruleKey || '', match: editingRule?.match, unitPrice: unitMode ? Number(values.unitPrice || 0) : 0, + sharingAutoFromOrder: + unitMode && values.sharingEnabled === true && !hasManualSharingOverride, }) message.success(editingRule ? '接单模板已更新' : '接单模板已创建') resetRuleForm() @@ -193,10 +204,16 @@ export default function ProductRulesPanel() { rewardAmount: rule.rewardAmount / 100, unitPrice: rule.unitPriceFen / 100, sharingEnabled: rule.sharing?.enabled || false, - sharingTotalQuantity: rule.sharing?.totalQuantity || 1, - sharingUnitReward: (rule.sharing?.unitReward || 0) / 100, - sharingTotalAmount: - ((rule.sharing?.totalQuantity || 1) * (rule.sharing?.unitReward || 0)) / 100, + sharingAutoFromOrder: rule.sharing?.autoFromOrder || false, + sharingTotalQuantity: rule.sharing?.autoFromOrder + ? undefined + : rule.sharing?.totalQuantity || 1, + sharingUnitReward: rule.sharing?.autoFromOrder + ? undefined + : (rule.sharing?.unitReward || 0) / 100, + sharingTotalAmount: rule.sharing?.autoFromOrder + ? undefined + : ((rule.sharing?.totalQuantity || 1) * (rule.sharing?.unitReward || 0)) / 100, timeoutMinutes: rule.timeoutMinutes || 0, timeoutPolicy: rule.timeoutPolicy || 'reopen', fieldsText: formatRuleFields(rule.requirement?.fields || []), @@ -340,7 +357,7 @@ export default function ProductRulesPanel() { render: (_, row) => (
- {formatMoney(row.rewardAmount)} + {row.sharing?.autoFromOrder ? '随订单自动计算' : formatMoney(row.rewardAmount)} {row.unitPriceFen > 0 ? ( @@ -461,11 +478,8 @@ export default function ProductRulesPanel() { pricingMode: 'fixed', rewardAmount: 25, enabled: true, - autoCreate: false, + autoCreate: true, sharingEnabled: false, - sharingTotalQuantity: 10, - sharingUnitReward: 5, - sharingTotalAmount: 50, fieldsText: 'gameId:游戏编号\ngameNickname:游戏昵称\nsystem:系统#安卓,苹果\nserverZone:区服#QQ区,微信区', }} @@ -563,8 +577,28 @@ export default function ProductRulesPanel() {
- {({ getFieldValue }) => - getFieldValue('sharingEnabled') ? ( + {({ getFieldValue }) => { + const sharingEnabled = getFieldValue('sharingEnabled') + const unitMode = getFieldValue('pricingMode') === 'unit' + if (!sharingEnabled) return null + + if (unitMode) { + return ( +
+ + 默认按匹配 SKU 数量和上方单价自动生成拼单;填写以下两项后改为固定配置。 + + + + + + + +
+ ) + } + + return (
- ) : null - } + ) + }}
{/* 资料字段: 默认展开 4 行全部清晰可见,无内嵌滚动条 */} diff --git a/apps/frontend/src/services/admin/worker-platform.ts b/apps/frontend/src/services/admin/worker-platform.ts index 581c1024..9ca62b91 100644 --- a/apps/frontend/src/services/admin/worker-platform.ts +++ b/apps/frontend/src/services/admin/worker-platform.ts @@ -124,6 +124,7 @@ export function saveAdminWorkProductRule(payload: { rewardAmount?: number unitPrice?: number sharingEnabled?: boolean + sharingAutoFromOrder?: boolean sharingTotalQuantity?: number sharingUnitReward?: number sharingTotalAmount?: number diff --git a/apps/frontend/src/types/worker-platform.ts b/apps/frontend/src/types/worker-platform.ts index de870ed9..193e3ce6 100644 --- a/apps/frontend/src/types/worker-platform.ts +++ b/apps/frontend/src/types/worker-platform.ts @@ -305,6 +305,7 @@ export type WorkOrderSharingConfig = { enabled: boolean totalQuantity: number unitReward: number + autoFromOrder?: boolean } export type WorkerAcceptance = {