feat(cost-center): 统一商品成本中心为全系统唯一标准价格源并闭环改价与工单同步

- 核心架构:将商品与成本中心确立为全系统唯一标准价格源,接单模板价格在接单平台禁止编辑并提示收敛至成本中心,后端 saveAdminWorkProductRule 严格拦截价格篡改
- 改价与同步:标准商品目录与自动发货专属面板提供统一改价弹窗,支持追溯/立即生效/指定时间;改价后自动或手动将新价格同步至接单大厅待抢单工单,保持下游业务一致
- 缺陷修复:修复 cost_versions 追溯改价时因同生效时间违反 chk_cost_versions_period 检查约束及 uq_cost_versions_scope 唯一索引冲突的问题,采用 active 版本就地更新机制
- 分类树优化:工单二级分类树限定仅在工单来源展示,自动发货与人工客服彻底隐藏分类树并重置过滤条件
- 验证方式:通过后端 784 项全量测试、前后端 tsc 类型检查及前端生产构建打包
This commit is contained in:
yml2213
2026-10-25 15:21:34 +08:00
parent 55a3e0df9c
commit b7ad69831a
11 changed files with 1824 additions and 236 deletions
@@ -18,11 +18,16 @@ import {
listAdminChannelMappings,
listAdminProducts,
listAdminUnmappedMappingGroups,
setAdminProductPrice,
syncAdminProductPrice,
updateAdminChannelMapping,
updateAdminProduct,
} from '../../services/cost-center/product-catalog-service.js'
import {
backfillAdminAffiliateDashSkus,
importAdminAffiliateDashPricesBaseline,
setAdminAffiliateProductPrice,
syncAdminAffiliateDashCostSnapshots,
syncAdminAffiliateDashProducts,
} from '../../services/cost-center/affiliate-product-service.js'
import {
@@ -97,6 +102,58 @@ router.post(
}),
)
router.post(
'/cost-center/affiliate-products/price-baseline',
requireAdminPermission('finance.config'),
createJsonHandler((req) => importAdminAffiliateDashPricesBaseline(sessionOf(req)), {
successMessage: '已从上游基线导入自动发货采购成本',
errorMessage: '导入自动发货采购成本基线失败',
scope: '[admin/cost-center/affiliate-products/price-baseline]',
audit: (_req, data) => ({
action: 'cost_affiliate_prices_baseline_imported',
targetType: 'cost_versions',
targetId: '',
data: data as Record<string, unknown>,
}),
}),
)
router.post(
'/cost-center/affiliate-products/:productId/price',
requireAdminPermission('finance.config'),
createJsonHandler(
(req) => setAdminAffiliateProductPrice(idOf(req, 'productId'), bodyOf(req), sessionOf(req)),
{
successMessage: '自动发货采购成本已配置',
errorMessage: '配置自动发货采购成本失败',
scope: '[admin/cost-center/affiliate-products/:productId/price]',
audit: (req, data) => ({
action: 'cost_affiliate_product_price_set',
targetType: 'cost_versions',
targetId: String(req.params.productId || ''),
data: data as Record<string, unknown>,
}),
},
),
)
router.post(
'/cost-center/affiliate-products/sync-snapshots',
requireAdminPermission('finance.config'),
createJsonHandler(() => syncAdminAffiliateDashCostSnapshots(), {
successMessage: '已重算自动发货预估成本快照与销售事实',
errorMessage: '重算自动发货成本快照失败',
scope: '[admin/cost-center/affiliate-products/sync-snapshots]',
audit: (_req, data) => ({
action: 'cost_affiliate_snapshots_synced',
targetType: 'cost_snapshots',
targetId: '',
data: data as Record<string, unknown>,
}),
}),
)
router.post(
'/cost-center/work-order-products/backfill',
requireAdminPermission('finance.config'),
@@ -201,6 +258,41 @@ router.patch(
}),
)
router.post(
'/cost-center/products/:productId/price',
requireAdminPermission('finance.config'),
createJsonHandler(
(req) => setAdminProductPrice(idOf(req, 'productId'), bodyOf(req), sessionOf(req)),
{
successMessage: '商品标准价格已更新',
errorMessage: '更新商品标准价格失败',
scope: '[admin/cost-center/products/:productId/price]',
audit: (req, data) => ({
action: 'cost_product_price_set',
targetType: 'cost_versions',
targetId: String(req.params.productId || ''),
data: data as Record<string, unknown>,
}),
},
),
)
router.post(
'/cost-center/products/:productId/sync-price',
requireAdminPermission('finance.config'),
createJsonHandler((req) => syncAdminProductPrice(idOf(req, 'productId'), sessionOf(req)), {
successMessage: '标准价格已同步到下游',
errorMessage: '同步标准价格失败',
scope: '[admin/cost-center/products/:productId/sync-price]',
audit: (req, data) => ({
action: 'cost_product_price_synced',
targetType: 'product_catalog',
targetId: String(req.params.productId || ''),
data: data as Record<string, unknown>,
}),
}),
)
router.delete(
'/cost-center/products/:productId',
requireAdminPermission('finance.config'),
@@ -1,10 +1,13 @@
/**
* 自动发货(affiliate_dash)商品同步与 SKU 回填。
*/
import fs from 'node:fs'
import { query } from '../../db/client.js'
import type { AdminSession } from '../admin/admin-auth-service.js'
import type { JsonObject } from '../../types/json.js'
import { createHttpError } from '../../utils/http.js'
import { shiftDate } from '../../utils/finance-date-range.js'
import { nowIso } from '../../utils/time.js'
import { rebuildSalesDailyFacts } from '../finance/sales-facts-service.js'
import { syncEstimatedOrderItemCostSnapshots } from './cost-snapshot-sync-service.js'
export async function syncAdminAffiliateDashProducts() {
const now = nowIso()
@@ -197,3 +200,269 @@ export async function backfillAdminAffiliateDashSkus() {
factsRebuild,
}
}
/**
* 为单个自动发货商品设置或更新采购成本版本。
*/
export async function setAdminAffiliateProductPrice(
productIdInput: unknown,
payload: JsonObject,
session: AdminSession,
) {
const productId = Number(productIdInput)
if (!Number.isFinite(productId) || productId <= 0) {
throw createHttpError('无效的商品 ID', { statusCode: 400 })
}
const productResult = await query<{
id: number
canonical_sku: string
canonical_name: string
}>(
'SELECT id, canonical_sku, canonical_name FROM product_catalog WHERE id = $1',
[productId],
)
const product = productResult.rows[0]
if (!product) {
throw createHttpError('商品不存在', { statusCode: 404 })
}
if (!product.canonical_sku.startsWith('aff:')) {
throw createHttpError('该商品不是自动发货商品,无法设置自动发货采购成本', { statusCode: 400 })
}
const amountFen = Number(payload.amountFen ?? payload.amount_fen)
if (!Number.isFinite(amountFen) || amountFen <= 0) {
throw createHttpError('采购成本必须大于 0', { statusCode: 400 })
}
const costItemResult = await query<{ id: number }>(
"SELECT id FROM cost_items WHERE code = 'sys_affiliate_purchase' OR cost_model = 'affiliate_purchase' ORDER BY (code = 'sys_affiliate_purchase') DESC LIMIT 1",
)
const costItem = costItemResult.rows[0]
if (!costItem) {
throw createHttpError('未找到自动发货采购成本项(sys_affiliate_purchase)', { statusCode: 500 })
}
const now = nowIso()
let effectiveFrom = payload.effectiveFrom ? String(payload.effectiveFrom) : now
if (payload.retroactive === true || payload.retroactive === 'true') {
effectiveFrom = '2026-07-10T00:00:00+08:00'
}
const changeReason = String(payload.changeReason || '配置自动发货采购成本').trim()
const existingResult = await query<{ id: number; amount_fen: number; effective_from: string }>(
`SELECT id, amount_fen, effective_from FROM cost_versions
WHERE cost_item_id = $1 AND canonical_product_id = $2 AND status = 'active'
ORDER BY effective_from DESC, id DESC LIMIT 1`,
[costItem.id, product.id],
)
const existing = existingResult.rows[0]
const existingTime = existing ? Date.parse(existing.effective_from) : null
const targetTime = Date.parse(effectiveFrom)
// 1. 金额未变,如果指定了更早的生效时间,则只更新生效时间
if (existing && Number(existing.amount_fen) === amountFen) {
if (existingTime !== null && targetTime < existingTime) {
await query(
'UPDATE cost_versions SET effective_from = $1, updated_at = $2 WHERE id = $3',
[effectiveFrom, now, existing.id],
)
}
return {
success: true,
product,
costVersionId: existing.id,
amountFen,
effectiveFrom,
message: '采购成本已是该金额',
}
}
// 2. 追溯改价或相同生效时间改价:直接就地更新当前 active 版本,避免破坏 effective_to > effective_from 约束或触发唯一索引冲突
if (existing && existingTime !== null && targetTime <= existingTime) {
await query(
`UPDATE cost_versions
SET amount_fen = $1,
effective_from = $2,
change_reason = $3,
created_by_admin_user_id = $4,
approved_by_admin_user_id = $4,
updated_at = $5
WHERE id = $6`,
[amountFen, effectiveFrom, changeReason, session.userId, now, existing.id],
)
return {
success: true,
product,
costVersionId: existing.id,
amountFen,
effectiveFrom,
message: '采购成本已更新',
}
}
// 3. 新生效时间晚于老版本:按标准版本退休老版本并插入新版本
if (existing) {
await query(
"UPDATE cost_versions SET status = 'retired', effective_to = $1, updated_at = $2 WHERE id = $3",
[effectiveFrom, now, existing.id],
)
}
const insertedResult = await query<{ id: number }>(
`
INSERT INTO cost_versions (
cost_item_id, scope_type, provider, shop_id, canonical_product_id,
work_product_rule_id, amount_fen, rate_bps, effective_from, effective_to,
source, status, change_reason, created_by_admin_user_id,
approved_by_admin_user_id, created_at, updated_at
) VALUES (
$1, 'product', '', '', $2,
NULL, $3, 0, $4, NULL,
'manual', 'active', $5, $6, $6, $7, $7
)
RETURNING id
`,
[
costItem.id,
product.id,
amountFen,
effectiveFrom,
changeReason,
session.userId,
now,
],
)
return {
success: true,
product,
costVersionId: insertedResult.rows[0]?.id,
amountFen,
effectiveFrom,
message: '采购成本已配置',
}
}
/**
* 批量从上游基线导入 38 条自动发货采购成本。
*/
export async function importAdminAffiliateDashPricesBaseline(session: AdminSession) {
const filePath = new URL('../../../scripts/data/affiliate-dash-prices.json', import.meta.url)
const rawData = fs.readFileSync(filePath, 'utf8')
const rows = JSON.parse(rawData) as Array<{
sku: string
name: string
category: string
costAmountFen: number
}>
const costItemResult = await query<{ id: number }>(
"SELECT id FROM cost_items WHERE code = 'sys_affiliate_purchase' OR cost_model = 'affiliate_purchase' ORDER BY (code = 'sys_affiliate_purchase') DESC LIMIT 1",
)
const costItem = costItemResult.rows[0]
if (!costItem) throw createHttpError('未找到自动发货采购成本项', { statusCode: 500 })
const now = nowIso()
const effectiveFrom = '2026-07-10T00:00:00+08:00'
let createdProductCount = 0
let createdVersionCount = 0
let updatedVersionCount = 0
let skippedCount = 0
for (const row of rows) {
const canonicalSku = `aff:${row.sku}`
const unitName = row.name.includes('个') ? '个' : '件'
const productInsert = await query<{ id: number; inserted: boolean }>(
`
INSERT INTO product_catalog (
canonical_sku, canonical_name, category, unit_name, status,
attributes_json, created_at, updated_at
) VALUES (
$1, $2, $3, $4, 'active',
jsonb_build_object('source', 'affiliate_dash_price_import', 'skuCode', $5),
$6, $6
)
ON CONFLICT (canonical_sku) DO UPDATE
SET canonical_name = CASE WHEN product_catalog.canonical_name = '' THEN EXCLUDED.canonical_name ELSE product_catalog.canonical_name END,
category = CASE WHEN product_catalog.category = '' THEN EXCLUDED.category ELSE product_catalog.category END,
updated_at = $6
RETURNING id, (xmax = 0) AS inserted
`,
[canonicalSku, row.name, row.category, unitName, row.sku, now],
)
const product = productInsert.rows[0]
if (!product) continue
if (product.inserted) createdProductCount += 1
const existing = await query<{ id: number; amount_fen: number; effective_from: string }>(
`SELECT id, amount_fen, effective_from FROM cost_versions
WHERE cost_item_id = $1 AND canonical_product_id = $2 AND status = 'active'
ORDER BY effective_from DESC, id DESC LIMIT 1`,
[costItem.id, product.id],
)
const current = existing.rows[0]
if (current && Number(current.amount_fen) === row.costAmountFen) {
if (Date.parse(effectiveFrom) < Date.parse(current.effective_from)) {
await query(
'UPDATE cost_versions SET effective_from = $1, updated_at = $2 WHERE id = $3',
[effectiveFrom, now, current.id],
)
updatedVersionCount += 1
} else {
skippedCount += 1
}
continue
}
if (current) {
await query(
"UPDATE cost_versions SET status = 'retired', effective_to = $1, updated_at = $2 WHERE id = $3",
[effectiveFrom, now, current.id],
)
}
await query(
`
INSERT INTO cost_versions (
cost_item_id, scope_type, provider, shop_id, canonical_product_id,
work_product_rule_id, amount_fen, rate_bps, effective_from, effective_to,
source, status, change_reason, created_by_admin_user_id,
approved_by_admin_user_id, created_at, updated_at
) VALUES (
$1, 'product', '', '', $2,
NULL, $3, 0, $4, NULL,
'upstream_sync', 'active', '从上游价格基线导入', $5, $5, $6, $6
)
`,
[costItem.id, product.id, row.costAmountFen, effectiveFrom, session.userId, now],
)
createdVersionCount += 1
}
return {
total: rows.length,
createdProductCount,
createdVersionCount,
updatedVersionCount,
skippedCount,
}
}
/**
* 一键重算自动发货预估成本快照并重算销售事实。
*/
export async function syncAdminAffiliateDashCostSnapshots() {
const snapshotResult = await syncEstimatedOrderItemCostSnapshots({
dateFrom: '2026-07-10',
dateTo: '2026-10-26',
})
const factsResult = await rebuildFactsForRange('2026-07-10', '2026-10-26')
return {
snapshotResult,
factsResult,
}
}
@@ -101,6 +101,13 @@ export function mapProduct(row: ProductRow) {
attributes: parseJsonObject(row.attributes_json),
mappingCount: Number(row.mapping_count || 0),
costVersionCount: Number(row.cost_version_count || 0),
activeCostAmountFen:
row.active_cost_amount_fen !== null && row.active_cost_amount_fen !== undefined
? Number(row.active_cost_amount_fen)
: null,
activeCostEffectiveFrom: row.active_cost_effective_from
? String(row.active_cost_effective_from)
: null,
affiliateTaskCount: Number(row.affiliate_task_count || 0),
affiliateLastFulfilledAt: row.affiliate_last_fulfilled_at || null,
createdAt: row.created_at,
@@ -24,6 +24,8 @@ export type ProductRow = {
updated_at: string
mapping_count?: number
cost_version_count?: number
active_cost_amount_fen?: number | null
active_cost_effective_from?: string | null
affiliate_task_count?: number
affiliate_last_fulfilled_at?: string | null
}
@@ -13,6 +13,11 @@ import { createHttpError } from '../../utils/http.js'
import { nowIso } from '../../utils/time.js'
import { normalizeMatchText, normalizeSkuSeriesEquivalence } from '../product-match/normalize.js'
import { resolveSkuNameQuantity } from '../product-match/pricing.js'
import {
setAdminAffiliateProductPrice,
syncAdminAffiliateDashCostSnapshots,
} from './affiliate-product-service.js'
import { syncAdminWorkProductRulePrice } from '../worker-platform/admin-worker-catalog-service.js'
import {
AFFILIATE_TASK_AGG_JOIN,
FULFILLMENT_MODES,
@@ -148,6 +153,48 @@ export async function listAdminProducts(payload: JsonObject = {}) {
AND p.canonical_sku LIKE 'tpl:%'
AND wpr.rule_key = substring(p.canonical_sku from 5)
) AS cost_version_count,
COALESCE(
(
SELECT v.amount_fen
FROM cost_versions v
WHERE v.status = 'active'
AND v.canonical_product_id = p.id
ORDER BY v.effective_from DESC, v.id DESC
LIMIT 1
),
(
SELECT v.amount_fen
FROM cost_versions v
JOIN work_product_rules wpr ON wpr.id = v.work_product_rule_id
WHERE v.status = 'active'
AND v.scope_type = 'rule'
AND p.canonical_sku LIKE 'tpl:%'
AND wpr.rule_key = substring(p.canonical_sku from 5)
ORDER BY v.effective_from DESC, v.id DESC
LIMIT 1
)
) AS active_cost_amount_fen,
COALESCE(
(
SELECT v.effective_from
FROM cost_versions v
WHERE v.status = 'active'
AND v.canonical_product_id = p.id
ORDER BY v.effective_from DESC, v.id DESC
LIMIT 1
),
(
SELECT v.effective_from
FROM cost_versions v
JOIN work_product_rules wpr ON wpr.id = v.work_product_rule_id
WHERE v.status = 'active'
AND v.scope_type = 'rule'
AND p.canonical_sku LIKE 'tpl:%'
AND wpr.rule_key = substring(p.canonical_sku from 5)
ORDER BY v.effective_from DESC, v.id DESC
LIMIT 1
)
) AS active_cost_effective_from,
COALESCE(aff.task_count, 0) AS affiliate_task_count,
aff.last_fulfilled_at AS affiliate_last_fulfilled_at
FROM product_catalog p
@@ -1226,3 +1273,306 @@ async function assertMappingKeyAvailable(
})
}
}
/**
* 统一商品改价接口:全系统只有商品与成本中心维护标准价格。
* 支持自动发货商品 (aff:)、接单模板商品 (tpl:)、工单商品 (wo:)、人工商品 (man:)。
*/
export async function setAdminProductPrice(
productIdInput: unknown,
payload: JsonObject,
session: AdminSession,
) {
const productId = Number(productIdInput)
if (!Number.isFinite(productId) || productId <= 0) {
throw createHttpError('无效的商品 ID', { statusCode: 400 })
}
const productResult = await query<{
id: number
canonical_sku: string
canonical_name: string
attributes_json: unknown
}>(
'SELECT id, canonical_sku, canonical_name, attributes_json FROM product_catalog WHERE id = $1',
[productId],
)
const product = productResult.rows[0]
if (!product) {
throw createHttpError('商品不存在', { statusCode: 404 })
}
// 1. 如果是自动发货商品,委托给 setAdminAffiliateProductPrice
if (product.canonical_sku.startsWith('aff:')) {
return setAdminAffiliateProductPrice(productId, payload, session)
}
const amountFen = Number(payload.amountFen ?? payload.amount_fen)
if (!Number.isFinite(amountFen) || amountFen <= 0) {
throw createHttpError('价格必须大于 0', { statusCode: 400 })
}
const now = nowIso()
let effectiveFrom = payload.effectiveFrom ? String(payload.effectiveFrom) : now
if (payload.retroactive === true || payload.retroactive === 'true') {
effectiveFrom = '2026-07-10T00:00:00+08:00'
}
const changeReason = String(payload.changeReason || '在商品与成本中心统一调整价格').trim()
// 2. 如果是接单模板商品 (tpl:)
if (product.canonical_sku.startsWith('tpl:')) {
const ruleKey = product.canonical_sku.slice(4)
const ruleResult = await query<{
id: number
rule_key: string
product_name: string
reward_amount: number
unit_price_fen: number
}>(
'SELECT id, rule_key, product_name, reward_amount, unit_price_fen FROM work_product_rules WHERE rule_key = $1 LIMIT 1',
[ruleKey],
)
const rule = ruleResult.rows[0]
const costItemResult = await query<{ id: number }>(
"SELECT id FROM cost_items WHERE code = 'sys_work_order_reward' OR cost_model = 'work_order_reward' LIMIT 1",
)
const costItemId = costItemResult.rows[0]?.id
if (!costItemId) {
throw createHttpError('未找到工单结算成本项(sys_work_order_reward)', { statusCode: 500 })
}
// 查找已有的 active 版本 (按 rule_id 或 canonical_product_id)
const existingResult = await query<{ id: number; amount_fen: number; effective_from: string }>(
`SELECT id, amount_fen, effective_from FROM cost_versions
WHERE cost_item_id = $1 AND (work_product_rule_id = $2 OR canonical_product_id = $3) AND status = 'active'
ORDER BY effective_from DESC, id DESC LIMIT 1`,
[costItemId, rule?.id ?? null, product.id],
)
const existing = existingResult.rows[0]
const existingTime = existing ? Date.parse(existing.effective_from) : null
const targetTime = Date.parse(effectiveFrom)
let costVersionId = existing?.id || 0
if (existing && existingTime !== null && targetTime <= existingTime) {
// 追溯改价或相同生效时间改价:就地更新 active 版本
await query(
`UPDATE cost_versions
SET amount_fen = $1,
effective_from = $2,
canonical_product_id = $3,
work_product_rule_id = $4,
change_reason = $5,
created_by_admin_user_id = $6,
approved_by_admin_user_id = $6,
updated_at = $7
WHERE id = $8`,
[
amountFen,
effectiveFrom,
product.id,
rule?.id ?? null,
changeReason,
session.userId,
now,
existing.id,
],
)
} else {
if (existing) {
await query(
"UPDATE cost_versions SET status = 'retired', effective_to = $1, updated_at = $2 WHERE id = $3",
[effectiveFrom, now, existing.id],
)
}
const inserted = await query<{ id: number }>(
`INSERT INTO cost_versions (
cost_item_id, scope_type, provider, shop_id, canonical_product_id,
work_product_rule_id, amount_fen, rate_bps, effective_from, effective_to,
source, status, change_reason, created_by_admin_user_id,
approved_by_admin_user_id, created_at, updated_at
) VALUES (
$1, 'rule', '', '', $2,
$3, $4, 0, $5, NULL,
'manual', 'active', $6, $7,
$7, $8, $8
) RETURNING id`,
[
costItemId,
product.id,
rule?.id ?? null,
amountFen,
effectiveFrom,
changeReason,
session.userId,
now,
],
)
costVersionId = inserted.rows[0]?.id || 0
}
// 同步更新 work_product_rules 保持数据一致
if (rule) {
await query(
`UPDATE work_product_rules
SET reward_amount = $1,
unit_price_fen = CASE WHEN unit_price_fen > 0 THEN $1 ELSE 0 END,
cost_item_id = $2,
updated_at = $3
WHERE id = $4`,
[amountFen, costItemId, now, rule.id],
)
}
// 更新 product_catalog.attributes_json
const attr =
typeof product.attributes_json === 'object' && product.attributes_json
? { ...(product.attributes_json as Record<string, unknown>) }
: {}
attr.rewardAmountFen = amountFen
await query(
'UPDATE product_catalog SET attributes_json = $1::jsonb, updated_at = $2 WHERE id = $3',
[JSON.stringify(attr), now, product.id],
)
// 如果指定了同步工单价格(默认 true)
let syncedOrders: unknown = null
if (payload.syncOpenOrders !== false && rule) {
syncedOrders = await syncAdminWorkProductRulePrice(rule.id, session.username)
}
return {
success: true,
product,
costVersionId,
amountFen,
effectiveFrom,
syncedOrders,
message: '接单模板结算价格已更新',
}
}
// 3. 其他标准商品 (wo: 或 man:)
const costItemCode = product.canonical_sku.startsWith('wo:')
? 'sys_work_order_reward'
: 'sys_manual_cost'
const costItemResult = await query<{ id: number }>(
'SELECT id FROM cost_items WHERE code = $1 LIMIT 1',
[costItemCode],
)
const costItemId = costItemResult.rows[0]?.id || 1
const existingResult = await query<{ id: number; amount_fen: number; effective_from: string }>(
`SELECT id, amount_fen, effective_from FROM cost_versions
WHERE cost_item_id = $1 AND canonical_product_id = $2 AND status = 'active'
ORDER BY effective_from DESC, id DESC LIMIT 1`,
[costItemId, product.id],
)
const existing = existingResult.rows[0]
const existingTime = existing ? Date.parse(existing.effective_from) : null
const targetTime = Date.parse(effectiveFrom)
let costVersionId = existing?.id || 0
if (existing && existingTime !== null && targetTime <= existingTime) {
await query(
`UPDATE cost_versions
SET amount_fen = $1,
effective_from = $2,
change_reason = $3,
created_by_admin_user_id = $4,
approved_by_admin_user_id = $4,
updated_at = $5
WHERE id = $6`,
[amountFen, effectiveFrom, changeReason, session.userId, now, existing.id],
)
} else {
if (existing) {
await query(
"UPDATE cost_versions SET status = 'retired', effective_to = $1, updated_at = $2 WHERE id = $3",
[effectiveFrom, now, existing.id],
)
}
const inserted = await query<{ id: number }>(
`INSERT INTO cost_versions (
cost_item_id, scope_type, provider, shop_id, canonical_product_id,
work_product_rule_id, amount_fen, rate_bps, effective_from, effective_to,
source, status, change_reason, created_by_admin_user_id,
approved_by_admin_user_id, created_at, updated_at
) VALUES (
$1, 'product', '', '', $2,
NULL, $3, 0, $4, NULL,
'manual', 'active', $5, $6,
$6, $7, $7
) RETURNING id`,
[costItemId, product.id, amountFen, effectiveFrom, changeReason, session.userId, now],
)
costVersionId = inserted.rows[0]?.id || 0
}
return {
success: true,
product,
costVersionId,
amountFen,
effectiveFrom,
message: '标准商品成本已更新',
}
}
/**
* 将商品当前标准价格同步到下游业务(接单大厅工单或自动发货快照)。
*/
export async function syncAdminProductPrice(productIdInput: unknown, session: AdminSession) {
const productId = Number(productIdInput)
if (!Number.isFinite(productId) || productId <= 0) {
throw createHttpError('无效的商品 ID', { statusCode: 400 })
}
const productResult = await query<{
id: number
canonical_sku: string
canonical_name: string
}>('SELECT id, canonical_sku, canonical_name FROM product_catalog WHERE id = $1', [productId])
const product = productResult.rows[0]
if (!product) {
throw createHttpError('商品不存在', { statusCode: 404 })
}
if (product.canonical_sku.startsWith('tpl:')) {
const ruleKey = product.canonical_sku.slice(4)
const ruleResult = await query<{ id: number }>(
'SELECT id FROM work_product_rules WHERE rule_key = $1 LIMIT 1',
[ruleKey],
)
const rule = ruleResult.rows[0]
if (!rule) {
throw createHttpError('未找到关联的接单模板', { statusCode: 404 })
}
const synced = await syncAdminWorkProductRulePrice(rule.id, session.username)
return {
success: true,
type: 'work_order_price_synced',
product,
synced,
message: `已同步 ${synced.updatedCount} 单:待抢单 ${synced.updatedNormalCount} 单,部分拼单 ${synced.updatedPartialSharingCount} 单`,
}
}
if (product.canonical_sku.startsWith('aff:')) {
const syncRes = await syncAdminAffiliateDashCostSnapshots()
return {
success: true,
type: 'affiliate_snapshots_synced',
product,
snapshots: syncRes,
message: `已重算成本快照(生成 ${syncRes.snapshotResult.createdCount} 笔快照,消除 ${syncRes.snapshotResult.resolvedExceptionCount} 笔异常),并重算销售事实`,
}
}
return {
success: true,
type: 'no_sync_needed',
product,
message: '该商品无需同步外部订单',
}
}
@@ -383,6 +383,29 @@ export async function saveAdminWorkProductRule(payload: JsonObject = {}) {
payload.unitPrice === undefined && payload.unitPriceYuan === undefined
? 0
: normalizeAmountFen(payload.unitPrice ?? payload.unitPriceYuan, 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',
},
)
}
}
const sharingEnabled = normalizeBoolean(payload.sharingEnabled ?? payload.sharing_enabled, false)
const acceptanceMode = normalizeWorkOrderAcceptanceMode(
hasOwnPayload(payload, 'acceptanceMode') || hasOwnPayload(payload, 'acceptance_mode')