feat(ops,store,partner): 用户权益列 + 门店合同多图上传
- admin 用户列表新增剩余/已用/累计好客权益金额三列(benefitCoupon groupBy 聚合,排除 VOID) - admin/partner 门店入驻合同支持多张照片与 PDF(CommonResource 多记录,复用 ENV 多图逻辑) - 新增 contract-urls.util 归一化工具,向后兼容旧 contractUrl 字段并标记 deprecated 需求1/2/3
This commit is contained in:
@@ -6,6 +6,28 @@ import type { AdminUsersQueryDto } from './dto/admin-query.dto';
|
||||
|
||||
const FINISHED_ORDER_STATUSES = ['COMPLETED', 'CANCELLED', 'REFUNDED'] as const;
|
||||
|
||||
/** 权益统计口径:VOID(退款作废)不计入,与 C 端 listCoupons 一致 */
|
||||
const BENEFIT_STAT_STATUSES = ['ACTIVE', 'USED_UP'] as const;
|
||||
|
||||
export type UserBenefitStat = {
|
||||
/** 累计获得(含已使用) */
|
||||
benefitTotalAmount: number;
|
||||
/** 已使用(已核销) */
|
||||
benefitUsedAmount: number;
|
||||
/** 剩余未使用(可核销余额) */
|
||||
benefitBalance: number;
|
||||
};
|
||||
|
||||
const EMPTY_BENEFIT_STAT: UserBenefitStat = {
|
||||
benefitTotalAmount: 0,
|
||||
benefitUsedAmount: 0,
|
||||
benefitBalance: 0,
|
||||
};
|
||||
|
||||
function toAmount(v: Prisma.Decimal | number | null | undefined) {
|
||||
return v == null ? 0 : Number(v);
|
||||
}
|
||||
|
||||
function mapAdminUserRow(u: {
|
||||
id: bigint;
|
||||
userNo: string;
|
||||
@@ -23,7 +45,7 @@ function mapAdminUserRow(u: {
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
_count: { orders: number };
|
||||
}) {
|
||||
}, benefit: UserBenefitStat = EMPTY_BENEFIT_STAT) {
|
||||
return {
|
||||
id: u.id,
|
||||
userNo: u.userNo,
|
||||
@@ -42,6 +64,7 @@ function mapAdminUserRow(u: {
|
||||
createdAt: u.createdAt,
|
||||
updatedAt: u.updatedAt,
|
||||
orderCount: u._count.orders,
|
||||
...benefit,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -90,14 +113,37 @@ export class AdminUsersService {
|
||||
this.prisma.user.count({ where }),
|
||||
]);
|
||||
|
||||
const benefitMap = await this.loadBenefitStats(items.map((u) => u.id));
|
||||
|
||||
return serializeBigInt({
|
||||
items: items.map((u) => mapAdminUserRow(u)),
|
||||
items: items.map((u) => mapAdminUserRow(u, benefitMap.get(u.id.toString()))),
|
||||
total,
|
||||
page,
|
||||
pageSize,
|
||||
});
|
||||
}
|
||||
|
||||
/** 批量聚合当前页用户的好客权益金额(累计获得 / 已使用 / 剩余) */
|
||||
private async loadBenefitStats(userIds: bigint[]): Promise<Map<string, UserBenefitStat>> {
|
||||
const map = new Map<string, UserBenefitStat>();
|
||||
if (!userIds.length) return map;
|
||||
|
||||
const grouped = await this.prisma.benefitCoupon.groupBy({
|
||||
by: ['userId'],
|
||||
where: { userId: { in: userIds }, status: { in: [...BENEFIT_STAT_STATUSES] } },
|
||||
_sum: { totalAmount: true, usedAmount: true, balance: true },
|
||||
});
|
||||
|
||||
for (const row of grouped) {
|
||||
map.set(row.userId.toString(), {
|
||||
benefitTotalAmount: toAmount(row._sum.totalAmount),
|
||||
benefitUsedAmount: toAmount(row._sum.usedAmount),
|
||||
benefitBalance: toAmount(row._sum.balance),
|
||||
});
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
async detail(id: bigint) {
|
||||
const user = await this.prisma.user.findUnique({
|
||||
where: { id },
|
||||
@@ -128,8 +174,11 @@ export class AdminUsersService {
|
||||
});
|
||||
}
|
||||
|
||||
const benefitMap = await this.loadBenefitStats([user.id]);
|
||||
|
||||
return serializeBigInt({
|
||||
...user,
|
||||
...(benefitMap.get(user.id.toString()) ?? EMPTY_BENEFIT_STAT),
|
||||
wechatVerified: !!user.wxOpenId,
|
||||
mergedFromCount: user._count.mergedFrom,
|
||||
orderCount: user._count.orders,
|
||||
|
||||
Reference in New Issue
Block a user