feat: 技术支持工单/企微权限/开发版本管理/消息推送等迭代

This commit is contained in:
2026-08-04 21:32:13 +08:00
parent c8ea5a3119
commit 9d96c73246
1341 changed files with 0 additions and 195605 deletions
@@ -1,839 +0,0 @@
import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
import { Prisma } from '@prisma/client';
import { validateBusinessHours } from '@dukang/domain';
import { PrismaService } from '../../common/prisma/prisma.module';
import { serializeBigInt } from '../../common/decorators/current-user.decorator';
import { mapStoreCompat } from '../../common/compat/v31-compat';
import type { AdminStoreAccountsQueryDto, AdminStoreMediaQueryDto, AdminStoresQueryDto } from './dto/admin-query.dto';
import { PartnerCityService } from '../city-scope/partner-city.service';
import { StoreCategoryService } from '../store/store-category.service';
import { AnalyticsService } from '../analytics/analytics.service';
import type {
CreateStoreAccountDto,
CreateStoreDto,
CreateStoreMediaDto,
UpdateStoreAccountDto,
UpdateStoreDto,
UpdateStoreMediaDto,
UpdateStoreStatusDto,
} from './dto/admin-mutate.dto';
/** 选填文案:空 / null / "null" 一律存库为 null,避免 String(null)==="null" */
function normalizeStoreOptionalText(value: unknown): string | null {
if (value == null) return null;
const s = String(value).trim();
if (!s || /^null$/i.test(s) || /^undefined$/i.test(s)) return null;
return s;
}
function normalizeVisibilityPhones(phones?: string[]): string[] {
if (!phones?.length) return [];
const out: string[] = [];
const seen = new Set<string>();
for (const raw of phones) {
const phone = String(raw || '')
.replace(/\D/g, '')
.trim();
if (!phone || seen.has(phone)) continue;
if (!/^1\d{10}$/.test(phone)) {
throw new BadRequestException(`手机号格式无效:${raw}`);
}
seen.add(phone);
out.push(phone);
}
return out;
}
@Injectable()
export class AdminStoresService {
constructor(
private readonly prisma: PrismaService,
private readonly partnerCityService: PartnerCityService,
private readonly storeCategoryService: StoreCategoryService,
private readonly analyticsService: AnalyticsService,
) {}
async listStores(query: AdminStoresQueryDto) {
const page = query.page ?? 1;
const pageSize = query.pageSize ?? 20;
const where: Prisma.StoreWhereInput = {};
if (query.name) where.name = { contains: query.name };
if (query.status) where.status = query.status as Prisma.EnumStoreStatusFilter['equals'];
if (query.auditStatus) {
where.auditStatus = query.auditStatus as Prisma.EnumStoreAuditStatusFilter['equals'];
}
if (query.cityId) where.cityId = BigInt(query.cityId);
if (query.partnerId) where.partnerAccountId = BigInt(query.partnerId);
if (query.phone) where.phone = { contains: query.phone };
const [items, total] = await Promise.all([
this.prisma.store.findMany({
where,
orderBy: { createdAt: 'desc' },
skip: (page - 1) * pageSize,
take: pageSize,
include: {
cityRef: { select: { id: true, name: true, code: true } },
partnerAccount: { select: { id: true, companyName: true } },
category: { select: { id: true, name: true, parentId: true } },
bindings: {
where: { storeAccount: { isPrimary: 1 } },
take: 1,
include: {
storeAccount: { select: { id: true, phone: true, name: true, status: true } },
},
},
coverResource: { select: { id: true, url: true } },
visibilityPhones: { select: { phone: true }, orderBy: { phone: 'asc' } },
},
}),
this.prisma.store.count({ where }),
]);
return serializeBigInt({
items: items.map((s) => {
const { visibilityPhones, ...rest } = s;
return mapStoreCompat({
...rest,
visibilityWhitelistEnabled: s.visibilityWhitelistEnabled,
visibilityPhones: visibilityPhones.map((p) => p.phone),
partner: s.partnerAccount,
account: s.bindings[0]?.storeAccount ?? null,
bindings: undefined,
});
}),
total,
page,
pageSize,
});
}
async detailStore(id: bigint) {
const store = await this.prisma.store.findUnique({
where: { id },
include: {
cityRef: true,
partnerAccount: true,
category: true,
bindings: {
where: { storeAccount: { isPrimary: 1 } },
take: 1,
include: { storeAccount: true },
},
coverResource: true,
visibilityPhones: { select: { phone: true }, orderBy: { phone: 'asc' } },
_count: { select: { redeemRecords: true, ratings: true } },
},
});
if (!store) throw new NotFoundException('门店不存在');
const [media, audits] = await Promise.all([
this.prisma.commonResource.findMany({
where: { ownerType: 'STORE', ownerId: id, status: 'ACTIVE' },
orderBy: { sortOrder: 'asc' },
}),
this.prisma.commonEvent.findMany({
where: { eventType: 'STORE_AUDIT', refType: 'STORE', refId: id },
orderBy: { createdAt: 'desc' },
take: 5,
}),
]);
const { visibilityPhones, ...rest } = store;
return serializeBigInt(mapStoreCompat({
...rest,
visibilityWhitelistEnabled: store.visibilityWhitelistEnabled,
visibilityPhones: visibilityPhones.map((p) => p.phone),
partner: store.partnerAccount,
account: store.bindings[0]?.storeAccount ?? null,
/** 门店端登录手机号(store_account.phone),与 store.phone 应对齐 */
loginPhone: store.bindings[0]?.storeAccount?.phone ?? store.phone,
bindings: undefined,
media,
audits,
redeemCount: store._count.redeemRecords,
ratingCount: store._count.ratings,
_count: undefined,
}));
}
async updateStoreStatus(id: bigint, dto: UpdateStoreStatusDto) {
const store = await this.prisma.store.update({
where: { id },
data: { status: dto.status as 'OPEN' | 'PAUSED' | 'CLOSED' },
});
return serializeBigInt(store);
}
async auditStore(id: bigint, dto: { approved: boolean; remark?: string }) {
const store = await this.prisma.store.findUnique({ where: { id } });
if (!store) throw new NotFoundException('门店不存在');
if (store.auditStatus !== 'PENDING' && store.auditStatus !== 'REJECTED') {
// 允许对已通过门店再次驳回/通过(总部纠错);PENDING/REJECTED/APPROVED 均可审核
}
if (!dto.approved) {
const reason = dto.remark?.trim();
if (!reason) throw new BadRequestException('驳回时必须填写原因');
}
const now = new Date();
const updated = await this.prisma.store.update({
where: { id },
data: dto.approved
? {
// 审核通过后保持闭店,由合伙人自行开门
status: store.status === 'CLOSED' ? 'CLOSED' : 'PAUSED',
auditStatus: 'APPROVED',
rejectReason: null,
auditedAt: now,
}
: {
status: 'PAUSED',
auditStatus: 'REJECTED',
rejectReason: dto.remark!.trim(),
auditedAt: now,
},
});
await this.prisma.commonEvent.create({
data: {
eventType: 'STORE_AUDIT',
refType: 'STORE',
refId: id,
actorType: 'HQ',
status: dto.approved ? 'APPROVED' : 'REJECTED',
remark: dto.approved
? (dto.remark?.trim() || '审核通过,可开门营业')
: dto.remark!.trim(),
param1: dto.approved ? 'APPROVE' : 'REJECT',
param1Desc: 'audit_action',
},
});
if (updated.partnerAccountId) {
this.analyticsService.trackPartnerOneSafe(undefined, 'HQ_WEB', {
partnerAccountId: updated.partnerAccountId,
eventName: dto.approved ? 'partner_store_audit_approved' : 'partner_store_audit_rejected',
refType: 'STORE',
refId: id,
extraJson: {
storeId: id.toString(),
remark: dto.remark?.trim() || null,
},
});
}
return serializeBigInt({
...updated,
notifyHint: dto.approved
? '已通过审核,合伙人可在端内开门营业'
: '已驳回,驳回原因已同步至合伙人端',
});
}
async updateStore(id: bigint, dto: UpdateStoreDto) {
const current = await this.prisma.store.findUnique({ where: { id } });
if (!current) throw new NotFoundException('门店不存在');
const openTime = dto.openTime !== undefined ? dto.openTime?.trim() || '' : current.openTime || '';
const closeTime = dto.closeTime !== undefined ? dto.closeTime?.trim() || '' : current.closeTime || '';
const openTime2 =
dto.openTime2 !== undefined ? dto.openTime2?.trim() || '' : current.openTime2 || '';
const closeTime2 =
dto.closeTime2 !== undefined ? dto.closeTime2?.trim() || '' : current.closeTime2 || '';
if (dto.openTime !== undefined || dto.closeTime !== undefined || dto.openTime2 !== undefined || dto.closeTime2 !== undefined) {
const hoursCheck = validateBusinessHours([
{ open: openTime || '10:00', close: closeTime || '22:00' },
...(openTime2 || closeTime2 ? [{ open: openTime2, close: closeTime2 }] : []),
]);
if (!hoursCheck.ok) throw new BadRequestException(hoursCheck.message);
}
const latitude = dto.latitude !== undefined ? (dto.latitude == null ? null : Number(dto.latitude)) : undefined;
const longitude = dto.longitude !== undefined ? (dto.longitude == null ? null : Number(dto.longitude)) : undefined;
if (latitude !== undefined || longitude !== undefined) {
if (latitude == null || longitude == null) {
throw new BadRequestException('经纬度须同时提供');
}
if (!Number.isFinite(latitude) || latitude < -90 || latitude > 90) {
throw new BadRequestException('纬度无效');
}
if (!Number.isFinite(longitude) || longitude < -180 || longitude > 180) {
throw new BadRequestException('经度无效');
}
}
let categoryId: bigint | undefined;
if (dto.categoryId !== undefined) {
if (!dto.categoryId?.trim()) {
throw new BadRequestException('请选择门店分类');
}
categoryId = BigInt(dto.categoryId);
await this.storeCategoryService.assertLeafCategoryId(categoryId);
}
if (dto.phone !== undefined) {
const normalizedPhone = dto.phone.trim();
if (!/^1[3-9]\d{9}$/.test(normalizedPhone)) {
throw new BadRequestException('请输入正确的手机号码');
}
}
const normalizedPhone = dto.phone !== undefined ? dto.phone.trim() : undefined;
if (dto.visibilityWhitelistEnabled !== undefined || dto.visibilityPhones !== undefined) {
const nextEnabled =
dto.visibilityWhitelistEnabled !== undefined
? !!dto.visibilityWhitelistEnabled
: current.visibilityWhitelistEnabled;
if (nextEnabled) {
const phones =
dto.visibilityPhones !== undefined
? normalizeVisibilityPhones(dto.visibilityPhones)
: (
await this.prisma.storeVisibilityPhone.findMany({
where: { storeId: id },
select: { phone: true },
})
).map((p) => p.phone);
if (!phones.length) {
throw new BadRequestException('开启白名单时请至少添加一个手机号');
}
}
}
const bankTouched =
dto.bankAccountName !== undefined ||
dto.bankAccountNo !== undefined ||
dto.bankBranch !== undefined;
const needAccountSync =
normalizedPhone !== undefined || dto.name !== undefined || bankTouched;
// 登录凭证在 store_account.phone;必须与门店展示手机号同步
let primaryBinding: {
storeAccount: { id: bigint; phone: string; name: string } | null;
} | null = null;
if (needAccountSync) {
primaryBinding = await this.prisma.storeAccountStore.findFirst({
where: { storeId: id, storeAccount: { isPrimary: 1 } },
include: {
storeAccount: { select: { id: true, phone: true, name: true } },
},
});
if (!primaryBinding) {
primaryBinding = await this.prisma.storeAccountStore.findFirst({
where: { storeId: id },
include: {
storeAccount: { select: { id: true, phone: true, name: true } },
},
orderBy: { storeAccountId: 'asc' },
});
}
if (normalizedPhone !== undefined && !primaryBinding?.storeAccount) {
throw new BadRequestException('门店未绑定登录账号,无法修改手机号');
}
const primaryAccount = primaryBinding?.storeAccount;
if (
normalizedPhone !== undefined &&
primaryAccount &&
normalizedPhone !== primaryAccount.phone
) {
const occupied = await this.prisma.storeAccount.findUnique({
where: { phone: normalizedPhone },
});
if (occupied && occupied.id !== primaryAccount.id) {
throw new BadRequestException('该手机号已被其他门店账号使用');
}
}
if (dto.bankAccountNo !== undefined) {
const no = dto.bankAccountNo?.trim() || null;
if (no && !/^\d{16,19}$/.test(no)) {
throw new BadRequestException('银行卡号须为 1619 位数字');
}
}
}
await this.prisma.$transaction(async (tx) => {
await tx.store.update({
where: { id },
data: {
...(dto.name !== undefined ? { name: dto.name.trim() } : {}),
...(normalizedPhone !== undefined ? { phone: normalizedPhone } : {}),
...(dto.intro !== undefined ? { intro: dto.intro } : {}),
...(dto.benefitUsageRule !== undefined
? { benefitUsageRule: normalizeStoreOptionalText(dto.benefitUsageRule) }
: {}),
...(dto.address !== undefined ? { address: dto.address.trim() } : {}),
...(dto.province !== undefined ? { province: dto.province.trim() } : {}),
...(dto.city !== undefined ? { cityName: dto.city.trim() } : {}),
...(dto.district !== undefined ? { district: dto.district.trim() } : {}),
...(categoryId !== undefined ? { categoryId } : {}),
...(dto.settlementRate !== undefined ? { settlementRate: dto.settlementRate } : {}),
...(dto.openTime !== undefined ? { openTime: dto.openTime } : {}),
...(dto.closeTime !== undefined ? { closeTime: dto.closeTime } : {}),
...(dto.openTime2 !== undefined ? { openTime2: dto.openTime2 || null } : {}),
...(dto.closeTime2 !== undefined ? { closeTime2: dto.closeTime2 || null } : {}),
...(dto.avgPrice !== undefined ? { avgPrice: dto.avgPrice } : {}),
...(dto.visibilityWhitelistEnabled !== undefined
? { visibilityWhitelistEnabled: !!dto.visibilityWhitelistEnabled }
: {}),
...(latitude != null && longitude != null ? { latitude, longitude } : {}),
},
});
if (dto.visibilityPhones !== undefined) {
const phones = normalizeVisibilityPhones(dto.visibilityPhones);
await tx.storeVisibilityPhone.deleteMany({ where: { storeId: id } });
if (phones.length) {
await tx.storeVisibilityPhone.createMany({
data: phones.map((phone) => ({ storeId: id, phone })),
});
}
}
if (dto.coverUrl) {
if (current.coverResourceId) {
await tx.commonResource.update({
where: { id: current.coverResourceId },
data: { url: dto.coverUrl, ossKey: dto.coverUrl },
});
} else {
const cover = await tx.commonResource.create({
data: {
ownerType: 'STORE',
ownerId: id,
bizType: 'COVER',
mediaType: 'IMAGE',
ossBucket: 'legacy',
ossKey: dto.coverUrl,
url: dto.coverUrl,
},
});
await tx.store.update({ where: { id }, data: { coverResourceId: cover.id } });
}
}
if (primaryBinding?.storeAccount) {
const account = primaryBinding.storeAccount;
const accountData: {
name?: string;
phone?: string;
bankAccountName?: string | null;
bankAccountNo?: string | null;
bankBranch?: string | null;
} = {};
if (dto.name !== undefined) accountData.name = dto.name.trim();
if (normalizedPhone !== undefined && normalizedPhone !== account.phone) {
accountData.phone = normalizedPhone;
}
if (dto.bankAccountName !== undefined) {
accountData.bankAccountName = dto.bankAccountName?.trim() || null;
}
if (dto.bankAccountNo !== undefined) {
accountData.bankAccountNo = dto.bankAccountNo?.trim() || null;
}
if (dto.bankBranch !== undefined) {
accountData.bankBranch = dto.bankBranch?.trim() || null;
}
if (Object.keys(accountData).length) {
await tx.storeAccount.update({
where: { id: account.id },
data: accountData,
});
}
}
});
return this.detailStore(id);
}
async createStore(dto: CreateStoreDto) {
const normalizedPhone = dto.phone.trim();
if (!/^1[3-9]\d{9}$/.test(normalizedPhone)) {
throw new BadRequestException('请输入正确的手机号码');
}
const existingAccount = await this.prisma.storeAccount.findUnique({
where: { phone: normalizedPhone },
});
if (existingAccount && existingAccount.isPrimary !== 1) {
throw new BadRequestException('该手机号已是门店子账号');
}
if (existingAccount && existingAccount.status !== 'ACTIVE') {
throw new BadRequestException('该手机号对应门店账号已停用');
}
const partnerAccountId = BigInt(dto.partnerAccountId);
const partnerAccount = await this.prisma.partnerAccount.findUnique({
where: { id: partnerAccountId },
});
if (!partnerAccount || partnerAccount.isPrimary !== 1) {
throw new BadRequestException('开城合伙人不存在');
}
const city = await this.prisma.commonCity.findUnique({ where: { id: BigInt(dto.cityId) } });
if (!city) throw new BadRequestException('开城城市不存在');
await this.partnerCityService.assertPartnerAccountBoundToCity(partnerAccountId, city.id);
if (!dto.categoryId?.trim()) {
throw new BadRequestException('请选择门店分类');
}
const categoryId = BigInt(dto.categoryId);
await this.storeCategoryService.assertLeafCategoryId(categoryId);
const latitude = dto.latitude != null ? Number(dto.latitude) : null;
const longitude = dto.longitude != null ? Number(dto.longitude) : null;
if ((latitude == null) !== (longitude == null)) {
throw new BadRequestException('经纬度须同时提供');
}
if (
latitude != null &&
(!Number.isFinite(latitude) || !Number.isFinite(longitude!) || latitude < -90 || latitude > 90)
) {
throw new BadRequestException('经纬度无效');
}
const openTime = dto.openTime?.trim() || '10:00';
const closeTime = dto.closeTime?.trim() || '22:00';
const openTime2 = dto.openTime2?.trim() || '';
const closeTime2 = dto.closeTime2?.trim() || '';
const hoursCheck = validateBusinessHours([
{ open: openTime, close: closeTime },
...(openTime2 || closeTime2 ? [{ open: openTime2, close: closeTime2 }] : []),
]);
if (!hoursCheck.ok) throw new BadRequestException(hoursCheck.message);
const intro = dto.intro?.trim() || null;
if (intro && (intro.length < 2 || intro.length > 500)) {
throw new BadRequestException('门店简介须为 2~500 字');
}
const benefitUsageRule = normalizeStoreOptionalText(dto.benefitUsageRule);
if (benefitUsageRule && benefitUsageRule.length > 1000) {
throw new BadRequestException('好客权益券使用规则最多 1000 字');
}
const visibilityPhones = normalizeVisibilityPhones(dto.visibilityPhones);
const whitelistEnabled = !!dto.visibilityWhitelistEnabled;
if (whitelistEnabled && !visibilityPhones.length) {
throw new BadRequestException('开启白名单时请至少添加一个手机号');
}
const store = await this.prisma.store.create({
data: {
cityId: city.id,
partnerAccountId,
settlementRate: dto.settlementRate ?? 0.6,
categoryId,
name: dto.name,
phone: normalizedPhone,
province: dto.province ?? city.province,
cityName: dto.city ?? city.name,
district: dto.district ?? '',
address: dto.address,
intro,
benefitUsageRule,
avgPrice: dto.avgPrice ?? null,
openTime,
closeTime,
openTime2: openTime2 || null,
closeTime2: closeTime2 || null,
visibilityWhitelistEnabled: whitelistEnabled,
...(latitude != null && longitude != null ? { latitude, longitude } : {}),
status: 'OPEN',
auditStatus: 'APPROVED',
auditedAt: new Date(),
rejectReason: null,
...(visibilityPhones.length
? {
visibilityPhones: {
create: visibilityPhones.map((phone) => ({ phone })),
},
}
: {}),
},
});
if (dto.coverUrl) {
const cover = await this.prisma.commonResource.create({
data: {
ownerType: 'STORE',
ownerId: store.id,
bizType: 'COVER',
mediaType: 'IMAGE',
ossBucket: 'legacy',
ossKey: dto.coverUrl,
url: dto.coverUrl,
},
});
await this.prisma.store.update({ where: { id: store.id }, data: { coverResourceId: cover.id } });
}
const envUrls = (dto.envPhotoUrls ?? []).filter(Boolean);
for (let i = 0; i < envUrls.length; i++) {
await this.prisma.commonResource.create({
data: {
ownerType: 'STORE',
ownerId: store.id,
bizType: 'ENV',
mediaType: 'IMAGE',
ossBucket: 'legacy',
ossKey: envUrls[i],
url: envUrls[i],
sortOrder: i,
},
});
}
if (dto.contractUrl) {
await this.prisma.commonResource.create({
data: {
ownerType: 'STORE',
ownerId: store.id,
bizType: 'CONTRACT',
mediaType: 'FILE',
ossBucket: 'legacy',
ossKey: dto.contractUrl,
url: dto.contractUrl,
},
});
}
await this.prisma.commonEvent.create({
data: {
eventType: 'STORE_AUDIT',
refType: 'STORE',
refId: store.id,
actorType: 'HQ',
status: 'APPROVED',
param1: 'NEW',
param1Desc: 'audit_type',
remark: 'HQ 后台新建',
},
});
const bankAccountName = dto.bankAccountName ?? null;
const bankAccountNo = dto.bankAccountNo ?? null;
const bankBranch = dto.bankBranch ?? null;
if (existingAccount) {
await this.prisma.storeAccountStore.create({
data: { storeAccountId: existingAccount.id, storeId: store.id },
});
if (bankAccountName || bankAccountNo || bankBranch) {
await this.prisma.storeAccount.update({
where: { id: existingAccount.id },
data: {
...(bankAccountName != null ? { bankAccountName } : {}),
...(bankAccountNo != null ? { bankAccountNo } : {}),
...(bankBranch != null ? { bankBranch } : {}),
},
});
}
} else {
await this.prisma.storeAccount.create({
data: {
phone: normalizedPhone,
name: dto.accountName ?? dto.name,
isPrimary: 1,
bankAccountName,
bankAccountNo,
bankBranch,
bindings: { create: [{ storeId: store.id }] },
},
});
}
return this.detailStore(store.id);
}
async createStoreAccount(dto: CreateStoreAccountDto) {
const store = await this.prisma.store.findUnique({
where: { id: BigInt(dto.storeId) },
include: { bindings: true },
});
if (!store) throw new BadRequestException('门店不存在');
const primaryBound = store.bindings.length > 0
? await this.prisma.storeAccount.findFirst({
where: {
isPrimary: 1,
bindings: { some: { storeId: store.id } },
},
})
: null;
if (primaryBound) throw new BadRequestException('门店已有主账号绑定');
const existing = await this.prisma.storeAccount.findUnique({ where: { phone: dto.phone } });
if (existing) {
if (existing.isPrimary !== 1) {
throw new BadRequestException('该手机号已是门店子账号');
}
await this.prisma.storeAccountStore.create({
data: { storeAccountId: existing.id, storeId: store.id },
});
return serializeBigInt(existing);
}
const account = await this.prisma.storeAccount.create({
data: {
phone: dto.phone,
name: dto.name,
isPrimary: 1,
bindings: { create: [{ storeId: store.id }] },
},
});
return serializeBigInt(account);
}
async listStoreMedia(query: AdminStoreMediaQueryDto) {
const page = query.page ?? 1;
const pageSize = query.pageSize ?? 20;
const where: Prisma.CommonResourceWhereInput = {
ownerType: 'STORE',
status: 'ACTIVE',
};
if (query.storeId) where.ownerId = BigInt(query.storeId);
if (query.mediaType) where.mediaType = query.mediaType as Prisma.EnumResourceMediaTypeFilter['equals'];
const [items, total] = await Promise.all([
this.prisma.commonResource.findMany({
where,
orderBy: [{ sortOrder: 'asc' }, { createdAt: 'desc' }],
skip: (page - 1) * pageSize,
take: pageSize,
}),
this.prisma.commonResource.count({ where }),
]);
return serializeBigInt({ items, total, page, pageSize });
}
async createStoreMedia(dto: CreateStoreMediaDto) {
const store = await this.prisma.store.findUnique({ where: { id: BigInt(dto.storeId) } });
if (!store) throw new BadRequestException('门店不存在');
const media = await this.prisma.commonResource.create({
data: {
ownerType: 'STORE',
ownerId: store.id,
bizType: 'ENV',
mediaType: dto.mediaType as 'IMAGE' | 'VIDEO',
ossBucket: 'legacy',
ossKey: dto.url,
url: dto.url,
sortOrder: dto.sortOrder ?? 0,
},
});
return serializeBigInt(media);
}
async updateStoreMedia(id: bigint, dto: UpdateStoreMediaDto) {
const media = await this.prisma.commonResource.update({
where: { id },
data: {
...(dto.url !== undefined ? { url: dto.url, ossKey: dto.url } : {}),
...(dto.mediaType !== undefined ? { mediaType: dto.mediaType as 'IMAGE' | 'VIDEO' } : {}),
...(dto.sortOrder !== undefined ? { sortOrder: dto.sortOrder } : {}),
},
});
return serializeBigInt(media);
}
async deleteStoreMedia(id: bigint) {
await this.prisma.commonResource.update({
where: { id },
data: { status: 'DELETED' },
});
return { ok: true };
}
async listStoreAccounts(query: AdminStoreAccountsQueryDto) {
const page = query.page ?? 1;
const pageSize = query.pageSize ?? 20;
const where: Prisma.StoreAccountWhereInput = { isPrimary: 1 };
if (query.phone) where.phone = { contains: query.phone };
if (query.storeId) {
where.bindings = { some: { storeId: BigInt(query.storeId) } };
}
if (query.status) where.status = query.status as Prisma.EnumAccountStatusFilter['equals'];
const [items, total] = await Promise.all([
this.prisma.storeAccount.findMany({
where,
orderBy: { createdAt: 'desc' },
skip: (page - 1) * pageSize,
take: pageSize,
include: {
bindings: {
include: {
store: { select: { id: true, name: true, status: true, cityName: true } },
},
},
_count: { select: { childAccounts: true, bindings: true } },
},
}),
this.prisma.storeAccount.count({ where }),
]);
const mapped = items.map((row) => ({
...row,
storeCount: row._count.bindings,
staffCount: row._count.childAccounts,
stores: row.bindings.map((b) => b.store),
store: row.bindings[0]?.store ?? null,
}));
return serializeBigInt({ items: mapped, total, page, pageSize });
}
async detailStoreAccount(id: bigint) {
const account = await this.prisma.storeAccount.findUnique({
where: { id },
include: {
bindings: {
include: {
store: { include: { cityRef: true, partnerAccount: true } },
},
},
childAccounts: {
include: {
bindings: {
include: {
store: { select: { id: true, name: true, status: true } },
},
},
},
},
},
});
if (!account) throw new NotFoundException('门店账号不存在');
return serializeBigInt({
...account,
stores: account.bindings.map((b) => b.store),
store: account.bindings[0]?.store ?? null,
staff: account.childAccounts,
});
}
async updateStoreAccount(id: bigint, dto: UpdateStoreAccountDto) {
const account = await this.prisma.storeAccount.update({
where: { id },
data: {
...(dto.name !== undefined ? { name: dto.name } : {}),
...(dto.phone !== undefined ? { phone: dto.phone } : {}),
...(dto.status !== undefined ? { status: dto.status as 'ACTIVE' | 'DISABLED' } : {}),
},
});
return serializeBigInt(account);
}
/** HQ 删除门店子账号(非主账号) */
async deleteStoreStaff(parentAccountId: bigint, staffId: bigint) {
const parent = await this.prisma.storeAccount.findUnique({ where: { id: parentAccountId } });
if (!parent || parent.isPrimary !== 1) {
throw new BadRequestException('主账号不存在');
}
const staff = await this.prisma.storeAccount.findFirst({
where: { id: staffId, parentAccountId, isPrimary: 0 },
});
if (!staff) throw new NotFoundException('子账号不存在');
const pending = await this.prisma.redeemPendingRecord.count({
where: { storeAccountId: staffId },
});
if (pending > 0) {
throw new BadRequestException('该子账号仍有待处理核销单,无法删除');
}
await this.prisma.storeAccount.delete({ where: { id: staffId } });
return { ok: true };
}
}