feat(wecom): 结算账单明细与成交播报用户明文

支付/核销补 HQ 备注且手机号不脱敏;结算通知按城市、合伙人、笔数和累计金额分块推送。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-27 11:44:49 +08:00
parent 3dbb73e428
commit 737efb89a3
11 changed files with 688 additions and 162 deletions
@@ -0,0 +1,157 @@
/** 企微结算账单正文:分字段块 + 字节上限(机器人 markdown 约 4096 字节) */
export type WecomBankLike = {
bankAccountName?: string | null;
bankName?: string | null;
bankBranch?: string | null;
bankAccountNo?: string | null;
};
export function formatYuan(n: number): string {
return `¥${Number(n).toFixed(2)}`;
}
export function formatPercent(rate: number): string {
const p = Number(rate) * 100;
if (!Number.isFinite(p)) return '—';
const rounded = Math.round(p * 100) / 100;
const text = Number.isInteger(rounded) ? String(rounded) : String(rounded);
return `${text}%`;
}
export function formatPartnerDashLabel(
company?: string | null,
name?: string | null,
): string {
const person = (name || '').trim() || '—';
const org = (company || '').trim();
return org ? `${org}-${person}` : person;
}
export function wecomBankParts(bank?: WecomBankLike | null): {
payee: string;
accountNo: string;
bankBranch: string;
} {
if (!bank) return { payee: '未登记', accountNo: '—', bankBranch: '—' };
const payee = bank.bankAccountName?.trim() || '';
const accountNo = bank.bankAccountNo?.trim() || '';
const bankBranch = bank.bankName?.trim() || bank.bankBranch?.trim() || '';
if (!payee && !accountNo) {
return { payee: '未登记', accountNo: '—', bankBranch: bankBranch || '—' };
}
return {
payee: payee || '—',
accountNo: accountNo || '—',
bankBranch: bankBranch || '—',
};
}
export function formatWecomFieldBlock(rows: Array<[string, string]>): string {
return rows.map(([k, v]) => `${k}${v}`).join('\n');
}
export function joinLimited(names: string[], unit: string, max = 8): string {
const uniq = [...new Set(names.map((n) => n.trim()).filter(Boolean))];
if (!uniq.length) return '—';
if (uniq.length <= max) return uniq.join('、');
return `${uniq.slice(0, max).join('、')}${uniq.length}${unit}`;
}
const BILL_DIGEST_MAX_BYTES = 3200;
export function capBillBlocks(blocks: string[]): string {
if (!blocks.length) return '无';
const out: string[] = [];
let bytes = 0;
let shown = 0;
for (const block of blocks) {
const extra = (shown ? 2 : 0) + Buffer.byteLength(block, 'utf8');
if (shown > 0 && bytes + extra > BILL_DIGEST_MAX_BYTES) {
out.push(`另有 ${blocks.length - shown} 笔未列出,请到 HQ 查看`);
break;
}
out.push(block);
bytes += extra;
shown += 1;
}
return out.join('\n\n');
}
export function formatStoreBillWecomBlock(row: {
cityName: string;
storeName: string;
partnerLabel: string;
redeemCount: number;
redeemAmount: number;
settlementRate: number;
amount: number;
bank?: WecomBankLike | null;
}): string {
const bank = wecomBankParts(row.bank);
return formatWecomFieldBlock([
['城市', row.cityName || '—'],
['门店', row.storeName || '—'],
['合伙人', row.partnerLabel || '—'],
['核销笔数', String(row.redeemCount)],
['核销金额', formatYuan(row.redeemAmount)],
['结算比例', formatPercent(row.settlementRate)],
['账单', formatYuan(row.amount)],
['收款人', bank.payee],
['收款账户', bank.accountNo],
['收款开户行', bank.bankBranch],
]);
}
export function formatPartnerBillWecomBlock(row: {
cityName: string;
partnerLabel: string;
orderCount?: number;
redeemCount?: number;
orderCommission: number;
redeemCommission: number;
amount: number;
bank?: WecomBankLike | null;
}): string {
const bank = wecomBankParts(row.bank);
const rows: Array<[string, string]> = [
['城市', row.cityName || '—'],
['合伙人', row.partnerLabel || '—'],
];
if (row.orderCount != null) rows.push(['订单笔数', String(row.orderCount)]);
if (row.redeemCount != null) rows.push(['核销笔数', String(row.redeemCount)]);
rows.push(
['订单佣金', formatYuan(row.orderCommission)],
['核销佣金', formatYuan(row.redeemCommission)],
['账单', formatYuan(row.amount)],
['收款人', bank.payee],
['收款账户', bank.accountNo],
['收款开户行', bank.bankBranch],
);
return formatWecomFieldBlock(rows);
}
export function formatLogisticsBillWecomBlock(row: {
cityNames: string;
partnerNames: string;
providerName: string;
settlementMethod: string;
orderCount: number;
bottleCount: number;
amount: number;
bank?: WecomBankLike | null;
}): string {
const bank = wecomBankParts(row.bank);
return formatWecomFieldBlock([
['城市', row.cityNames || '—'],
['合伙人', row.partnerNames || '—'],
['承运商', row.providerName || '—'],
['结算方式', row.settlementMethod || '—'],
['发货笔数', String(row.orderCount)],
['瓶数', String(row.bottleCount)],
['账单', formatYuan(row.amount)],
['收款人', bank.payee],
['收款账户', bank.accountNo],
['收款开户行', bank.bankBranch],
]);
}