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:
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user