v3.5.3版本更新2
CI / verify (pull_request) Has been cancelled

This commit is contained in:
2026-08-20 20:09:05 +08:00
parent fc2e5b65de
commit 26334ed072
26 changed files with 1238 additions and 117 deletions
@@ -19,11 +19,13 @@ export class AdminInvoicesController {
@Get()
list(
@Query('status') status?: string,
@Query('invoiceNo') invoiceNo?: string,
@Query('page') page = '1',
@Query('pageSize') pageSize = '20',
) {
return this.tradeService.adminListInvoices({
status,
invoiceNo,
page: Number(page),
pageSize: Number(pageSize),
});
@@ -0,0 +1,69 @@
import {
BadRequestException,
Body,
Controller,
Get,
Param,
Post,
Put,
UseGuards,
} from '@nestjs/common';
import type { UpdateWecomPushTemplateRequest } from '@dukang/shared-types';
import { HqAuthGuard } from '../../common/guards/hq-auth.guard';
import {
HqPermissionGuard,
RequireHqPermissions,
} from '../../common/guards/hq-permission.guard';
import { HqOperation } from '../../common/hq-operation/hq-operation.decorator';
import { HqOperationAction } from '../../common/hq-operation/hq-operation.constants';
import { WecomMessagePushService } from '../../integrations/wecom/wecom-message-push.service';
@Controller('admin/wecom-push-templates')
@UseGuards(HqAuthGuard, HqPermissionGuard)
@RequireHqPermissions('wecom_bots')
export class AdminWecomPushTemplatesController {
constructor(private readonly wecomPush: WecomMessagePushService) {}
@Get()
list() {
return this.wecomPush.listTemplates();
}
@Get(':eventKey')
detail(@Param('eventKey') eventKey: string) {
return this.wecomPush.getTemplate(eventKey);
}
@Put(':eventKey')
@HqOperation({
action: HqOperationAction.WECOM_MESSAGE_PUSH_UPDATE,
refType: 'WECOM_PUSH_TEMPLATE',
refIdField: 'eventKey',
includeBody: true,
})
update(@Param('eventKey') eventKey: string, @Body() body: UpdateWecomPushTemplateRequest) {
return this.wecomPush.updateTemplate(eventKey, body);
}
@Post(':eventKey/reset')
@HqOperation({
action: HqOperationAction.WECOM_MESSAGE_PUSH_UPDATE,
refType: 'WECOM_PUSH_TEMPLATE',
refIdField: 'eventKey',
})
reset(@Param('eventKey') eventKey: string) {
return this.wecomPush.resetTemplate(eventKey);
}
@Post(':eventKey/test')
@HqOperation({
action: HqOperationAction.WECOM_MESSAGE_PUSH_TEST,
refType: 'WECOM_PUSH_TEMPLATE',
refIdField: 'eventKey',
})
async test(@Param('eventKey') eventKey: string) {
const result = await this.wecomPush.testTemplate(eventKey);
if (!result.ok) throw new BadRequestException(result.message);
return result;
}
}
@@ -69,6 +69,7 @@ import { AdminWecomBotsController } from './admin-wecom-bots.controller';
import { AdminWecomBotsService } from './admin-wecom-bots.service';
import { AdminWecomMessagePushesController } from './admin-wecom-message-pushes.controller';
import { AdminWecomMessagePushesService } from './admin-wecom-message-pushes.service';
import { AdminWecomPushTemplatesController } from './admin-wecom-push-templates.controller';
import { AdminWecomBotLogsController } from './admin-wecom-bot-logs.controller';
import { AdminWecomBotLogsService } from './admin-wecom-bot-logs.service';
import { AdminLlmConfigsController } from './admin-llm-configs.controller';
@@ -124,6 +125,7 @@ import { AdminTestWhitelistController } from './admin-test-whitelist.controller'
AdminSystemConfigController,
AdminWecomBotsController,
AdminWecomMessagePushesController,
AdminWecomPushTemplatesController,
AdminWecomBotLogsController,
AdminLlmConfigsController,
AdminKnowledgeBasesController,
@@ -30,6 +30,7 @@ import { SettlementService } from '../settlement/settlement.service';
import { BenefitService } from '../benefit/benefit.service';
import { AuthService } from '../iam/auth.service';
import { PayRedeemAnomalyService } from '../../common/alert/pay-redeem-anomaly.service';
import { WecomMessagePushService } from '../../integrations/wecom/wecom-message-push.service';
type TokenPayload = {
userId: string;
@@ -82,6 +83,7 @@ export class RedeemService {
private readonly analyticsService: AnalyticsService,
private readonly authService: AuthService,
private readonly payRedeemAnomaly: PayRedeemAnomalyService,
private readonly wecomPush: WecomMessagePushService,
) {}
private maskPhoneForStore(phone: string) {
@@ -276,6 +278,22 @@ export class RedeemService {
extraJson: redeemExtra,
});
if (!isTest) {
const channelLabel = redeemChannel === 'PHONE' ? '手机号' : '扫码';
void this.wecomPush.dispatchEvent(
'redeem.success',
{
redeemNo: record.redeemNo,
amount: amountNum.toFixed(2),
storeName: account.store.name || String(account.storeId),
channel: channelLabel,
},
{
handlePath: `/redeem-records?redeemNo=${encodeURIComponent(record.redeemNo)}`,
},
);
}
return {
...record,
amount: amountNum,
@@ -18,6 +18,7 @@ import {
import { PrismaService } from '../../common/prisma/prisma.module';
import { serializeBigInt } from '../../common/decorators/current-user.decorator';
import { AlertService } from '../../common/alert/alert.service';
import { WecomMessagePushService } from '../../integrations/wecom/wecom-message-push.service';
import { AnalyticsService } from '../analytics/analytics.service';
import { PartnerCityService } from '../city-scope/partner-city.service';
import { FulfillmentProviderService } from '../fulfillment/fulfillment-provider.service';
@@ -86,6 +87,7 @@ export class SettlementService {
private readonly partnerCityService: PartnerCityService,
private readonly fulfillmentProviderService: FulfillmentProviderService,
private readonly alert: AlertService,
private readonly wecomPush: WecomMessagePushService,
) {}
// ─── Store payout (line) ─────────────────────────────
@@ -386,20 +388,19 @@ export class SettlementService {
},
});
this.alert.notify({
level: 'P1',
category: 'finance',
title: '门店提现待审',
detail: [
`门店:${store.name}${store.cityName || '-'} / ${store.phone || '-'}`,
`单号:${created.withdrawNo}`,
`金额:¥${Number(created.amount).toFixed(2)}`,
`明细:${created.payoutCount} 笔未出账核销(已锁定,不进入次日 T+1 出账)`,
'请尽快在 HQ「财务 → 门店账单(手动提现)」处理。',
].join('\n'),
dedupeKey: `store_withdraw_applied|${created.id.toString()}`,
dedupeTtlSec: 3600,
});
void this.wecomPush.dispatchEvent(
'store.withdraw_pending',
{
storeName: store.name,
withdrawNo: created.withdrawNo,
amount: Number(created.amount).toFixed(2),
payoutCount: String(created.payoutCount),
storeId: storeId.toString(),
},
{
handlePath: `/finance/store-bills?kind=WITHDRAW&status=PENDING_REVIEW&storeId=${storeId.toString()}`,
},
);
return serializeBigInt(created);
}
@@ -10,6 +10,7 @@ import {
} from '@dukang/domain';
import {
STORE_INFO_CHANGEABLE_FIELDS,
formatStoreInfoChangeFieldLabels,
type StoreInfoChangeFieldDiff,
type StoreInfoChangeRequestDto,
type StoreInfoChangeStatus,
@@ -19,6 +20,7 @@ import { PrismaService } from '../../common/prisma/prisma.module';
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';
type ChangeableField = (typeof STORE_INFO_CHANGEABLE_FIELDS)[number];
@@ -142,6 +144,7 @@ export class StoreInfoChangeService {
private readonly prisma: PrismaService,
private readonly storeService: StoreService,
private readonly partnerCityService: PartnerCityService,
private readonly wecomPush: WecomMessagePushService,
) {}
private async loadLiveMediaFields(storeId: bigint, coverResourceId: bigint | null) {
@@ -329,6 +332,22 @@ export class StoreInfoChangeService {
this.logger.log(
`Store info change submitted storeId=${input.storeId} fields=${changedFields.join(',')}`,
);
const submitterLabel = input.submitterType === 'PARTNER' ? '合伙人' : '门店';
void this.wecomPush.dispatchEvent(
'store.info_change_pending',
{
storeName: String(store.name ?? input.storeId),
cityName: String(store.cityName ?? '—'),
submitter: submitterLabel,
changedFields: formatStoreInfoChangeFieldLabels(changedFields),
requestId: created.id.toString(),
},
{
handlePath: `/store-package-audits?tab=info&infoRequestId=${created.id.toString()}`,
},
);
return serializeBigInt(this.toDto(created as unknown as Record<string, unknown>));
}
@@ -191,28 +191,18 @@ export class StorePackageService {
where: { id: storeId },
select: { name: true, cityName: true },
});
const now = new Date().toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' });
const submitterLabel = submitterType === 'PARTNER' ? '合伙人' : '门店';
void this.wecomPush
.dispatchMarkdown(
'store.package_audit_pending',
[
'**套餐变更待审核**',
`门店:${store?.name ?? storeId}`,
store?.cityName ? `城市:${store.cityName}` : null,
`提交端:${submitterLabel}`,
`套餐条数:${packages.length}`,
`时间:${now}`,
]
.filter(Boolean)
.join('\n'),
{ applyMention: false },
)
.catch((e) =>
this.logger.warn(
`store.package_audit_pending wecom push failed: ${e instanceof Error ? e.message : String(e)}`,
),
);
void this.wecomPush.dispatchEvent(
'store.package_audit_pending',
{
storeName: store?.name ?? String(storeId),
cityName: store?.cityName || '—',
submitter: submitterLabel,
packageCount: String(packages.length),
requestId: req.id.toString(),
},
{ handlePath: `/store-package-audits?requestId=${req.id.toString()}` },
);
return serializeBigInt({
id: req.id.toString(),
@@ -78,26 +78,25 @@ export class StoreService {
/** 门店进入 PENDING 时通知企微(失败不挡业务) */
private notifyStoreAuditPending(opts: {
storeId: bigint;
storeName: string;
cityName?: string | null;
partnerLabel?: string | null;
submitType: '新建' | '重提';
}) {
const now = new Date().toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' });
const lines = [
`**门店审核待处理 · ${opts.submitType}**`,
`门店:${opts.storeName}`,
opts.cityName ? `城市:${opts.cityName}` : null,
opts.partnerLabel ? `合伙人:${opts.partnerLabel}` : null,
`时间:${now}`,
].filter(Boolean);
void this.wecomPush
.dispatchMarkdown('store.audit_pending', lines.join('\n'), { applyMention: false })
.catch((e) =>
this.logger.warn(
`store.audit_pending wecom push failed: ${e instanceof Error ? e.message : String(e)}`,
),
);
void this.wecomPush.dispatchEvent(
'store.audit_pending',
{
storeName: opts.storeName,
cityName: opts.cityName || '—',
partnerLabel: opts.partnerLabel || '—',
action: opts.submitType,
storeId: opts.storeId.toString(),
},
{
handlePath: `/stores?auditStatus=PENDING&storeId=${opts.storeId.toString()}`,
},
);
}
private async whitelistPhoneSet(): Promise<Set<string>> {
@@ -593,6 +592,7 @@ export class StoreService {
select: { name: true, phone: true },
});
this.notifyStoreAuditPending({
storeId: store.id,
storeName: store.name,
cityName: store.cityName,
partnerLabel: partner?.name || partner?.phone || String(primaryId),
@@ -762,6 +762,7 @@ export class StoreService {
select: { name: true, phone: true },
});
this.notifyStoreAuditPending({
storeId: updated.id,
storeName: updated.name,
cityName: updated.cityName,
partnerLabel: partner?.name || partner?.phone || String(primaryId),
@@ -886,6 +887,7 @@ export class StoreService {
select: { name: true, phone: true },
});
this.notifyStoreAuditPending({
storeId: store.id,
storeName: store.name,
cityName: store.cityName,
partnerLabel: partner?.name || partner?.phone || String(primaryId),
@@ -35,6 +35,7 @@ import { FulfillmentService } from '../fulfillment/fulfillment.service';
import { WechatOrderShippingService } from '../../integrations/wechat/wechat-order-shipping.service';
import { AlertService } from '../../common/alert/alert.service';
import { PayRedeemAnomalyService } from '../../common/alert/pay-redeem-anomaly.service';
import { WecomMessagePushService } from '../../integrations/wecom/wecom-message-push.service';
import type { Request } from 'express';
@Injectable()
@@ -56,6 +57,7 @@ export class TradeService {
private readonly wechatOrderShipping: WechatOrderShippingService,
private readonly payRedeemAnomaly: PayRedeemAnomalyService,
private readonly alert: AlertService,
private readonly wecomPush: WecomMessagePushService,
) {}
private readonly logger = new Logger(TradeService.name);
@@ -429,9 +431,33 @@ export class TradeService {
private async afterOrderPaid(orderId: bigint) {
await this.benefitService.grantOnOrderPaid(orderId);
const order = await this.prisma.order.findUnique({ where: { id: orderId } });
const order = await this.prisma.order.findUnique({
where: { id: orderId },
include: {
city: { select: { name: true } },
user: { select: { phone: true } },
},
});
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 || '—';
void this.wecomPush.dispatchEvent(
'order.paid',
{
orderNo: order.orderNo,
payAmount: Number(order.payAmount).toFixed(2),
cityName: order.city?.name || '—',
skuSummary,
phoneMasked,
},
{ handlePath: `/orders?orderNo=${encodeURIComponent(order.orderNo)}` },
);
}
const delivery = await this.prisma.orderDelivery.findUnique({ where: { orderId } });
if (!delivery) {
await this.prisma.orderDelivery.create({
@@ -1090,6 +1116,26 @@ export class TradeService {
remark: body.remark?.trim() || null,
},
});
if (!order.isTest) {
const phone = resolved.phone.trim();
const phoneMasked =
phone.length >= 7 ? `${phone.slice(0, 3)}****${phone.slice(-4)}` : phone || '—';
void this.wecomPush.dispatchEvent(
'invoice.pending',
{
invoiceNo: invoice.invoiceNo,
orderNo: order.orderNo,
payAmount: Number(order.payAmount).toFixed(2),
titleName: invoice.titleName,
phoneMasked,
},
{
handlePath: `/invoices?status=PENDING&invoiceNo=${encodeURIComponent(invoice.invoiceNo)}`,
},
);
}
return serializeBigInt({ ...invoice, orderNo: order.orderNo });
}
@@ -1177,11 +1223,17 @@ export class TradeService {
return this.createInvoice(order.userId, order.id, body);
}
async adminListInvoices(query: { status?: string; page?: number; pageSize?: number }) {
async adminListInvoices(query: {
status?: string;
invoiceNo?: string;
page?: number;
pageSize?: number;
}) {
const page = query.page ?? 1;
const pageSize = query.pageSize ?? 20;
const where: { status?: never } = {};
const where: { status?: never; invoiceNo?: { contains: string } } = {};
if (query.status) where.status = query.status as never;
if (query.invoiceNo?.trim()) where.invoiceNo = { contains: query.invoiceNo.trim() };
const [items, total] = await Promise.all([
this.prisma.userInvoice.findMany({
where,