后端: - 删除 services/admin/admin-cost-center-service.ts(4,248 行),拆为 services/cost-center/ 下 5 个模块: cost-center-shared(类型/枚举/归一化/映射/costModelScopeSql)、 cost-center-overview(总览与选项)、 product-catalog(商品与渠道映射)、 template-bootstrap(模板引导、自动发货同步、工单证据回填)、 cost-definition(成本项/版本/异常/预览/快照) - cost_items.carrier_type / cost_type 改为单一 cost_model;专属模型只对 对应履约方式生效,通用模型对所有履约方式生效 - cost_snapshots / cost_exceptions 删除载体列;按履约方式筛异常改用 IN 半连接(248k 异常下 EXISTS 逐行回表 2.7s -> 170ms) - sales_daily_facts.carrier_type -> fulfillment_mode - 人工支出按 cost_model 归类;履约方式只读 order_items.fulfillment_mode - 移除 syncOpenCostExceptionCarrierTypes:异常表已无载体列可同步 前端: - AdminCostCenterPage.tsx(2,887 行 -> 18 行转发),拆为 pages/admin/cost-center/ 下 8 个 Panel + shared + CostCenterContext - CostCarrierType 删除;新增 FulfillmentMode / ProductSource / CostModel - 销售毛利页 carrierType -> fulfillmentMode 同步补两个索引:idx_order_items_fulfillment_id(异常半连接)、 idx_work_categories_name(商品目录分类 lateral join)。
87 lines
2.2 KiB
TypeScript
87 lines
2.2 KiB
TypeScript
import process from 'node:process'
|
||
|
||
import { closeDb, query } from '../src/db/client.js'
|
||
import {
|
||
getFulfillmentModeCounts,
|
||
reconcilePendingFulfillmentModes,
|
||
} from '../src/services/fulfillment/fulfillment-mode-service.js'
|
||
|
||
const options = parseOptions(process.argv.slice(2))
|
||
const batchSize = normalizeInteger(options.batchSize, 5000, 1, 50_000)
|
||
const maxBatches = normalizeInteger(options.maxBatches, 1000, 1, 10_000)
|
||
|
||
async function main() {
|
||
const column = await query<{ exists: boolean }>(
|
||
`
|
||
SELECT EXISTS (
|
||
SELECT 1
|
||
FROM information_schema.columns
|
||
WHERE table_schema = 'public'
|
||
AND table_name = 'order_items'
|
||
AND column_name = 'fulfillment_mode'
|
||
) AS exists
|
||
`,
|
||
)
|
||
|
||
if (!column.rows[0]?.exists) {
|
||
throw new Error('order_items.fulfillment_mode 不存在,请先执行 npm run db:migrate(迁移 107)')
|
||
}
|
||
|
||
const startedAt = Date.now()
|
||
const result = await reconcilePendingFulfillmentModes({
|
||
batchSize,
|
||
maxBatches,
|
||
reason: 'backfill_2026_10_23',
|
||
})
|
||
const counts = await getFulfillmentModeCounts()
|
||
|
||
console.log(
|
||
JSON.stringify(
|
||
{
|
||
...result,
|
||
batchSize,
|
||
maxBatches,
|
||
counts,
|
||
durationMs: Date.now() - startedAt,
|
||
},
|
||
null,
|
||
2,
|
||
),
|
||
)
|
||
|
||
if (result.remainingCount > 0) {
|
||
console.error(`仍有 ${result.remainingCount} 个订单项未回填;请增大 --max-batches 或检查数据。`)
|
||
process.exitCode = 2
|
||
}
|
||
}
|
||
|
||
main()
|
||
.catch((error) => {
|
||
console.error(error instanceof Error ? error.message : error)
|
||
process.exitCode = 1
|
||
})
|
||
.finally(async () => {
|
||
await closeDb()
|
||
})
|
||
|
||
function parseOptions(args: string[]) {
|
||
const result: Record<string, string> = {}
|
||
for (const arg of args) {
|
||
if (!arg.startsWith('--')) continue
|
||
const body = arg.slice(2)
|
||
const index = body.indexOf('=')
|
||
if (index === -1) {
|
||
result[body] = 'true'
|
||
} else {
|
||
result[body.slice(0, index)] = body.slice(index + 1)
|
||
}
|
||
}
|
||
return result
|
||
}
|
||
|
||
function normalizeInteger(value: unknown, fallback: number, min: number, max: number) {
|
||
const parsed = Number(value)
|
||
if (!Number.isInteger(parsed)) return fallback
|
||
return Math.min(max, Math.max(min, parsed))
|
||
}
|