diff --git a/apps/backend/src/services/cost-center/affiliate-product-service.ts b/apps/backend/src/services/cost-center/affiliate-product-service.ts index 3ea198d9..067556d4 100644 --- a/apps/backend/src/services/cost-center/affiliate-product-service.ts +++ b/apps/backend/src/services/cost-center/affiliate-product-service.ts @@ -1,6 +1,7 @@ import { query } from '../../db/client.js' import type { AdminSession } from '../admin/admin-auth-service.js' import type { JsonObject } from '../../types/json.js' +import { resolvePriceEffectiveFrom } from './cost-center-normalizers.js' import { createHttpError } from '../../utils/http.js' import { formatShanghaiDate, shiftDate } from '../../utils/finance-date-range.js' import { nowIso } from '../../utils/time.js' @@ -304,11 +305,7 @@ export async function setAdminAffiliateProductPrice( } const now = nowIso() - let effectiveFrom = payload.effectiveFrom ? String(payload.effectiveFrom) : now - if (payload.retroactive === true || payload.retroactive === 'true') { - const today = formatShanghaiDate(new Date()) - effectiveFrom = shiftDate(today, -30) + 'T00:00:00+08:00' - } + const effectiveFrom = resolvePriceEffectiveFrom(payload) const changeReason = String(payload.changeReason || '配置自动发货采购成本').trim() const existingResult = await query<{ id: number; amount_fen: number; effective_from: string }>( diff --git a/apps/backend/src/services/cost-center/cost-center-normalizers.ts b/apps/backend/src/services/cost-center/cost-center-normalizers.ts index 31449fa1..8a36b358 100644 --- a/apps/backend/src/services/cost-center/cost-center-normalizers.ts +++ b/apps/backend/src/services/cost-center/cost-center-normalizers.ts @@ -246,3 +246,32 @@ export function requiredRow(row: T | undefined | null, message: string): T { } return row } + +/** + * 成本改价的生效时间:只允许当前或未来,不允许追溯。 + * 历史订单的成本与成交价已固化在快照里,改价不得回写过去。 + */ +export function resolvePriceEffectiveFrom(payload: Record): string { + if (payload.retroactive === true || payload.retroactive === 'true') { + throw createHttpError('成本改价不支持追溯历史订单;历史订单成本已固化', { + statusCode: 400, + errorCode: 'cost_retroactive_not_allowed', + }) + } + const raw = payload.effectiveFrom ?? payload.effective_from + if (!raw) return new Date().toISOString() + const time = Date.parse(String(raw)) + if (!Number.isFinite(time)) { + throw createHttpError('生效时间格式无效', { + statusCode: 400, + errorCode: 'cost_effective_from_invalid', + }) + } + if (time < Date.now() - 60_000) { + throw createHttpError('生效时间不能早于当前时间;历史订单成本已固化', { + statusCode: 400, + errorCode: 'cost_effective_from_in_past', + }) + } + return new Date(time).toISOString() +} 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 47e7f874..c0161286 100644 --- a/apps/backend/src/services/cost-center/product-catalog-service.ts +++ b/apps/backend/src/services/cost-center/product-catalog-service.ts @@ -16,6 +16,7 @@ import { nowIso } from '../../utils/time.js' import { normalizeMatchText, normalizeSkuSeriesEquivalence } from '../product-match/normalize.js' import { resolveSkuNameQuantity } from '../product-match/pricing.js' import { setAdminAffiliateProductPrice } from './affiliate-product-service.js' +import { resolvePriceEffectiveFrom } from './cost-center-normalizers.js' import { ensureVoucherRuleForSku } from '../fulfillment/channel-assign-service.js' import { syncAdminWorkProductRulePrice } from '../worker-platform/admin-worker-catalog-service.js' import { @@ -1367,11 +1368,7 @@ export async function setAdminProductPrice( } const now = nowIso() - let effectiveFrom = payload.effectiveFrom ? String(payload.effectiveFrom) : now - if (payload.retroactive === true || payload.retroactive === 'true') { - const today = formatShanghaiDate(new Date()) - effectiveFrom = shiftDate(today, -30) + 'T00:00:00+08:00' - } + const effectiveFrom = resolvePriceEffectiveFrom(payload) const changeReason = String(payload.changeReason || '在商品与成本中心统一调整价格').trim() // 2. 如果是接单模板商品 (tpl:) diff --git a/apps/frontend/src/pages/admin/cost-center/AffiliateSkuPanel.tsx b/apps/frontend/src/pages/admin/cost-center/AffiliateSkuPanel.tsx index 8c1cd347..127e7b8f 100644 --- a/apps/frontend/src/pages/admin/cost-center/AffiliateSkuPanel.tsx +++ b/apps/frontend/src/pages/admin/cost-center/AffiliateSkuPanel.tsx @@ -41,7 +41,7 @@ import { useCostCenter } from './CostCenterContext' interface PriceFormValues { priceYuan: number - effectiveMode: 'retroactive' | 'now' | 'custom' + effectiveMode: 'now' | 'custom' customEffectiveDate?: dayjs.Dayjs changeReason?: string } @@ -108,7 +108,7 @@ export function AffiliateSkuPanel() { priceForm.setFieldsValue({ priceYuan: currentYuan, - effectiveMode: 'retroactive', + effectiveMode: 'now', changeReason: currentYuan ? '上游采购成本调整' : '首次配置自动发货采购成本', }) setPriceModalOpen(true) @@ -123,15 +123,10 @@ export function AffiliateSkuPanel() { } let effectiveFrom: string | undefined - let retroactive = false - - if (values.effectiveMode === 'retroactive') { - retroactive = true - effectiveFrom = dayjs().subtract(30, 'day').format('YYYY-MM-DDTHH:mm:ss+08:00') - } else if (values.effectiveMode === 'now') { + if (values.effectiveMode === 'now') { effectiveFrom = new Date().toISOString() } else if (values.effectiveMode === 'custom' && values.customEffectiveDate) { - effectiveFrom = values.customEffectiveDate.startOf('day').toISOString() + effectiveFrom = values.customEffectiveDate.toISOString() } setPriceSubmitting(true) @@ -139,7 +134,6 @@ export function AffiliateSkuPanel() { await setAdminAffiliateProductPrice(selectedProduct.id, { amountFen, effectiveFrom, - retroactive, changeReason: values.changeReason, }) message.success(`「${selectedProduct.canonicalName}」采购单价已设置为 ¥${values.priceYuan}`) @@ -379,20 +373,16 @@ export function AffiliateSkuPanel() { > - + - 追溯至历史订单 (推荐) + 自当前时间立即生效 - 自近一个月业务起点(30 天前)生效,重算快照后可直接消除近一个月 cost_missing - 异常 + 只影响之后的新订单;历史订单的成本与成交价已固化,不会被本次改价覆盖 - - 自当前时间立即生效(仅对后续订单生效) - - 指定生效日期 + 指定生效时间(不能早于当前时间) diff --git a/apps/frontend/src/pages/admin/cost-center/ProductCatalogPanel.tsx b/apps/frontend/src/pages/admin/cost-center/ProductCatalogPanel.tsx index 59ed505c..22b072df 100644 --- a/apps/frontend/src/pages/admin/cost-center/ProductCatalogPanel.tsx +++ b/apps/frontend/src/pages/admin/cost-center/ProductCatalogPanel.tsx @@ -352,7 +352,7 @@ export function ProductCatalogPanel() { async function submitProductPrice(values: { amountYuan: number | string - effectiveScope: 'retroactive' | 'immediate' | 'custom' + effectiveScope: 'immediate' | 'custom' effectiveFromCustom?: string changeReason?: string syncOpenOrders?: boolean @@ -365,12 +365,7 @@ export function ProductCatalogPanel() { } const amountFen = Math.round(yuan * 100) let effectiveFrom: string | undefined - let retroactive = false - - if (values.effectiveScope === 'retroactive') { - retroactive = true - effectiveFrom = dayjs().subtract(30, 'day').format('YYYY-MM-DDTHH:mm:ss+08:00') - } else if (values.effectiveScope === 'custom' && values.effectiveFromCustom) { + if (values.effectiveScope === 'custom' && values.effectiveFromCustom) { effectiveFrom = values.effectiveFromCustom } @@ -379,7 +374,6 @@ export function ProductCatalogPanel() { const response = await setAdminProductPrice(pricingProduct.id, { amountFen, effectiveFrom, - retroactive, changeReason: values.changeReason || '在商品与成本中心统一调整价格', syncOpenOrders: values.syncOpenOrders !== false, }) @@ -939,19 +933,13 @@ export function ProductCatalogPanel() { > - 自当前时间立即生效(推荐,仅对后续订单生效) + 自当前时间立即生效(推荐)
- 保留旧版本历史,自新的价格版本创建时刻起计算后续履约结算 -
-
- - 追溯至历史订单 -
- 自近一个月业务起点(30 天前)生效,直接更新基线并消除近一个月 cost_missing 异常 + 只影响之后的新订单;历史订单的成本与成交价已固化,不会被改价覆盖
- 指定生效日期 + 指定生效时间(不能早于当前时间)
diff --git a/apps/frontend/src/services/admin/cost-center.ts b/apps/frontend/src/services/admin/cost-center.ts index 96abe88a..4c4149b8 100644 --- a/apps/frontend/src/services/admin/cost-center.ts +++ b/apps/frontend/src/services/admin/cost-center.ts @@ -145,7 +145,6 @@ export function setAdminAffiliateProductPrice( payload: { amountFen: number effectiveFrom?: string - retroactive?: boolean changeReason?: string }, ) { @@ -486,7 +485,6 @@ export function setAdminProductPrice( payload: { amountFen: number effectiveFrom?: string - retroactive?: boolean changeReason?: string syncOpenOrders?: boolean },