feat(finance): 补齐自动发货与接单平台成本和对账

自动发货:价格导入支持 --effective-from,把首次基线版本回填到历史起点;41,676 个订单项全部归一并生成采购成本快照,缺失 0。\n\n接单平台:新增按工单 product_rule_id 回填商品的接口与按钮,长尾项按 provider+shop_id+sku_code+normalized_sku_name 补建 wo: 商品和 sku_code_name 渠道映射;118,053 个订单项全部归一。\n\n成本:已验收工单按 reward_amount 写 actual;未验收但已确定报酬的工单写 estimated,验收后升级。全量 95,870 actual + 21,217 estimated,actual 与 reward 0 差异。\n\n剩余 966 条取消/零报酬工单保留在成本异常队列,不自动记 0;人工块未改动。\n\n实测自动发货与接单平台未归一收入均为 0;整体未归一 79.67% 降至 57.54%,全部来自人工块。
This commit is contained in:
yml2213
2026-10-22 18:59:53 +08:00
parent 54d2eb2f59
commit 72e1141c8a
9 changed files with 566 additions and 57 deletions
@@ -2,12 +2,13 @@
* 按上游自动发货商品价格,批量生成「自动发货采购成本」成本版本。
*
* 用法:
* npx tsx scripts/sync-affiliate-dash-prices.ts [价格 JSON 路径] [--cost-item-id=5] [--dry-run]
* npx tsx scripts/sync-affiliate-dash-prices.ts [价格 JSON 路径] [--cost-item-id=5] [--effective-from=YYYY-MM-DD] [--dry-run]
*
* 规则:
* - 标准商品编码固定为 `aff:<sku>`,载体 affiliate_dash;
* - 成本版本范围 = 标准商品,金额 = 上游 cost_amount(分);
* - 同一商品已有相同金额的生效版本时跳过;
* - 同一商品已有相同金额的生效版本时跳过;指定 --effective-from 时,
* 若现版本生效时间更晚,则把首次基线版本的生效时间回填到该日期;
* - 金额变化时先停用旧版本,再插入新版本(历史快照不变)。
*/
import fs from 'node:fs'
@@ -33,6 +34,7 @@ type CostItemRow = {
function parseArgs(argv: string[]) {
let filePath = ''
let costItemId: number | null = null
let effectiveFrom = ''
let dryRun = false
for (const arg of argv) {
if (arg === '--dry-run') {
@@ -44,11 +46,16 @@ function parseArgs(argv: string[]) {
if (Number.isFinite(parsed) && parsed > 0) costItemId = parsed
continue
}
if (arg.startsWith('--effective-from=')) {
effectiveFrom = arg.slice('--effective-from='.length).trim()
continue
}
if (!arg.startsWith('--') && !filePath) filePath = arg
}
return {
filePath: path.resolve(filePath || 'scripts/data/affiliate-dash-prices.json'),
costItemId,
effectiveFrom,
dryRun,
}
}
@@ -90,18 +97,21 @@ async function resolveCostItem(costItemId: number | null): Promise<CostItemRow>
}
async function main() {
const { filePath, costItemId, dryRun } = parseArgs(process.argv.slice(2))
const { filePath, costItemId, effectiveFrom, dryRun } = parseArgs(process.argv.slice(2))
const effectiveFromIso = normalizeEffectiveFrom(effectiveFrom)
const rows = JSON.parse(fs.readFileSync(filePath, 'utf8')) as PriceRow[]
const costItem = await resolveCostItem(costItemId)
console.log(
`[affiliate-price] 成本项 #${costItem.id} ${costItem.name}(${costItem.code});价格条目 ${rows.length} 条${dryRun ? '(dry-run)' : ''}`,
`[affiliate-price] 成本项 #${costItem.id} ${costItem.name}(${costItem.code});价格条目 ${rows.length} 条` +
`${effectiveFromIso ? `;生效时间 ${effectiveFromIso}` : ''}${dryRun ? '(dry-run)' : ''}`,
)
let createdProductCount = 0
let updatedProductCount = 0
let createdVersionCount = 0
let retiredVersionCount = 0
let backdatedVersionCount = 0
let skippedCount = 0
for (const row of rows) {
@@ -166,14 +176,27 @@ async function main() {
if (product.inserted) createdProductCount += 1
else updatedProductCount += 1
const currentResult = await query<{ id: number; amount_fen: number }>(
`SELECT id, amount_fen FROM cost_versions
const currentResult = 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 scope_type = 'product' AND canonical_product_id = $2 AND status = 'active'
ORDER BY effective_from DESC, id DESC LIMIT 1`,
[costItem.id, product.id],
)
const current = currentResult.rows[0]
if (current && Number(current.amount_fen) === row.costAmountFen) {
if (
effectiveFromIso &&
new Date(current.effective_from).getTime() > new Date(effectiveFromIso).getTime()
) {
await query(
`UPDATE cost_versions
SET effective_from = $1, change_reason = '上游基线价格回填', updated_at = $2
WHERE id = $3`,
[effectiveFromIso, now, current.id],
)
backdatedVersionCount += 1
continue
}
skippedCount += 1
continue
}
@@ -194,18 +217,31 @@ async function main() {
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',
'上游自动发货价格同步', NULL, NULL, $4, $4)`,
[costItem.id, product.id, row.costAmountFen, now],
'上游自动发货价格同步', NULL, NULL, $5, $5)`,
[costItem.id, product.id, row.costAmountFen, effectiveFromIso || now, now],
)
createdVersionCount += 1
}
console.log(
`[affiliate-price] 完成:新增商品 ${createdProductCount}、更新商品 ${updatedProductCount}、` +
`新增版本 ${createdVersionCount}、停用旧版本 ${retiredVersionCount}、金额未变跳过 ${skippedCount}`,
`新增版本 ${createdVersionCount}、停用旧版本 ${retiredVersionCount}、` +
`回填生效时间 ${backdatedVersionCount}、金额未变跳过 ${skippedCount}`,
)
}
function normalizeEffectiveFrom(value: string) {
if (!value) return ''
if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) {
throw new Error('--effective-from 必须是 YYYY-MM-DD')
}
const parsed = new Date(`${value}T00:00:00+08:00`)
if (!Number.isFinite(parsed.getTime())) {
throw new Error('--effective-from 日期无效')
}
return parsed.toISOString()
}
main()
.then(async () => {
await closeDb()