feat(store): v4.0.18 门店多收款账户与子账号继承二维码
C 端门店列表拼接省市区县地址;门店多银行账户与默认打款账户;子账号独立 sa_ 关联码及统计维度;同步 v4.0.18 开发文档。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,43 @@
|
||||
-- v4.0.18:门店多收款银行账户(列表 + 默认账户)
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `store_bank_account` (
|
||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`store_id` BIGINT UNSIGNED NOT NULL,
|
||||
`bank_account_name` VARCHAR(64) NOT NULL,
|
||||
`bank_account_no` VARCHAR(32) NOT NULL,
|
||||
`bank_branch` VARCHAR(128) NULL,
|
||||
`is_default` TINYINT NOT NULL DEFAULT 0,
|
||||
`status` ENUM('ACTIVE','DISABLED') NOT NULL DEFAULT 'ACTIVE',
|
||||
`sort_order` INT NOT NULL DEFAULT 0,
|
||||
`created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
`updated_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_store_bank_account_store_status` (`store_id`, `status`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='门店收款银行账户';
|
||||
|
||||
-- 回填:按门店主账号(is_primary=1,缺失则取最早绑定账号)的收款字段,为每店生成一条默认账户
|
||||
INSERT INTO `store_bank_account`
|
||||
(`store_id`, `bank_account_name`, `bank_account_no`, `bank_branch`, `is_default`, `status`, `sort_order`)
|
||||
SELECT
|
||||
b.`store_id`,
|
||||
a.`bank_account_name`,
|
||||
a.`bank_account_no`,
|
||||
a.`bank_branch`,
|
||||
1,
|
||||
'ACTIVE',
|
||||
0
|
||||
FROM `store_account_store` b
|
||||
JOIN `store_account` a ON a.`id` = b.`store_account_id`
|
||||
WHERE a.`bank_account_name` IS NOT NULL
|
||||
AND TRIM(a.`bank_account_name`) <> ''
|
||||
AND a.`bank_account_no` IS NOT NULL
|
||||
AND TRIM(a.`bank_account_no`) <> ''
|
||||
AND b.`store_account_id` = (
|
||||
SELECT b2.`store_account_id`
|
||||
FROM `store_account_store` b2
|
||||
JOIN `store_account` a2 ON a2.`id` = b2.`store_account_id`
|
||||
WHERE b2.`store_id` = b.`store_id`
|
||||
ORDER BY (a2.`is_primary` = 1) DESC, a2.`id` ASC
|
||||
LIMIT 1
|
||||
)
|
||||
ON DUPLICATE KEY UPDATE `updated_at` = `updated_at`;
|
||||
@@ -0,0 +1,5 @@
|
||||
-- v4.0.18:子账号独立继承二维码 —— 记录带来用户的子账号归属
|
||||
|
||||
ALTER TABLE `user_user`
|
||||
ADD COLUMN `assoc_sub_account_id` BIGINT UNSIGNED NULL AFTER `assoc_partner_account_id`,
|
||||
ADD KEY `idx_user_user_assoc_sub_account_id` (`assoc_sub_account_id`);
|
||||
@@ -1278,6 +1278,7 @@ model PartnerAccount {
|
||||
stores Store[]
|
||||
bills PartnerBill[]
|
||||
assocUsers User[] @relation("UserPartnerAssoc")
|
||||
assocSubUsers User[] @relation("UserSubAccountAssoc")
|
||||
userNotes PartnerUserNote[]
|
||||
assocQrcodeResource CommonResource? @relation("PartnerAssocQrcode", fields: [assocQrcodeResourceId], references: [id], onDelete: SetNull)
|
||||
activityPoster ActivityPoster? @relation(fields: [activityPosterId], references: [id], onDelete: SetNull)
|
||||
@@ -1457,6 +1458,7 @@ model User {
|
||||
sourceLabel String? @map("source_label") @db.VarChar(128)
|
||||
referrerUserId BigInt? @map("referrer_user_id") @db.UnsignedBigInt
|
||||
assocPartnerAccountId BigInt? @map("assoc_partner_account_id") @db.UnsignedBigInt
|
||||
assocSubAccountId BigInt? @map("assoc_sub_account_id") @db.UnsignedBigInt
|
||||
assocBoundAt DateTime? @map("assoc_bound_at") @db.DateTime(3)
|
||||
createdAt DateTime @default(now()) @map("created_at") @db.DateTime(3)
|
||||
updatedAt DateTime @updatedAt @map("updated_at") @db.DateTime(3)
|
||||
@@ -1466,6 +1468,7 @@ model User {
|
||||
referrer User? @relation("UserReferrer", fields: [referrerUserId], references: [id], onDelete: SetNull)
|
||||
referrers User[] @relation("UserReferrer")
|
||||
assocPartner PartnerAccount? @relation("UserPartnerAssoc", fields: [assocPartnerAccountId], references: [id], onDelete: SetNull)
|
||||
assocSubAccount PartnerAccount? @relation("UserSubAccountAssoc", fields: [assocSubAccountId], references: [id], onDelete: SetNull)
|
||||
partnerNotes PartnerUserNote[]
|
||||
avatar CommonResource? @relation("UserAvatar", fields: [avatarResourceId], references: [id], onDelete: SetNull)
|
||||
addresses UserAddress[]
|
||||
@@ -1482,6 +1485,7 @@ model User {
|
||||
@@index([sourceType, sourceRefId])
|
||||
@@index([referrerUserId])
|
||||
@@index([assocPartnerAccountId])
|
||||
@@index([assocSubAccountId])
|
||||
@@index([mergedIntoUserId])
|
||||
@@index([wxOpenId])
|
||||
@@index([isTest])
|
||||
@@ -1598,6 +1602,7 @@ model Store {
|
||||
packageChangeRequests StorePackageChangeRequest[]
|
||||
infoChangeRequests StoreInfoChangeRequest[]
|
||||
categoryLinks StoreCategoryLink[]
|
||||
bankAccounts StoreBankAccount[]
|
||||
|
||||
@@index([cityId, status])
|
||||
@@index([partnerAccountId])
|
||||
@@ -1607,6 +1612,25 @@ model Store {
|
||||
@@map("store_store")
|
||||
}
|
||||
|
||||
/// 门店收款银行账户(一个门店可维护多个,is_default 为打款默认账户)
|
||||
model StoreBankAccount {
|
||||
id BigInt @id @default(autoincrement()) @db.UnsignedBigInt
|
||||
storeId BigInt @map("store_id") @db.UnsignedBigInt
|
||||
bankAccountName String @map("bank_account_name") @db.VarChar(64)
|
||||
bankAccountNo String @map("bank_account_no") @db.VarChar(32)
|
||||
bankBranch String? @map("bank_branch") @db.VarChar(128)
|
||||
isDefault Int @default(0) @map("is_default") @db.TinyInt
|
||||
status AccountStatus @default(ACTIVE)
|
||||
sortOrder Int @default(0) @map("sort_order")
|
||||
createdAt DateTime @default(now()) @map("created_at") @db.DateTime(3)
|
||||
updatedAt DateTime @updatedAt @map("updated_at") @db.DateTime(3)
|
||||
|
||||
store Store @relation(fields: [storeId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([storeId, status])
|
||||
@@map("store_bank_account")
|
||||
}
|
||||
|
||||
/// Store visibility whitelist phones (match by bound user phone)
|
||||
model StoreVisibilityPhone {
|
||||
id BigInt @id @default(autoincrement()) @db.UnsignedBigInt
|
||||
|
||||
@@ -6,6 +6,55 @@ export type StoreBankAccountSnapshot = {
|
||||
bankBranch: string | null;
|
||||
};
|
||||
|
||||
export type StoreBankAccountRow = StoreBankAccountSnapshot & {
|
||||
id: bigint;
|
||||
storeId: bigint;
|
||||
isDefault: boolean;
|
||||
status: string;
|
||||
sortOrder: number;
|
||||
};
|
||||
|
||||
/** 门店收款账户列表(默认账户优先,其次 sortOrder/创建顺序) */
|
||||
export async function loadStoreBankAccounts(
|
||||
prisma: PrismaClient,
|
||||
storeId: bigint,
|
||||
): Promise<StoreBankAccountRow[]> {
|
||||
const rows = await prisma.storeBankAccount.findMany({
|
||||
where: { storeId, status: 'ACTIVE' },
|
||||
orderBy: [{ isDefault: 'desc' }, { sortOrder: 'asc' }, { id: 'asc' }],
|
||||
});
|
||||
return rows.map((r) => ({
|
||||
id: r.id,
|
||||
storeId: r.storeId,
|
||||
bankAccountName: r.bankAccountName,
|
||||
bankAccountNo: r.bankAccountNo,
|
||||
bankBranch: r.bankBranch,
|
||||
isDefault: r.isDefault === 1,
|
||||
status: r.status,
|
||||
sortOrder: r.sortOrder,
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* 门店默认收款账户:优先 is_default=1;否则第一个 ACTIVE;再退回旧主账号字段(兼容未迁移数据)。
|
||||
* 返回 null 表示无收款账户。
|
||||
*/
|
||||
export async function loadStoreDefaultBank(
|
||||
prisma: PrismaClient,
|
||||
storeId: bigint,
|
||||
): Promise<StoreBankAccountSnapshot | null> {
|
||||
const accounts = await loadStoreBankAccounts(prisma, storeId);
|
||||
if (accounts.length) {
|
||||
const def = accounts.find((a) => a.isDefault) ?? accounts[0];
|
||||
return {
|
||||
bankAccountName: def.bankAccountName,
|
||||
bankAccountNo: def.bankAccountNo,
|
||||
bankBranch: def.bankBranch,
|
||||
};
|
||||
}
|
||||
return loadStorePrimaryBank(prisma, storeId);
|
||||
}
|
||||
|
||||
export async function loadStorePrimaryBank(
|
||||
prisma: PrismaClient,
|
||||
storeId: bigint,
|
||||
@@ -37,28 +86,50 @@ export async function loadStorePrimaryBanksMap(
|
||||
): Promise<Map<string, StoreBankAccountSnapshot>> {
|
||||
const map = new Map<string, StoreBankAccountSnapshot>();
|
||||
if (!storeIds.length) return map;
|
||||
const bindings = await prisma.storeAccountStore.findMany({
|
||||
where: { storeId: { in: storeIds } },
|
||||
include: {
|
||||
storeAccount: {
|
||||
select: {
|
||||
isPrimary: true,
|
||||
bankAccountName: true,
|
||||
bankAccountNo: true,
|
||||
bankBranch: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: [{ storeAccount: { isPrimary: 'desc' } }, { storeAccountId: 'asc' }],
|
||||
|
||||
// 优先从门店多账户表读取默认账户
|
||||
const accounts = await prisma.storeBankAccount.findMany({
|
||||
where: { storeId: { in: storeIds }, status: 'ACTIVE' },
|
||||
orderBy: [{ storeId: 'asc' }, { isDefault: 'desc' }, { sortOrder: 'asc' }, { id: 'asc' }],
|
||||
});
|
||||
for (const binding of bindings) {
|
||||
const key = String(binding.storeId);
|
||||
const seenStoreIds = new Set<string>();
|
||||
for (const a of accounts) {
|
||||
const key = String(a.storeId);
|
||||
if (map.has(key)) continue;
|
||||
map.set(key, {
|
||||
bankAccountName: binding.storeAccount.bankAccountName,
|
||||
bankAccountNo: binding.storeAccount.bankAccountNo,
|
||||
bankBranch: binding.storeAccount.bankBranch,
|
||||
bankAccountName: a.bankAccountName,
|
||||
bankAccountNo: a.bankAccountNo,
|
||||
bankBranch: a.bankBranch,
|
||||
});
|
||||
seenStoreIds.add(key);
|
||||
}
|
||||
|
||||
// 未迁移/无多账户的门店,回退旧主账号字段
|
||||
const missingIds = storeIds.filter((id) => !seenStoreIds.has(String(id)));
|
||||
if (missingIds.length) {
|
||||
const bindings = await prisma.storeAccountStore.findMany({
|
||||
where: { storeId: { in: missingIds } },
|
||||
include: {
|
||||
storeAccount: {
|
||||
select: {
|
||||
isPrimary: true,
|
||||
bankAccountName: true,
|
||||
bankAccountNo: true,
|
||||
bankBranch: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: [{ storeAccount: { isPrimary: 'desc' } }, { storeAccountId: 'asc' }],
|
||||
});
|
||||
for (const binding of bindings) {
|
||||
const key = String(binding.storeId);
|
||||
if (map.has(key)) continue;
|
||||
map.set(key, {
|
||||
bankAccountName: binding.storeAccount.bankAccountName,
|
||||
bankAccountNo: binding.storeAccount.bankAccountNo,
|
||||
bankBranch: binding.storeAccount.bankBranch,
|
||||
});
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Injectable, NotFoundException } from '@nestjs/common';
|
||||
import type { DeliveryProvider } from '@prisma/client';
|
||||
import { Prisma } from '@prisma/client';
|
||||
import { PrismaService } from '../../common/prisma/prisma.module';
|
||||
import { loadStorePrimaryBank } from '../../common/store/store-bank.util';
|
||||
import { loadStoreDefaultBank } from '../../common/store/store-bank.util';
|
||||
import { serializeBigInt } from '../../common/decorators/current-user.decorator';
|
||||
import { FulfillmentProviderService } from '../fulfillment/fulfillment-provider.service';
|
||||
import { calcDeliveryFreightAmount } from '../fulfillment/delivery-freight.util';
|
||||
@@ -116,7 +116,7 @@ export class AdminRedeemService {
|
||||
},
|
||||
});
|
||||
if (!record) throw new NotFoundException('核销记录不存在');
|
||||
const primaryAccount = await loadStorePrimaryBank(this.prisma, record.storeId);
|
||||
const primaryAccount = await loadStoreDefaultBank(this.prisma, record.storeId);
|
||||
return serializeBigInt({
|
||||
...record,
|
||||
store: {
|
||||
|
||||
@@ -47,7 +47,7 @@ import { AnalyticsService } from '../analytics/analytics.service';
|
||||
import { PartnerCityService } from '../city-scope/partner-city.service';
|
||||
import { FulfillmentProviderService } from '../fulfillment/fulfillment-provider.service';
|
||||
import {
|
||||
loadStorePrimaryBank,
|
||||
loadStoreDefaultBank,
|
||||
loadStorePrimaryBanksMap,
|
||||
loadWineryBankConfig,
|
||||
formatWecomBankAccount,
|
||||
@@ -540,16 +540,14 @@ export class SettlementService implements OnModuleInit {
|
||||
|
||||
async getShopWithdrawSummary(storeAccountId: bigint, storeId: bigint) {
|
||||
await this.assertShopStoreAccess(storeAccountId, storeId);
|
||||
const [account, available, pending, todayApplied] = await Promise.all([
|
||||
const [account, bank, available, pending, todayApplied] = await Promise.all([
|
||||
this.prisma.storeAccount.findUniqueOrThrow({
|
||||
where: { id: storeAccountId },
|
||||
select: {
|
||||
isPrimary: true,
|
||||
bankAccountName: true,
|
||||
bankAccountNo: true,
|
||||
bankBranch: true,
|
||||
},
|
||||
}),
|
||||
loadStoreDefaultBank(this.prisma, storeId),
|
||||
this.listAvailableUnbilledPayouts(storeId),
|
||||
this.prisma.storeWithdrawRequest.findFirst({
|
||||
where: { storeId, status: 'PENDING_REVIEW' },
|
||||
@@ -562,10 +560,7 @@ export class SettlementService implements OnModuleInit {
|
||||
available.map((p) => ({ payoutAmount: Number(p.payoutAmount) })),
|
||||
);
|
||||
const dailyLimit = getStoreWithdrawDailyLimit();
|
||||
const hasBankAccount = !!(
|
||||
account.bankAccountName?.trim() &&
|
||||
account.bankAccountNo?.trim()
|
||||
);
|
||||
const hasBankAccount = !!(bank?.bankAccountName?.trim() && bank?.bankAccountNo?.trim());
|
||||
|
||||
return {
|
||||
availableAmount,
|
||||
@@ -575,11 +570,13 @@ export class SettlementService implements OnModuleInit {
|
||||
remainingDailyLimit: Math.max(0, round2(dailyLimit - todayApplied)),
|
||||
isPrimary: account.isPrimary === 1,
|
||||
hasBankAccount,
|
||||
bankAccount: {
|
||||
bankAccountName: account.bankAccountName,
|
||||
bankAccountNo: account.bankAccountNo,
|
||||
bankBranch: account.bankBranch,
|
||||
},
|
||||
bankAccount: bank
|
||||
? {
|
||||
bankAccountName: bank.bankAccountName,
|
||||
bankAccountNo: bank.bankAccountNo,
|
||||
bankBranch: bank.bankBranch,
|
||||
}
|
||||
: null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -590,7 +587,7 @@ export class SettlementService implements OnModuleInit {
|
||||
) {
|
||||
await this.assertShopStoreAccess(storeAccountId, storeId);
|
||||
|
||||
const [store, account, available, pending, todayApplied] = await Promise.all([
|
||||
const [store, account, bank, available, pending, todayApplied] = await Promise.all([
|
||||
this.prisma.store.findUniqueOrThrow({
|
||||
where: { id: storeId },
|
||||
select: { id: true, name: true, phone: true, cityName: true },
|
||||
@@ -599,10 +596,9 @@ export class SettlementService implements OnModuleInit {
|
||||
where: { id: storeAccountId },
|
||||
select: {
|
||||
isPrimary: true,
|
||||
bankAccountName: true,
|
||||
bankAccountNo: true,
|
||||
},
|
||||
}),
|
||||
loadStoreDefaultBank(this.prisma, storeId),
|
||||
this.listAvailableUnbilledPayouts(storeId),
|
||||
this.prisma.storeWithdrawRequest.findFirst({
|
||||
where: { storeId, status: 'PENDING_REVIEW' },
|
||||
@@ -619,10 +615,7 @@ export class SettlementService implements OnModuleInit {
|
||||
available.map((p) => ({ payoutAmount: Number(p.payoutAmount) })),
|
||||
);
|
||||
const dailyLimit = getStoreWithdrawDailyLimit();
|
||||
const hasBankAccount = !!(
|
||||
account.bankAccountName?.trim() &&
|
||||
account.bankAccountNo?.trim()
|
||||
);
|
||||
const hasBankAccount = !!(bank?.bankAccountName?.trim() && bank?.bankAccountNo?.trim());
|
||||
const requestAmount =
|
||||
dto?.amount != null && Number.isFinite(Number(dto.amount))
|
||||
? round2(Number(dto.amount))
|
||||
@@ -827,8 +820,10 @@ export class SettlementService implements OnModuleInit {
|
||||
},
|
||||
});
|
||||
if (!row) throw new NotFoundException('提现申请不存在');
|
||||
const bankAccount = await loadStoreDefaultBank(this.prisma, row.storeId);
|
||||
return serializeBigInt({
|
||||
...row,
|
||||
bankAccount,
|
||||
paymentProofUrls: parsePaymentProofUrls(row.paymentProofUrls),
|
||||
overdue:
|
||||
row.status === 'PENDING_REVIEW' ? isWithdrawOverdue(row.appliedAt) : false,
|
||||
@@ -900,7 +895,7 @@ export class SettlementService implements OnModuleInit {
|
||||
},
|
||||
},
|
||||
}),
|
||||
loadStorePrimaryBank(this.prisma, row.storeId),
|
||||
loadStoreDefaultBank(this.prisma, row.storeId),
|
||||
]);
|
||||
const meta = storeWecomMeta(store, String(row.storeId));
|
||||
const bankParts = wecomBankParts(bank);
|
||||
@@ -1543,7 +1538,7 @@ export class SettlementService implements OnModuleInit {
|
||||
},
|
||||
});
|
||||
if (!bill) throw new NotFoundException('门店对账单不存在');
|
||||
const storeAccount = await loadStorePrimaryBank(this.prisma, bill.storeId);
|
||||
const storeAccount = await loadStoreDefaultBank(this.prisma, bill.storeId);
|
||||
return serializeBigInt({
|
||||
...bill,
|
||||
...mapStoreBillDates(bill.billDate),
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
Logger,
|
||||
NotFoundException,
|
||||
} from '@nestjs/common';
|
||||
import { Prisma } from '@prisma/client';
|
||||
import { Prisma, type PartnerAccount } from '@prisma/client';
|
||||
import { PrismaService } from '../../common/prisma/prisma.module';
|
||||
import { serializeBigInt } from '../../common/decorators/current-user.decorator';
|
||||
import { PartnerCityService } from '../city-scope/partner-city.service';
|
||||
@@ -13,7 +13,8 @@ import { OSS_PROVIDER, WECHAT_PROVIDER } from '../../integrations/integrations.c
|
||||
import type { IOssProvider } from '../../integrations/oss/oss.interface';
|
||||
import type { IWechatProvider } from '../../integrations/wechat/wechat.interface';
|
||||
|
||||
const ASSOC_SCENE_PREFIX = 'pa_';
|
||||
const PRIMARY_SCENE_PREFIX = 'pa_';
|
||||
const SUB_SCENE_PREFIX = 'sa_';
|
||||
|
||||
function maskPhoneNumber(phone: string | null) {
|
||||
if (!phone || phone.length < 7) return phone;
|
||||
@@ -27,12 +28,20 @@ function dayBounds(now = new Date()) {
|
||||
return { todayStart, monthStart };
|
||||
}
|
||||
|
||||
export function parseAssocScene(raw?: string | null): string | null {
|
||||
export type AssocScene = { kind: 'primary' | 'sub'; id: string };
|
||||
|
||||
export function parseAssocScene(raw?: string | null): AssocScene | null {
|
||||
const s = String(raw ?? '').trim();
|
||||
if (!s) return null;
|
||||
if (s.startsWith(ASSOC_SCENE_PREFIX)) {
|
||||
const id = s.slice(ASSOC_SCENE_PREFIX.length);
|
||||
return /^\d+$/.test(id) ? id : null;
|
||||
const prefixes: Array<[string, AssocScene['kind']]> = [
|
||||
[PRIMARY_SCENE_PREFIX, 'primary'],
|
||||
[SUB_SCENE_PREFIX, 'sub'],
|
||||
];
|
||||
for (const [prefix, kind] of prefixes) {
|
||||
if (s.startsWith(prefix)) {
|
||||
const id = s.slice(prefix.length);
|
||||
return /^\d+$/.test(id) ? { kind, id } : null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -48,15 +57,42 @@ export class PartnerAssocService {
|
||||
@Inject(OSS_PROVIDER) private readonly oss: IOssProvider,
|
||||
) {}
|
||||
|
||||
async bindUser(userId: bigint, input: { scene?: string; partnerId?: string }) {
|
||||
const partnerIdRaw = parseAssocScene(input.scene) || input.partnerId?.trim();
|
||||
if (!partnerIdRaw || !/^\d+$/.test(partnerIdRaw)) {
|
||||
throw new BadRequestException('关联码无效');
|
||||
/** 解析 scene/partnerId,返回主账号与(可选的)子账号;校验激活状态 */
|
||||
private async resolveAssocTarget(input: { scene?: string; partnerId?: string }) {
|
||||
const parsed = parseAssocScene(input.scene);
|
||||
let accountId: bigint;
|
||||
let sub: PartnerAccount | null = null;
|
||||
|
||||
if (parsed) {
|
||||
accountId = BigInt(parsed.id);
|
||||
if (parsed.kind === 'sub') {
|
||||
const subAccount = await this.prisma.partnerAccount.findUnique({
|
||||
where: { id: accountId },
|
||||
});
|
||||
if (!subAccount || subAccount.isPrimary === 1 || !subAccount.parentAccountId) {
|
||||
throw new BadRequestException('子账号不存在或无效');
|
||||
}
|
||||
if (subAccount.status !== 'ACTIVE') {
|
||||
throw new BadRequestException('子账号已停用');
|
||||
}
|
||||
sub = subAccount;
|
||||
accountId = subAccount.parentAccountId;
|
||||
}
|
||||
} else {
|
||||
const raw = input.partnerId?.trim();
|
||||
if (!raw || !/^\d+$/.test(raw)) throw new BadRequestException('关联码无效');
|
||||
accountId = BigInt(raw);
|
||||
}
|
||||
const primary = await this.partnerCityService.resolvePrimaryAccount(BigInt(partnerIdRaw));
|
||||
|
||||
const primary = await this.partnerCityService.resolvePrimaryAccount(accountId);
|
||||
if (primary.isPrimary !== 1 || primary.status !== 'ACTIVE') {
|
||||
throw new BadRequestException('合伙人不存在或已停用');
|
||||
}
|
||||
return { primary, sub };
|
||||
}
|
||||
|
||||
async bindUser(userId: bigint, input: { scene?: string; partnerId?: string }) {
|
||||
const { primary, sub } = await this.resolveAssocTarget(input);
|
||||
|
||||
const user = await this.prisma.user.findUnique({ where: { id: userId } });
|
||||
if (!user) throw new NotFoundException('用户不存在');
|
||||
@@ -67,6 +103,7 @@ export class PartnerAssocService {
|
||||
bound: true,
|
||||
alreadyBound: true,
|
||||
partnerId: primary.id.toString(),
|
||||
subAccountId: sub ? sub.id.toString() : null,
|
||||
partnerName: primary.companyName || primary.name,
|
||||
};
|
||||
}
|
||||
@@ -77,6 +114,7 @@ export class PartnerAssocService {
|
||||
where: { id: userId },
|
||||
data: {
|
||||
assocPartnerAccountId: primary.id,
|
||||
assocSubAccountId: sub ? sub.id : null,
|
||||
assocBoundAt: new Date(),
|
||||
...(user.sourceType === 'ORGANIC'
|
||||
? { sourceType: 'PARTNER_ASSOC', sourceRefId: primary.id }
|
||||
@@ -88,31 +126,37 @@ export class PartnerAssocService {
|
||||
bound: true,
|
||||
alreadyBound: false,
|
||||
partnerId: primary.id.toString(),
|
||||
subAccountId: sub ? sub.id.toString() : null,
|
||||
partnerName: primary.companyName || primary.name,
|
||||
};
|
||||
}
|
||||
|
||||
async touchScan(input: { scene?: string; partnerId?: string; countScan?: boolean }) {
|
||||
const partnerIdRaw = parseAssocScene(input.scene) || input.partnerId?.trim();
|
||||
if (!partnerIdRaw || !/^\d+$/.test(partnerIdRaw)) {
|
||||
throw new BadRequestException('关联码无效');
|
||||
}
|
||||
const primary = await this.partnerCityService.resolvePrimaryAccount(BigInt(partnerIdRaw));
|
||||
if (primary.isPrimary !== 1 || primary.status !== 'ACTIVE') {
|
||||
throw new BadRequestException('合伙人不存在或已停用');
|
||||
}
|
||||
const { primary, sub } = await this.resolveAssocTarget(input);
|
||||
const shouldCountScan = input.countScan !== false;
|
||||
let scanCount = primary.assocScanCount ?? 0;
|
||||
let scanCount = 0;
|
||||
if (shouldCountScan) {
|
||||
const updated = await this.prisma.partnerAccount.update({
|
||||
where: { id: primary.id },
|
||||
data: { assocScanCount: { increment: 1 } },
|
||||
select: { assocScanCount: true },
|
||||
});
|
||||
scanCount = updated.assocScanCount;
|
||||
if (sub) {
|
||||
const updated = await this.prisma.partnerAccount.update({
|
||||
where: { id: sub.id },
|
||||
data: { assocScanCount: { increment: 1 } },
|
||||
select: { assocScanCount: true },
|
||||
});
|
||||
scanCount = updated.assocScanCount;
|
||||
} else {
|
||||
const updated = await this.prisma.partnerAccount.update({
|
||||
where: { id: primary.id },
|
||||
data: { assocScanCount: { increment: 1 } },
|
||||
select: { assocScanCount: true },
|
||||
});
|
||||
scanCount = updated.assocScanCount;
|
||||
}
|
||||
} else {
|
||||
scanCount = sub ? (sub.assocScanCount ?? 0) : (primary.assocScanCount ?? 0);
|
||||
}
|
||||
return {
|
||||
partnerId: primary.id.toString(),
|
||||
subAccountId: sub ? sub.id.toString() : null,
|
||||
scanCounted: shouldCountScan,
|
||||
scanCount,
|
||||
};
|
||||
@@ -157,11 +201,35 @@ export class PartnerAssocService {
|
||||
}
|
||||
|
||||
async getSummary(partnerAccountId: bigint) {
|
||||
const primary = await this.partnerCityService.resolvePrimaryAccount(partnerAccountId);
|
||||
const { self, primary, subAccountId } = await this.scopeOf(partnerAccountId);
|
||||
|
||||
if (subAccountId) {
|
||||
const ensured = await this.ensureSubQrcode(subAccountId);
|
||||
const userCount = await this.prisma.user.count({
|
||||
where: { assocPartnerAccountId: primary.id, assocSubAccountId: subAccountId },
|
||||
});
|
||||
return {
|
||||
partnerId: subAccountId.toString(),
|
||||
primaryAccountId: primary.id.toString(),
|
||||
isSubAccount: true,
|
||||
qrcodeUrl: ensured.qrcodeUrl,
|
||||
userCount,
|
||||
scanCount: self.assocScanCount ?? 0,
|
||||
companyName: primary.companyName,
|
||||
name: self.name,
|
||||
activityPosterId: null,
|
||||
};
|
||||
}
|
||||
|
||||
const ensured = await this.ensureQrcode(primary.id);
|
||||
const userCount = await this.prisma.user.count({
|
||||
where: { assocPartnerAccountId: primary.id },
|
||||
});
|
||||
const childrenAgg = await this.prisma.partnerAccount.aggregate({
|
||||
where: { parentAccountId: primary.id },
|
||||
_sum: { assocScanCount: true },
|
||||
});
|
||||
const scanCount = (primary.assocScanCount ?? 0) + Number(childrenAgg._sum.assocScanCount ?? 0);
|
||||
const selectedPoster = primary.activityPosterId
|
||||
? await this.prisma.activityPoster.findUnique({
|
||||
where: { id: primary.activityPosterId },
|
||||
@@ -175,13 +243,24 @@ export class PartnerAssocService {
|
||||
partnerId: primary.id.toString(),
|
||||
qrcodeUrl: ensured.qrcodeUrl,
|
||||
userCount,
|
||||
scanCount: primary.assocScanCount ?? 0,
|
||||
scanCount,
|
||||
companyName: primary.companyName,
|
||||
name: primary.name,
|
||||
activityPosterId: partnerAccountId === primary.id ? activityPosterId : null,
|
||||
activityPosterId,
|
||||
};
|
||||
}
|
||||
|
||||
/** 返回调用者账号、主账号、以及子账号维度(主账号为 null) */
|
||||
private async scopeOf(partnerAccountId: bigint) {
|
||||
const self = await this.prisma.partnerAccount.findUnique({
|
||||
where: { id: partnerAccountId },
|
||||
});
|
||||
if (!self) throw new NotFoundException('合伙人账号不存在');
|
||||
const primary = await this.partnerCityService.resolvePrimaryAccount(partnerAccountId);
|
||||
const subAccountId = self.isPrimary === 1 ? null : self.id;
|
||||
return { self, primary, subAccountId };
|
||||
}
|
||||
|
||||
async getSelectedActivityPosterId(partnerAccountId: bigint): Promise<string | null> {
|
||||
const primary = await this.partnerCityService.resolvePrimaryAccount(partnerAccountId);
|
||||
if (!primary.activityPosterId) return null;
|
||||
@@ -209,12 +288,16 @@ export class PartnerAssocService {
|
||||
}
|
||||
|
||||
async getStats(partnerAccountId: bigint) {
|
||||
const primary = await this.partnerCityService.resolvePrimaryAccount(partnerAccountId);
|
||||
const { primary, subAccountId } = await this.scopeOf(partnerAccountId);
|
||||
const { todayStart, monthStart } = dayBounds();
|
||||
const userWhere = { assocPartnerAccountId: primary.id };
|
||||
const userWhere: Prisma.UserWhereInput = subAccountId
|
||||
? { assocPartnerAccountId: primary.id, assocSubAccountId: subAccountId }
|
||||
: { assocPartnerAccountId: primary.id };
|
||||
const orderWhere = {
|
||||
payStatus: 'PAID' as const,
|
||||
user: { assocPartnerAccountId: primary.id },
|
||||
user: subAccountId
|
||||
? { assocSubAccountId: subAccountId }
|
||||
: { assocPartnerAccountId: primary.id },
|
||||
};
|
||||
const [userTotal, userToday, userMonth, orderTotal, orderToday, orderMonth] = await Promise.all([
|
||||
this.prisma.user.count({ where: userWhere }),
|
||||
@@ -234,9 +317,11 @@ export class PartnerAssocService {
|
||||
maskPhone = false,
|
||||
opts: { keyword?: string; sort?: 'createdAt' | 'boundAt' | 'orderCount' } = {},
|
||||
) {
|
||||
const primary = await this.partnerCityService.resolvePrimaryAccount(partnerAccountId);
|
||||
const { primary, subAccountId } = await this.scopeOf(partnerAccountId);
|
||||
const keyword = opts.keyword?.trim();
|
||||
const where: Prisma.UserWhereInput = { assocPartnerAccountId: primary.id };
|
||||
const where: Prisma.UserWhereInput = subAccountId
|
||||
? { assocPartnerAccountId: primary.id, assocSubAccountId: subAccountId }
|
||||
: { assocPartnerAccountId: primary.id };
|
||||
if (keyword) {
|
||||
where.OR = [
|
||||
{ userNo: { contains: keyword } },
|
||||
@@ -301,7 +386,7 @@ export class PartnerAssocService {
|
||||
}
|
||||
|
||||
async listAssocOrders(partnerAccountId: bigint, page = 1, pageSize = 20, userId?: bigint) {
|
||||
const primary = await this.partnerCityService.resolvePrimaryAccount(partnerAccountId);
|
||||
const { primary, subAccountId } = await this.scopeOf(partnerAccountId);
|
||||
if (userId) {
|
||||
const user = await this.prisma.user.findUnique({
|
||||
where: { id: userId },
|
||||
@@ -313,7 +398,9 @@ export class PartnerAssocService {
|
||||
}
|
||||
const where: Prisma.OrderWhereInput = {
|
||||
payStatus: 'PAID',
|
||||
user: { assocPartnerAccountId: primary.id },
|
||||
user: subAccountId
|
||||
? { assocSubAccountId: subAccountId }
|
||||
: { assocPartnerAccountId: primary.id },
|
||||
...(userId ? { userId } : {}),
|
||||
};
|
||||
const [items, total] = await Promise.all([
|
||||
@@ -421,8 +508,29 @@ export class PartnerAssocService {
|
||||
return { qrcodeId: primary.assocQrcodeId, qrcodeUrl: resource.url };
|
||||
}
|
||||
}
|
||||
const scene = `${PRIMARY_SCENE_PREFIX}${primary.id.toString()}`;
|
||||
return this.generateAccountQrcode(primary, scene);
|
||||
}
|
||||
|
||||
const scene = `${ASSOC_SCENE_PREFIX}${primary.id.toString()}`;
|
||||
/** 生成/复用子账号自己的二维码(scene = sa_{subId}),写入子账号行 */
|
||||
async ensureSubQrcode(subAccountId: bigint, force = false) {
|
||||
const sub = await this.prisma.partnerAccount.findUnique({ where: { id: subAccountId } });
|
||||
if (!sub || sub.isPrimary === 1 || !sub.parentAccountId) {
|
||||
throw new BadRequestException('子账号不存在或无效');
|
||||
}
|
||||
if (!force && sub.assocQrcodeResourceId) {
|
||||
const resource = await this.prisma.commonResource.findUnique({
|
||||
where: { id: sub.assocQrcodeResourceId },
|
||||
});
|
||||
if (resource?.url) {
|
||||
return { qrcodeId: sub.assocQrcodeId, qrcodeUrl: resource.url };
|
||||
}
|
||||
}
|
||||
const scene = `${SUB_SCENE_PREFIX}${sub.id.toString()}`;
|
||||
return this.generateAccountQrcode(sub, scene);
|
||||
}
|
||||
|
||||
private async generateAccountQrcode(account: PartnerAccount, scene: string) {
|
||||
if (scene.length > 32) {
|
||||
throw new BadRequestException('合伙人 ID 过长,无法写入小程序码');
|
||||
}
|
||||
@@ -436,11 +544,11 @@ export class PartnerAssocService {
|
||||
checkPath: false,
|
||||
});
|
||||
} catch (err) {
|
||||
this.logger.warn(`assoc qrcode failed partner=${primary.id}: ${err instanceof Error ? err.message : err}`);
|
||||
this.logger.warn(`assoc qrcode failed account=${account.id}: ${err instanceof Error ? err.message : err}`);
|
||||
throw new BadRequestException('生成关联码失败,请稍后重试');
|
||||
}
|
||||
|
||||
const fileName = `partner-assoc-${primary.id}.png`;
|
||||
const fileName = `partner-assoc-${account.id}.png`;
|
||||
const uploaded = await this.oss.putObject({
|
||||
bizType: 'QRCODE',
|
||||
mediaType: 'IMAGE',
|
||||
@@ -451,7 +559,7 @@ export class PartnerAssocService {
|
||||
const resource = await this.prisma.commonResource.create({
|
||||
data: {
|
||||
ownerType: 'PARTNER',
|
||||
ownerId: primary.id,
|
||||
ownerId: account.id,
|
||||
bizType: 'QRCODE',
|
||||
mediaType: 'IMAGE',
|
||||
ossBucket: uploaded.bucket,
|
||||
@@ -463,7 +571,7 @@ export class PartnerAssocService {
|
||||
},
|
||||
});
|
||||
await this.prisma.partnerAccount.update({
|
||||
where: { id: primary.id },
|
||||
where: { id: account.id },
|
||||
data: { assocQrcodeId: scene, assocQrcodeResourceId: resource.id },
|
||||
});
|
||||
return { qrcodeId: scene, qrcodeUrl: resource.url };
|
||||
@@ -482,16 +590,16 @@ export class PartnerAssocService {
|
||||
|
||||
/** 只读已有 OSS 关联码,不调微信补码。无码返回 null。 */
|
||||
async getExistingQrcodeBuffer(partnerAccountId: bigint): Promise<{ buffer: Buffer; fileName: string } | null> {
|
||||
const primary = await this.partnerCityService.resolvePrimaryAccount(partnerAccountId);
|
||||
if (!primary.assocQrcodeResourceId) return null;
|
||||
const account = await this.partnerCityService.resolvePrimaryAccount(partnerAccountId);
|
||||
if (!account.assocQrcodeResourceId) return null;
|
||||
const resource = await this.prisma.commonResource.findUnique({
|
||||
where: { id: primary.assocQrcodeResourceId },
|
||||
where: { id: account.assocQrcodeResourceId },
|
||||
select: { url: true },
|
||||
});
|
||||
if (!resource?.url) return null;
|
||||
const res = await fetch(resource.url);
|
||||
if (!res.ok) return null;
|
||||
const buffer = Buffer.from(await res.arrayBuffer());
|
||||
return { buffer, fileName: `partner-assoc-${primary.id.toString()}.png` };
|
||||
return { buffer, fileName: `partner-assoc-${account.id.toString()}.png` };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
import { Body, Controller, Delete, Get, Param, Post, Put, UseGuards } from '@nestjs/common';
|
||||
import { IsNotEmpty, IsOptional, IsString, MaxLength } from 'class-validator';
|
||||
import { JwtAuthGuard, AuthUser } from '../../common/guards/jwt-auth.guard';
|
||||
import { ShopStoreGuard } from '../../common/guards/shop-store.guard';
|
||||
import { ShopPrimaryGuard } from '../../common/guards/shop-primary.guard';
|
||||
import { HqAuthGuard } from '../../common/guards/hq-auth.guard';
|
||||
import { CurrentUser } from '../../common/decorators/current-user.decorator';
|
||||
import { StoreBankService } from './store-bank.service';
|
||||
|
||||
class StoreBankDto {
|
||||
@IsString()
|
||||
@IsNotEmpty({ message: '请填写收款人' })
|
||||
@MaxLength(64)
|
||||
bankAccountName!: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty({ message: '请填写银行账号' })
|
||||
@MaxLength(32)
|
||||
bankAccountNo!: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(128)
|
||||
bankBranch?: string;
|
||||
}
|
||||
|
||||
/** 门店端:主账号管理本店收款账户(子账号只读) */
|
||||
@Controller('shop/store/bank-accounts')
|
||||
@UseGuards(JwtAuthGuard, ShopStoreGuard)
|
||||
export class ShopStoreBankController {
|
||||
constructor(private readonly storeBankService: StoreBankService) {}
|
||||
|
||||
@Get()
|
||||
list(@CurrentUser() user: AuthUser) {
|
||||
return this.storeBankService.list(user.storeId!);
|
||||
}
|
||||
|
||||
@Post()
|
||||
@UseGuards(ShopPrimaryGuard)
|
||||
create(@CurrentUser() user: AuthUser, @Body() dto: StoreBankDto) {
|
||||
return this.storeBankService.create(user.storeId!, dto);
|
||||
}
|
||||
|
||||
@Put(':id')
|
||||
@UseGuards(ShopPrimaryGuard)
|
||||
update(@CurrentUser() user: AuthUser, @Param('id') id: string, @Body() dto: StoreBankDto) {
|
||||
return this.storeBankService.update(user.storeId!, BigInt(id), dto);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@UseGuards(ShopPrimaryGuard)
|
||||
remove(@CurrentUser() user: AuthUser, @Param('id') id: string) {
|
||||
return this.storeBankService.remove(user.storeId!, BigInt(id));
|
||||
}
|
||||
|
||||
@Post(':id/default')
|
||||
@UseGuards(ShopPrimaryGuard)
|
||||
setDefault(@CurrentUser() user: AuthUser, @Param('id') id: string) {
|
||||
return this.storeBankService.setDefault(user.storeId!, BigInt(id));
|
||||
}
|
||||
}
|
||||
|
||||
/** 总部端:管理指定门店的收款账户 */
|
||||
@Controller('admin/stores/:storeId/bank-accounts')
|
||||
@UseGuards(HqAuthGuard)
|
||||
export class AdminStoreBankController {
|
||||
constructor(private readonly storeBankService: StoreBankService) {}
|
||||
|
||||
@Get()
|
||||
list(@Param('storeId') storeId: string) {
|
||||
return this.storeBankService.list(BigInt(storeId));
|
||||
}
|
||||
|
||||
@Post()
|
||||
create(@Param('storeId') storeId: string, @Body() dto: StoreBankDto) {
|
||||
return this.storeBankService.create(BigInt(storeId), dto);
|
||||
}
|
||||
|
||||
@Put(':id')
|
||||
update(@Param('storeId') storeId: string, @Param('id') id: string, @Body() dto: StoreBankDto) {
|
||||
return this.storeBankService.update(BigInt(storeId), BigInt(id), dto);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
remove(@Param('storeId') storeId: string, @Param('id') id: string) {
|
||||
return this.storeBankService.remove(BigInt(storeId), BigInt(id));
|
||||
}
|
||||
|
||||
@Post(':id/default')
|
||||
setDefault(@Param('storeId') storeId: string, @Param('id') id: string) {
|
||||
return this.storeBankService.setDefault(BigInt(storeId), BigInt(id));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
import { BadRequestException, ForbiddenException, Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { PrismaService } from '../../common/prisma/prisma.module';
|
||||
|
||||
export interface StoreBankInput {
|
||||
bankAccountName?: string;
|
||||
bankAccountNo?: string;
|
||||
bankBranch?: string;
|
||||
}
|
||||
|
||||
const BANK_NO_RE = /^\d{8,32}$/;
|
||||
|
||||
@Injectable()
|
||||
export class StoreBankService {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
|
||||
private serialize(row: {
|
||||
id: bigint;
|
||||
storeId: bigint;
|
||||
bankAccountName: string;
|
||||
bankAccountNo: string;
|
||||
bankBranch: string | null;
|
||||
isDefault: number;
|
||||
status: string;
|
||||
sortOrder: number;
|
||||
}) {
|
||||
return {
|
||||
id: row.id.toString(),
|
||||
storeId: row.storeId.toString(),
|
||||
bankAccountName: row.bankAccountName,
|
||||
bankAccountNo: row.bankAccountNo,
|
||||
bankBranch: row.bankBranch,
|
||||
isDefault: row.isDefault === 1,
|
||||
status: row.status,
|
||||
sortOrder: row.sortOrder,
|
||||
};
|
||||
}
|
||||
|
||||
private normalize(dto: StoreBankInput) {
|
||||
const bankAccountName = dto.bankAccountName?.trim() ?? '';
|
||||
const bankAccountNo = dto.bankAccountNo?.replace(/\s+/g, '') ?? '';
|
||||
const bankBranch = dto.bankBranch?.trim() || null;
|
||||
if (!bankAccountName) throw new BadRequestException('请填写收款人');
|
||||
if (!BANK_NO_RE.test(bankAccountNo)) throw new BadRequestException('请填写正确的银行账号');
|
||||
return { bankAccountName, bankAccountNo, bankBranch };
|
||||
}
|
||||
|
||||
async list(storeId: bigint) {
|
||||
const rows = await this.prisma.storeBankAccount.findMany({
|
||||
where: { storeId },
|
||||
orderBy: [{ isDefault: 'desc' }, { sortOrder: 'asc' }, { id: 'asc' }],
|
||||
});
|
||||
return rows.map((r) => this.serialize(r));
|
||||
}
|
||||
|
||||
async create(storeId: bigint, dto: StoreBankInput) {
|
||||
const data = this.normalize(dto);
|
||||
const count = await this.prisma.storeBankAccount.count({
|
||||
where: { storeId, status: 'ACTIVE' },
|
||||
});
|
||||
const created = await this.prisma.storeBankAccount.create({
|
||||
data: {
|
||||
storeId,
|
||||
bankAccountName: data.bankAccountName,
|
||||
bankAccountNo: data.bankAccountNo,
|
||||
bankBranch: data.bankBranch,
|
||||
isDefault: count === 0 ? 1 : 0,
|
||||
status: 'ACTIVE',
|
||||
sortOrder: count,
|
||||
},
|
||||
});
|
||||
return this.serialize(created);
|
||||
}
|
||||
|
||||
async update(storeId: bigint, accountId: bigint, dto: StoreBankInput) {
|
||||
const account = await this.findAccount(storeId, accountId);
|
||||
const data = this.normalize(dto);
|
||||
const updated = await this.prisma.storeBankAccount.update({
|
||||
where: { id: account.id },
|
||||
data: {
|
||||
bankAccountName: data.bankAccountName,
|
||||
bankAccountNo: data.bankAccountNo,
|
||||
bankBranch: data.bankBranch,
|
||||
},
|
||||
});
|
||||
return this.serialize(updated);
|
||||
}
|
||||
|
||||
async remove(storeId: bigint, accountId: bigint) {
|
||||
const account = await this.findAccount(storeId, accountId);
|
||||
if (account.isDefault === 1) {
|
||||
throw new BadRequestException('请先取消默认账户再删除');
|
||||
}
|
||||
await this.prisma.storeBankAccount.delete({ where: { id: account.id } });
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
async setDefault(storeId: bigint, accountId: bigint) {
|
||||
const account = await this.findAccount(storeId, accountId);
|
||||
if (account.isDefault === 1) return this.serialize(account);
|
||||
await this.prisma.$transaction([
|
||||
this.prisma.storeBankAccount.updateMany({
|
||||
where: { storeId },
|
||||
data: { isDefault: 0 },
|
||||
}),
|
||||
this.prisma.storeBankAccount.update({
|
||||
where: { id: account.id },
|
||||
data: { isDefault: 1, status: 'ACTIVE' },
|
||||
}),
|
||||
]);
|
||||
const updated = await this.findAccount(storeId, accountId);
|
||||
return this.serialize(updated);
|
||||
}
|
||||
|
||||
private async findAccount(storeId: bigint, accountId: bigint) {
|
||||
const account = await this.prisma.storeBankAccount.findFirst({
|
||||
where: { id: accountId, storeId },
|
||||
});
|
||||
if (!account) throw new NotFoundException('收款账户不存在');
|
||||
return account;
|
||||
}
|
||||
}
|
||||
@@ -31,6 +31,8 @@ import {
|
||||
} from './store-info-change.controller';
|
||||
import { StoreInfoChangeService } from './store-info-change.service';
|
||||
import { PartnerAssocService } from './partner-assoc.service';
|
||||
import { StoreBankService } from './store-bank.service';
|
||||
import { AdminStoreBankController, ShopStoreBankController } from './store-bank.controller';
|
||||
import {
|
||||
PartnerAssocController,
|
||||
PartnerCommissionController,
|
||||
@@ -67,8 +69,10 @@ import {
|
||||
UserPartnerAssocController,
|
||||
PartnerAssocController,
|
||||
PartnerCommissionController,
|
||||
ShopStoreBankController,
|
||||
AdminStoreBankController,
|
||||
],
|
||||
providers: [StoreService, StoreCategoryService, StorePackageService, StoreInfoChangeService, PartnerAssocService],
|
||||
exports: [StoreService, StoreCategoryService, StorePackageService, PartnerAssocService],
|
||||
providers: [StoreService, StoreCategoryService, StorePackageService, StoreInfoChangeService, PartnerAssocService, StoreBankService],
|
||||
exports: [StoreService, StoreCategoryService, StorePackageService, PartnerAssocService, StoreBankService],
|
||||
})
|
||||
export class StoreModule {}
|
||||
|
||||
Reference in New Issue
Block a user