feat(finance): 完成成本快照运行层、销售事实与人工支出登记
迁移 108-111:快照业务时间与预估转实际、销售事实表、系统成本项、公司人工支出登记。接单平台按工单实际 reward 入账;人工支出支持登记/审批/驳回/报销并写 actual 快照。新增销售毛利页、人工支出页与订单入口。
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user