Files
dukang/server/dukang-api/src/integrations/wecom/wecom-plugin-query.service.ts
T

1056 lines
34 KiB
TypeScript

import { BadRequestException, ForbiddenException, Injectable, NotFoundException } from '@nestjs/common';
import {
clampWecomPluginPage,
clampWecomPluginPageSize,
maskContactPhone,
MOBILE_PHONE_RE,
parseWecomPluginMetricsKind,
parseWecomPluginDateRange,
toWecomPluginMetricsView,
toWecomPluginUserView,
wecomPluginMetricsPeriod,
type WecomReportStats,
} from '@dukang/domain';
import {
WECOM_PLUGIN_TOOL_PATHS,
wecomPluginHasPermission,
STORE_INFO_CHANGEABLE_FIELD_LABELS,
type StoreInfoChangeableField,
type WecomPluginPermission,
} from '@dukang/shared-types';
import { Prisma } from '@prisma/client';
import { PrismaService } from '../../common/prisma/prisma.module';
import { PromoCodeService } from '../../modules/promo/promo-code.service';
import { WecomBotAuditService } from './wecom-bot-audit.service';
import { wecomPluginAuditBot, type WecomPluginRuntime } from './wecom-plugin.types';
import { filterWecomPluginOpenApi, WECOM_PLUGIN_OPENAPI } from './wecom-plugin.openapi';
function asNumber(v: Prisma.Decimal | number | null | undefined): number {
if (v == null) return 0;
if (typeof v === 'number') return Number.isFinite(v) ? v : 0;
return Number(v);
}
function requireQuery(q?: string): string {
const v = String(q ?? '').trim();
if (!v) throw new BadRequestException('请提供查询关键词 q');
return v;
}
@Injectable()
export class WecomPluginQueryService {
constructor(
private readonly prisma: PrismaService,
private readonly promo: PromoCodeService,
private readonly audit: WecomBotAuditService,
) {}
info(plugin: WecomPluginRuntime) {
return {
name: plugin.name,
description: '企微智能机器人只读 API 插件。查询订单、用户、门店经营、核销、推广码、经营指标、审核对照与合伙人关联数据。',
auth: { header: 'X-Api-Key' },
permissions: plugin.permissions,
tools: plugin.permissions.flatMap((p) => WECOM_PLUGIN_TOOL_PATHS[p]),
};
}
openapi(plugin: WecomPluginRuntime) {
return filterWecomPluginOpenApi(WECOM_PLUGIN_OPENAPI, plugin.permissions, plugin.name);
}
private requirePerm(plugin: WecomPluginRuntime, permission: WecomPluginPermission) {
if (!wecomPluginHasPermission(plugin.permissions, permission)) {
throw new ForbiddenException(`当前插件无权限:${permission}`);
}
}
queryOrders(
plugin: WecomPluginRuntime,
wecomUserId: string,
q?: string,
page?: string,
pageSize?: string,
) {
this.requirePerm(plugin, 'order.read');
const keyword = requireQuery(q);
const take = clampWecomPluginPageSize(pageSize);
const skip = (clampWecomPluginPage(page) - 1) * take;
return this.audit.run(
{
bot: wecomPluginAuditBot(plugin),
wecomUserId,
action: 'plugin.order.read',
permission: 'order.read',
inputSummary: keyword,
},
async () => {
const [items, total] = await Promise.all([
this.prisma.order.findMany({
where: { orderNo: { contains: keyword } },
orderBy: { createdAt: 'desc' },
skip,
take,
include: {
user: { select: { userNo: true, nickname: true, phone: true } },
delivery: { select: { trackingNo: true, provider: true } },
},
}),
this.prisma.order.count({ where: { orderNo: { contains: keyword } } }),
]);
return {
total,
items: items.map((o) => ({
orderNo: o.orderNo,
status: o.status,
payStatus: o.payStatus,
deliveryType: o.deliveryType,
productName: o.productName,
quantity: o.quantity,
payAmount: asNumber(o.payAmount),
user: toWecomPluginUserView({
userNo: o.user?.userNo || '—',
nickname: o.user?.nickname,
phone: o.user?.phone,
}),
receiverName: o.receiverName,
receiverPhone: maskContactPhone(o.receiverPhone),
receiverCity: o.receiverCity,
trackingNo: o.delivery?.trackingNo || null,
createdAt: o.createdAt.toISOString(),
})),
};
},
);
}
queryUsers(
plugin: WecomPluginRuntime,
wecomUserId: string,
q?: string,
page?: string,
pageSize?: string,
) {
this.requirePerm(plugin, 'user.read');
const keyword = requireQuery(q);
const take = clampWecomPluginPageSize(pageSize);
const skip = (clampWecomPluginPage(page) - 1) * take;
return this.audit.run(
{
bot: wecomPluginAuditBot(plugin),
wecomUserId,
action: 'plugin.user.read',
permission: 'user.read',
inputSummary: MOBILE_PHONE_RE.test(keyword) ? maskContactPhone(keyword) : keyword,
},
async () => {
const where = MOBILE_PHONE_RE.test(keyword)
? { phone: keyword, mergedIntoUserId: null }
: { userNo: { contains: keyword }, mergedIntoUserId: null };
const [rows, total] = await Promise.all([
this.prisma.user.findMany({
where,
orderBy: { createdAt: 'desc' },
skip,
take,
select: {
id: true,
userNo: true,
nickname: true,
phone: true,
status: true,
createdAt: true,
_count: { select: { orders: true } },
},
}),
this.prisma.user.count({ where }),
]);
const balances = await Promise.all(
rows.map((u) =>
this.prisma.benefitCoupon.aggregate({
where: { userId: u.id, status: 'ACTIVE' },
_sum: { balance: true },
}),
),
);
return {
total,
items: rows.map((u, i) => ({
...toWecomPluginUserView(u),
status: u.status,
orderCount: u._count.orders,
benefitBalance: asNumber(balances[i]?._sum.balance),
createdAt: u.createdAt.toISOString(),
})),
};
},
);
}
queryStores(
plugin: WecomPluginRuntime,
wecomUserId: string,
q?: string,
page?: string,
pageSize?: string,
) {
this.requirePerm(plugin, 'store.read');
const keyword = requireQuery(q);
const take = clampWecomPluginPageSize(pageSize);
const skip = (clampWecomPluginPage(page) - 1) * take;
return this.audit.run(
{
bot: wecomPluginAuditBot(plugin),
wecomUserId,
action: 'plugin.store.read',
permission: 'store.read',
inputSummary: keyword,
},
async () => {
const where = { name: { contains: keyword } };
const [rows, total] = await Promise.all([
this.prisma.store.findMany({
where,
orderBy: { updatedAt: 'desc' },
skip,
take,
select: {
id: true,
name: true,
status: true,
auditStatus: true,
cityName: true,
district: true,
address: true,
contactPhone: true,
phone: true,
rating: true,
createdAt: true,
partnerAccount: { select: { id: true, name: true, companyName: true } },
_count: { select: { redeemRecords: true } },
},
}),
this.prisma.store.count({ where }),
]);
const redeemSums =
rows.length > 0
? await this.prisma.redeemRecord.groupBy({
by: ['storeId'],
where: { storeId: { in: rows.map((s) => s.id) } },
_sum: { amount: true },
})
: [];
const redeemedByStore = new Map(
redeemSums.map((r) => [r.storeId.toString(), asNumber(r._sum.amount)]),
);
return {
total,
items: rows.map((s) => ({
id: s.id.toString(),
name: s.name,
status: s.status,
auditStatus: s.auditStatus,
cityName: s.cityName,
district: s.district,
address: s.address,
contactPhone: maskContactPhone(s.contactPhone || s.phone),
rating: s.rating != null ? asNumber(s.rating) : null,
redeemCount: s._count.redeemRecords,
totalRedeemedBenefitAmount: redeemedByStore.get(s.id.toString()) ?? 0,
partnerName: s.partnerAccount.companyName || s.partnerAccount.name,
partnerId: s.partnerAccount.id.toString(),
createdAt: s.createdAt.toISOString(),
})),
};
},
);
}
queryRedeems(
plugin: WecomPluginRuntime,
wecomUserId: string,
q?: string,
page?: string,
pageSize?: string,
) {
this.requirePerm(plugin, 'redeem.read');
const keyword = requireQuery(q);
const take = clampWecomPluginPageSize(pageSize);
const skip = (clampWecomPluginPage(page) - 1) * take;
return this.audit.run(
{
bot: wecomPluginAuditBot(plugin),
wecomUserId,
action: 'plugin.redeem.read',
permission: 'redeem.read',
inputSummary: keyword,
},
async () => {
const byNo = await this.prisma.redeemRecord.findMany({
where: { redeemNo: { contains: keyword } },
orderBy: { createdAt: 'desc' },
skip,
take,
include: { store: { select: { name: true } } },
});
const rows =
byNo.length > 0
? byNo
: await this.prisma.redeemRecord.findMany({
where: { store: { name: { contains: keyword } } },
orderBy: { createdAt: 'desc' },
skip,
take,
include: { store: { select: { name: true } } },
});
const total =
byNo.length > 0
? await this.prisma.redeemRecord.count({ where: { redeemNo: { contains: keyword } } })
: await this.prisma.redeemRecord.count({
where: { store: { name: { contains: keyword } } },
});
return {
total,
items: rows.map((r) => ({
redeemNo: r.redeemNo,
amount: asNumber(r.amount),
channel: r.channel,
storeName: r.store?.name || '—',
createdAt: r.createdAt.toISOString(),
})),
};
},
);
}
queryPromoCodes(
plugin: WecomPluginRuntime,
wecomUserId: string,
q?: string,
page?: string,
pageSize?: string,
) {
this.requirePerm(plugin, 'promo.read');
const keyword = requireQuery(q);
const take = clampWecomPluginPageSize(pageSize);
const skip = (clampWecomPluginPage(page) - 1) * take;
return this.audit.run(
{
bot: wecomPluginAuditBot(plugin),
wecomUserId,
action: 'plugin.promo.read',
permission: 'promo.read',
inputSummary: keyword,
},
async () => {
const where = {
OR: [
{ code: { contains: keyword.toUpperCase() } },
{ name: { contains: keyword } },
],
};
const [rows, total] = await Promise.all([
this.prisma.commonPromoCode.findMany({
where,
orderBy: { createdAt: 'desc' },
skip,
take,
select: {
code: true,
name: true,
scene: true,
status: true,
scanCount: true,
orderCount: true,
},
}),
this.prisma.commonPromoCode.count({ where }),
]);
return { total, items: rows };
},
);
}
queryPromoCodeStats(plugin: WecomPluginRuntime, wecomUserId: string, code?: string) {
this.requirePerm(plugin, 'promo.read');
const keyword = requireQuery(code);
return this.audit.run(
{
bot: wecomPluginAuditBot(plugin),
wecomUserId,
action: 'plugin.promo.stats',
permission: 'promo.read',
inputSummary: keyword,
},
async () => {
const row =
(await this.prisma.commonPromoCode.findUnique({
where: { code: keyword.toUpperCase() },
select: { id: true, code: true, name: true, status: true },
})) ||
(await this.prisma.commonPromoCode.findFirst({
where: { code: { contains: keyword.toUpperCase() } },
select: { id: true, code: true, name: true, status: true },
}));
if (!row) throw new NotFoundException(`未找到推广码:${keyword}`);
const stats = await this.promo.stats(row.id);
return { code: row.code, name: row.name, status: row.status, stats };
},
);
}
queryMetrics(plugin: WecomPluginRuntime, wecomUserId: string, kindRaw?: string) {
this.requirePerm(plugin, 'metrics.read');
const kind = parseWecomPluginMetricsKind(kindRaw);
if (!kind) {
throw new BadRequestException('kind 须为 today | daily | weekly | monthly');
}
return this.audit.run(
{
bot: wecomPluginAuditBot(plugin),
wecomUserId,
action: 'plugin.metrics.read',
permission: 'metrics.read',
inputSummary: kind,
},
async () => {
const period = wecomPluginMetricsPeriod(kind);
const stats = await this.loadStats(period.start, period.endExclusive);
return {
kind: period.kind,
title: period.title,
rangeLabel: period.rangeLabel,
incrementLabel: period.incrementLabel,
periodKey: period.periodKey,
stats: toWecomPluginMetricsView(stats),
};
},
);
}
/** 日报口径:用户=有效未合并;订单金额=已付 payAmount;核销=RedeemRecord。today 期末为当前时刻。 */
private async loadStats(start: Date, cutoff: Date): Promise<WecomReportStats> {
const userBase = { status: 1, mergedIntoUserId: null } as const;
const partnerBase = { isPrimary: 1 } as const;
const paid = { payStatus: 'PAID' as const };
const [
usersTotal,
usersIncrement,
partnersTotal,
partnersIncrement,
storesTotal,
storesIncrement,
ordersTotal,
ordersIncrement,
orderAmountTotal,
orderAmountIncrement,
redeemsTotal,
redeemsIncrement,
redeemAmountTotal,
redeemAmountIncrement,
] = await Promise.all([
this.prisma.user.count({ where: { ...userBase, createdAt: { lt: cutoff } } }),
this.prisma.user.count({
where: { ...userBase, createdAt: { gte: start, lt: cutoff } },
}),
this.prisma.partnerAccount.count({
where: { ...partnerBase, createdAt: { lt: cutoff } },
}),
this.prisma.partnerAccount.count({
where: { ...partnerBase, createdAt: { gte: start, lt: cutoff } },
}),
this.prisma.store.count({ where: { createdAt: { lt: cutoff } } }),
this.prisma.store.count({ where: { createdAt: { gte: start, lt: cutoff } } }),
this.prisma.order.count({ where: { createdAt: { lt: cutoff } } }),
this.prisma.order.count({ where: { createdAt: { gte: start, lt: cutoff } } }),
this.prisma.order.aggregate({
_sum: { payAmount: true },
where: { ...paid, paidAt: { lt: cutoff } },
}),
this.prisma.order.aggregate({
_sum: { payAmount: true },
where: { ...paid, paidAt: { gte: start, lt: cutoff } },
}),
this.prisma.redeemRecord.count({ where: { createdAt: { lt: cutoff } } }),
this.prisma.redeemRecord.count({ where: { createdAt: { gte: start, lt: cutoff } } }),
this.prisma.redeemRecord.aggregate({
_sum: { amount: true },
where: { createdAt: { lt: cutoff } },
}),
this.prisma.redeemRecord.aggregate({
_sum: { amount: true },
where: { createdAt: { gte: start, lt: cutoff } },
}),
]);
return {
usersTotal,
usersIncrement,
partnersTotal,
partnersIncrement,
storesTotal,
storesIncrement,
ordersTotal,
ordersIncrement,
orderAmountTotal: asNumber(orderAmountTotal._sum.payAmount),
orderAmountIncrement: asNumber(orderAmountIncrement._sum.payAmount),
redeemsTotal,
redeemsIncrement,
redeemAmountTotal: asNumber(redeemAmountTotal._sum.amount),
redeemAmountIncrement: asNumber(redeemAmountIncrement._sum.amount),
};
}
queryStoreAudits(
plugin: WecomPluginRuntime,
wecomUserId: string,
q?: string,
status?: string,
page?: string,
pageSize?: string,
) {
this.requirePerm(plugin, 'store.audit.read');
const take = clampWecomPluginPageSize(pageSize);
const skip = (clampWecomPluginPage(page) - 1) * take;
const auditStatus = (status?.trim() || 'PENDING') as 'PENDING' | 'APPROVED' | 'REJECTED';
const where: Prisma.StoreWhereInput = { auditStatus };
const keyword = String(q ?? '').trim();
if (keyword) where.name = { contains: keyword };
return this.audit.run(
{
bot: wecomPluginAuditBot(plugin),
wecomUserId,
action: 'plugin.store.audit.list',
permission: 'store.audit.read',
inputSummary: keyword || auditStatus,
},
async () => {
const [rows, total] = await Promise.all([
this.prisma.store.findMany({
where,
orderBy: { createdAt: 'desc' },
skip,
take,
select: {
id: true,
name: true,
status: true,
auditStatus: true,
rejectReason: true,
cityName: true,
district: true,
address: true,
contactPhone: true,
phone: true,
createdAt: true,
partnerAccount: { select: { id: true, name: true, companyName: true } },
},
}),
this.prisma.store.count({ where }),
]);
return {
total,
items: rows.map((s) => ({
id: s.id.toString(),
name: s.name,
status: s.status,
auditStatus: s.auditStatus,
rejectReason: s.rejectReason,
cityName: s.cityName,
district: s.district,
address: s.address,
contactPhone: maskContactPhone(s.contactPhone || s.phone),
partnerName: s.partnerAccount.companyName || s.partnerAccount.name,
partnerId: s.partnerAccount.id.toString(),
createdAt: s.createdAt.toISOString(),
})),
};
},
);
}
queryStoreInfoAudits(
plugin: WecomPluginRuntime,
wecomUserId: string,
status?: string,
page?: string,
pageSize?: string,
) {
this.requirePerm(plugin, 'store.audit.read');
const take = clampWecomPluginPageSize(pageSize);
const skip = (clampWecomPluginPage(page) - 1) * take;
const where: Prisma.StoreInfoChangeRequestWhereInput = {};
if (status?.trim()) {
where.status = status.trim() as Prisma.EnumStoreInfoChangeStatusFilter['equals'];
} else {
where.status = 'PENDING';
}
return this.audit.run(
{
bot: wecomPluginAuditBot(plugin),
wecomUserId,
action: 'plugin.store.info_audit.list',
permission: 'store.audit.read',
inputSummary: where.status as string,
},
async () => {
const [rows, total] = await Promise.all([
this.prisma.storeInfoChangeRequest.findMany({
where,
orderBy: { createdAt: 'desc' },
skip,
take,
include: { store: { select: { name: true } } },
}),
this.prisma.storeInfoChangeRequest.count({ where }),
]);
return {
total,
items: rows.map((r) => this.mapStoreInfoAuditRow(r)),
};
},
);
}
queryStoreInfoAuditDetail(plugin: WecomPluginRuntime, wecomUserId: string, id: string) {
this.requirePerm(plugin, 'store.audit.read');
return this.audit.run(
{
bot: wecomPluginAuditBot(plugin),
wecomUserId,
action: 'plugin.store.info_audit.detail',
permission: 'store.audit.read',
inputSummary: id,
},
async () => {
const row = await this.prisma.storeInfoChangeRequest.findUnique({
where: { id: BigInt(id) },
include: { store: { select: { name: true } } },
});
if (!row) throw new NotFoundException('信息变更审核不存在');
return this.mapStoreInfoAuditRow(row, true);
},
);
}
queryStorePackageAudits(
plugin: WecomPluginRuntime,
wecomUserId: string,
status?: string,
page?: string,
pageSize?: string,
) {
this.requirePerm(plugin, 'store.audit.read');
const take = clampWecomPluginPageSize(pageSize);
const skip = (clampWecomPluginPage(page) - 1) * take;
const where: Prisma.StorePackageChangeRequestWhereInput = {};
if (status?.trim()) {
where.status = status.trim() as Prisma.EnumStorePackageChangeStatusFilter['equals'];
} else {
where.status = 'PENDING';
}
return this.audit.run(
{
bot: wecomPluginAuditBot(plugin),
wecomUserId,
action: 'plugin.store.package_audit.list',
permission: 'store.audit.read',
inputSummary: where.status as string,
},
async () => {
const [rows, total] = await Promise.all([
this.prisma.storePackageChangeRequest.findMany({
where,
orderBy: { createdAt: 'desc' },
skip,
take,
include: { store: { select: { id: true, name: true } } },
}),
this.prisma.storePackageChangeRequest.count({ where }),
]);
return {
total,
items: rows.map((r) => ({
id: r.id.toString(),
storeId: r.storeId.toString(),
storeName: r.store.name,
status: r.status,
packageCount: Array.isArray(r.packagesJson) ? r.packagesJson.length : 0,
submitterType: r.submitterType,
rejectReason: r.rejectReason,
createdAt: r.createdAt.toISOString(),
})),
};
},
);
}
queryStorePackageAuditDetail(plugin: WecomPluginRuntime, wecomUserId: string, id: string) {
this.requirePerm(plugin, 'store.audit.read');
return this.audit.run(
{
bot: wecomPluginAuditBot(plugin),
wecomUserId,
action: 'plugin.store.package_audit.detail',
permission: 'store.audit.read',
inputSummary: id,
},
async () => {
const req = await this.prisma.storePackageChangeRequest.findUnique({
where: { id: BigInt(id) },
include: { store: { select: { name: true } } },
});
if (!req) throw new NotFoundException('套餐审核不存在');
const livePackages = await this.prisma.storePackage.findMany({
where: { storeId: req.storeId },
orderBy: [{ sortOrder: 'asc' }, { id: 'asc' }],
select: { name: true, price: true, dishes: true, usableTime: true, otherNotes: true },
});
return {
id: req.id.toString(),
storeId: req.storeId.toString(),
storeName: req.store.name,
status: req.status,
proposedPackages: req.packagesJson,
livePackages: livePackages.map((p) => ({
name: p.name,
price: asNumber(p.price),
dishes: p.dishes,
usableTime: p.usableTime,
otherNotes: p.otherNotes,
})),
submitterType: req.submitterType,
rejectReason: req.rejectReason,
reviewedAt: req.reviewedAt?.toISOString() ?? null,
createdAt: req.createdAt.toISOString(),
};
},
);
}
queryPartners(
plugin: WecomPluginRuntime,
wecomUserId: string,
q?: string,
page?: string,
pageSize?: string,
) {
this.requirePerm(plugin, 'partner.read');
const keyword = requireQuery(q);
const take = clampWecomPluginPageSize(pageSize);
const skip = (clampWecomPluginPage(page) - 1) * take;
return this.audit.run(
{
bot: wecomPluginAuditBot(plugin),
wecomUserId,
action: 'plugin.partner.read',
permission: 'partner.read',
inputSummary: keyword,
},
async () => {
const or: Prisma.PartnerAccountWhereInput[] = [
{ name: { contains: keyword } },
{ companyName: { contains: keyword } },
{ phone: { contains: keyword } },
];
if (/^\d+$/.test(keyword)) or.push({ id: BigInt(keyword) });
const where: Prisma.PartnerAccountWhereInput = { isPrimary: 1, OR: or };
const [rows, total] = await Promise.all([
this.prisma.partnerAccount.findMany({
where,
orderBy: { createdAt: 'desc' },
skip,
take,
select: {
id: true,
name: true,
companyName: true,
phone: true,
status: true,
city: { select: { name: true } },
_count: { select: { stores: true, assocUsers: true } },
},
}),
this.prisma.partnerAccount.count({ where }),
]);
return {
total,
items: rows.map((p) => ({
id: p.id.toString(),
name: p.name,
companyName: p.companyName,
phone: maskContactPhone(p.phone),
status: p.status,
cityName: p.city?.name ?? null,
storeCount: p._count.stores,
userCount: p._count.assocUsers,
})),
};
},
);
}
queryPartnerUsers(
plugin: WecomPluginRuntime,
wecomUserId: string,
partnerId: string,
from?: string,
to?: string,
page?: string,
pageSize?: string,
) {
this.requirePerm(plugin, 'partner.read');
const take = clampWecomPluginPageSize(pageSize);
const skip = (clampWecomPluginPage(page) - 1) * take;
const createdAt = parseWecomPluginDateRange(from, to);
return this.audit.run(
{
bot: wecomPluginAuditBot(plugin),
wecomUserId,
action: 'plugin.partner.users',
permission: 'partner.read',
inputSummary: `${partnerId}:${from || ''}-${to || ''}`,
},
async () => {
const primary = await this.resolvePartnerPrimary(partnerId);
const where: Prisma.UserWhereInput = {
assocPartnerAccountId: primary.id,
mergedIntoUserId: null,
...(createdAt ? { createdAt } : {}),
};
const [rows, total] = await Promise.all([
this.prisma.user.findMany({
where,
orderBy: { createdAt: 'desc' },
skip,
take,
select: {
userNo: true,
nickname: true,
phone: true,
status: true,
createdAt: true,
_count: { select: { orders: true } },
},
}),
this.prisma.user.count({ where }),
]);
return {
partnerId: primary.id.toString(),
partnerName: primary.companyName || primary.name,
total,
items: rows.map((u) => ({
...toWecomPluginUserView(u),
status: u.status,
orderCount: u._count.orders,
createdAt: u.createdAt.toISOString(),
})),
};
},
);
}
queryPartnerStores(
plugin: WecomPluginRuntime,
wecomUserId: string,
partnerId: string,
from?: string,
to?: string,
page?: string,
pageSize?: string,
) {
this.requirePerm(plugin, 'partner.read');
const take = clampWecomPluginPageSize(pageSize);
const skip = (clampWecomPluginPage(page) - 1) * take;
const createdAt = parseWecomPluginDateRange(from, to);
return this.audit.run(
{
bot: wecomPluginAuditBot(plugin),
wecomUserId,
action: 'plugin.partner.stores',
permission: 'partner.read',
inputSummary: `${partnerId}:${from || ''}-${to || ''}`,
},
async () => {
const primary = await this.resolvePartnerPrimary(partnerId);
const where: Prisma.StoreWhereInput = {
partnerAccountId: primary.id,
...(createdAt ? { createdAt } : {}),
};
const [rows, total] = await Promise.all([
this.prisma.store.findMany({
where,
orderBy: { createdAt: 'desc' },
skip,
take,
select: {
id: true,
name: true,
status: true,
auditStatus: true,
cityName: true,
rating: true,
createdAt: true,
_count: { select: { redeemRecords: true } },
},
}),
this.prisma.store.count({ where }),
]);
const redeemSums =
rows.length > 0
? await this.prisma.redeemRecord.groupBy({
by: ['storeId'],
where: { storeId: { in: rows.map((s) => s.id) } },
_sum: { amount: true },
})
: [];
const redeemedByStore = new Map(
redeemSums.map((r) => [r.storeId.toString(), asNumber(r._sum.amount)]),
);
return {
partnerId: primary.id.toString(),
partnerName: primary.companyName || primary.name,
total,
items: rows.map((s) => ({
id: s.id.toString(),
name: s.name,
status: s.status,
auditStatus: s.auditStatus,
cityName: s.cityName,
rating: s.rating != null ? asNumber(s.rating) : null,
redeemCount: s._count.redeemRecords,
totalRedeemedBenefitAmount: redeemedByStore.get(s.id.toString()) ?? 0,
createdAt: s.createdAt.toISOString(),
})),
};
},
);
}
queryPartnerOrders(
plugin: WecomPluginRuntime,
wecomUserId: string,
partnerId: string,
from?: string,
to?: string,
page?: string,
pageSize?: string,
) {
this.requirePerm(plugin, 'partner.read');
const take = clampWecomPluginPageSize(pageSize);
const skip = (clampWecomPluginPage(page) - 1) * take;
const createdAt = parseWecomPluginDateRange(from, to);
return this.audit.run(
{
bot: wecomPluginAuditBot(plugin),
wecomUserId,
action: 'plugin.partner.orders',
permission: 'partner.read',
inputSummary: `${partnerId}:${from || ''}-${to || ''}`,
},
async () => {
const primary = await this.resolvePartnerPrimary(partnerId);
const where: Prisma.OrderWhereInput = {
partnerAccountIdAtPay: primary.id,
...(createdAt ? { createdAt } : {}),
};
const [rows, total] = await Promise.all([
this.prisma.order.findMany({
where,
orderBy: { createdAt: 'desc' },
skip,
take,
select: {
orderNo: true,
status: true,
payStatus: true,
productName: true,
quantity: true,
payAmount: true,
createdAt: true,
user: { select: { userNo: true, nickname: true, phone: true } },
},
}),
this.prisma.order.count({ where }),
]);
return {
partnerId: primary.id.toString(),
partnerName: primary.companyName || primary.name,
total,
items: rows.map((o) => ({
orderNo: o.orderNo,
status: o.status,
payStatus: o.payStatus,
productName: o.productName,
quantity: o.quantity,
payAmount: asNumber(o.payAmount),
user: toWecomPluginUserView({
userNo: o.user?.userNo || '—',
nickname: o.user?.nickname,
phone: o.user?.phone,
}),
createdAt: o.createdAt.toISOString(),
})),
};
},
);
}
private mapStoreInfoAuditRow(
row: {
id: bigint;
storeId: bigint;
status: string;
liveSnapshot: unknown;
proposedSnapshot: unknown;
changedFields: unknown;
submitterType: string;
submitterId: bigint;
rejectReason: string | null;
reviewedAt: Date | null;
createdAt: Date;
store?: { name?: string | null };
},
withDiffs = false,
) {
const changed = Array.isArray(row.changedFields)
? (row.changedFields as StoreInfoChangeableField[])
: [];
const live = (row.liveSnapshot || {}) as Record<string, unknown>;
const proposed = (row.proposedSnapshot || {}) as Record<string, unknown>;
const base = {
id: row.id.toString(),
storeId: row.storeId.toString(),
storeName: row.store?.name ?? null,
status: row.status,
changedFields: changed,
changedFieldLabels: changed.map((f) => STORE_INFO_CHANGEABLE_FIELD_LABELS[f] ?? f),
submitterType: row.submitterType,
submitterId: row.submitterId.toString(),
rejectReason: row.rejectReason,
reviewedAt: row.reviewedAt?.toISOString() ?? null,
createdAt: row.createdAt.toISOString(),
};
if (!withDiffs) return base;
return {
...base,
diffs: changed.map((field) => ({
field,
label: STORE_INFO_CHANGEABLE_FIELD_LABELS[field] ?? field,
live: live[field] ?? null,
proposed: proposed[field] ?? null,
})),
};
}
private async resolvePartnerPrimary(partnerId: string) {
const id = BigInt(partnerId);
const account = await this.prisma.partnerAccount.findUnique({ where: { id } });
if (!account) throw new NotFoundException('合伙人不存在');
if (account.isPrimary === 1) return account;
if (!account.parentAccountId) throw new NotFoundException('合伙人账号无效');
return this.prisma.partnerAccount.findUniqueOrThrow({ where: { id: account.parentAccountId } });
}
}