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 { 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; const proposed = (row.proposedSnapshot || {}) as Record; 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 } }); } }