feat(finance): 完成成本快照运行层、销售事实与人工支出登记

迁移 108-111:快照业务时间与预估转实际、销售事实表、系统成本项、公司人工支出登记。接单平台按工单实际 reward 入账;人工支出支持登记/审批/驳回/报销并写 actual 快照。新增销售毛利页、人工支出页与订单入口。
This commit is contained in:
yml2213
2026-10-22 16:41:03 +08:00
parent 15989af87c
commit 0889235870
39 changed files with 4824 additions and 143 deletions
@@ -41,4 +41,5 @@ test('normalizeScheduledJobsConfig ensures default jobs exist', () => {
assert.ok(ids.includes('deposit-unfreeze'))
assert.ok(ids.includes('data-retention'))
assert.ok(ids.includes('pdd_order_sync'))
assert.ok(ids.includes('cost-finance-sync'))
})
@@ -10,6 +10,7 @@ const WORK_ORDER_TIMEOUT_JOB_ID = 'work-order-timeout'
const DEPOSIT_UNFREEZE_JOB_ID = 'deposit-unfreeze'
const DATA_RETENTION_JOB_ID = 'data-retention'
const PDD_ORDER_SYNC_JOB_ID = 'pdd_order_sync'
const COST_FINANCE_SYNC_JOB_ID = 'cost-finance-sync'
export function getScheduledJobsFilePath() {
return SCHEDULED_JOBS_FILE_PATH
@@ -75,6 +76,7 @@ export function normalizeScheduledJobsConfig(rawValue: unknown) {
const hasDepositUnfreeze = jobs.some((item) => item.id === DEPOSIT_UNFREEZE_JOB_ID)
const hasDataRetention = jobs.some((item) => item.id === DATA_RETENTION_JOB_ID)
const hasPddOrderSync = jobs.some((item) => item.id === PDD_ORDER_SYNC_JOB_ID)
const hasCostFinanceSync = jobs.some((item) => item.id === COST_FINANCE_SYNC_JOB_ID)
if (!hasWorkOrderTimeout) {
jobs.push(createDefaultWorkOrderTimeoutJob())
@@ -88,6 +90,9 @@ export function normalizeScheduledJobsConfig(rawValue: unknown) {
if (!hasPddOrderSync) {
jobs.push(createDefaultPddOrderSyncJob())
}
if (!hasCostFinanceSync) {
jobs.push(createDefaultCostFinanceSyncJob())
}
return {
enabled: typeof source.enabled === 'boolean' ? source.enabled : true,
@@ -117,6 +122,9 @@ function normalizeScheduledJob(rawValue: unknown) {
if (type === 'pdd_order_sync') {
return normalizePddOrderSyncJob(rawValue)
}
if (type === 'cost_finance_sync') {
return normalizeCostFinanceSyncJob(rawValue)
}
return null
}
@@ -135,6 +143,20 @@ function normalizePddOrderSyncJob(rawValue: JsonObject) {
}
}
function normalizeCostFinanceSyncJob(rawValue: JsonObject) {
const config = isPlainObject(rawValue.config) ? rawValue.config : {}
return {
id: COST_FINANCE_SYNC_JOB_ID,
type: 'cost_finance_sync',
enabled: rawValue.enabled !== false,
intervalSeconds: normalizeRangeInteger(rawValue.intervalSeconds, 300, 60, 3600),
config: {
days: normalizeRangeInteger(config.days, 7, 1, 90),
},
}
}
function normalizeDepositUnfreezeJob(rawValue: JsonObject) {
const config = isPlainObject(rawValue.config) ? rawValue.config : {}
@@ -181,6 +203,7 @@ function createDefaultScheduledJobsConfig() {
createDefaultDepositUnfreezeJob(),
createDefaultDataRetentionJob(),
createDefaultPddOrderSyncJob(),
createDefaultCostFinanceSyncJob(),
],
}
}
@@ -197,6 +220,18 @@ function createDefaultPddOrderSyncJob() {
}
}
function createDefaultCostFinanceSyncJob() {
return {
id: COST_FINANCE_SYNC_JOB_ID,
type: 'cost_finance_sync',
enabled: true,
intervalSeconds: 300,
config: {
days: 7,
},
}
}
function createDefaultDataRetentionJob() {
return {
id: DATA_RETENTION_JOB_ID,
@@ -0,0 +1,62 @@
import type { JsonObject } from '../../types/json.js'
import {
syncActualWorkOrderCostSnapshots,
syncEstimatedOrderItemCostSnapshots,
} from '../cost-center/cost-snapshot-sync-service.js'
import { rebuildSalesDailyFacts } from '../finance/sales-facts-service.js'
/**
* 财务闭环同步:预估成本快照 -> 工单实际快照 -> 销售事实重算。
* 只处理最近 N 天,避免定时任务扫全量历史。
*/
export async function runCostFinanceSyncJob(job: JsonObject = {}) {
const config = job.config && typeof job.config === 'object' ? (job.config as JsonObject) : {}
const days = clampInteger(config.days, 7, 1, 90)
const estimated = await syncEstimatedOrderItemCostSnapshots({
dateFrom: shiftDays(-(days - 1)),
dateTo: today(),
})
const actual = await syncActualWorkOrderCostSnapshots({
dateFrom: shiftDays(-(days - 1)),
dateTo: today(),
})
const facts = await rebuildSalesDailyFacts({
dateFrom: shiftDays(-(days - 1)),
dateTo: today(),
})
return {
status: 'ok',
message: `成本快照 ${estimated.createdCount} 预估 / ${actual.createdCount} 实际,销售事实 ${facts.insertedCount} 行`,
days,
estimatedCreated: estimated.createdCount,
actualCreated: actual.createdCount,
exceptionsCreated: estimated.exceptionCreatedCount,
factsInserted: facts.insertedCount,
dateFrom: facts.range.fromDate,
dateTo: facts.range.toDate,
}
}
function today() {
return new Intl.DateTimeFormat('en-CA', {
timeZone: 'Asia/Shanghai',
year: 'numeric',
month: '2-digit',
day: '2-digit',
}).format(new Date())
}
function shiftDays(days: number) {
const [year, month, day] = today().split('-').map(Number)
const date = new Date(Date.UTC(year || 1970, (month || 1) - 1, day || 1))
date.setUTCDate(date.getUTCDate() + days)
return date.toISOString().slice(0, 10)
}
function clampInteger(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))
}
@@ -6,6 +6,7 @@ import { runDepositUnfreezeJob } from './deposit-unfreeze-job.js'
import { runWorkOrderTimeoutJob } from './work-order-timeout-job.js'
import { runDataRetentionJob } from './data-retention-job.js'
import { runPddOrderSyncJob } from './pdd-order-sync-job.js'
import { runCostFinanceSyncJob } from './cost-finance-sync-job.js'
const timers = new Map<string, NodeJS.Timeout>()
const jobStates = new Map<string, JsonObject>()
@@ -204,6 +205,9 @@ function dispatchJob(job: JsonObject) {
if (job.type === 'pdd_order_sync') {
return runPddOrderSyncJob(job)
}
if (job.type === 'cost_finance_sync') {
return runCostFinanceSyncJob(job)
}
throw createHttpError(`不支持的定时任务类型:${job.type}`, {
statusCode: 400,