b626db5d84
Unify product/store visibility on HQ whitelist, mark isTest snapshots, and fix SUPER_ADMIN access for the new module.
336 lines
11 KiB
TypeScript
336 lines
11 KiB
TypeScript
import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
|
|
import { Prisma } from '@prisma/client';
|
|
import { PrismaService } from '../prisma/prisma.module';
|
|
import { serializeBigInt } from '../decorators/current-user.decorator';
|
|
|
|
export function normalizeTestPhone(phone: string | null | undefined): string {
|
|
return (phone || '').replace(/\D/g, '').trim();
|
|
}
|
|
|
|
export function assertMobilePhone(phone: string): string {
|
|
const p = normalizeTestPhone(phone);
|
|
if (!/^1\d{10}$/.test(p)) {
|
|
throw new BadRequestException(`手机号格式无效:${phone}`);
|
|
}
|
|
return p;
|
|
}
|
|
|
|
@Injectable()
|
|
export class TestWhitelistService {
|
|
constructor(private readonly prisma: PrismaService) {}
|
|
|
|
async isPhoneInWhitelist(phone: string | null | undefined): Promise<boolean> {
|
|
const p = normalizeTestPhone(phone);
|
|
if (!p) return false;
|
|
const row = await this.prisma.commonTestWhitelistPhone.findUnique({
|
|
where: { phone: p },
|
|
select: { id: true },
|
|
});
|
|
return !!row;
|
|
}
|
|
|
|
async assertGlobalWhitelistNotEmpty() {
|
|
const count = await this.prisma.commonTestWhitelistPhone.count();
|
|
if (count === 0) {
|
|
throw new BadRequestException('全局测试白名单为空,请先在「白名单管理」添加手机号');
|
|
}
|
|
}
|
|
|
|
async listPhones(query: { phone?: string; page?: number; pageSize?: number }) {
|
|
const page = query.page ?? 1;
|
|
const pageSize = query.pageSize ?? 20;
|
|
const where: Prisma.CommonTestWhitelistPhoneWhereInput = {};
|
|
if (query.phone) {
|
|
where.phone = { contains: normalizeTestPhone(query.phone) || query.phone };
|
|
}
|
|
const [items, total] = await Promise.all([
|
|
this.prisma.commonTestWhitelistPhone.findMany({
|
|
where,
|
|
orderBy: { createdAt: 'desc' },
|
|
skip: (page - 1) * pageSize,
|
|
take: pageSize,
|
|
}),
|
|
this.prisma.commonTestWhitelistPhone.count({ where }),
|
|
]);
|
|
return serializeBigInt({ items, total, page, pageSize });
|
|
}
|
|
|
|
async addPhone(input: { phone: string; note?: string; createdByHqId?: bigint }) {
|
|
const phone = assertMobilePhone(input.phone);
|
|
const existing = await this.prisma.commonTestWhitelistPhone.findUnique({ where: { phone } });
|
|
if (existing) {
|
|
throw new BadRequestException('该手机号已在白名单中');
|
|
}
|
|
const row = await this.prisma.commonTestWhitelistPhone.create({
|
|
data: {
|
|
phone,
|
|
note: input.note?.trim() || null,
|
|
createdByHqId: input.createdByHqId ?? null,
|
|
},
|
|
});
|
|
await this.syncTestFlagsForPhone(phone, true);
|
|
return serializeBigInt(row);
|
|
}
|
|
|
|
async updatePhone(id: bigint, input: { note?: string | null }) {
|
|
const row = await this.prisma.commonTestWhitelistPhone.findUnique({ where: { id } });
|
|
if (!row) throw new NotFoundException('白名单记录不存在');
|
|
const updated = await this.prisma.commonTestWhitelistPhone.update({
|
|
where: { id },
|
|
data: { note: input.note === undefined ? undefined : input.note?.trim() || null },
|
|
});
|
|
return serializeBigInt(updated);
|
|
}
|
|
|
|
async removePhone(id: bigint) {
|
|
const row = await this.prisma.commonTestWhitelistPhone.findUnique({ where: { id } });
|
|
if (!row) throw new NotFoundException('白名单记录不存在');
|
|
await this.prisma.commonTestWhitelistPhone.delete({ where: { id } });
|
|
await this.syncTestFlagsForPhone(row.phone, false);
|
|
return { ok: true };
|
|
}
|
|
|
|
/** 同步账号/门店 isTest,并回填订单/核销快照 */
|
|
async syncTestFlagsForPhone(phone: string, isTest: boolean) {
|
|
const p = normalizeTestPhone(phone);
|
|
if (!p) return;
|
|
|
|
await this.prisma.user.updateMany({ where: { phone: p }, data: { isTest } });
|
|
await this.prisma.storeAccount.updateMany({ where: { phone: p }, data: { isTest } });
|
|
await this.prisma.partnerAccount.updateMany({ where: { phone: p }, data: { isTest } });
|
|
|
|
if (isTest) {
|
|
await this.prisma.store.updateMany({ where: { phone: p }, data: { isTest: true } });
|
|
} else {
|
|
// 仅清除「联系电话命中且当前不在白名单」的自动标;手动标的门店若电话已不在名单则保持 isTest(运营可再关)
|
|
// 简化:电话命中且移出名单时置 false;手动标的非该电话门店不受影响
|
|
await this.prisma.store.updateMany({ where: { phone: p }, data: { isTest: false } });
|
|
}
|
|
|
|
const users = await this.prisma.user.findMany({
|
|
where: { phone: p },
|
|
select: { id: true },
|
|
});
|
|
const userIds = users.map((u) => u.id);
|
|
if (userIds.length) {
|
|
await this.prisma.order.updateMany({
|
|
where: { userId: { in: userIds } },
|
|
data: { isTest },
|
|
});
|
|
}
|
|
|
|
const stores = await this.prisma.store.findMany({
|
|
where: { phone: p },
|
|
select: { id: true },
|
|
});
|
|
const storeIds = stores.map((s) => s.id);
|
|
|
|
if (userIds.length || storeIds.length) {
|
|
const or: Prisma.RedeemRecordWhereInput[] = [];
|
|
if (userIds.length) or.push({ userId: { in: userIds } });
|
|
if (storeIds.length) or.push({ storeId: { in: storeIds } });
|
|
await this.prisma.redeemRecord.updateMany({
|
|
where: { OR: or },
|
|
data: { isTest },
|
|
});
|
|
}
|
|
}
|
|
|
|
async listAccounts(query: {
|
|
type: 'user' | 'store_account' | 'partner' | 'store' | 'order';
|
|
phone?: string;
|
|
page?: number;
|
|
pageSize?: number;
|
|
}) {
|
|
const page = query.page ?? 1;
|
|
const pageSize = query.pageSize ?? 20;
|
|
const phone = query.phone ? normalizeTestPhone(query.phone) : '';
|
|
|
|
if (query.type === 'user') {
|
|
const where: Prisma.UserWhereInput = { isTest: true };
|
|
if (phone) where.phone = { contains: phone };
|
|
const [items, total] = await Promise.all([
|
|
this.prisma.user.findMany({
|
|
where,
|
|
orderBy: { createdAt: 'desc' },
|
|
skip: (page - 1) * pageSize,
|
|
take: pageSize,
|
|
select: {
|
|
id: true,
|
|
userNo: true,
|
|
phone: true,
|
|
nickname: true,
|
|
status: true,
|
|
createdAt: true,
|
|
isTest: true,
|
|
},
|
|
}),
|
|
this.prisma.user.count({ where }),
|
|
]);
|
|
return serializeBigInt({ items, total, page, pageSize, type: query.type });
|
|
}
|
|
|
|
if (query.type === 'store_account') {
|
|
const where: Prisma.StoreAccountWhereInput = { isTest: true };
|
|
if (phone) where.phone = { contains: phone };
|
|
const [items, total] = await Promise.all([
|
|
this.prisma.storeAccount.findMany({
|
|
where,
|
|
orderBy: { createdAt: 'desc' },
|
|
skip: (page - 1) * pageSize,
|
|
take: pageSize,
|
|
select: {
|
|
id: true,
|
|
phone: true,
|
|
name: true,
|
|
status: true,
|
|
createdAt: true,
|
|
isTest: true,
|
|
},
|
|
}),
|
|
this.prisma.storeAccount.count({ where }),
|
|
]);
|
|
return serializeBigInt({ items, total, page, pageSize, type: query.type });
|
|
}
|
|
|
|
if (query.type === 'partner') {
|
|
const where: Prisma.PartnerAccountWhereInput = { isTest: true };
|
|
if (phone) where.phone = { contains: phone };
|
|
const [items, total] = await Promise.all([
|
|
this.prisma.partnerAccount.findMany({
|
|
where,
|
|
orderBy: { createdAt: 'desc' },
|
|
skip: (page - 1) * pageSize,
|
|
take: pageSize,
|
|
select: {
|
|
id: true,
|
|
phone: true,
|
|
name: true,
|
|
companyName: true,
|
|
status: true,
|
|
createdAt: true,
|
|
isTest: true,
|
|
},
|
|
}),
|
|
this.prisma.partnerAccount.count({ where }),
|
|
]);
|
|
return serializeBigInt({ items, total, page, pageSize, type: query.type });
|
|
}
|
|
|
|
if (query.type === 'store') {
|
|
const where: Prisma.StoreWhereInput = { isTest: true };
|
|
if (phone) where.phone = { contains: phone };
|
|
const [items, total] = await Promise.all([
|
|
this.prisma.store.findMany({
|
|
where,
|
|
orderBy: { createdAt: 'desc' },
|
|
skip: (page - 1) * pageSize,
|
|
take: pageSize,
|
|
select: {
|
|
id: true,
|
|
name: true,
|
|
phone: true,
|
|
status: true,
|
|
cityName: true,
|
|
createdAt: true,
|
|
isTest: true,
|
|
},
|
|
}),
|
|
this.prisma.store.count({ where }),
|
|
]);
|
|
return serializeBigInt({ items, total, page, pageSize, type: query.type });
|
|
}
|
|
|
|
const where: Prisma.OrderWhereInput = { isTest: true };
|
|
if (phone) {
|
|
where.OR = [
|
|
{ receiverPhone: { contains: phone } },
|
|
{ user: { phone: { contains: phone } } },
|
|
];
|
|
}
|
|
const [items, total] = await Promise.all([
|
|
this.prisma.order.findMany({
|
|
where,
|
|
orderBy: { createdAt: 'desc' },
|
|
skip: (page - 1) * pageSize,
|
|
take: pageSize,
|
|
select: {
|
|
id: true,
|
|
orderNo: true,
|
|
status: true,
|
|
payStatus: true,
|
|
payAmount: true,
|
|
receiverPhone: true,
|
|
createdAt: true,
|
|
isTest: true,
|
|
user: { select: { id: true, phone: true, userNo: true } },
|
|
},
|
|
}),
|
|
this.prisma.order.count({ where }),
|
|
]);
|
|
return serializeBigInt({ items, total, page, pageSize, type: query.type });
|
|
}
|
|
|
|
async linkedForPhoneId(id: bigint) {
|
|
const row = await this.prisma.commonTestWhitelistPhone.findUnique({ where: { id } });
|
|
if (!row) throw new NotFoundException('白名单记录不存在');
|
|
const phone = row.phone;
|
|
const [users, storeAccounts, partners, stores] = await Promise.all([
|
|
this.prisma.user.findMany({
|
|
where: { phone },
|
|
select: { id: true, userNo: true, phone: true, nickname: true, isTest: true, status: true },
|
|
}),
|
|
this.prisma.storeAccount.findMany({
|
|
where: { phone },
|
|
select: { id: true, phone: true, name: true, isTest: true, status: true },
|
|
}),
|
|
this.prisma.partnerAccount.findMany({
|
|
where: { phone },
|
|
select: { id: true, phone: true, name: true, companyName: true, isTest: true, status: true },
|
|
}),
|
|
this.prisma.store.findMany({
|
|
where: { phone },
|
|
select: { id: true, name: true, phone: true, isTest: true, status: true },
|
|
}),
|
|
]);
|
|
return serializeBigInt({
|
|
phone: row,
|
|
users,
|
|
storeAccounts,
|
|
partners,
|
|
stores,
|
|
});
|
|
}
|
|
|
|
/** 从旧商品/门店可见性子表导入全局名单(幂等) */
|
|
async migrateVisibilityPhones(createdByHqId?: bigint) {
|
|
const [productPhones, storePhones] = await Promise.all([
|
|
this.prisma.commonProductVisibilityPhone.findMany({ select: { phone: true } }),
|
|
this.prisma.storeVisibilityPhone.findMany({ select: { phone: true } }),
|
|
]);
|
|
const set = new Set<string>();
|
|
for (const row of [...productPhones, ...storePhones]) {
|
|
const p = normalizeTestPhone(row.phone);
|
|
if (/^1\d{10}$/.test(p)) set.add(p);
|
|
}
|
|
let added = 0;
|
|
for (const phone of set) {
|
|
const exists = await this.prisma.commonTestWhitelistPhone.findUnique({ where: { phone } });
|
|
if (exists) {
|
|
await this.syncTestFlagsForPhone(phone, true);
|
|
continue;
|
|
}
|
|
await this.prisma.commonTestWhitelistPhone.create({
|
|
data: {
|
|
phone,
|
|
note: '自可见性白名单迁移',
|
|
createdByHqId: createdByHqId ?? null,
|
|
},
|
|
});
|
|
await this.syncTestFlagsForPhone(phone, true);
|
|
added += 1;
|
|
}
|
|
return { importedCandidates: set.size, added };
|
|
}
|
|
}
|