@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user