diff --git a/apps/backend/src/db/migrations/113_fund_account_no_readable.sql b/apps/backend/src/db/migrations/113_fund_account_no_readable.sql new file mode 100644 index 00000000..a9d25c92 --- /dev/null +++ b/apps/backend/src/db/migrations/113_fund_account_no_readable.sql @@ -0,0 +1,35 @@ +-- 113_fund_account_no_readable.sql —— 备用金账户编号改为可读的 FA-<用户名>。 +-- +-- 背景:历史编号是 FA + 10 位随机十六进制(如 FA7683D6B58B), +-- 在「备用金」列表里无法一眼对应到具体客服。 +-- 新规则:FA-<大写用户名>,例如 FA-KF_2、FA-JIAJIALI; +-- 同一用户名重名时追加 -2、-3。 + +WITH base AS ( + SELECT + fa.id, + 'FA-' || COALESCE( + NULLIF( + trim( + both '_' FROM upper( + regexp_replace(COALESCE(NULLIF(au.username, ''), 'account-' || fa.id), '[^A-Za-z0-9_]+', '_', 'g') + ) + ), + '' + ), + 'ACCOUNT-' || fa.id + ) AS base_no + FROM fund_accounts fa + LEFT JOIN admin_users au ON au.id = fa.owner_admin_user_id +), +numbered AS ( + SELECT + id, + base_no, + row_number() OVER (PARTITION BY base_no ORDER BY id) AS rn + FROM base +) +UPDATE fund_accounts fa +SET account_no = CASE WHEN n.rn = 1 THEN n.base_no ELSE n.base_no || '-' || n.rn END +FROM numbered n +WHERE fa.id = n.id; diff --git a/apps/backend/src/services/admin/admin-fund-service.ts b/apps/backend/src/services/admin/admin-fund-service.ts index 3bc5fd75..7b94f6b4 100644 --- a/apps/backend/src/services/admin/admin-fund-service.ts +++ b/apps/backend/src/services/admin/admin-fund-service.ts @@ -278,6 +278,37 @@ function generateNo(prefix: string) { return randomId(prefix, 5).toUpperCase() } +function buildFundAccountNoBase(username: string): string { + const slug = + String(username || '') + .trim() + .replace(/[^A-Za-z0-9_]+/g, '_') + .replace(/^_+|_+$/g, '') + .toUpperCase() + .slice(0, 32) || 'ACCOUNT' + return `FA-${slug}` +} + +/** 账户编号直接带用户名,列表里一眼能对应到人;重名时追加 -2、-3。 */ +async function allocateFundAccountNo(client: PoolClient, username: string): Promise { + const base = buildFundAccountNoBase(username) + let candidate = base + for (let suffix = 2; suffix <= 99; suffix += 1) { + const existing = await client.query( + 'SELECT 1 FROM fund_accounts WHERE account_no = $1 LIMIT 1', + [candidate], + ) + if (!existing.rowCount) { + return candidate + } + candidate = `${base}-${suffix}` + } + throw createHttpError('生成备用金账户编号失败', { + statusCode: 500, + errorCode: 'fund_account_no_conflict', + }) +} + function requiredRow(row: T | undefined, message: string): T { if (!row) { throw createHttpError(message, { @@ -974,7 +1005,7 @@ export async function createAdminFundAccount(payload: JsonObject, session: Admin }) } - const accountNo = generateNo('FA') + const accountNo = await allocateFundAccountNo(client, user.username) const insertResult = await client.query( ` INSERT INTO fund_accounts ( @@ -1062,7 +1093,7 @@ export async function ensureAdminFundAgentAccounts(payload: JsonObject, session: const created: Array<{ id: number; accountNo: string; ownerName: string }> = [] for (const user of usersResult.rows) { - const accountNo = generateNo('FA') + const accountNo = await allocateFundAccountNo(client, user.username) const insertResult = await client.query( ` INSERT INTO fund_accounts ( diff --git a/docs/README.md b/docs/README.md index 1b1f2d63..48289c82 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,6 +1,6 @@ # order_site 文档索引 -> 最后校准:2026-10-22,代码基线 `dev @ 7c883207 + 财务 103–112`。 +> 最后校准:2026-10-23,代码基线 `dev @ a7f60414 + 财务 103–113`。 > 本文是 `docs/` 的唯一入口。新增文档必须在上表登记状态;过期文档不再直接改写结论,统一移入 `docs/archive/`。 ## 一、先记住五条口径 diff --git a/docs/人工支出登记落地方案-2026-10-17.md b/docs/人工支出登记落地方案-2026-10-17.md index 94c69aeb..2128b07c 100644 --- a/docs/人工支出登记落地方案-2026-10-17.md +++ b/docs/人工支出登记落地方案-2026-10-17.md @@ -2,7 +2,7 @@ > 状态:**已实施 P0(`dev` 分支,迁移 111;订单页人工录单、备用金扣款为 2026-10-22/23 增量)** > 最后校准:2026-10-23 -> 代码基线:`dev @ cd9bb141 + 财务 108–112` +> 代码基线:`dev @ a7f60414 + 财务 108–113` > 依据:业务口径「人工块当前不以 SLA 监管为目标,先把流程搞清楚:哪个客服处理了哪些东西、付了多少钱、什么项目、关联哪个订单」。 > 数据来源:本地只读克隆(生产快照,数据截至 2026-10-14)+ 现有代码盘点。 > 重要:**客服备用金支出已经实施**(`manual_expenses`);**公司人工支出登记**(`manual_expense_records`)也已实施,两张表通过 `fund_expense_id` 关联,不是同一张表。 diff --git a/docs/客服备用金台账实施方案-2026-10-18.md b/docs/客服备用金台账实施方案-2026-10-18.md index c45efae9..d6c04b27 100644 --- a/docs/客服备用金台账实施方案-2026-10-18.md +++ b/docs/客服备用金台账实施方案-2026-10-18.md @@ -124,7 +124,7 @@ P0 四张表即可。 | 字段 | 说明 | | --- | --- | | id | 主键 | -| account_no | 账户编号,唯一 | +| account_no | 账户编号,唯一;格式 `FA-<大写用户名>`(如 `FA-KF_2`、`FA-JIAJIALI`),重名追加 `-2`、`-3`;迁移 `113` 已回填历史随机编号 | | account_type | `supervisor` / `agent` | | owner_admin_user_id | 主管或客服的后台用户 ID | | owner_name | 姓名快照 | diff --git a/docs/成本中心与三块履约模型.md b/docs/成本中心与三块履约模型.md index be8ed83e..0738db1e 100644 --- a/docs/成本中心与三块履约模型.md +++ b/docs/成本中心与三块履约模型.md @@ -1,8 +1,8 @@ # 成本中心与三块履约模型 > 状态:**现行(P0 已实施)**。商品主数据、渠道映射、成本中心、成本快照运行层、销售事实与公司人工支出登记(含订单页逐单实际金额「人工录单」)已落地;轻量人工 case 与影子总账仍为后续方案。 -> 最后校准:2026-10-22 -> 代码基线:`dev @ 7c883207 + 财务 108–112` +> 最后校准:2026-10-23 +> 代码基线:`dev @ a7f60414 + 财务 108–113` > 数据基线:2026-10-17 生产库只读聚合(`default_transaction_read_only=on`,仅 SELECT,无写入) > 实施顺序以 [README.md](README.md) §三 为准;本文负责“商品、成本、销售事实”的数据模型,不重复总账与页面设计。 > 关联: