fix(cost-center): 禁止追溯改价,改价只能当前或未来生效
- 后端统一校验:retroactive 请求直接拒绝,effectiveFrom 早于当前时间拒绝 - 去掉「追溯至历史订单」入口,改价只影响之后的新订单 - 历史订单成本与成交价由快照固化,不再受改价影响
This commit is contained in:
@@ -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 }>(
|
||||
|
||||
@@ -246,3 +246,32 @@ export function requiredRow<T>(row: T | undefined | null, message: string): T {
|
||||
}
|
||||
return row
|
||||
}
|
||||
|
||||
/**
|
||||
* 成本改价的生效时间:只允许当前或未来,不允许追溯。
|
||||
* 历史订单的成本与成交价已固化在快照里,改价不得回写过去。
|
||||
*/
|
||||
export function resolvePriceEffectiveFrom(payload: Record<string, unknown>): 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()
|
||||
}
|
||||
|
||||
@@ -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:)
|
||||
|
||||
@@ -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() {
|
||||
>
|
||||
<Radio.Group>
|
||||
<Space direction="vertical">
|
||||
<Radio value="retroactive">
|
||||
<Radio value="now">
|
||||
<Space direction="vertical" size={0}>
|
||||
<Typography.Text strong>追溯至历史订单 (推荐)</Typography.Text>
|
||||
<Typography.Text strong>自当前时间立即生效</Typography.Text>
|
||||
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
|
||||
自近一个月业务起点(30 天前)生效,重算快照后可直接消除近一个月 cost_missing
|
||||
异常
|
||||
只影响之后的新订单;历史订单的成本与成交价已固化,不会被本次改价覆盖
|
||||
</Typography.Text>
|
||||
</Space>
|
||||
</Radio>
|
||||
<Radio value="now">
|
||||
<span>自当前时间立即生效(仅对后续订单生效)</span>
|
||||
</Radio>
|
||||
<Radio value="custom">
|
||||
<span>指定生效日期</span>
|
||||
<span>指定生效时间(不能早于当前时间)</span>
|
||||
</Radio>
|
||||
</Space>
|
||||
</Radio.Group>
|
||||
|
||||
@@ -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() {
|
||||
>
|
||||
<Radio.Group style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
||||
<Radio value="immediate">
|
||||
<strong>自当前时间立即生效(推荐,仅对后续订单生效)</strong>
|
||||
<strong>自当前时间立即生效(推荐)</strong>
|
||||
<div style={{ color: '#64748b', fontSize: 12, marginTop: 2 }}>
|
||||
保留旧版本历史,自新的价格版本创建时刻起计算后续履约结算
|
||||
</div>
|
||||
</Radio>
|
||||
<Radio value="retroactive">
|
||||
<strong>追溯至历史订单</strong>
|
||||
<div style={{ color: '#64748b', fontSize: 12, marginTop: 2 }}>
|
||||
自近一个月业务起点(30 天前)生效,直接更新基线并消除近一个月 cost_missing 异常
|
||||
只影响之后的新订单;历史订单的成本与成交价已固化,不会被改价覆盖
|
||||
</div>
|
||||
</Radio>
|
||||
<Radio value="custom">
|
||||
<strong>指定生效日期</strong>
|
||||
<strong>指定生效时间(不能早于当前时间)</strong>
|
||||
</Radio>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
|
||||
@@ -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
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user