From a7bac9c1d8e9475023ed82f59a326ff821d55390 Mon Sep 17 00:00:00 2001 From: yml2213 Date: Mon, 2 Nov 2026 17:57:49 +0800 Subject: [PATCH] =?UTF-8?q?fix(admin):=20=E6=81=A2=E5=A4=8D=E6=8E=A5?= =?UTF-8?q?=E5=8D=95=E6=A8=A1=E6=9D=BF=E8=AE=A1=E4=BB=B7=E9=85=8D=E7=BD=AE?= =?UTF-8?q?=E5=B9=B6=E7=BB=9F=E4=B8=80=E6=88=90=E6=9C=AC=E4=BB=B7=E6=A0=BC?= =?UTF-8?q?=E9=93=BE=E8=B7=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../123_work_product_rule_pricing_mode.sql | 54 ++++++++++++++++++ .../src/repositories/worker-platform/types.ts | 1 + .../worker-platform/work-product-rule-repo.ts | 7 ++- .../cost-center/cost-center-mappers.ts | 6 +- .../services/cost-center/cost-center-types.ts | 1 + .../cost-center/cost-definition-service.ts | 1 + .../cost-center/cost-snapshot-service.test.ts | 19 +++++++ .../cost-center/cost-snapshot-service.ts | 12 +++- .../cost-center/product-catalog-service.ts | 21 +++---- .../cost-center/template-bootstrap-service.ts | 3 + .../cost-center/work-order-product-service.ts | 8 +-- ...rk-order-template-cost.integration.test.ts | 53 ++++++++++++++++-- .../work-order-template-product-service.ts | 5 +- .../src/services/product-match/pricing.ts | 3 +- .../product-match/product-match.test.ts | 3 + .../admin-product-match-test-service.ts | 3 +- .../admin-worker-catalog-service.ts | 54 ++++++++++++++++-- .../mock-work-order-rule-match.test.ts | 1 + .../sync-worker-orders-service.ts | 1 + .../work-product-match-mappers.ts | 1 + .../worker-platform-service.test.ts | 34 +++++++++++- apps/frontend/src/layouts/AdminLayout.tsx | 4 ++ .../admin/cost-center/ProductCatalogPanel.tsx | 34 +++++------- .../pages/admin/panels/ProductRulesPanel.tsx | 55 ++++++++++++------- .../src/services/admin/cost-center.ts | 1 - .../src/services/admin/worker-platform.ts | 1 + apps/frontend/src/types/worker-platform.ts | 1 + 27 files changed, 304 insertions(+), 83 deletions(-) create mode 100644 apps/backend/src/db/migrations/123_work_product_rule_pricing_mode.sql diff --git a/apps/backend/src/db/migrations/123_work_product_rule_pricing_mode.sql b/apps/backend/src/db/migrations/123_work_product_rule_pricing_mode.sql new file mode 100644 index 00000000..18f930f2 --- /dev/null +++ b/apps/backend/src/db/migrations/123_work_product_rule_pricing_mode.sql @@ -0,0 +1,54 @@ +-- 接单模板显式保存计价方式,金额字段只由商品与成本中心写投影。 +-- 历史数据按旧运行口径回填:拼单自动按件优先识别,其次识别“单价>0 且固定金额=0”。 + +ALTER TABLE work_product_rules + ADD COLUMN IF NOT EXISTS pricing_mode TEXT; + +UPDATE work_product_rules +SET pricing_mode = CASE + WHEN sharing_enabled = TRUE + AND sharing_auto_from_order = TRUE + AND unit_price_fen > 0 THEN 'unit' + WHEN unit_price_fen > 0 AND reward_amount = 0 THEN 'unit' + ELSE 'fixed' +END +WHERE pricing_mode IS NULL + OR pricing_mode NOT IN ('fixed', 'unit'); + +ALTER TABLE work_product_rules + ALTER COLUMN pricing_mode SET DEFAULT 'fixed', + ALTER COLUMN pricing_mode SET NOT NULL; + +-- 自动拼单只能依赖每件金额;固定金额模板不能保留“随订单自动算拼单”的旧标志。 +UPDATE work_product_rules +SET sharing_auto_from_order = FALSE +WHERE pricing_mode = 'fixed' + AND sharing_auto_from_order = TRUE; + +DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 + FROM pg_constraint + WHERE conname = 'chk_work_product_rules_pricing_mode' + AND conrelid = 'work_product_rules'::regclass + ) THEN + ALTER TABLE work_product_rules + ADD CONSTRAINT chk_work_product_rules_pricing_mode + CHECK (pricing_mode IN ('fixed', 'unit')); + END IF; +END $$; + +COMMENT ON COLUMN work_product_rules.pricing_mode IS + '接单计价方式:fixed 固定整单金额 / unit 按 SKU 数量乘每件金额'; + +-- 同步模板商品属性中的计价方式,避免继续从两个金额字段反推。 +UPDATE product_catalog p +SET attributes_json = COALESCE(p.attributes_json, '{}'::jsonb) || jsonb_build_object( + 'pricingMode', wpr.pricing_mode, + 'rewardAmountFen', wpr.reward_amount, + 'unitPriceFen', wpr.unit_price_fen + ), + updated_at = NOW() +FROM work_product_rules wpr +WHERE p.canonical_sku = LEFT('tpl:' || BTRIM(wpr.rule_key), 64); diff --git a/apps/backend/src/repositories/worker-platform/types.ts b/apps/backend/src/repositories/worker-platform/types.ts index 9511a1b0..0d12436b 100644 --- a/apps/backend/src/repositories/worker-platform/types.ts +++ b/apps/backend/src/repositories/worker-platform/types.ts @@ -288,6 +288,7 @@ export type WorkProductRuleRow = { auto_create: boolean reward_amount: number unit_price_fen: number + pricing_mode: 'fixed' | 'unit' required_deposit_amount: number deposit_threshold_amount: number sharing_enabled: boolean diff --git a/apps/backend/src/repositories/worker-platform/work-product-rule-repo.ts b/apps/backend/src/repositories/worker-platform/work-product-rule-repo.ts index 8bb1f58d..06318a2f 100644 --- a/apps/backend/src/repositories/worker-platform/work-product-rule-repo.ts +++ b/apps/backend/src/repositories/worker-platform/work-product-rule-repo.ts @@ -356,6 +356,7 @@ export async function upsertWorkProductRule(input: { autoCreate: boolean rewardAmount: number unitPriceFen?: number + pricingMode: 'fixed' | 'unit' requiredDepositAmount: number depositThresholdAmount: number sharingEnabled?: boolean @@ -385,7 +386,7 @@ export async function upsertWorkProductRule(input: { timeout_minutes, timeout_policy, acceptance_mode, gift_cooldown_hours, gift_cooldown_minutes, requirement_json, match_json, - sort_order, created_at, updated_at + sort_order, pricing_mode, created_at, updated_at ) VALUES ( $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, @@ -395,7 +396,7 @@ export async function upsertWorkProductRule(input: { $19, $20, $21, $22, $23, $24::jsonb, $25::jsonb, - $26, $27, $28 + $26, $27, $28, $29 ) ON CONFLICT (rule_key) DO UPDATE SET @@ -424,6 +425,7 @@ export async function upsertWorkProductRule(input: { requirement_json = EXCLUDED.requirement_json, match_json = EXCLUDED.match_json, sort_order = EXCLUDED.sort_order, + pricing_mode = EXCLUDED.pricing_mode, updated_at = EXCLUDED.updated_at RETURNING * `, @@ -454,6 +456,7 @@ export async function upsertWorkProductRule(input: { input.requirementJson, input.matchJson || '{}', input.sortOrder, + input.pricingMode, input.now, input.now, ], diff --git a/apps/backend/src/services/cost-center/cost-center-mappers.ts b/apps/backend/src/services/cost-center/cost-center-mappers.ts index fac496db..0e33fb50 100644 --- a/apps/backend/src/services/cost-center/cost-center-mappers.ts +++ b/apps/backend/src/services/cost-center/cost-center-mappers.ts @@ -188,13 +188,11 @@ export function mapCostVersion(row: CostVersionRow) { const revenueFen = Number(row.revenue_fen || 0) const rewardAvgFen = Number(row.reward_avg_fen || 0) const rewardFen = Number(row.reward_fen || 0) - const ruleUnitPriceFen = Number(row.rule_unit_price_fen || 0) const ruleRewardAmountFen = Number(row.rule_reward_amount || 0) const amountKind = row.cost_item_code === 'sys_work_order_reward' && row.scope_type === 'rule' && - ruleUnitPriceFen > 0 && - ruleRewardAmountFen === 0 + row.work_product_rule_pricing_mode === 'unit' ? 'unit_price' : 'fixed' return { @@ -214,7 +212,7 @@ export function mapCostVersion(row: CostVersionRow) { workProductRuleName: row.work_product_rule_name || '', workProductRuleKey: row.work_product_rule_key || '', ruleRewardAmountFen, - ruleUnitPriceFen, + ruleUnitPriceFen: Number(row.rule_unit_price_fen || 0), revenueItemCount, revenueFen, revenueAvgFen: revenueItemCount > 0 ? Math.round(revenueFen / revenueItemCount) : 0, diff --git a/apps/backend/src/services/cost-center/cost-center-types.ts b/apps/backend/src/services/cost-center/cost-center-types.ts index 99cc9c99..db0ab4b6 100644 --- a/apps/backend/src/services/cost-center/cost-center-types.ts +++ b/apps/backend/src/services/cost-center/cost-center-types.ts @@ -113,6 +113,7 @@ export type CostVersionRow = { work_product_rule_id: number | null work_product_rule_name: string | null work_product_rule_key: string | null + work_product_rule_pricing_mode?: 'fixed' | 'unit' | null rule_reward_amount: number | null rule_unit_price_fen: number | null revenue_item_count: number | null diff --git a/apps/backend/src/services/cost-center/cost-definition-service.ts b/apps/backend/src/services/cost-center/cost-definition-service.ts index 84700ef6..75f9bf59 100644 --- a/apps/backend/src/services/cost-center/cost-definition-service.ts +++ b/apps/backend/src/services/cost-center/cost-definition-service.ts @@ -324,6 +324,7 @@ export async function listAdminCostVersions(payload: JsonObject = {}) { p.canonical_name, wpr.product_name AS work_product_rule_name, wpr.rule_key AS work_product_rule_key, + wpr.pricing_mode AS work_product_rule_pricing_mode, wpr.reward_amount AS rule_reward_amount, wpr.unit_price_fen AS rule_unit_price_fen, rev.revenue_item_count, diff --git a/apps/backend/src/services/cost-center/cost-snapshot-service.test.ts b/apps/backend/src/services/cost-center/cost-snapshot-service.test.ts index 0dd3f99d..1e14a4ab 100644 --- a/apps/backend/src/services/cost-center/cost-snapshot-service.test.ts +++ b/apps/backend/src/services/cost-center/cost-snapshot-service.test.ts @@ -103,6 +103,25 @@ test('成本解析:卡券渠道按件计,取业务日期前的最新版本', assert.equal(resolution.unitAmountFen, 5000) }) +test('成本解析:接单模板按 SKU 计件时,预估成本按数量乘每件金额', () => { + const resolution = resolveCost( + item({ + fulfillment_channel: 'work_order', + canonical_product_id: null, + quantity: 2, + work_product_rule_id: 9, + work_product_rule_pricing_mode: 'unit', + }), + costItems, + versions, + pricingModes, + ) + assert.ok(resolution) + assert.equal(resolution.costVersionId, 101) + assert.equal(resolution.unitAmountFen, 840) + assert.equal(resolution.totalAmountFen, 1680) +}) + test('成本解析:生效日期晚于业务日期的版本不参与', () => { const version = selectCostVersion(versions, { costItemId: 4, diff --git a/apps/backend/src/services/cost-center/cost-snapshot-service.ts b/apps/backend/src/services/cost-center/cost-snapshot-service.ts index dcb65109..e25874e2 100644 --- a/apps/backend/src/services/cost-center/cost-snapshot-service.ts +++ b/apps/backend/src/services/cost-center/cost-snapshot-service.ts @@ -62,6 +62,7 @@ export type CostCandidateItem = { basis_at: string work_order_id: number | null work_product_rule_id: number | null + work_product_rule_pricing_mode?: 'fixed' | 'unit' | null work_order_status: string | null reward_amount: number | null revenue_fen: number @@ -157,7 +158,14 @@ export function resolveCost( }) if (!version) return null - const pricingMode = pricingModeByItemId.get(costItem.id) || costItem.pricing_mode + const pricingMode = + item.fulfillment_channel === 'work_order' && + costItem.code === 'sys_work_order_reward' && + item.work_product_rule_pricing_mode + ? item.work_product_rule_pricing_mode === 'unit' + ? 'per_item' + : 'per_order' + : pricingModeByItemId.get(costItem.id) || costItem.pricing_mode const amount = Number(version.amount_fen) || 0 const quantity = Math.max(1, Number(item.quantity) || 1) @@ -306,11 +314,13 @@ const CANDIDATE_SQL = ` ), 0)::bigint AS refund_fen, wo.id AS work_order_id, wo.product_rule_id AS work_product_rule_id, + wpr.pricing_mode AS work_product_rule_pricing_mode, wo.status AS work_order_status, wo.reward_amount FROM order_items oi JOIN orders o ON o.id = oi.order_id LEFT JOIN work_orders wo ON wo.order_item_id = oi.id + LEFT JOIN work_product_rules wpr ON wpr.id = wo.product_rule_id WHERE oi.fulfillment_status = 'assigned' AND oi.fulfillment_channel IN ( 'affiliate_dash', 'third_party_voucher', 'work_order', 'manual' diff --git a/apps/backend/src/services/cost-center/product-catalog-service.ts b/apps/backend/src/services/cost-center/product-catalog-service.ts index 601e9d1b..f317601f 100644 --- a/apps/backend/src/services/cost-center/product-catalog-service.ts +++ b/apps/backend/src/services/cost-center/product-catalog-service.ts @@ -153,11 +153,7 @@ export async function listAdminProducts(payload: JsonObject = {}) { ` SELECT p.*, - CASE - WHEN tpl_rule.unit_price_fen > 0 AND tpl_rule.reward_amount = 0 THEN 'unit' - WHEN tpl_rule.id IS NOT NULL THEN 'fixed' - ELSE NULL - END AS template_pricing_mode, + tpl_rule.pricing_mode AS template_pricing_mode, tpl_rule.enabled AS template_enabled, tpl_rule.sharing_enabled AS template_sharing_enabled, ( @@ -1551,10 +1547,11 @@ export async function setAdminProductPrice( enabled: boolean reward_amount: number unit_price_fen: number + pricing_mode: 'fixed' | 'unit' sharing_enabled: boolean sharing_auto_from_order: boolean }>( - `SELECT id, rule_key, product_name, enabled, reward_amount, unit_price_fen, + `SELECT id, rule_key, product_name, enabled, reward_amount, unit_price_fen, pricing_mode, sharing_enabled, sharing_auto_from_order FROM work_product_rules WHERE rule_key = $1 @@ -1569,17 +1566,13 @@ export async function setAdminProductPrice( }) } const rawPricingMode = text(payload.pricingMode ?? payload.pricing_mode) - if (rawPricingMode && rawPricingMode !== 'fixed' && rawPricingMode !== 'unit') { - throw createHttpError('接单模板计价方式无效', { + if (rawPricingMode) { + throw createHttpError('接单模板计价方式只能在【接单模板】中设置', { statusCode: 400, - errorCode: 'cost_work_order_pricing_mode_invalid', + errorCode: 'cost_work_order_pricing_mode_managed_by_template', }) } - const pricingMode = - rawPricingMode || - (Number(rule.unit_price_fen || 0) > 0 && Number(rule.reward_amount || 0) === 0 - ? 'unit' - : 'fixed') + const pricingMode: 'fixed' | 'unit' = rule.pricing_mode === 'unit' ? 'unit' : 'fixed' if (pricingMode === 'fixed' && rule.sharing_enabled && rule.sharing_auto_from_order) { throw createHttpError( '拼单自动按件计价与固定金额冲突,请先在接单模板关闭拼单或改为手工拼单价', diff --git a/apps/backend/src/services/cost-center/template-bootstrap-service.ts b/apps/backend/src/services/cost-center/template-bootstrap-service.ts index 08554a08..a9c64b4f 100644 --- a/apps/backend/src/services/cost-center/template-bootstrap-service.ts +++ b/apps/backend/src/services/cost-center/template-bootstrap-service.ts @@ -128,6 +128,7 @@ export async function applyAdminTemplateProductBootstrap( skuNick: template.sku_nick, relSkuId: template.rel_sku_id, sellerIds: template.seller_ids_json, + pricingMode: template.pricing_mode === 'unit' ? 'unit' : 'fixed', unitPriceFen: Number(template.unit_price_fen || 0), rewardAmountFen: Number(template.reward_amount || 0), templateEnabled: template.enabled, @@ -282,6 +283,7 @@ type TemplateBootstrapTemplateRow = { category: string unit_price_fen: number reward_amount: number + pricing_mode: 'fixed' | 'unit' mapping_id: number | null mapping_type: string sku_nick: string @@ -696,6 +698,7 @@ async function loadTemplateProductBootstrapData(range: { fromDate: string; toDat COALESCE(c.name, '') AS category, r.unit_price_fen, r.reward_amount, + r.pricing_mode, m.id AS mapping_id, COALESCE(m.mapping_type, '') AS mapping_type, COALESCE(m.sku_nick, '') AS sku_nick, diff --git a/apps/backend/src/services/cost-center/work-order-product-service.ts b/apps/backend/src/services/cost-center/work-order-product-service.ts index f0a9ed4a..4df12ebf 100644 --- a/apps/backend/src/services/cost-center/work-order-product-service.ts +++ b/apps/backend/src/services/cost-center/work-order-product-service.ts @@ -327,7 +327,7 @@ export async function syncAdminWorkOrderTemplateCostVersions(ruleId?: number) { SELECT wpr.id AS rule_id, CASE - WHEN wpr.unit_price_fen > 0 AND wpr.reward_amount = 0 THEN wpr.unit_price_fen + WHEN wpr.pricing_mode = 'unit' THEN wpr.unit_price_fen ELSE wpr.reward_amount END AS amount_fen FROM work_product_rules wpr @@ -364,7 +364,7 @@ export async function syncAdminWorkOrderTemplateCostVersions(ruleId?: number) { SELECT wpr.id AS rule_id, CASE - WHEN wpr.unit_price_fen > 0 AND wpr.reward_amount = 0 THEN wpr.unit_price_fen + WHEN wpr.pricing_mode = 'unit' THEN wpr.unit_price_fen ELSE wpr.reward_amount END AS amount_fen FROM work_product_rules wpr @@ -416,7 +416,7 @@ export async function syncAdminWorkOrderTemplateCostVersions(ruleId?: number) { 'import', 'active', CASE - WHEN wpr.unit_price_fen > 0 AND wpr.reward_amount = 0 + WHEN wpr.pricing_mode = 'unit' THEN '模板按件发单单价(数量取订单 SKU)' ELSE '模板固定发单金额' END, @@ -469,7 +469,7 @@ export async function syncAdminWorkOrderTemplateCostVersions(ruleId?: number) { SELECT COUNT(*)::int AS rule_count, COUNT(*) FILTER ( - WHERE wpr.unit_price_fen > 0 AND wpr.reward_amount = 0 + WHERE wpr.pricing_mode = 'unit' )::int AS unit_price_rule_count, COUNT(v.id) FILTER ( WHERE v.scope_type = 'rule' AND v.status = 'active' diff --git a/apps/backend/src/services/cost-center/work-order-template-cost.integration.test.ts b/apps/backend/src/services/cost-center/work-order-template-cost.integration.test.ts index ecec92e0..225c1531 100644 --- a/apps/backend/src/services/cost-center/work-order-template-cost.integration.test.ts +++ b/apps/backend/src/services/cost-center/work-order-template-cost.integration.test.ts @@ -45,6 +45,7 @@ test( assert.ok(ruleId > 0) assert.ok(productId > 0) assert.equal(created.rule.enabled, false) + assert.equal(created.rule.pricingMode, 'fixed') assert.equal(created.costConfigurationRequired, true) const product = await query<{ canonical_sku: string; status: string }>( @@ -77,7 +78,6 @@ test( productId, { amountFen: 1234, - pricingMode: 'fixed', changeReason: '接单模板成本集成测试', syncOpenOrders: false, }, @@ -100,10 +100,11 @@ test( enabled: boolean reward_amount: number unit_price_fen: number + pricing_mode: string amount_fen: number }>( ` - SELECT wpr.enabled, wpr.reward_amount, wpr.unit_price_fen, v.amount_fen + SELECT wpr.enabled, wpr.reward_amount, wpr.unit_price_fen, wpr.pricing_mode, v.amount_fen FROM work_product_rules wpr JOIN cost_versions v ON v.work_product_rule_id = wpr.id AND v.status = 'active' WHERE wpr.id = $1 @@ -115,6 +116,7 @@ test( assert.equal(projection.rows[0]?.enabled, false) assert.equal(Number(projection.rows[0]?.reward_amount), 1234) assert.equal(Number(projection.rows[0]?.unit_price_fen), 0) + assert.equal(projection.rows[0]?.pricing_mode, 'fixed') assert.equal(Number(projection.rows[0]?.amount_fen), 1234) await assert.rejects( @@ -124,6 +126,30 @@ test( { amountFen: 2000, pricingMode: 'fixed', + syncOpenOrders: false, + }, + { + sessionId: `it-${suffix}`, + userId: Number(adminUser.id), + username: adminUser.username, + displayName: adminUser.display_name || adminUser.username, + role: 'admin', + expiresAt: new Date(Date.now() + 60_000).toISOString(), + absoluteExpiresAt: new Date(Date.now() + 60_000).toISOString(), + sessionVersion: 1, + deviceId: 'integration-test', + permissions: [], + }, + ), + /计价方式只能在【接单模板】中设置/, + ) + + await assert.rejects( + () => + setAdminProductPrice( + productId, + { + amountFen: 2000, effectiveFrom: new Date(Date.now() + 60 * 60 * 1000).toISOString(), syncOpenOrders: false, }, @@ -143,11 +169,19 @@ test( /暂不支持未来生效/, ) + const unitModeRule = await saveAdminWorkProductRule({ + ruleId, + productName, + pricingMode: 'unit', + }) + assert.equal(unitModeRule.rule.pricingMode, 'unit') + assert.equal(unitModeRule.rule.unitPriceFen, 1234) + assert.equal(unitModeRule.rule.rewardAmount, 0) + await setAdminProductPrice( productId, { amountFen: 1300, - pricingMode: 'unit', changeReason: '接单模板按件成本集成测试', syncOpenOrders: false, }, @@ -167,10 +201,11 @@ test( const unitProjection = await query<{ reward_amount: number unit_price_fen: number + pricing_mode: string amount_fen: number }>( ` - SELECT wpr.reward_amount, wpr.unit_price_fen, v.amount_fen + SELECT wpr.reward_amount, wpr.unit_price_fen, wpr.pricing_mode, v.amount_fen FROM work_product_rules wpr JOIN cost_versions v ON v.work_product_rule_id = wpr.id AND v.status = 'active' WHERE wpr.id = $1 @@ -181,8 +216,18 @@ test( ) assert.equal(Number(unitProjection.rows[0]?.reward_amount), 0) assert.equal(Number(unitProjection.rows[0]?.unit_price_fen), 1300) + assert.equal(unitProjection.rows[0]?.pricing_mode, 'unit') assert.equal(Number(unitProjection.rows[0]?.amount_fen), 1300) + const fixedModeRule = await saveAdminWorkProductRule({ + ruleId, + productName, + pricingMode: 'fixed', + }) + assert.equal(fixedModeRule.rule.pricingMode, 'fixed') + assert.equal(fixedModeRule.rule.rewardAmount, 1300) + assert.equal(fixedModeRule.rule.unitPriceFen, 0) + const enabled = await saveAdminWorkProductRule({ ruleId, productName, diff --git a/apps/backend/src/services/cost-center/work-order-template-product-service.ts b/apps/backend/src/services/cost-center/work-order-template-product-service.ts index 0b4ebe67..acac9036 100644 --- a/apps/backend/src/services/cost-center/work-order-template-product-service.ts +++ b/apps/backend/src/services/cost-center/work-order-template-product-service.ts @@ -97,10 +97,7 @@ export async function syncWorkOrderTemplateProduct( ruleKey: rule.rule_key, categoryId: rule.category_id ? Number(rule.category_id) : null, templateEnabled: rule.enabled === true, - pricingMode: - Number(rule.unit_price_fen || 0) > 0 && Number(rule.reward_amount || 0) === 0 - ? 'unit' - : 'fixed', + pricingMode: rule.pricing_mode === 'unit' ? 'unit' : 'fixed', rewardAmountFen: Number(rule.reward_amount || 0), unitPriceFen: Number(rule.unit_price_fen || 0), sharingEnabled: rule.sharing_enabled === true, diff --git a/apps/backend/src/services/product-match/pricing.ts b/apps/backend/src/services/product-match/pricing.ts index 4f013ec2..cc269668 100644 --- a/apps/backend/src/services/product-match/pricing.ts +++ b/apps/backend/src/services/product-match/pricing.ts @@ -28,6 +28,7 @@ export function resolveWorkOrderRulePricing(input: { skuQuantity: number unitPriceFen: number fixedRewardAmount: number + pricingMode: 'fixed' | 'unit' | string sharingEnabled: boolean sharingAutoFromOrder: boolean sharingTotalQuantity: number @@ -43,7 +44,7 @@ export function resolveWorkOrderRulePricing(input: { : Math.max(0, Number(input.sharingUnitReward) || 0) const rewardAmount = input.sharingEnabled ? sharingTotalQuantity * sharingUnitReward - : unitPriceFen > 0 + : input.pricingMode === 'unit' ? unitPriceFen * skuQuantity : Math.max(0, Number(input.fixedRewardAmount) || 0) diff --git a/apps/backend/src/services/product-match/product-match.test.ts b/apps/backend/src/services/product-match/product-match.test.ts index cbe2aef6..9529b80b 100644 --- a/apps/backend/src/services/product-match/product-match.test.ts +++ b/apps/backend/src/services/product-match/product-match.test.ts @@ -68,6 +68,7 @@ function buildRule(overrides: Partial = {}): WorkProductRule auto_create: true, reward_amount: 0, unit_price_fen: 0, + pricing_mode: 'fixed', required_deposit_amount: 0, deposit_threshold_amount: 20000, sharing_enabled: false, @@ -141,6 +142,7 @@ const commanderRule = buildRule({ rule_key: 'bb', product_name: '指挥官密钥', unit_price_fen: 950, + pricing_mode: 'unit', sharing_enabled: true, sharing_auto_from_order: true, }) @@ -199,6 +201,7 @@ test('抖音:命中指挥官模板后按数量计价并开拼单(9.5 元/个 skuQuantity, unitPriceFen: commanderRule.unit_price_fen, fixedRewardAmount: commanderRule.reward_amount, + pricingMode: commanderRule.pricing_mode, sharingEnabled: commanderRule.sharing_enabled === true, sharingAutoFromOrder: commanderRule.sharing_auto_from_order === true, sharingTotalQuantity: commanderRule.sharing_total_quantity, diff --git a/apps/backend/src/services/worker-platform/admin-product-match-test-service.ts b/apps/backend/src/services/worker-platform/admin-product-match-test-service.ts index 8f778f51..12b6df4f 100644 --- a/apps/backend/src/services/worker-platform/admin-product-match-test-service.ts +++ b/apps/backend/src/services/worker-platform/admin-product-match-test-service.ts @@ -108,6 +108,7 @@ function buildWorkOrderPlan( skuQuantity, unitPriceFen, fixedRewardAmount: Number(rule.reward_amount || 0), + pricingMode: rule.pricing_mode, sharingEnabled: rule.sharing_enabled === true, sharingAutoFromOrder: rule.sharing_auto_from_order === true, sharingTotalQuantity: Number(rule.sharing_total_quantity || 1), @@ -120,7 +121,7 @@ function buildWorkOrderPlan( return { skuQuantity, unitPriceFen, - pricingMode: unitPriceFen > 0 ? 'unit' : 'fixed', + pricingMode: rule.pricing_mode === 'unit' ? 'unit' : 'fixed', rewardAmount: pricing.rewardAmount, sharingEnabled, sharingTotalQuantity: sharingEnabled ? pricing.sharingTotalQuantity : 1, diff --git a/apps/backend/src/services/worker-platform/admin-worker-catalog-service.ts b/apps/backend/src/services/worker-platform/admin-worker-catalog-service.ts index 8947969d..31fd115a 100644 --- a/apps/backend/src/services/worker-platform/admin-worker-catalog-service.ts +++ b/apps/backend/src/services/worker-platform/admin-worker-catalog-service.ts @@ -27,7 +27,7 @@ import { upsertWorkProductRule, } from '../../repositories/worker-platform/work-product-rule-repo.js' import type { WorkOrderRow, WorkProductRuleRow } from '../../repositories/worker-platform/types.js' -import { withTransaction } from '../../db/client.js' +import { query, withTransaction } from '../../db/client.js' import { archiveDeletedWorkOrderTemplateProduct, assertWorkOrderTemplateCanEnable, @@ -394,6 +394,34 @@ export async function saveAdminWorkProductRule(payload: JsonObject = {}) { const incomingUnitPriceFen = normalizeAmountFen(payload.unitPrice ?? payload.unitPriceYuan, 0) const existingRewardAmount = Number(existingRule?.reward_amount || 0) const existingUnitPriceFen = Number(existingRule?.unit_price_fen || 0) + const rawPricingMode = String(payload.pricingMode ?? payload.pricing_mode ?? '').trim() + if (rawPricingMode && rawPricingMode !== 'fixed' && rawPricingMode !== 'unit') { + throw createHttpError('接单模板计价方式无效', { + statusCode: 400, + errorCode: 'work_product_rule_pricing_mode_invalid', + }) + } + const existingPricingMode = existingRule?.pricing_mode === 'unit' ? 'unit' : 'fixed' + const pricingMode: 'fixed' | 'unit' = + rawPricingMode === 'unit' ? 'unit' : rawPricingMode === 'fixed' ? 'fixed' : existingPricingMode + let carriedPricingAmount = + existingPricingMode === 'unit' ? existingUnitPriceFen : existingRewardAmount + if (existingRule && pricingMode !== existingPricingMode) { + const activeCostResult = await query<{ amount_fen: number }>( + `SELECT amount_fen + FROM cost_versions + WHERE work_product_rule_id = $1 + AND scope_type = 'rule' + AND status = 'active' + AND effective_from <= NOW() + AND (effective_to IS NULL OR effective_to > NOW()) + ORDER BY effective_from DESC, id DESC + LIMIT 1`, + [existingRule.id], + ) + const activeCostAmount = Number(activeCostResult.rows[0]?.amount_fen || 0) + if (activeCostAmount > 0) carriedPricingAmount = activeCostAmount + } // 模板保存只维护业务配置。价格只能由商品与成本中心的统一命令写入。 if ( @@ -409,8 +437,18 @@ export async function saveAdminWorkProductRule(payload: JsonObject = {}) { }, ) } - const rewardAmount = existingRewardAmount - const unitPriceFen = existingUnitPriceFen + const rewardAmount = + pricingMode !== existingPricingMode || !existingRule + ? pricingMode === 'fixed' + ? carriedPricingAmount + : 0 + : existingRewardAmount + const unitPriceFen = + pricingMode !== existingPricingMode || !existingRule + ? pricingMode === 'unit' + ? carriedPricingAmount + : 0 + : existingUnitPriceFen const sharingEnabled = hasOwnPayload(payload, 'sharingEnabled') ? normalizeBoolean(payload.sharingEnabled, false) : hasOwnPayload(payload, 'sharing_enabled') @@ -431,12 +469,14 @@ export async function saveAdminWorkProductRule(payload: JsonObject = {}) { hasConfiguredSharingValue(payload.sharingTotalQuantity ?? payload.sharing_total_quantity) || hasConfiguredSharingValue(payload.sharingUnitReward ?? payload.sharingUnitRewardYuan) || hasConfiguredSharingValue(payload.sharingTotalAmount ?? payload.sharingTotalAmountYuan) - const sharingAutoFromOrder = hasOwnPayload(payload, 'sharingAutoFromOrder') + const requestedSharingAutoFromOrder = hasOwnPayload(payload, 'sharingAutoFromOrder') ? normalizeBoolean(payload.sharingAutoFromOrder, false) : hasOwnPayload(payload, 'sharing_auto_from_order') ? normalizeBoolean(payload.sharing_auto_from_order, false) : (existingRule?.sharing_auto_from_order === true && !hasManualSharingConfig) || - (!existingRule && sharingEnabled && unitPriceFen > 0 && !hasManualSharingConfig) + (!existingRule && sharingEnabled && pricingMode === 'unit' && !hasManualSharingConfig) + const sharingAutoFromOrder = + pricingMode === 'unit' && requestedSharingAutoFromOrder && !hasManualSharingConfig const hasManualSharingQuantity = hasConfiguredSharingValue( payload.sharingTotalQuantity ?? payload.sharing_total_quantity, ) @@ -445,7 +485,7 @@ export async function saveAdminWorkProductRule(payload: JsonObject = {}) { ) if ( sharingEnabled && - unitPriceFen > 0 && + pricingMode === 'unit' && !sharingAutoFromOrder && hasManualSharingQuantity !== hasManualSharingUnitReward ) { @@ -525,6 +565,7 @@ export async function saveAdminWorkProductRule(payload: JsonObject = {}) { autoCreate: normalizeBoolean(payload.autoCreate ?? payload.auto_create, true), rewardAmount: finalRewardAmount, unitPriceFen, + pricingMode, // 商品规则不配置押金;工单创建后由运营按需手工填写。 requiredDepositAmount: 0, depositThresholdAmount: 0, @@ -585,6 +626,7 @@ export function resolveWorkOrderTemplateSyncPrice(input: { skuQuantity, unitPriceFen: Number(input.rule.unit_price_fen || 0), fixedRewardAmount: Number(input.rule.reward_amount || 0), + pricingMode: input.rule.pricing_mode, sharingEnabled: input.rule.sharing_enabled === true, sharingAutoFromOrder: input.rule.sharing_auto_from_order === true, sharingTotalQuantity: Number(input.rule.sharing_total_quantity || 1), diff --git a/apps/backend/src/services/worker-platform/mock-work-order-rule-match.test.ts b/apps/backend/src/services/worker-platform/mock-work-order-rule-match.test.ts index 5a44b115..bbe8de0d 100644 --- a/apps/backend/src/services/worker-platform/mock-work-order-rule-match.test.ts +++ b/apps/backend/src/services/worker-platform/mock-work-order-rule-match.test.ts @@ -16,6 +16,7 @@ function rule( sort_order: input.sort_order ?? 100, unit_price_fen: input.unit_price_fen ?? 0, reward_amount: input.reward_amount ?? 0, + pricing_mode: input.pricing_mode ?? 'fixed', sharing_enabled: input.sharing_enabled ?? false, sharing_total_quantity: input.sharing_total_quantity ?? 1, sharing_unit_reward: input.sharing_unit_reward ?? 0, diff --git a/apps/backend/src/services/worker-platform/sync-worker-orders-service.ts b/apps/backend/src/services/worker-platform/sync-worker-orders-service.ts index 000716a0..d420147d 100644 --- a/apps/backend/src/services/worker-platform/sync-worker-orders-service.ts +++ b/apps/backend/src/services/worker-platform/sync-worker-orders-service.ts @@ -231,6 +231,7 @@ export async function syncWorkerOrdersForSourceOrder( skuQuantity, unitPriceFen: Number(rule.unit_price_fen || 0), fixedRewardAmount: Number(rule.reward_amount || 0), + pricingMode: rule.pricing_mode, sharingEnabled: rule.sharing_enabled === true, sharingAutoFromOrder: rule.sharing_auto_from_order === true, sharingTotalQuantity: Number(rule.sharing_total_quantity || 1), diff --git a/apps/backend/src/services/worker-platform/work-product-match-mappers.ts b/apps/backend/src/services/worker-platform/work-product-match-mappers.ts index c852a02a..b1602481 100644 --- a/apps/backend/src/services/worker-platform/work-product-match-mappers.ts +++ b/apps/backend/src/services/worker-platform/work-product-match-mappers.ts @@ -64,6 +64,7 @@ export function mapWorkProductRule(rule: WorkProductRuleRow | null | undefined) autoCreate: Boolean(rule.auto_create), rewardAmount: Number(rule.reward_amount || 0), unitPriceFen: Number(rule.unit_price_fen || 0), + pricingMode: rule.pricing_mode === 'unit' ? 'unit' : 'fixed', sharing: { enabled: rule.sharing_enabled === true, totalQuantity: Number(rule.sharing_total_quantity || 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 a5ac8ad6..7ae97e9e 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 @@ -745,6 +745,7 @@ function buildProductRule(overrides: Partial = {}): WorkProd auto_create: true, reward_amount: 2500, unit_price_fen: 0, + pricing_mode: 'fixed', required_deposit_amount: 0, deposit_threshold_amount: 20000, sharing_enabled: false, @@ -1624,6 +1625,7 @@ test('按件拼单未手动覆盖时,使用匹配数量与模板单价', () => skuQuantity: 10, unitPriceFen: 20, fixedRewardAmount: 0, + pricingMode: 'unit', sharingEnabled: true, sharingAutoFromOrder: true, sharingTotalQuantity: 1, @@ -1642,6 +1644,7 @@ test('按件拼单手动覆盖数量和单价时,保留手工配置', () => { skuQuantity: 10, unitPriceFen: 20, fixedRewardAmount: 0, + pricingMode: 'unit', sharingEnabled: true, sharingAutoFromOrder: false, sharingTotalQuantity: 3, @@ -1657,7 +1660,7 @@ test('按件拼单手动覆盖数量和单价时,保留手工配置', () => { test('模板同步价格按工单来源规格重新计算普通按件单', () => { const price = resolveWorkOrderTemplateSyncPrice({ - rule: buildProductRule({ unit_price_fen: 200 }), + rule: buildProductRule({ unit_price_fen: 200, pricing_mode: 'unit' }), workOrder: buildWorkOrderRow({ product_name: '指挥官秘钥15个', material_json: { source: { skuQuantity: 8 } }, @@ -1667,10 +1670,39 @@ test('模板同步价格按工单来源规格重新计算普通按件单', () => assert.deepEqual(price, { rewardAmount: 1600 }) }) +test('模板同步价格以显式计价方式为准,不按两个金额字段反推', () => { + const workOrder = buildWorkOrderRow({ product_name: '指挥官秘钥15个' }) + + assert.deepEqual( + resolveWorkOrderTemplateSyncPrice({ + rule: buildProductRule({ + reward_amount: 2500, + unit_price_fen: 200, + pricing_mode: 'fixed', + }), + workOrder, + }), + { rewardAmount: 2500 }, + ) + + assert.deepEqual( + resolveWorkOrderTemplateSyncPrice({ + rule: buildProductRule({ + reward_amount: 2500, + unit_price_fen: 200, + pricing_mode: 'unit', + }), + workOrder, + }), + { rewardAmount: 3000 }, + ) +}) + test('模板同步价格为拼单返回新的份额单价', () => { const price = resolveWorkOrderTemplateSyncPrice({ rule: buildProductRule({ unit_price_fen: 200, + pricing_mode: 'unit', sharing_enabled: true, sharing_auto_from_order: true, }), diff --git a/apps/frontend/src/layouts/AdminLayout.tsx b/apps/frontend/src/layouts/AdminLayout.tsx index 701ff733..4026d75c 100644 --- a/apps/frontend/src/layouts/AdminLayout.tsx +++ b/apps/frontend/src/layouts/AdminLayout.tsx @@ -3,6 +3,7 @@ import { AccountBookOutlined, ApartmentOutlined, BellOutlined, + BranchesOutlined, BugOutlined, DashboardOutlined, FileSearchOutlined, @@ -182,6 +183,7 @@ export default function AdminLayout() { const isFinanceKey = [ '/admin/sales-profit', '/admin/cost-center', + '/admin/fulfillment-channel', '/admin/fund', '/admin/finance', '/admin/finance/ledger', @@ -198,6 +200,7 @@ export default function AdminLayout() { const financeSubItems: MenuProps['items'] = [ { key: '/admin/sales-profit', icon: , label: '销售毛利' }, { key: '/admin/cost-center', icon: , label: '商品与成本' }, + { key: '/admin/fulfillment-channel', icon: , label: '履约渠道' }, { key: '/admin/fund', icon: , label: '钱包管理' }, { key: '/admin/finance', icon: , label: '收支明细' }, { key: '/admin/finance/ledger', icon: , label: '总账与对账' }, @@ -846,6 +849,7 @@ function resolveSelectedKey(pathname: string) { if (pathname.startsWith('/admin/orders')) return '/admin/orders' if (pathname.startsWith('/admin/sales-profit')) return '/admin/sales-profit' if (pathname.startsWith('/admin/cost-center')) return '/admin/cost-center' + if (pathname.startsWith('/admin/fulfillment-channel')) return '/admin/fulfillment-channel' if (pathname.startsWith('/admin/fund')) return '/admin/fund' if (pathname.startsWith('/admin/finance/ledger')) return '/admin/finance/ledger' if (pathname.startsWith('/admin/finance')) return '/admin/finance' diff --git a/apps/frontend/src/pages/admin/cost-center/ProductCatalogPanel.tsx b/apps/frontend/src/pages/admin/cost-center/ProductCatalogPanel.tsx index af05be39..a2fadcb4 100644 --- a/apps/frontend/src/pages/admin/cost-center/ProductCatalogPanel.tsx +++ b/apps/frontend/src/pages/admin/cost-center/ProductCatalogPanel.tsx @@ -342,7 +342,6 @@ export function ProductCatalogPanel() { } function openSetProductPrice(item: ProductCatalogItem) { - const isTemplateProduct = item.canonicalSku.startsWith('tpl:') setPricingProduct(item) priceForm.resetFields() priceForm.setFieldsValue({ @@ -350,7 +349,6 @@ export function ProductCatalogPanel() { item.activeCostAmountFen !== null && item.activeCostAmountFen !== undefined ? (item.activeCostAmountFen / 100).toFixed(2) : undefined, - pricingMode: isTemplateProduct ? item.templatePricingMode || 'fixed' : undefined, effectiveScope: 'immediate', changeReason: '在商品与成本中心统一调整价格', syncSnapshots: true, @@ -360,7 +358,6 @@ export function ProductCatalogPanel() { async function submitProductPrice(values: { amountYuan: number | string - pricingMode?: 'fixed' | 'unit' effectiveScope: 'immediate' | 'custom' effectiveFromCustom?: string changeReason?: string @@ -381,7 +378,6 @@ export function ProductCatalogPanel() { try { const response = await setAdminProductPrice(pricingProduct.id, { amountFen, - pricingMode: values.pricingMode, effectiveFrom, changeReason: values.changeReason || '在商品与成本中心统一调整价格', syncOpenOrders: true, @@ -1022,17 +1018,15 @@ export function ProductCatalogPanel() { {pricingProduct.canonicalSku.startsWith('tpl:') ? ( - - - 固定金额 - 按件计价 - - + ) : null} diff --git a/apps/frontend/src/pages/admin/panels/ProductRulesPanel.tsx b/apps/frontend/src/pages/admin/panels/ProductRulesPanel.tsx index bfa4bc79..9d6c346a 100644 --- a/apps/frontend/src/pages/admin/panels/ProductRulesPanel.tsx +++ b/apps/frontend/src/pages/admin/panels/ProductRulesPanel.tsx @@ -293,6 +293,7 @@ export default function ProductRulesPanel() { itemTitleMatchType: rule.match.itemTitleMatchType, skuNickMatchType: rule.match.skuNickMatchType, categoryId: rule.categoryId || undefined, + pricingMode: rule.pricingMode || 'fixed', sharingEnabled: rule.sharing?.enabled || false, sharingAutoFromOrder: rule.sharing?.autoFromOrder || false, sharingTotalQuantity: hasManualSharingOverride ? rule.sharing?.totalQuantity : undefined, @@ -349,6 +350,7 @@ export default function ProductRulesPanel() { fieldsText?: string enabled?: boolean autoCreate?: boolean + pricingMode?: 'fixed' | 'unit' sortOrder?: number }) { try { @@ -374,9 +376,7 @@ export default function ProductRulesPanel() { isFriendGift && giftCooldownUnit === 'minutes' ? Number(giftCooldownValue || 0) : 0, sharingEnabled: isFriendGift ? false : values.sharingEnabled === true, sharingAutoFromOrder: - editingRule?.unitPriceFen !== undefined && - editingRule.unitPriceFen > 0 && - editingRule.rewardAmount === 0 && + values.pricingMode === 'unit' && values.sharingEnabled === true && !hasManualSharingOverride, }) @@ -420,9 +420,12 @@ export default function ProductRulesPanel() { platform: rule.platform, productName: rule.productName, categoryId: rule.categoryId || undefined, + pricingMode: rule.pricingMode, sharingEnabled: rule.sharing?.enabled || false, sharingAutoFromOrder: - rule.unitPriceFen > 0 && rule.sharing?.enabled === true && !hasManualSharingOverride, + rule.pricingMode === 'unit' && + rule.sharing?.enabled === true && + !hasManualSharingOverride, sharingTotalQuantity: hasManualSharingOverride ? rule.sharing?.totalQuantity : undefined, sharingUnitReward: hasManualSharingOverride ? (rule.sharing?.unitReward || 0) / 100 @@ -753,22 +756,17 @@ export default function ProductRulesPanel() { }, }, { - title: '接单金额', + title: '接单价格', key: 'reward', width: '20%', render: (_, row) => (
- {row.rewardAmount > 0 || row.unitPriceFen > 0 ? ( - <> - - {row.sharing?.autoFromOrder ? '随订单计算' : formatMoney(row.rewardAmount)} - - {row.unitPriceFen > 0 ? ( - - 按件: {formatMoney(row.unitPriceFen)} - - ) : null} - + {(row.pricingMode === 'unit' ? row.unitPriceFen : row.rewardAmount) > 0 ? ( + + {row.pricingMode === 'unit' + ? `${formatMoney(row.unitPriceFen)} / 件` + : formatMoney(row.rewardAmount)} + ) : ( 待成本配置 @@ -889,6 +887,7 @@ export default function ProductRulesPanel() { skuNickMatchType: 'exact', enabled: false, autoCreate: true, + pricingMode: 'fixed', sharingEnabled: false, acceptanceMode: 'standard', giftCooldownUnit: 'hours', @@ -902,10 +901,10 @@ export default function ProductRulesPanel() { type="info" showIcon style={{ marginBottom: 16 }} - message="接单模板只维护匹配和履约规则" + message="接单模板维护计价方式和履约规则" description={ - 固定金额、按件单价和价格同步统一由 + 在这里选择“固定金额”或“按 SKU 按件”;具体金额与价格同步统一由 商品与成本中心 @@ -940,6 +939,22 @@ export default function ProductRulesPanel() { + + +
{/* 验收模式:好友赠送模式与拼单互斥 */} @@ -1047,9 +1062,7 @@ export default function ProductRulesPanel() { {({ getFieldValue }) => { const sharingEnabled = getFieldValue('sharingEnabled') - const unitMode = - Number(editingRule?.unitPriceFen || 0) > 0 && - Number(editingRule?.rewardAmount || 0) === 0 + const unitMode = getFieldValue('pricingMode') === 'unit' const friendGiftMode = getFieldValue('acceptanceMode') === 'friend_gift' const hasConfiguredPrice = Number(editingRule?.unitPriceFen || 0) > 0 || diff --git a/apps/frontend/src/services/admin/cost-center.ts b/apps/frontend/src/services/admin/cost-center.ts index 61503fcf..9e780390 100644 --- a/apps/frontend/src/services/admin/cost-center.ts +++ b/apps/frontend/src/services/admin/cost-center.ts @@ -499,7 +499,6 @@ export function setAdminProductPrice( productId: number, payload: { amountFen: number - pricingMode?: 'fixed' | 'unit' effectiveFrom?: string changeReason?: string syncOpenOrders?: boolean diff --git a/apps/frontend/src/services/admin/worker-platform.ts b/apps/frontend/src/services/admin/worker-platform.ts index 9b6fca9f..ccae9b59 100644 --- a/apps/frontend/src/services/admin/worker-platform.ts +++ b/apps/frontend/src/services/admin/worker-platform.ts @@ -290,6 +290,7 @@ export function saveAdminWorkProductRule(payload: { categoryId?: number enabled?: boolean autoCreate?: boolean + pricingMode?: 'fixed' | 'unit' sharingEnabled?: boolean sharingAutoFromOrder?: boolean sharingTotalQuantity?: number diff --git a/apps/frontend/src/types/worker-platform.ts b/apps/frontend/src/types/worker-platform.ts index 542fb12d..2f8c44da 100644 --- a/apps/frontend/src/types/worker-platform.ts +++ b/apps/frontend/src/types/worker-platform.ts @@ -438,6 +438,7 @@ export type WorkProductRule = { autoCreate: boolean rewardAmount: number unitPriceFen: number + pricingMode: 'fixed' | 'unit' sharing?: WorkOrderSharingConfig requirement: { fields: CollectField[]