fix(admin): 恢复接单模板计价配置并统一成本价格链路

This commit is contained in:
yml2213
2026-11-02 17:57:49 +08:00
parent d871b93fde
commit a7bac9c1d8
27 changed files with 304 additions and 83 deletions
@@ -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);
@@ -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
@@ -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,
],
@@ -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,
@@ -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
@@ -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,
@@ -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,
@@ -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'
@@ -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(
'拼单自动按件计价与固定金额冲突,请先在接单模板关闭拼单或改为手工拼单价',
@@ -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,
@@ -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'
@@ -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,
@@ -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,
@@ -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)
@@ -68,6 +68,7 @@ function buildRule(overrides: Partial<WorkProductRuleRow> = {}): 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,
@@ -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,
@@ -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),
@@ -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,
@@ -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),
@@ -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),
@@ -745,6 +745,7 @@ function buildProductRule(overrides: Partial<WorkProductRuleRow> = {}): 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,
}),
@@ -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: <LineChartOutlined />, label: '销售毛利' },
{ key: '/admin/cost-center', icon: <ApartmentOutlined />, label: '商品与成本' },
{ key: '/admin/fulfillment-channel', icon: <BranchesOutlined />, label: '履约渠道' },
{ key: '/admin/fund', icon: <WalletOutlined />, label: '钱包管理' },
{ key: '/admin/finance', icon: <AccountBookOutlined />, label: '收支明细' },
{ key: '/admin/finance/ledger', icon: <AuditOutlined />, 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'
@@ -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() {
</div>
{pricingProduct.canonicalSku.startsWith('tpl:') ? (
<Form.Item
name="pricingMode"
label="接单计价方式"
rules={[{ required: true, message: '请选择计价方式' }]}
extra="保存后会同步到接单模板;固定金额按整单,按件单价按 SKU 数量计算"
>
<Radio.Group optionType="button" buttonStyle="solid">
<Radio.Button value="fixed">固定金额</Radio.Button>
<Radio.Button value="unit">按件计价</Radio.Button>
</Radio.Group>
</Form.Item>
<Alert
type="info"
showIcon
style={{ marginBottom: 16 }}
message={`当前计价方式:${
pricingProduct.templatePricingMode === 'unit' ? '按 SKU 按件' : '固定金额'
}`}
description="计价方式由接单模板统一设置;这里只维护对应金额。"
/>
) : null}
<Form.Item
@@ -1041,9 +1035,9 @@ export function ProductCatalogPanel() {
pricingProduct.source === 'affiliate_dash'
? '采购单价(元)'
: pricingProduct.canonicalSku.startsWith('tpl:')
? pricingProduct.templateSharingEnabled
? '拼单总成本(元)'
: '接单成本(元)'
? pricingProduct.templatePricingMode === 'unit'
? '每件接单成本(元)'
: '整单接单成本(元)'
: '标准结算单价(元)'
}
rules={[{ required: true, message: '请填写价格' }]}
@@ -1051,7 +1045,9 @@ export function ProductCatalogPanel() {
pricingProduct.source === 'affiliate_dash'
? '上游每发货 1 件扣除的采购成本金额'
: pricingProduct.canonicalSku.startsWith('tpl:')
? '固定金额按整单计;按件计价时填写每个 SKU 的接单成本'
? pricingProduct.templatePricingMode === 'unit'
? `订单有 N 件时,本单接单成本 = 当前金额 × N`
: '无论订单件数,当前金额就是整单接单成本'
: '接单打手发单/验收的基础结算单价'
}
>
@@ -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) => (
<div className="cell-stack">
{row.rewardAmount > 0 || row.unitPriceFen > 0 ? (
<>
<Typography.Text strong style={{ color: '#3f8600', fontSize: 12 }}>
{row.sharing?.autoFromOrder ? '随订单计算' : formatMoney(row.rewardAmount)}
</Typography.Text>
{row.unitPriceFen > 0 ? (
<Typography.Text type="secondary" style={{ fontSize: 11 }}>
按件: {formatMoney(row.unitPriceFen)}
</Typography.Text>
) : null}
</>
{(row.pricingMode === 'unit' ? row.unitPriceFen : row.rewardAmount) > 0 ? (
<Typography.Text strong style={{ color: '#3f8600', fontSize: 12 }}>
{row.pricingMode === 'unit'
? `${formatMoney(row.unitPriceFen)} / 件`
: formatMoney(row.rewardAmount)}
</Typography.Text>
) : (
<Tag color="orange" style={{ width: 'fit-content', margin: 0 }}>
待成本配置
@@ -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={
<span>
固定金额、按件单价和价格同步统一由
在这里选择“固定金额”或“按 SKU 按件”;具体金额与价格同步统一由
<a href="#/admin/cost-center" style={{ margin: '0 4px', fontWeight: 500 }}>
商品与成本中心
</a>
@@ -940,6 +939,22 @@ export default function ProductRulesPanel() {
<Form.Item label="任务时限" name="timeoutMinutes">
<InputNumber min={0} step={5} addonAfter="分钟" className="full-width" />
</Form.Item>
<Form.Item
label="计价方式"
name="pricingMode"
rules={[{ required: true, message: '请选择计价方式' }]}
extra="固定金额按整单;按 SKU 按件会把成本中心金额乘以订单件数"
>
<Radio.Group
optionType="button"
buttonStyle="solid"
className="full-width"
options={[
{ value: 'fixed', label: '固定金额' },
{ value: 'unit', label: '按 SKU 按件' },
]}
/>
</Form.Item>
</div>
{/* 验收模式:好友赠送模式与拼单互斥 */}
@@ -1047,9 +1062,7 @@ export default function ProductRulesPanel() {
<Form.Item noStyle shouldUpdate>
{({ 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 ||
@@ -499,7 +499,6 @@ export function setAdminProductPrice(
productId: number,
payload: {
amountFen: number
pricingMode?: 'fixed' | 'unit'
effectiveFrom?: string
changeReason?: string
syncOpenOrders?: boolean
@@ -290,6 +290,7 @@ export function saveAdminWorkProductRule(payload: {
categoryId?: number
enabled?: boolean
autoCreate?: boolean
pricingMode?: 'fixed' | 'unit'
sharingEnabled?: boolean
sharingAutoFromOrder?: boolean
sharingTotalQuantity?: number
@@ -438,6 +438,7 @@ export type WorkProductRule = {
autoCreate: boolean
rewardAmount: number
unitPriceFen: number
pricingMode: 'fixed' | 'unit'
sharing?: WorkOrderSharingConfig
requirement: {
fields: CollectField[]