feat(cost-center): 统一接单模板成本并自动同步商品

This commit is contained in:
yml2213
2026-11-02 16:14:51 +08:00
parent 55e3c4e21b
commit 290890e848
22 changed files with 963 additions and 508 deletions
@@ -27,7 +27,12 @@ import {
upsertWorkProductRule,
} from '../../repositories/worker-platform/work-product-rule-repo.js'
import type { WorkOrderRow, WorkProductRuleRow } from '../../repositories/worker-platform/types.js'
import { syncAdminWorkOrderTemplateCostVersions } from '../cost-center/work-order-product-service.js'
import { withTransaction } from '../../db/client.js'
import {
archiveDeletedWorkOrderTemplateProduct,
assertWorkOrderTemplateCanEnable,
syncWorkOrderTemplateProduct,
} from '../cost-center/work-order-template-product-service.js'
import {
normalizeWorkOrderAcceptanceMode,
normalizeWorkOrderGiftCooldownHours,
@@ -379,35 +384,38 @@ export async function saveAdminWorkProductRule(payload: JsonObject = {}) {
})
}
const rewardAmount = normalizeAmountFen(payload.rewardAmount ?? payload.rewardAmountYuan, 0)
const unitPriceFen =
payload.unitPrice === undefined && payload.unitPriceYuan === undefined
? 0
: normalizeAmountFen(payload.unitPrice ?? payload.unitPriceYuan, 0)
const hasRewardPayload =
payload.rewardAmount !== undefined || payload.rewardAmountYuan !== undefined
const hasUnitPricePayload = payload.unitPrice !== undefined || payload.unitPriceYuan !== undefined
const incomingRewardAmount = normalizeAmountFen(
payload.rewardAmount ?? payload.rewardAmountYuan,
0,
)
const incomingUnitPriceFen = normalizeAmountFen(payload.unitPrice ?? payload.unitPriceYuan, 0)
const existingRewardAmount = Number(existingRule?.reward_amount || 0)
const existingUnitPriceFen = Number(existingRule?.unit_price_fen || 0)
// 全系统只有【商品与成本中心】是标准价格,接单模板价格禁止在其他地方修改
if (existingRule && !payload.internalAllowPriceChange) {
const existingRewardAmount = Number(existingRule.reward_amount || 0)
const existingUnitPrice = Number(existingRule.unit_price_fen || 0)
const hasPricePayload =
payload.rewardAmount !== undefined ||
payload.rewardAmountYuan !== undefined ||
payload.unitPrice !== undefined ||
payload.unitPriceYuan !== undefined
if (
hasPricePayload &&
(rewardAmount !== existingRewardAmount || unitPriceFen !== existingUnitPrice)
) {
throw createHttpError(
'接单模板价格已由【商品与成本中心】统一定价,价格更新只能走商品与成本中心。请前往成本中心修改标准价格',
{
statusCode: 400,
errorCode: 'price_update_restricted_to_cost_center',
},
)
}
// 模板保存只维护业务配置。价格只能由商品与成本中心的统一命令写入。
if (
(hasRewardPayload && incomingRewardAmount !== existingRewardAmount) ||
(hasUnitPricePayload && incomingUnitPriceFen !== existingUnitPriceFen) ||
(!existingRule && (incomingRewardAmount > 0 || incomingUnitPriceFen > 0))
) {
throw createHttpError(
'接单模板价格只能由【商品与成本中心】维护。请先保存模板,再到成本中心配置价格',
{
statusCode: 400,
errorCode: 'price_update_restricted_to_cost_center',
},
)
}
const sharingEnabled = normalizeBoolean(payload.sharingEnabled ?? payload.sharing_enabled, false)
const rewardAmount = existingRewardAmount
const unitPriceFen = existingUnitPriceFen
const sharingEnabled = hasOwnPayload(payload, 'sharingEnabled')
? normalizeBoolean(payload.sharingEnabled, false)
: hasOwnPayload(payload, 'sharing_enabled')
? normalizeBoolean(payload.sharing_enabled, false)
: existingRule?.sharing_enabled === true
const acceptanceMode = normalizeWorkOrderAcceptanceMode(
hasOwnPayload(payload, 'acceptanceMode') || hasOwnPayload(payload, 'acceptance_mode')
? payload.acceptanceMode || payload.acceptance_mode
@@ -423,11 +431,12 @@ 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 =
sharingEnabled &&
unitPriceFen > 0 &&
(normalizeBoolean(payload.sharingAutoFromOrder ?? payload.sharing_auto_from_order, false) ||
!hasManualSharingConfig)
const sharingAutoFromOrder = 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)
const hasManualSharingQuantity = hasConfiguredSharingValue(
payload.sharingTotalQuantity ?? payload.sharing_total_quantity,
)
@@ -445,17 +454,19 @@ export async function saveAdminWorkProductRule(payload: JsonObject = {}) {
errorCode: 'work_product_rule_sharing_override_incomplete',
})
}
const sharingUnitReward = normalizeAmountFen(
payload.sharingUnitReward ?? payload.sharingUnitRewardYuan,
0,
)
const sharingUnitReward =
payload.sharingUnitReward !== undefined || payload.sharingUnitRewardYuan !== undefined
? normalizeAmountFen(payload.sharingUnitReward ?? payload.sharingUnitRewardYuan, 0)
: Number(existingRule?.sharing_unit_reward || 0)
const sharingTotalAmount = normalizeAmountFen(
payload.sharingTotalAmount ?? payload.sharingTotalAmountYuan,
0,
)
const sharingTotalQuantity = resolveSharingQuantity(
sharingTotalAmount,
normalizePositiveInteger(payload.sharingTotalQuantity ?? payload.sharing_total_quantity, 1),
payload.sharingTotalQuantity !== undefined || payload.sharing_total_quantity !== undefined
? normalizePositiveInteger(payload.sharingTotalQuantity ?? payload.sharing_total_quantity, 1)
: normalizePositiveInteger(existingRule?.sharing_total_quantity, 1),
sharingUnitReward,
)
const resolvedSharingTotalAmount =
@@ -473,77 +484,89 @@ export async function saveAdminWorkProductRule(payload: JsonObject = {}) {
? 0
: resolvedSharingTotalAmount
: rewardAmount
const requestedEnabled = hasOwnPayload(payload, 'enabled')
? normalizeBoolean(payload.enabled, false)
: existingRule?.enabled === true
const enabled = existingRule ? requestedEnabled : false
if (existingRule && enabled) {
await assertWorkOrderTemplateCanEnable(existingRule.id)
}
const fields = normalizeRequirementFieldsFromPayload(payload)
const ruleKey = existingRule?.rule_key || randomId('rule-')
const rule = await upsertWorkProductRule({
ruleKey,
provider: hasOwnPayload(payload, 'provider')
? String(payload.provider || '').trim()
: String(existingRule?.provider || '').trim(),
platform: hasOwnPayload(payload, 'platform')
? String(payload.platform || '').trim()
: String(existingRule?.platform || '').trim(),
shopId:
hasOwnPayload(payload, 'shopId') || hasOwnPayload(payload, 'shop_id')
? String(payload.shopId || payload.shop_id || '').trim()
: String(existingRule?.shop_id || '').trim(),
skuCode:
hasOwnPayload(payload, 'skuCode') || hasOwnPayload(payload, 'sku_code')
? skuCode
: String(existingRule?.sku_code || '').trim(),
productName: productName || matchedProductName,
matchType:
hasOwnPayload(payload, 'matchType') || hasOwnPayload(payload, 'match_type')
? normalizeMatchType(payload.matchType || payload.match_type)
: normalizeMatchType(existingRule?.match_type),
categoryId:
normalizeOptionalId(payload.categoryId || payload.category_id) ||
defaults.category?.id ||
null,
enabled: normalizeBoolean(payload.enabled, true),
autoCreate: normalizeBoolean(payload.autoCreate ?? payload.auto_create, true),
rewardAmount: finalRewardAmount,
unitPriceFen,
// 商品规则不配置押金;工单创建后由运营按需手工填写。
requiredDepositAmount: 0,
depositThresholdAmount: 0,
sharingEnabled,
sharingTotalQuantity,
sharingUnitReward,
sharingAutoFromOrder,
timeoutMinutes: Math.max(0, normalizeInteger(payload.timeoutMinutes, 0)),
timeoutPolicy: normalizeWorkOrderTimeoutPolicy(payload.timeoutPolicy),
acceptanceMode,
giftCooldownHours:
hasOwnPayload(payload, 'giftCooldownHours') || hasOwnPayload(payload, 'gift_cooldown_hours')
? normalizeWorkOrderGiftCooldownHours(
payload.giftCooldownHours ?? payload.gift_cooldown_hours,
)
: normalizeWorkOrderGiftCooldownHours(existingRule?.gift_cooldown_hours),
giftCooldownMinutes:
hasOwnPayload(payload, 'giftCooldownMinutes') ||
hasOwnPayload(payload, 'gift_cooldown_minutes')
? normalizeWorkOrderGiftCooldownMinutes(
payload.giftCooldownMinutes ?? payload.gift_cooldown_minutes,
)
: normalizeWorkOrderGiftCooldownMinutes(existingRule?.gift_cooldown_minutes),
requirementJson: JSON.stringify({ fields }),
matchJson: JSON.stringify(match),
sortOrder: normalizeInteger(payload.sortOrder, 100),
now: nowIso(),
const hasCategoryPayload =
hasOwnPayload(payload, 'categoryId') || hasOwnPayload(payload, 'category_id')
const categoryId = hasCategoryPayload
? normalizeOptionalId(payload.categoryId ?? payload.category_id)
: existingRule?.category_id || defaults.category?.id || null
const projection = await withTransaction(async (client) => {
const savedRule = await upsertWorkProductRule({
ruleKey,
provider: hasOwnPayload(payload, 'provider')
? String(payload.provider || '').trim()
: String(existingRule?.provider || '').trim(),
platform: hasOwnPayload(payload, 'platform')
? String(payload.platform || '').trim()
: String(existingRule?.platform || '').trim(),
shopId:
hasOwnPayload(payload, 'shopId') || hasOwnPayload(payload, 'shop_id')
? String(payload.shopId || payload.shop_id || '').trim()
: String(existingRule?.shop_id || '').trim(),
skuCode:
hasOwnPayload(payload, 'skuCode') || hasOwnPayload(payload, 'sku_code')
? skuCode
: String(existingRule?.sku_code || '').trim(),
productName: productName || matchedProductName,
matchType:
hasOwnPayload(payload, 'matchType') || hasOwnPayload(payload, 'match_type')
? normalizeMatchType(payload.matchType || payload.match_type)
: normalizeMatchType(existingRule?.match_type),
categoryId,
enabled,
autoCreate: normalizeBoolean(payload.autoCreate ?? payload.auto_create, true),
rewardAmount: finalRewardAmount,
unitPriceFen,
// 商品规则不配置押金;工单创建后由运营按需手工填写。
requiredDepositAmount: 0,
depositThresholdAmount: 0,
sharingEnabled,
sharingTotalQuantity,
sharingUnitReward,
sharingAutoFromOrder,
timeoutMinutes: Math.max(0, normalizeInteger(payload.timeoutMinutes, 0)),
timeoutPolicy: normalizeWorkOrderTimeoutPolicy(payload.timeoutPolicy),
acceptanceMode,
giftCooldownHours:
hasOwnPayload(payload, 'giftCooldownHours') || hasOwnPayload(payload, 'gift_cooldown_hours')
? normalizeWorkOrderGiftCooldownHours(
payload.giftCooldownHours ?? payload.gift_cooldown_hours,
)
: normalizeWorkOrderGiftCooldownHours(existingRule?.gift_cooldown_hours),
giftCooldownMinutes:
hasOwnPayload(payload, 'giftCooldownMinutes') ||
hasOwnPayload(payload, 'gift_cooldown_minutes')
? normalizeWorkOrderGiftCooldownMinutes(
payload.giftCooldownMinutes ?? payload.gift_cooldown_minutes,
)
: normalizeWorkOrderGiftCooldownMinutes(existingRule?.gift_cooldown_minutes),
requirementJson: JSON.stringify({ fields }),
matchJson: JSON.stringify(match),
sortOrder: normalizeInteger(payload.sortOrder, 100),
now: nowIso(),
client,
})
if (!savedRule) {
throw createHttpError('接单模板保存失败', { statusCode: 500 })
}
const productProjection = await syncWorkOrderTemplateProduct(savedRule.id, client)
return { rule: savedRule, productProjection }
})
// 模板价格变更即同步成本版本(事件驱动);失败不阻塞模板保存,
// 夜间巡检与后台「同步发单金额」按钮仍可修复。
if (rule) {
try {
await syncAdminWorkOrderTemplateCostVersions(rule.id)
} catch {
// 成本版本同步失败不允许影响模板本身;由巡检补齐。
}
return {
rule: mapWorkProductRule(projection.rule),
productId: projection.productProjection.productId,
productStatus: projection.productProjection.status,
costConfigurationRequired: !projection.productProjection.hasActiveCost,
}
return { rule: mapWorkProductRule(rule) }
}
/** 根据工单创建时的规格,计算当前模板可同步的价格。 */
@@ -669,5 +692,6 @@ export async function deleteAdminWorkProductRule(ruleId: number | string) {
errorCode: 'work_product_rule_not_found',
})
}
await archiveDeletedWorkOrderTemplateProduct(rule.rule_key)
return { deleted, retiredCostVersionCount: detached.updated }
}