feat(finance): 订单页人工录单按单实际金额计入成本

This commit is contained in:
yml2213
2026-10-22 19:48:56 +08:00
parent 7c8832078f
commit 74b62f3494
10 changed files with 465 additions and 40 deletions
+28
View File
@@ -14,6 +14,7 @@ import {
import {
approveManualExpense,
createManualExpense,
createManualOrderCost,
getManualExpense,
getManualExpenseOverview,
listManualExpenseCostItems,
@@ -184,6 +185,33 @@ router.post(
}),
)
router.post(
'/finance/manual-order-costs',
requireAnyAdminPermission(['fund.expense', 'finance.config']),
createJsonHandler((req) => createManualOrderCost(bodyOf(req), sessionOf(req)), {
successMessage: '订单人工成本已录入',
errorMessage: '录入订单人工成本失败',
scope: '[admin/finance/manual-order-costs]',
audit: (req, data) => {
const result = data as {
record?: { id?: number; expenseNo?: string; amountFen?: number; orderId?: number }
effective?: boolean
}
return {
action: 'manual_order_cost_created',
targetType: 'manual_expense',
targetId: String(result.record?.id || ''),
data: {
expenseNo: result.record?.expenseNo || '',
amountFen: result.record?.amountFen || 0,
orderId: result.record?.orderId || req.body?.orderId || '',
effective: result.effective === true,
},
}
},
}),
)
router.post(
'/finance/manual-expenses/:expenseId/approve',
requireAnyAdminPermission(['finance.review']),
@@ -424,6 +424,106 @@ export async function createManualExpense(payload: JsonObject, session: AdminSes
})
}
export async function createManualOrderCost(payload: JsonObject, session: AdminSession) {
const orderId = normalizeId(payload.orderId ?? payload.order_id)
const requestedItemId = normalizeOptionalId(payload.orderItemId ?? payload.order_item_id)
const itemsResult = await query<{ id: number }>(
'SELECT id FROM order_items WHERE order_id = $1 ORDER BY id ASC',
[orderId],
)
if (itemsResult.rows.length === 0) {
throw createHttpError('订单没有订单项,无法录入人工成本', {
statusCode: 400,
errorCode: 'manual_order_cost_items_missing',
})
}
let orderItemId = requestedItemId
if (orderItemId) {
if (!itemsResult.rows.some((row) => Number(row.id) === orderItemId)) {
throw createHttpError('订单项不属于该订单', {
statusCode: 400,
errorCode: 'manual_order_cost_item_mismatch',
})
}
} else if (itemsResult.rows.length === 1) {
orderItemId = Number(itemsResult.rows[0]?.id || 0)
} else {
throw createHttpError('订单有多个订单项,请选择要计入的订单项', {
statusCode: 400,
errorCode: 'manual_order_cost_item_required',
})
}
const costItemResult = await query<{ id: number }>(
`SELECT id FROM cost_items
WHERE code = 'sys_manual_service' AND status = 'active'
LIMIT 1`,
)
const costItemId = Number(costItemResult.rows[0]?.id || 0)
if (!costItemId) {
throw createHttpError('人工客服履约成本项未启用', {
statusCode: 409,
errorCode: 'manual_order_cost_item_unavailable',
})
}
const amountFen = normalizeAmountFen(payload.amountFen ?? payload.amount_fen)
const note = optionalText(payload.note, 500)
const expenseCategory = normalizeStatus(
payload.expenseCategory ?? payload.expense_category,
EXPENSE_CATEGORIES,
'service_fee',
'支出分类无效',
)
const payeeName = optionalText(payload.payeeName ?? payload.payee_name, 120) || '内部人工'
const evidenceJson = normalizeEvidence(payload.evidenceJson ?? payload.evidence_json)
if (!hasEvidence(evidenceJson)) {
evidenceJson.note = note || '订单页人工录单'
}
const canApprove =
session.role === 'admin' ||
session.role === 'finance' ||
(session.permissions || []).includes('finance.review')
const effective = payload.effective !== false && canApprove
const created = await createManualExpense(
{
orderId,
orderItemId,
costItemId,
amountFen,
expenseCategory,
payeeName,
payeeType: payload.payeeType ?? payload.payee_type ?? 'other',
paymentMethod: payload.paymentMethod ?? payload.payment_method ?? 'other',
note: note || '订单页人工录单',
evidenceJson,
status: 'submitted',
},
session,
)
if (!effective) {
return { record: created, effective: false, reason: 'pending_review' as const }
}
try {
const approved = await approveManualExpense(
created.id,
{ note: '订单页人工录单直接生效' },
session,
)
return { record: approved, effective: true, reason: 'approved' as const }
} catch (error) {
return {
record: created,
effective: false,
reason: 'approval_failed' as const,
message: error instanceof Error ? error.message : '自动生效失败',
}
}
}
export async function approveManualExpense(
expenseId: number | string,
payload: JsonObject,
@@ -94,8 +94,7 @@ export async function rebuildSalesDailyFacts(
COUNT(*)::int AS snapshot_count,
COUNT(*) FILTER (WHERE cs.snapshot_stage = 'actual')::int AS actual_count
FROM cost_snapshots cs
WHERE cs.subject_type = 'order_item'
AND cs.subject_id = oi.id
WHERE cs.order_item_id = oi.id
AND cs.superseded_at IS NULL
) cost ON TRUE
WHERE o.pay_status IN ('paid', 'refunded')