@@ -63,6 +63,20 @@ export async function loadStorePrimaryBanksMap(
|
||||
return map;
|
||||
}
|
||||
|
||||
export function formatWecomBankAccount(bank?: {
|
||||
bankAccountName?: string | null;
|
||||
bankName?: string | null;
|
||||
bankBranch?: string | null;
|
||||
bankAccountNo?: string | null;
|
||||
} | null): string {
|
||||
if (!bank) return '未登记收款账户';
|
||||
const name = bank.bankAccountName?.trim() || '';
|
||||
const no = bank.bankAccountNo?.trim() || '';
|
||||
if (!name && !no) return '未登记收款账户';
|
||||
const bankLine = bank.bankName?.trim() || bank.bankBranch?.trim() || '';
|
||||
return [name, bankLine, no].filter(Boolean).join(' / ');
|
||||
}
|
||||
|
||||
export async function loadWineryBankConfig(
|
||||
prisma: PrismaClient,
|
||||
): Promise<StoreBankAccountSnapshot & { bankName?: string | null }> {
|
||||
|
||||
@@ -7,7 +7,9 @@ import {
|
||||
WECOM_PUSH_DEFAULT_BIZ_TODO_CONDITIONS,
|
||||
WECOM_PUSH_DEFAULT_DEAL_BROADCAST_CONDITIONS,
|
||||
WECOM_PUSH_DEFAULT_DEV_DISPATCH_CONDITIONS,
|
||||
WECOM_PUSH_DEFAULT_SETTLEMENT_CONDITIONS,
|
||||
WECOM_PUSH_DEFAULT_STORE_AUDIT_CONDITIONS,
|
||||
WECOM_SETTLEMENT_PUSH_NAME,
|
||||
WECOM_STORE_AUDIT_PLACEHOLDER_WEBHOOK,
|
||||
WECOM_STORE_AUDIT_PUSH_NAME,
|
||||
WECOM_TEMPLATE_EVENT_KEYS,
|
||||
@@ -22,6 +24,7 @@ import { PrismaService } from '../../common/prisma/prisma.module';
|
||||
import { applyWecomAtMentionInContent } from '../../modules/dev-plan/dev-plan-wecom-mention.util';
|
||||
import {
|
||||
WECOM_PUSH_TEMPLATE_DEFAULTS,
|
||||
WECOM_PUSH_TEMPLATE_LEGACY_BODIES,
|
||||
buildHqHandleUrl,
|
||||
getDefaultTemplate,
|
||||
renderWecomTemplate,
|
||||
@@ -66,7 +69,7 @@ export class WecomMessagePushService implements OnModuleInit {
|
||||
}
|
||||
}
|
||||
|
||||
/** 表空时从 .env / 旧设置迁移;并确保门店审核 / 业务待办 / 成交播报 / 模板缺行 */
|
||||
/** 表空时从 .env / 旧设置迁移;并确保门店审核 / 业务待办 / 成交播报 / 结算通知 / 模板缺行 */
|
||||
async ensureDefaults(): Promise<void> {
|
||||
const count = await this.prisma.wecomMessagePush.count();
|
||||
if (count === 0) {
|
||||
@@ -141,6 +144,12 @@ export class WecomMessagePushService implements OnModuleInit {
|
||||
30,
|
||||
process.env.WECOM_DEAL_BROADCAST_WEBHOOK_URL,
|
||||
);
|
||||
await this.ensureNamedPush(
|
||||
WECOM_SETTLEMENT_PUSH_NAME,
|
||||
WECOM_PUSH_DEFAULT_SETTLEMENT_CONDITIONS,
|
||||
35,
|
||||
process.env.WECOM_SETTLEMENT_WEBHOOK_URL,
|
||||
);
|
||||
await this.ensureTemplates();
|
||||
}
|
||||
|
||||
@@ -181,22 +190,36 @@ export class WecomMessagePushService implements OnModuleInit {
|
||||
this.logger.log(`seeded wecom message push: ${name} (placeholder, disabled)`);
|
||||
}
|
||||
|
||||
/** 仅插入缺失 eventKey,不覆盖已有文案 */
|
||||
/** 插入缺失 eventKey;仅当 body 仍为上一版默认文案时升级,不覆盖 HQ 手改 */
|
||||
async ensureTemplates(): Promise<void> {
|
||||
for (const def of WECOM_PUSH_TEMPLATE_DEFAULTS) {
|
||||
const existing = await this.prisma.wecomPushTemplate.findUnique({
|
||||
where: { eventKey: def.eventKey },
|
||||
});
|
||||
if (existing) continue;
|
||||
await this.prisma.wecomPushTemplate.create({
|
||||
data: {
|
||||
eventKey: def.eventKey,
|
||||
title: def.title,
|
||||
body: def.body,
|
||||
handleLabel: def.handleLabel,
|
||||
},
|
||||
});
|
||||
this.logger.log(`seeded wecom push template: ${def.eventKey}`);
|
||||
if (!existing) {
|
||||
await this.prisma.wecomPushTemplate.create({
|
||||
data: {
|
||||
eventKey: def.eventKey,
|
||||
title: def.title,
|
||||
body: def.body,
|
||||
handleLabel: def.handleLabel,
|
||||
},
|
||||
});
|
||||
this.logger.log(`seeded wecom push template: ${def.eventKey}`);
|
||||
continue;
|
||||
}
|
||||
const legacy = WECOM_PUSH_TEMPLATE_LEGACY_BODIES[def.eventKey];
|
||||
if (legacy && existing.body === legacy) {
|
||||
await this.prisma.wecomPushTemplate.update({
|
||||
where: { eventKey: def.eventKey },
|
||||
data: {
|
||||
title: def.title,
|
||||
body: def.body,
|
||||
handleLabel: def.handleLabel,
|
||||
},
|
||||
});
|
||||
this.logger.log(`upgraded wecom push template from legacy default: ${def.eventKey}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -447,7 +470,9 @@ export class WecomMessagePushService implements OnModuleInit {
|
||||
orderNo: 'DK202608200001',
|
||||
payAmount: '199.00',
|
||||
cityName: '郑州',
|
||||
skuSummary: '杜康原浆 ×2',
|
||||
skuSummary: '杜康原浆 52度 瓶×2',
|
||||
deliveryLabel: '同城',
|
||||
receiverInfo: '张三 13800008000 河南省郑州市金水区示例路1号',
|
||||
phoneMasked: '138****8000',
|
||||
},
|
||||
handlePath: '/orders?orderNo=DK202608200001',
|
||||
@@ -458,6 +483,8 @@ export class WecomMessagePushService implements OnModuleInit {
|
||||
amount: '88.00',
|
||||
storeName: '示例门店',
|
||||
channel: '扫码',
|
||||
userName: '好客用户',
|
||||
userPhone: '138****8000',
|
||||
},
|
||||
handlePath: '/redeem-records?redeemNo=RD202608200001',
|
||||
},
|
||||
@@ -465,7 +492,8 @@ export class WecomMessagePushService implements OnModuleInit {
|
||||
vars: {
|
||||
storeName: '示例门店',
|
||||
cityName: '郑州',
|
||||
partnerLabel: '示例合伙人',
|
||||
submitter: '郑州某某商贸 · 张三',
|
||||
partnerLabel: '郑州某某商贸 · 张三',
|
||||
action: '新建',
|
||||
storeId: '1',
|
||||
},
|
||||
@@ -475,7 +503,7 @@ export class WecomMessagePushService implements OnModuleInit {
|
||||
vars: {
|
||||
storeName: '示例门店',
|
||||
cityName: '郑州',
|
||||
submitter: '合伙人',
|
||||
submitter: '郑州某某商贸 · 张三',
|
||||
packageCount: '3',
|
||||
requestId: '1',
|
||||
},
|
||||
@@ -485,7 +513,7 @@ export class WecomMessagePushService implements OnModuleInit {
|
||||
vars: {
|
||||
storeName: '示例门店',
|
||||
cityName: '郑州',
|
||||
submitter: '门店',
|
||||
submitter: '门店 · 李四',
|
||||
changedFields: '门店名称、详细地址',
|
||||
requestId: '1',
|
||||
},
|
||||
@@ -501,6 +529,16 @@ export class WecomMessagePushService implements OnModuleInit {
|
||||
},
|
||||
handlePath: '/finance/store-bills?kind=WITHDRAW&status=PENDING_REVIEW&storeId=1',
|
||||
},
|
||||
'store.withdraw_approved': {
|
||||
vars: {
|
||||
storeName: '示例门店',
|
||||
withdrawNo: 'SW202608200001',
|
||||
amount: '500.00',
|
||||
bankAccount: '张三 / 郑州支行 / 6222000011112222',
|
||||
storeId: '1',
|
||||
},
|
||||
handlePath: '/finance/store-bills?kind=WITHDRAW&status=PAID&storeId=1',
|
||||
},
|
||||
'invoice.pending': {
|
||||
vars: {
|
||||
invoiceNo: 'IV202608200001',
|
||||
@@ -511,6 +549,39 @@ export class WecomMessagePushService implements OnModuleInit {
|
||||
},
|
||||
handlePath: '/invoices?status=PENDING&invoiceNo=IV202608200001',
|
||||
},
|
||||
'finance.store_bill': {
|
||||
vars: {
|
||||
period: '2026-08-25',
|
||||
billCount: '2',
|
||||
billSummary: '示例门店 ¥300.00 张三 / 郑州支行 / 6222000011112222',
|
||||
},
|
||||
handlePath: '/finance/store-bills?kind=T1_BILL',
|
||||
},
|
||||
'finance.partner_bill': {
|
||||
vars: {
|
||||
period: '2026-07',
|
||||
billCount: '1',
|
||||
billSummary: '郑州某某商贸 ¥120.00 张三 / 郑州支行 / 6222000011112222',
|
||||
},
|
||||
handlePath: '/finance/partner-bills',
|
||||
},
|
||||
'finance.winery_bill': {
|
||||
vars: {
|
||||
period: '2026-08-23',
|
||||
amount: '1990.00',
|
||||
orderCount: '8',
|
||||
bankAccount: '杜康酒厂 / 洛阳支行 / 6222000099998888',
|
||||
},
|
||||
handlePath: '/finance/winery-bills',
|
||||
},
|
||||
'finance.logistics_bill': {
|
||||
vars: {
|
||||
period: '2026-07',
|
||||
billCount: '1',
|
||||
billSummary: '小飞侠 ¥86.00 承运商 / 郑州支行 / 6222000033334444',
|
||||
},
|
||||
handlePath: '/finance/logistics-bills',
|
||||
},
|
||||
};
|
||||
return samples[eventKey];
|
||||
}
|
||||
|
||||
@@ -18,6 +18,8 @@ export const WECOM_PUSH_TEMPLATE_DEFAULTS: WecomTemplateDefault[] = [
|
||||
'实付:¥{{payAmount}}',
|
||||
'城市:{{cityName}}',
|
||||
'商品:{{skuSummary}}',
|
||||
'物流:{{deliveryLabel}}',
|
||||
'收货:{{receiverInfo}}',
|
||||
'用户:{{phoneMasked}}',
|
||||
'时间:{{time}}',
|
||||
'[{{handleLabel}}]({{handleUrl}})',
|
||||
@@ -33,6 +35,7 @@ export const WECOM_PUSH_TEMPLATE_DEFAULTS: WecomTemplateDefault[] = [
|
||||
'金额:¥{{amount}}',
|
||||
'门店:{{storeName}}',
|
||||
'渠道:{{channel}}',
|
||||
'用户:{{userName}} {{userPhone}}',
|
||||
'时间:{{time}}',
|
||||
'[{{handleLabel}}]({{handleUrl}})',
|
||||
].join('\n'),
|
||||
@@ -45,6 +48,7 @@ export const WECOM_PUSH_TEMPLATE_DEFAULTS: WecomTemplateDefault[] = [
|
||||
'**门店审核待处理 · {{action}}**',
|
||||
'门店:{{storeName}}',
|
||||
'城市:{{cityName}}',
|
||||
'提交人:{{submitter}}',
|
||||
'合伙人:{{partnerLabel}}',
|
||||
'时间:{{time}}',
|
||||
'[{{handleLabel}}]({{handleUrl}})',
|
||||
@@ -58,7 +62,7 @@ export const WECOM_PUSH_TEMPLATE_DEFAULTS: WecomTemplateDefault[] = [
|
||||
'**套餐变更待审核**',
|
||||
'门店:{{storeName}}',
|
||||
'城市:{{cityName}}',
|
||||
'提交端:{{submitter}}',
|
||||
'提交人:{{submitter}}',
|
||||
'套餐条数:{{packageCount}}',
|
||||
'时间:{{time}}',
|
||||
'[{{handleLabel}}]({{handleUrl}})',
|
||||
@@ -72,7 +76,7 @@ export const WECOM_PUSH_TEMPLATE_DEFAULTS: WecomTemplateDefault[] = [
|
||||
'**门店信息变更待审**',
|
||||
'门店:{{storeName}}',
|
||||
'城市:{{cityName}}',
|
||||
'提交端:{{submitter}}',
|
||||
'提交人:{{submitter}}',
|
||||
'变更字段:{{changedFields}}',
|
||||
'时间:{{time}}',
|
||||
'[{{handleLabel}}]({{handleUrl}})',
|
||||
@@ -93,6 +97,20 @@ export const WECOM_PUSH_TEMPLATE_DEFAULTS: WecomTemplateDefault[] = [
|
||||
].join('\n'),
|
||||
handleLabel: '去处理',
|
||||
},
|
||||
{
|
||||
eventKey: 'store.withdraw_approved',
|
||||
title: '门店提现已通过',
|
||||
body: [
|
||||
'**门店提现已通过**',
|
||||
'门店:{{storeName}}',
|
||||
'提现单号:{{withdrawNo}}',
|
||||
'金额:¥{{amount}}',
|
||||
'收款账户:{{bankAccount}}',
|
||||
'时间:{{time}}',
|
||||
'[{{handleLabel}}]({{handleUrl}})',
|
||||
].join('\n'),
|
||||
handleLabel: '去处理',
|
||||
},
|
||||
{
|
||||
eventKey: 'invoice.pending',
|
||||
title: '发票申请待开票',
|
||||
@@ -108,8 +126,110 @@ export const WECOM_PUSH_TEMPLATE_DEFAULTS: WecomTemplateDefault[] = [
|
||||
].join('\n'),
|
||||
handleLabel: '去处理',
|
||||
},
|
||||
{
|
||||
eventKey: 'finance.store_bill',
|
||||
title: '门店日账单已生成',
|
||||
body: [
|
||||
'**门店日账单已生成**',
|
||||
'账期:{{period}}',
|
||||
'笔数:{{billCount}}',
|
||||
'{{billSummary}}',
|
||||
'时间:{{time}}',
|
||||
'[{{handleLabel}}]({{handleUrl}})',
|
||||
].join('\n'),
|
||||
handleLabel: '去处理',
|
||||
},
|
||||
{
|
||||
eventKey: 'finance.partner_bill',
|
||||
title: '合伙人月账单已生成',
|
||||
body: [
|
||||
'**合伙人月账单已生成**',
|
||||
'账期:{{period}}',
|
||||
'笔数:{{billCount}}',
|
||||
'{{billSummary}}',
|
||||
'时间:{{time}}',
|
||||
'[{{handleLabel}}]({{handleUrl}})',
|
||||
].join('\n'),
|
||||
handleLabel: '去处理',
|
||||
},
|
||||
{
|
||||
eventKey: 'finance.winery_bill',
|
||||
title: '酒厂日账单已生成',
|
||||
body: [
|
||||
'**酒厂日账单已生成**',
|
||||
'账期:{{period}}',
|
||||
'订单数:{{orderCount}}',
|
||||
'金额:¥{{amount}}',
|
||||
'收款账户:{{bankAccount}}',
|
||||
'时间:{{time}}',
|
||||
'[{{handleLabel}}]({{handleUrl}})',
|
||||
].join('\n'),
|
||||
handleLabel: '去处理',
|
||||
},
|
||||
{
|
||||
eventKey: 'finance.logistics_bill',
|
||||
title: '物流月对账已生成',
|
||||
body: [
|
||||
'**物流月对账已生成**',
|
||||
'账期:{{period}}',
|
||||
'笔数:{{billCount}}',
|
||||
'{{billSummary}}',
|
||||
'时间:{{time}}',
|
||||
'[{{handleLabel}}]({{handleUrl}})',
|
||||
].join('\n'),
|
||||
handleLabel: '去处理',
|
||||
},
|
||||
];
|
||||
|
||||
/** v3.5.3 默认正文;ensureTemplates 仅在库中仍为此文案时升级,不覆盖 HQ 手改 */
|
||||
export const WECOM_PUSH_TEMPLATE_LEGACY_BODIES: Partial<Record<WecomTemplateEventKey, string>> = {
|
||||
'order.paid': [
|
||||
'**订单支付成功**',
|
||||
'订单号:{{orderNo}}',
|
||||
'实付:¥{{payAmount}}',
|
||||
'城市:{{cityName}}',
|
||||
'商品:{{skuSummary}}',
|
||||
'用户:{{phoneMasked}}',
|
||||
'时间:{{time}}',
|
||||
'[{{handleLabel}}]({{handleUrl}})',
|
||||
].join('\n'),
|
||||
'redeem.success': [
|
||||
'**门店核销成功**',
|
||||
'核销单号:{{redeemNo}}',
|
||||
'金额:¥{{amount}}',
|
||||
'门店:{{storeName}}',
|
||||
'渠道:{{channel}}',
|
||||
'时间:{{time}}',
|
||||
'[{{handleLabel}}]({{handleUrl}})',
|
||||
].join('\n'),
|
||||
'store.audit_pending': [
|
||||
'**门店审核待处理 · {{action}}**',
|
||||
'门店:{{storeName}}',
|
||||
'城市:{{cityName}}',
|
||||
'合伙人:{{partnerLabel}}',
|
||||
'时间:{{time}}',
|
||||
'[{{handleLabel}}]({{handleUrl}})',
|
||||
].join('\n'),
|
||||
'store.package_audit_pending': [
|
||||
'**套餐变更待审核**',
|
||||
'门店:{{storeName}}',
|
||||
'城市:{{cityName}}',
|
||||
'提交端:{{submitter}}',
|
||||
'套餐条数:{{packageCount}}',
|
||||
'时间:{{time}}',
|
||||
'[{{handleLabel}}]({{handleUrl}})',
|
||||
].join('\n'),
|
||||
'store.info_change_pending': [
|
||||
'**门店信息变更待审**',
|
||||
'门店:{{storeName}}',
|
||||
'城市:{{cityName}}',
|
||||
'提交端:{{submitter}}',
|
||||
'变更字段:{{changedFields}}',
|
||||
'时间:{{time}}',
|
||||
'[{{handleLabel}}]({{handleUrl}})',
|
||||
].join('\n'),
|
||||
};
|
||||
|
||||
export function getDefaultTemplate(eventKey: string): WecomTemplateDefault | undefined {
|
||||
return WECOM_PUSH_TEMPLATE_DEFAULTS.find((t) => t.eventKey === eventKey);
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ export class SettlementScheduler {
|
||||
this.logger.error('Winery bill job failed', e instanceof Error ? e.stack : e);
|
||||
this.alert.notify({
|
||||
level: 'P0',
|
||||
category: 'job',
|
||||
category: 'settlement',
|
||||
title: '酒厂日账单任务失败',
|
||||
detail: e instanceof Error ? e.message : String(e),
|
||||
dedupeKey: `job_winery_bill|${new Date().toISOString().slice(0, 10)}`,
|
||||
@@ -41,7 +41,7 @@ export class SettlementScheduler {
|
||||
this.logger.error('Store bill job failed', e instanceof Error ? e.stack : e);
|
||||
this.alert.notify({
|
||||
level: 'P0',
|
||||
category: 'job',
|
||||
category: 'settlement',
|
||||
title: '门店日账单任务失败',
|
||||
detail: e instanceof Error ? e.message : String(e),
|
||||
dedupeKey: `job_store_bill|${new Date().toISOString().slice(0, 10)}`,
|
||||
@@ -60,7 +60,7 @@ export class SettlementScheduler {
|
||||
this.logger.error('Store withdraw overdue scan failed', e instanceof Error ? e.stack : e);
|
||||
this.alert.notify({
|
||||
level: 'P1',
|
||||
category: 'job',
|
||||
category: 'settlement',
|
||||
title: '门店提现超时扫描失败',
|
||||
detail: e instanceof Error ? e.message : String(e),
|
||||
dedupeKey: `job_store_withdraw_overdue_fail|${new Date().toISOString().slice(0, 10)}`,
|
||||
@@ -79,7 +79,7 @@ export class SettlementScheduler {
|
||||
if (result.failed > 0) {
|
||||
this.alert.notify({
|
||||
level: 'P0',
|
||||
category: 'job',
|
||||
category: 'settlement',
|
||||
title: '合伙人月账单部分失败',
|
||||
detail: `total=${result.total} success=${result.success} failed=${result.failed}`,
|
||||
dedupeKey: `job_partner_bill|${new Date().toISOString().slice(0, 7)}`,
|
||||
@@ -89,7 +89,7 @@ export class SettlementScheduler {
|
||||
this.logger.error('Partner bill job failed', e instanceof Error ? e.stack : e);
|
||||
this.alert.notify({
|
||||
level: 'P0',
|
||||
category: 'job',
|
||||
category: 'settlement',
|
||||
title: '合伙人月账单任务失败',
|
||||
detail: e instanceof Error ? e.message : String(e),
|
||||
dedupeKey: `job_partner_bill|${new Date().toISOString().slice(0, 7)}`,
|
||||
@@ -103,7 +103,7 @@ export class SettlementScheduler {
|
||||
if (logistics.failed > 0) {
|
||||
this.alert.notify({
|
||||
level: 'P0',
|
||||
category: 'job',
|
||||
category: 'settlement',
|
||||
title: '物流月对账部分失败',
|
||||
detail: `total=${logistics.total} success=${logistics.success} failed=${logistics.failed}`,
|
||||
dedupeKey: `job_logistics_bill|${new Date().toISOString().slice(0, 7)}`,
|
||||
@@ -113,7 +113,7 @@ export class SettlementScheduler {
|
||||
this.logger.error('Logistics bill job failed', e instanceof Error ? e.stack : e);
|
||||
this.alert.notify({
|
||||
level: 'P0',
|
||||
category: 'job',
|
||||
category: 'settlement',
|
||||
title: '物流月对账任务失败',
|
||||
detail: e instanceof Error ? e.message : String(e),
|
||||
dedupeKey: `job_logistics_bill|${new Date().toISOString().slice(0, 7)}`,
|
||||
|
||||
@@ -20,6 +20,8 @@ import type {
|
||||
} from './dto/admin-mutate.dto';
|
||||
import { TestWhitelistService } from '../../common/test-whitelist/test-whitelist.service';
|
||||
import { HqPermissionsResolver, hqStoreCityWhere, mergeHqStoreCityWhere, type HqCityScope } from '../../common/guards/hq-permission.guard';
|
||||
import { WecomMessagePushService } from '../../integrations/wecom/wecom-message-push.service';
|
||||
import { formatHqWecomLabel, formatPartnerWecomLabel } from '../store/wecom-submitter-label';
|
||||
|
||||
/** 选填文案:空 / null / "null" 一律存库为 null,避免 String(null)==="null" */
|
||||
function normalizeStoreOptionalText(value: unknown): string | null {
|
||||
@@ -56,6 +58,7 @@ export class AdminStoresService {
|
||||
private readonly analyticsService: AnalyticsService,
|
||||
private readonly testWhitelist: TestWhitelistService,
|
||||
private readonly hqPermissions: HqPermissionsResolver,
|
||||
private readonly wecomPush: WecomMessagePushService,
|
||||
) {}
|
||||
|
||||
private async storeIdsInScope(scope: HqCityScope): Promise<bigint[] | null> {
|
||||
@@ -776,6 +779,23 @@ export class AdminStoresService {
|
||||
});
|
||||
}
|
||||
|
||||
const [submitter, partnerLabel] = await Promise.all([
|
||||
formatHqWecomLabel(this.prisma, actorId),
|
||||
formatPartnerWecomLabel(this.prisma, partnerAccountId),
|
||||
]);
|
||||
void this.wecomPush.dispatchEvent(
|
||||
'store.audit_pending',
|
||||
{
|
||||
storeName: store.name,
|
||||
cityName: store.cityName,
|
||||
submitter,
|
||||
partnerLabel,
|
||||
action: '新建',
|
||||
storeId: store.id.toString(),
|
||||
},
|
||||
{ handlePath: `/stores?storeId=${store.id.toString()}` },
|
||||
);
|
||||
|
||||
return this.detailStore(store.id, actorId);
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
calcRedeemSettleAmount,
|
||||
generateRedeemNo,
|
||||
generateRedeemPendingNo,
|
||||
maskContactPhone,
|
||||
REDEEM_WEAKNET_FAIL_THRESHOLD,
|
||||
resolveSettlementRate,
|
||||
validateRedeemAmount,
|
||||
@@ -194,7 +195,10 @@ export class RedeemService {
|
||||
const redeemChannel = analyticsExtra?.channel === 'phone' ? 'PHONE' : 'SCAN';
|
||||
|
||||
const [userRow, storeRow] = await Promise.all([
|
||||
this.prisma.user.findUnique({ where: { id: userId }, select: { isTest: true } }),
|
||||
this.prisma.user.findUnique({
|
||||
where: { id: userId },
|
||||
select: { isTest: true, nickname: true, phone: true },
|
||||
}),
|
||||
this.prisma.store.findUnique({
|
||||
where: { id: account.storeId },
|
||||
select: { isTest: true },
|
||||
@@ -290,6 +294,8 @@ export class RedeemService {
|
||||
amount: amountNum.toFixed(2),
|
||||
storeName: account.store.name || String(account.storeId),
|
||||
channel: channelLabel,
|
||||
userName: userRow?.nickname?.trim() || '—',
|
||||
userPhone: maskContactPhone(userRow?.phone),
|
||||
},
|
||||
{
|
||||
handlePath: `/redeem-records?redeemNo=${encodeURIComponent(record.redeemNo)}`,
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
loadStorePrimaryBank,
|
||||
loadStorePrimaryBanksMap,
|
||||
loadWineryBankConfig,
|
||||
formatWecomBankAccount,
|
||||
} from '../../common/store/store-bank.util';
|
||||
|
||||
function generateBillNo(prefix: string) {
|
||||
@@ -62,6 +63,14 @@ function round2(n: number) {
|
||||
return Math.round(n * 100) / 100;
|
||||
}
|
||||
|
||||
const BILL_DIGEST_MAX_LINES = 15;
|
||||
|
||||
function capBillSummary(lines: string[]): string {
|
||||
if (!lines.length) return '无';
|
||||
if (lines.length <= BILL_DIGEST_MAX_LINES) return lines.join('\n');
|
||||
return `${lines.slice(0, BILL_DIGEST_MAX_LINES).join('\n')}\n…等共 ${lines.length} 笔`;
|
||||
}
|
||||
|
||||
function getStoreWithdrawDailyLimit(): number {
|
||||
const raw = process.env.STORE_WITHDRAW_DAILY_LIMIT;
|
||||
const n = raw != null && raw !== '' ? Number(raw) : DEFAULT_STORE_WITHDRAW_DAILY_LIMIT;
|
||||
@@ -95,6 +104,59 @@ export class SettlementService {
|
||||
private readonly wecomPush: WecomMessagePushService,
|
||||
) {}
|
||||
|
||||
private notifyPartnerBillDigest(
|
||||
period: string,
|
||||
rows: Array<{
|
||||
label: string;
|
||||
amount: number;
|
||||
bankAccountName?: string | null;
|
||||
bankBranch?: string | null;
|
||||
bankAccountNo?: string | null;
|
||||
}>,
|
||||
) {
|
||||
if (!rows.length) return;
|
||||
const lines = rows.map(
|
||||
(r) =>
|
||||
`${r.label} ¥${Number(r.amount).toFixed(2)} ${formatWecomBankAccount(r)}`,
|
||||
);
|
||||
void this.wecomPush.dispatchEvent(
|
||||
'finance.partner_bill',
|
||||
{
|
||||
period,
|
||||
billCount: String(rows.length),
|
||||
billSummary: capBillSummary(lines),
|
||||
},
|
||||
{ handlePath: '/finance/partner-bills' },
|
||||
);
|
||||
}
|
||||
|
||||
private notifyLogisticsBillDigest(
|
||||
period: string,
|
||||
rows: Array<{
|
||||
label: string;
|
||||
amount: number;
|
||||
bankAccountName?: string | null;
|
||||
bankName?: string | null;
|
||||
bankBranch?: string | null;
|
||||
bankAccountNo?: string | null;
|
||||
}>,
|
||||
) {
|
||||
if (!rows.length) return;
|
||||
const lines = rows.map(
|
||||
(r) =>
|
||||
`${r.label} ¥${Number(r.amount).toFixed(2)} ${formatWecomBankAccount(r)}`,
|
||||
);
|
||||
void this.wecomPush.dispatchEvent(
|
||||
'finance.logistics_bill',
|
||||
{
|
||||
period,
|
||||
billCount: String(rows.length),
|
||||
billSummary: capBillSummary(lines),
|
||||
},
|
||||
{ handlePath: '/finance/logistics-bills' },
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Store payout (line) ─────────────────────────────
|
||||
|
||||
async createStorePayout(
|
||||
@@ -584,6 +646,27 @@ export class SettlementService {
|
||||
},
|
||||
});
|
||||
|
||||
const [store, bank] = await Promise.all([
|
||||
this.prisma.store.findUnique({
|
||||
where: { id: row.storeId },
|
||||
select: { name: true },
|
||||
}),
|
||||
loadStorePrimaryBank(this.prisma, row.storeId),
|
||||
]);
|
||||
void this.wecomPush.dispatchEvent(
|
||||
'store.withdraw_approved',
|
||||
{
|
||||
storeName: store?.name || String(row.storeId),
|
||||
withdrawNo: row.withdrawNo,
|
||||
amount: Number(row.amount).toFixed(2),
|
||||
bankAccount: formatWecomBankAccount(bank),
|
||||
storeId: row.storeId.toString(),
|
||||
},
|
||||
{
|
||||
handlePath: `/finance/store-bills?kind=WITHDRAW&status=PAID&storeId=${row.storeId.toString()}`,
|
||||
},
|
||||
);
|
||||
|
||||
return serializeBigInt(updated);
|
||||
}
|
||||
|
||||
@@ -651,7 +734,7 @@ export class SettlementService {
|
||||
);
|
||||
this.alert.notify({
|
||||
level: 'P1',
|
||||
category: 'finance',
|
||||
category: 'settlement',
|
||||
title: '门店提现超时未审',
|
||||
detail: [
|
||||
`待审 ${summary.pendingCount} 笔,超时 ${summary.overdueCount} 笔,超时金额 ¥${summary.overdueAmount.toFixed(2)}`,
|
||||
@@ -847,7 +930,7 @@ export class SettlementService {
|
||||
withdrawItem: { is: null },
|
||||
status: 'PENDING',
|
||||
},
|
||||
include: { store: { select: { id: true, settlementRate: true } } },
|
||||
include: { store: { select: { id: true, name: true, settlementRate: true } } },
|
||||
});
|
||||
|
||||
const byStore = new Map<string, typeof payouts>();
|
||||
@@ -860,8 +943,10 @@ export class SettlementService {
|
||||
|
||||
let created = 0;
|
||||
let skipped = 0;
|
||||
const createdBills: Array<{ storeId: bigint; storeName: string; amount: number }> = [];
|
||||
for (const [storeIdStr, list] of byStore) {
|
||||
const storeId = BigInt(storeIdStr);
|
||||
const storeName = list[0]?.store?.name || storeIdStr;
|
||||
const existing = await this.prisma.storeBill.findUnique({
|
||||
where: { storeId_billDate: { storeId, billDate } },
|
||||
});
|
||||
@@ -873,13 +958,14 @@ export class SettlementService {
|
||||
const redeemAmount = round2(list.reduce((s, p) => s + Number(p.redeemAmount), 0));
|
||||
const payoutAmount = round2(list.reduce((s, p) => s + Number(p.payoutAmount), 0));
|
||||
const rate = Number(list[0].settlementRate);
|
||||
const updatedTotal = round2(Number(existing.payoutAmount) + payoutAmount);
|
||||
await this.prisma.$transaction(async (tx) => {
|
||||
await tx.storeBill.update({
|
||||
where: { id: existing.id },
|
||||
data: {
|
||||
redeemCount: existing.redeemCount + list.length,
|
||||
redeemAmount: round2(Number(existing.redeemAmount) + redeemAmount),
|
||||
payoutAmount: round2(Number(existing.payoutAmount) + payoutAmount),
|
||||
payoutAmount: updatedTotal,
|
||||
settlementRate: rate,
|
||||
},
|
||||
});
|
||||
@@ -889,6 +975,7 @@ export class SettlementService {
|
||||
});
|
||||
});
|
||||
created += 1;
|
||||
createdBills.push({ storeId, storeName, amount: updatedTotal });
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -914,9 +1001,31 @@ export class SettlementService {
|
||||
});
|
||||
});
|
||||
created += 1;
|
||||
createdBills.push({ storeId, storeName, amount: payoutAmount });
|
||||
}
|
||||
|
||||
return { billDate: billDate.toISOString().slice(0, 10), created, skipped, payoutLinked: payouts.length };
|
||||
const period = billDate.toISOString().slice(0, 10);
|
||||
if (createdBills.length) {
|
||||
const banks = await loadStorePrimaryBanksMap(
|
||||
this.prisma,
|
||||
createdBills.map((b) => b.storeId),
|
||||
);
|
||||
const lines = createdBills.map(
|
||||
(b) =>
|
||||
`${b.storeName} ¥${b.amount.toFixed(2)} ${formatWecomBankAccount(banks.get(b.storeId.toString()) ?? null)}`,
|
||||
);
|
||||
void this.wecomPush.dispatchEvent(
|
||||
'finance.store_bill',
|
||||
{
|
||||
period,
|
||||
billCount: String(createdBills.length),
|
||||
billSummary: capBillSummary(lines),
|
||||
},
|
||||
{ handlePath: '/finance/store-bills?kind=T1_BILL' },
|
||||
);
|
||||
}
|
||||
|
||||
return { billDate: period, created, skipped, payoutLinked: payouts.length };
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1360,17 +1469,43 @@ export class SettlementService {
|
||||
async generateAllPartnerBills(body: { year: number; month: number }) {
|
||||
const partners = await this.prisma.partnerAccount.findMany({
|
||||
where: { isPrimary: 1, status: 'ACTIVE' },
|
||||
select: { id: true, companyName: true },
|
||||
select: {
|
||||
id: true,
|
||||
companyName: true,
|
||||
name: true,
|
||||
bankAccountName: true,
|
||||
bankBranch: true,
|
||||
bankAccountNo: true,
|
||||
},
|
||||
});
|
||||
const results: Array<{ partnerId: string; companyName: string; ok: boolean; message?: string }> = [];
|
||||
const created: Array<{
|
||||
label: string;
|
||||
amount: number;
|
||||
bankAccountName: string | null;
|
||||
bankBranch: string | null;
|
||||
bankAccountNo: string | null;
|
||||
}> = [];
|
||||
for (const p of partners) {
|
||||
try {
|
||||
await this.generatePartnerBill({
|
||||
partnerId: p.id.toString(),
|
||||
year: body.year,
|
||||
month: body.month,
|
||||
});
|
||||
const bill = await this.generatePartnerBill(
|
||||
{
|
||||
partnerId: p.id.toString(),
|
||||
year: body.year,
|
||||
month: body.month,
|
||||
},
|
||||
{ notify: false },
|
||||
);
|
||||
results.push({ partnerId: p.id.toString(), companyName: p.companyName ?? '', ok: true });
|
||||
const label =
|
||||
[p.companyName?.trim(), p.name?.trim()].filter(Boolean).join(' · ') || p.id.toString();
|
||||
created.push({
|
||||
label,
|
||||
amount: Number(bill.totalAmount),
|
||||
bankAccountName: p.bankAccountName,
|
||||
bankBranch: p.bankBranch,
|
||||
bankAccountNo: p.bankAccountNo,
|
||||
});
|
||||
} catch (e) {
|
||||
results.push({
|
||||
partnerId: p.id.toString(),
|
||||
@@ -1380,6 +1515,10 @@ export class SettlementService {
|
||||
});
|
||||
}
|
||||
}
|
||||
this.notifyPartnerBillDigest(
|
||||
`${body.year}-${String(body.month).padStart(2, '0')}`,
|
||||
created,
|
||||
);
|
||||
return {
|
||||
total: partners.length,
|
||||
success: results.filter((r) => r.ok).length,
|
||||
@@ -1403,7 +1542,10 @@ export class SettlementService {
|
||||
return serializeBigInt(bill);
|
||||
}
|
||||
|
||||
async generatePartnerBill(body: { partnerId: string; year: number; month: number }) {
|
||||
async generatePartnerBill(
|
||||
body: { partnerId: string; year: number; month: number },
|
||||
opts?: { notify?: boolean },
|
||||
) {
|
||||
const partnerAccountId = BigInt(body.partnerId);
|
||||
const primary = await this.partnerCityService.resolvePrimaryAccount(partnerAccountId);
|
||||
const periodStart = new Date(body.year, body.month - 1, 1);
|
||||
@@ -1485,6 +1627,24 @@ export class SettlementService {
|
||||
},
|
||||
});
|
||||
|
||||
if (opts?.notify !== false) {
|
||||
const label =
|
||||
[primary.companyName?.trim(), primary.name?.trim()].filter(Boolean).join(' · ') ||
|
||||
primary.id.toString();
|
||||
this.notifyPartnerBillDigest(
|
||||
`${body.year}-${String(body.month).padStart(2, '0')}`,
|
||||
[
|
||||
{
|
||||
label,
|
||||
amount: totalAmount,
|
||||
bankAccountName: primary.bankAccountName,
|
||||
bankBranch: primary.bankBranch,
|
||||
bankAccountNo: primary.bankAccountNo,
|
||||
},
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
return serializeBigInt(bill);
|
||||
}
|
||||
|
||||
@@ -1775,8 +1935,21 @@ export class SettlementService {
|
||||
return header;
|
||||
});
|
||||
|
||||
const period = billDate.toISOString().slice(0, 10);
|
||||
const wineryBank = await loadWineryBankConfig(this.prisma);
|
||||
void this.wecomPush.dispatchEvent(
|
||||
'finance.winery_bill',
|
||||
{
|
||||
period,
|
||||
amount: wineryAmount.toFixed(2),
|
||||
orderCount: String(orders.length),
|
||||
bankAccount: formatWecomBankAccount(wineryBank),
|
||||
},
|
||||
{ handlePath: '/finance/winery-bills' },
|
||||
);
|
||||
|
||||
return serializeBigInt({
|
||||
billDate: billDate.toISOString().slice(0, 10),
|
||||
billDate: period,
|
||||
skipped: false,
|
||||
bill,
|
||||
});
|
||||
@@ -1997,14 +2170,25 @@ export class SettlementService {
|
||||
message?: string;
|
||||
billId?: string;
|
||||
}> = [];
|
||||
const created: Array<{
|
||||
label: string;
|
||||
amount: number;
|
||||
bankAccountName: string | null;
|
||||
bankName: string | null;
|
||||
bankBranch: string | null;
|
||||
bankAccountNo: string | null;
|
||||
}> = [];
|
||||
|
||||
for (const p of providers) {
|
||||
try {
|
||||
const bill = await this.generateLogisticsBill({
|
||||
providerId: p.id.toString(),
|
||||
year: body.year,
|
||||
month: body.month,
|
||||
});
|
||||
const bill = await this.generateLogisticsBill(
|
||||
{
|
||||
providerId: p.id.toString(),
|
||||
year: body.year,
|
||||
month: body.month,
|
||||
},
|
||||
{ notify: false },
|
||||
);
|
||||
results.push({
|
||||
providerId: p.id.toString(),
|
||||
providerCode: p.code,
|
||||
@@ -2019,6 +2203,16 @@ export class SettlementService {
|
||||
: String(bill.bill.id)
|
||||
: undefined,
|
||||
});
|
||||
if (!bill.skipped && bill.bill) {
|
||||
created.push({
|
||||
label: p.name,
|
||||
amount: Number(bill.bill.logisticsAmount ?? 0),
|
||||
bankAccountName: p.bankAccountName,
|
||||
bankName: p.bankName,
|
||||
bankBranch: p.bankBranch,
|
||||
bankAccountNo: p.bankAccountNo,
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
results.push({
|
||||
providerId: p.id.toString(),
|
||||
@@ -2030,6 +2224,11 @@ export class SettlementService {
|
||||
}
|
||||
}
|
||||
|
||||
this.notifyLogisticsBillDigest(
|
||||
`${body.year}-${String(body.month).padStart(2, '0')}`,
|
||||
created,
|
||||
);
|
||||
|
||||
return {
|
||||
total: providers.length,
|
||||
success: results.filter((r) => r.ok).length,
|
||||
@@ -2038,7 +2237,10 @@ export class SettlementService {
|
||||
};
|
||||
}
|
||||
|
||||
async generateLogisticsBill(body: { providerId: string; year: number; month: number }) {
|
||||
async generateLogisticsBill(
|
||||
body: { providerId: string; year: number; month: number },
|
||||
opts?: { notify?: boolean },
|
||||
) {
|
||||
const fulfillmentProviderId = BigInt(body.providerId);
|
||||
const provider = await this.prisma.fulfillmentProvider.findUnique({
|
||||
where: { id: fulfillmentProviderId },
|
||||
@@ -2190,6 +2392,22 @@ export class SettlementService {
|
||||
return header;
|
||||
});
|
||||
|
||||
if (opts?.notify !== false) {
|
||||
this.notifyLogisticsBillDigest(
|
||||
`${body.year}-${String(body.month).padStart(2, '0')}`,
|
||||
[
|
||||
{
|
||||
label: provider.name,
|
||||
amount: Number(bill.logisticsAmount),
|
||||
bankAccountName: provider.bankAccountName,
|
||||
bankName: provider.bankName,
|
||||
bankBranch: provider.bankBranch,
|
||||
bankAccountNo: provider.bankAccountNo,
|
||||
},
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
skipped: false,
|
||||
bill: serializeBigInt(bill),
|
||||
|
||||
@@ -21,6 +21,7 @@ import { serializeBigInt } from '../../common/decorators/current-user.decorator'
|
||||
import { StoreService } from './store.service';
|
||||
import { PartnerCityService } from '../city-scope/partner-city.service';
|
||||
import { WecomMessagePushService } from '../../integrations/wecom/wecom-message-push.service';
|
||||
import { formatHqWecomLabel, formatPartnerWecomLabel, formatShopWecomLabel } from './wecom-submitter-label';
|
||||
import { HqPermissionsResolver, hqStoreCityWhere } from '../../common/guards/hq-permission.guard';
|
||||
|
||||
type ChangeableField = (typeof STORE_INFO_CHANGEABLE_FIELDS)[number];
|
||||
@@ -335,13 +336,18 @@ export class StoreInfoChangeService {
|
||||
`Store info change submitted storeId=${input.storeId} fields=${changedFields.join(',')}`,
|
||||
);
|
||||
|
||||
const submitterLabel = input.submitterType === 'PARTNER' ? '合伙人' : '门店';
|
||||
const submitter =
|
||||
input.submitterType === 'PARTNER'
|
||||
? await formatPartnerWecomLabel(this.prisma, input.submitterId)
|
||||
: input.submitterType === 'HQ_DIRECT_ADMIN'
|
||||
? await formatHqWecomLabel(this.prisma, input.submitterId)
|
||||
: await formatShopWecomLabel(this.prisma, input.submitterId);
|
||||
void this.wecomPush.dispatchEvent(
|
||||
'store.info_change_pending',
|
||||
{
|
||||
storeName: String(store.name ?? input.storeId),
|
||||
cityName: String(store.cityName ?? '—'),
|
||||
submitter: submitterLabel,
|
||||
submitter,
|
||||
changedFields: formatStoreInfoChangeFieldLabels(changedFields),
|
||||
requestId: created.id.toString(),
|
||||
},
|
||||
|
||||
@@ -15,6 +15,7 @@ import { PrismaService } from '../../common/prisma/prisma.module';
|
||||
import { serializeBigInt } from '../../common/decorators/current-user.decorator';
|
||||
import { StoreService } from './store.service';
|
||||
import { WecomMessagePushService } from '../../integrations/wecom/wecom-message-push.service';
|
||||
import { formatPartnerWecomLabel, formatShopWecomLabel } from './wecom-submitter-label';
|
||||
import { HqPermissionsResolver, hqStoreCityWhere } from '../../common/guards/hq-permission.guard';
|
||||
|
||||
type PackageInput = Record<string, unknown>;
|
||||
@@ -193,13 +194,16 @@ export class StorePackageService {
|
||||
where: { id: storeId },
|
||||
select: { name: true, cityName: true },
|
||||
});
|
||||
const submitterLabel = submitterType === 'PARTNER' ? '合伙人' : '门店';
|
||||
const submitter =
|
||||
submitterType === 'PARTNER'
|
||||
? await formatPartnerWecomLabel(this.prisma, submitterId)
|
||||
: await formatShopWecomLabel(this.prisma, submitterId);
|
||||
void this.wecomPush.dispatchEvent(
|
||||
'store.package_audit_pending',
|
||||
{
|
||||
storeName: store?.name ?? String(storeId),
|
||||
cityName: store?.cityName || '—',
|
||||
submitter: submitterLabel,
|
||||
submitter,
|
||||
packageCount: String(packages.length),
|
||||
requestId: req.id.toString(),
|
||||
},
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
normalizeTestPhone,
|
||||
} from '../../common/test-whitelist/test-whitelist.service';
|
||||
import { WecomMessagePushService } from '../../integrations/wecom/wecom-message-push.service';
|
||||
import { formatPartnerWecomLabel } from './wecom-submitter-label';
|
||||
|
||||
function haversineMeters(lat1: number, lng1: number, lat2: number, lng2: number): number {
|
||||
const toRad = (d: number) => (d * Math.PI) / 180;
|
||||
@@ -76,25 +77,29 @@ export class StoreService {
|
||||
private readonly wecomPush: WecomMessagePushService,
|
||||
) {}
|
||||
|
||||
/** 门店进入 PENDING 时通知企微(失败不挡业务) */
|
||||
/** 门店进入 PENDING 或 HQ 新建时通知企微(失败不挡业务) */
|
||||
private notifyStoreAuditPending(opts: {
|
||||
storeId: bigint;
|
||||
storeName: string;
|
||||
cityName?: string | null;
|
||||
partnerLabel?: string | null;
|
||||
submitter?: string | null;
|
||||
submitType: '新建' | '重提';
|
||||
handlePath?: string;
|
||||
}) {
|
||||
void this.wecomPush.dispatchEvent(
|
||||
'store.audit_pending',
|
||||
{
|
||||
storeName: opts.storeName,
|
||||
cityName: opts.cityName || '—',
|
||||
submitter: opts.submitter || opts.partnerLabel || '—',
|
||||
partnerLabel: opts.partnerLabel || '—',
|
||||
action: opts.submitType,
|
||||
storeId: opts.storeId.toString(),
|
||||
},
|
||||
{
|
||||
handlePath: `/stores?auditStatus=PENDING&storeId=${opts.storeId.toString()}`,
|
||||
handlePath:
|
||||
opts.handlePath || `/stores?auditStatus=PENDING&storeId=${opts.storeId.toString()}`,
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -587,15 +592,16 @@ export class StoreService {
|
||||
});
|
||||
|
||||
if (!this.config.autoApproveStore) {
|
||||
const partner = await this.prisma.partnerAccount.findUnique({
|
||||
where: { id: primaryId },
|
||||
select: { name: true, phone: true },
|
||||
});
|
||||
const [submitter, partnerLabel] = await Promise.all([
|
||||
formatPartnerWecomLabel(this.prisma, partnerAccountId),
|
||||
formatPartnerWecomLabel(this.prisma, primaryId),
|
||||
]);
|
||||
this.notifyStoreAuditPending({
|
||||
storeId: store.id,
|
||||
storeName: store.name,
|
||||
cityName: store.cityName,
|
||||
partnerLabel: partner?.name || partner?.phone || String(primaryId),
|
||||
submitter,
|
||||
partnerLabel,
|
||||
submitType: '新建',
|
||||
});
|
||||
}
|
||||
@@ -757,15 +763,16 @@ export class StoreService {
|
||||
remark: '合伙人修改资料后重新提交审核',
|
||||
},
|
||||
});
|
||||
const partner = await this.prisma.partnerAccount.findUnique({
|
||||
where: { id: primaryId },
|
||||
select: { name: true, phone: true },
|
||||
});
|
||||
const [submitter, partnerLabel] = await Promise.all([
|
||||
formatPartnerWecomLabel(this.prisma, partnerAccountId),
|
||||
formatPartnerWecomLabel(this.prisma, primaryId),
|
||||
]);
|
||||
this.notifyStoreAuditPending({
|
||||
storeId: updated.id,
|
||||
storeName: updated.name,
|
||||
cityName: updated.cityName,
|
||||
partnerLabel: partner?.name || partner?.phone || String(primaryId),
|
||||
submitter,
|
||||
partnerLabel,
|
||||
submitType: '重提',
|
||||
});
|
||||
}
|
||||
@@ -882,15 +889,16 @@ export class StoreService {
|
||||
remark: '合伙人重新上传资料后重新提交审核',
|
||||
},
|
||||
});
|
||||
const partner = await this.prisma.partnerAccount.findUnique({
|
||||
where: { id: primaryId },
|
||||
select: { name: true, phone: true },
|
||||
});
|
||||
const [submitter, partnerLabel] = await Promise.all([
|
||||
formatPartnerWecomLabel(this.prisma, partnerAccountId),
|
||||
formatPartnerWecomLabel(this.prisma, primaryId),
|
||||
]);
|
||||
this.notifyStoreAuditPending({
|
||||
storeId: store.id,
|
||||
storeName: store.name,
|
||||
cityName: store.cityName,
|
||||
partnerLabel: partner?.name || partner?.phone || String(primaryId),
|
||||
submitter,
|
||||
partnerLabel,
|
||||
submitType: '重提',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
import type { PrismaClient } from '@prisma/client';
|
||||
|
||||
export function joinCompanyAndName(
|
||||
company?: string | null,
|
||||
name?: string | null,
|
||||
fallback = '—',
|
||||
): string {
|
||||
const person = (name || '').trim() || fallback;
|
||||
const org = (company || '').trim();
|
||||
return org ? `${org} · ${person}` : person;
|
||||
}
|
||||
|
||||
export async function formatPartnerWecomLabel(
|
||||
prisma: PrismaClient,
|
||||
accountId: bigint,
|
||||
): Promise<string> {
|
||||
const acc = await prisma.partnerAccount.findUnique({
|
||||
where: { id: accountId },
|
||||
select: {
|
||||
name: true,
|
||||
phone: true,
|
||||
companyName: true,
|
||||
isPrimary: true,
|
||||
parentAccountId: true,
|
||||
},
|
||||
});
|
||||
if (!acc) return '—';
|
||||
let company = acc.companyName?.trim() || '';
|
||||
if (!company && acc.isPrimary !== 1 && acc.parentAccountId) {
|
||||
const primary = await prisma.partnerAccount.findUnique({
|
||||
where: { id: acc.parentAccountId },
|
||||
select: { companyName: true },
|
||||
});
|
||||
company = primary?.companyName?.trim() || '';
|
||||
}
|
||||
const name = acc.name?.trim() || acc.phone || String(accountId);
|
||||
return joinCompanyAndName(company, name, String(accountId));
|
||||
}
|
||||
|
||||
export async function formatHqWecomLabel(prisma: PrismaClient, hqAccountId: bigint): Promise<string> {
|
||||
const hq = await prisma.hqAccount.findUnique({
|
||||
where: { id: hqAccountId },
|
||||
select: { name: true },
|
||||
});
|
||||
return hq?.name?.trim() || 'HQ';
|
||||
}
|
||||
|
||||
export async function formatShopWecomLabel(
|
||||
prisma: PrismaClient,
|
||||
storeAccountId: bigint,
|
||||
): Promise<string> {
|
||||
const acc = await prisma.storeAccount.findUnique({
|
||||
where: { id: storeAccountId },
|
||||
select: { name: true, phone: true },
|
||||
});
|
||||
const name = acc?.name?.trim() || acc?.phone || String(storeAccountId);
|
||||
return `门店 · ${name}`;
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import type { FreightPayType } from '@prisma/client';
|
||||
import {
|
||||
calcBenefitAmount,
|
||||
generateOrderNo,
|
||||
maskContactPhone,
|
||||
minBottleQtyForDelivery,
|
||||
orderTabToStatuses,
|
||||
toMinSaleQuantity,
|
||||
@@ -501,10 +502,22 @@ export class TradeService {
|
||||
if (!order) return;
|
||||
|
||||
if (!order.isTest) {
|
||||
const skuSummary = `${order.productName}×${order.quantity}`;
|
||||
const phone = order.user?.phone || '';
|
||||
const phoneMasked =
|
||||
phone.length >= 7 ? `${phone.slice(0, 3)}****${phone.slice(-4)}` : phone || '—';
|
||||
const unit = order.saleUnit === 'BOX' ? '箱' : '瓶';
|
||||
const spec = (order.productSpec || '').trim();
|
||||
const skuSummary = spec
|
||||
? `${order.productName} ${spec} ${unit}×${order.quantity}`
|
||||
: `${order.productName} ${unit}×${order.quantity}`;
|
||||
const deliveryLabel =
|
||||
order.deliveryType === 'ON_SITE_PICKUP'
|
||||
? '现场提货'
|
||||
: order.deliveryType === 'CROSS_CITY'
|
||||
? '跨城'
|
||||
: '同城';
|
||||
const receiverInfo =
|
||||
order.deliveryType === 'ON_SITE_PICKUP'
|
||||
? `${order.receiverName} ${order.receiverPhone}`.trim()
|
||||
: `${order.receiverName} ${order.receiverPhone} ${order.receiverAddress}`.trim();
|
||||
const phoneMasked = maskContactPhone(order.user?.phone || order.receiverPhone);
|
||||
void this.wecomPush.dispatchEvent(
|
||||
'order.paid',
|
||||
{
|
||||
@@ -512,6 +525,8 @@ export class TradeService {
|
||||
payAmount: Number(order.payAmount).toFixed(2),
|
||||
cityName: order.city?.name || '—',
|
||||
skuSummary,
|
||||
deliveryLabel,
|
||||
receiverInfo,
|
||||
phoneMasked,
|
||||
},
|
||||
{ handlePath: `/orders?orderNo=${encodeURIComponent(order.orderNo)}` },
|
||||
|
||||
Reference in New Issue
Block a user