V4.0.9版本更新合伙人H5端调整
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
-- v4.0.9:关联码已扫码计数
|
||||
|
||||
ALTER TABLE `partner_account`
|
||||
ADD COLUMN `assoc_scan_count` INT NOT NULL DEFAULT 0 AFTER `assoc_qrcode_resource_id`;
|
||||
@@ -1208,6 +1208,7 @@ model PartnerAccount {
|
||||
managedWarehouseId BigInt? @unique @map("managed_warehouse_id") @db.UnsignedBigInt
|
||||
assocQrcodeId String? @unique @map("assoc_qrcode_id") @db.VarChar(64)
|
||||
assocQrcodeResourceId BigInt? @map("assoc_qrcode_resource_id") @db.UnsignedBigInt
|
||||
assocScanCount Int @default(0) @map("assoc_scan_count")
|
||||
activityPosterId BigInt? @map("activity_poster_id") @db.UnsignedBigInt
|
||||
/// 测试合伙人账号
|
||||
isTest Boolean @default(false) @map("is_test")
|
||||
|
||||
@@ -7,7 +7,8 @@ import { AlertService } from '../common/alert/alert.service';
|
||||
/**
|
||||
* 财务对账单定时任务(Asia/Shanghai)
|
||||
* - 每日 08:00:酒厂 T+3 期账单(每 3 天出一期,纳入上期 3 天已完成已付订单)+ 近 45 日出账周期核对补漏 + 门店日账单
|
||||
* - 每月 1 日 08:00:合伙人上一自然月账单 + 物流承运商上一自然月对账
|
||||
* - 每周一 08:00:合伙人上一自然周账单
|
||||
* - 每月 1 日 08:00:物流承运商上一自然月对账
|
||||
* - 工作日 18:05:门店提现 T+0 审完预警(FIN-003)
|
||||
*/
|
||||
@Injectable()
|
||||
@@ -75,11 +76,11 @@ export class SettlementScheduler {
|
||||
}
|
||||
}
|
||||
|
||||
@Cron('0 8 1 * *', { timeZone: 'Asia/Shanghai' })
|
||||
async handleMonthlyPartnerBills() {
|
||||
this.logger.log('Monthly partner bills job start');
|
||||
@Cron('0 8 * * 1', { timeZone: 'Asia/Shanghai' })
|
||||
async handleWeeklyPartnerBills() {
|
||||
this.logger.log('Weekly partner bills job start');
|
||||
try {
|
||||
const result = await this.settlementService.generatePreviousMonthPartnerBills();
|
||||
const result = await this.settlementService.generatePreviousWeekPartnerBills();
|
||||
this.logger.log(
|
||||
`Partner bills: total=${result.total} success=${result.success} failed=${result.failed}`,
|
||||
);
|
||||
@@ -87,9 +88,9 @@ export class SettlementScheduler {
|
||||
this.alert.notify({
|
||||
level: 'P0',
|
||||
category: 'settlement',
|
||||
title: '合伙人月账单部分失败',
|
||||
title: '合伙人周账单部分失败',
|
||||
detail: `total=${result.total} success=${result.success} failed=${result.failed}`,
|
||||
dedupeKey: `job_partner_bill|${new Date().toISOString().slice(0, 7)}`,
|
||||
dedupeKey: `job_partner_bill|${new Date().toISOString().slice(0, 10)}`,
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -97,11 +98,16 @@ export class SettlementScheduler {
|
||||
this.alert.notify({
|
||||
level: 'P0',
|
||||
category: 'settlement',
|
||||
title: '合伙人月账单任务失败',
|
||||
title: '合伙人周账单任务失败',
|
||||
detail: e instanceof Error ? e.message : String(e),
|
||||
dedupeKey: `job_partner_bill|${new Date().toISOString().slice(0, 7)}`,
|
||||
dedupeKey: `job_partner_bill|${new Date().toISOString().slice(0, 10)}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Cron('0 8 1 * *', { timeZone: 'Asia/Shanghai' })
|
||||
async handleMonthlyPartnerBills() {
|
||||
this.logger.log('Monthly logistics bills job start');
|
||||
try {
|
||||
const logistics = await this.settlementService.generatePreviousMonthLogisticsBills();
|
||||
this.logger.log(
|
||||
|
||||
@@ -115,7 +115,7 @@ export class PartnerStaffService {
|
||||
permissions,
|
||||
isPrimary: 0,
|
||||
parentAccountId: parent.id,
|
||||
status: 'DISABLED',
|
||||
status: 'ACTIVE',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { BadRequestException, Body, Controller, Get, Param, Post, Put, Query, UseGuards } from '@nestjs/common';
|
||||
import { IsOptional, IsString } from 'class-validator';
|
||||
import { BadRequestException, Body, Controller, ForbiddenException, Get, Param, Post, Put, Query, UseGuards } from '@nestjs/common';
|
||||
import { IsNotEmpty, IsOptional, IsString, MaxLength } from 'class-validator';
|
||||
import { parseShanghaiYmd } from '@dukang/domain';
|
||||
import { DEFAULT_PARTNER_STORE_STAFF_PERMISSIONS } from '@dukang/shared-types';
|
||||
import { SettlementService } from './settlement.service';
|
||||
@@ -20,11 +20,38 @@ class UpdatePartnerMeDto {
|
||||
name?: string;
|
||||
}
|
||||
|
||||
class UpdatePartnerBankDto {
|
||||
@IsString()
|
||||
@IsNotEmpty({ message: '请填写收款人' })
|
||||
@MaxLength(64)
|
||||
bankAccountName!: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty({ message: '请填写银行账号' })
|
||||
@MaxLength(32)
|
||||
bankAccountNo!: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty({ message: '请填写开户行名称' })
|
||||
@MaxLength(128)
|
||||
bankBranch!: string;
|
||||
}
|
||||
|
||||
@Controller('partner/settlement')
|
||||
@UseGuards(JwtAuthGuard, PartnerPrimaryGuard)
|
||||
export class SettlementController {
|
||||
constructor(private readonly settlementService: SettlementService) {}
|
||||
|
||||
@Get('cycle')
|
||||
cycle() {
|
||||
return this.settlementService.getPartnerSettlementCycle();
|
||||
}
|
||||
|
||||
@Get('preview')
|
||||
preview(@CurrentUser() user: AuthUser) {
|
||||
return this.settlementService.getPartnerSettlementPreview(user.actorId);
|
||||
}
|
||||
|
||||
@Get('bills')
|
||||
bills(@CurrentUser() user: AuthUser) {
|
||||
return this.settlementService.listPartnerBills(user.actorId);
|
||||
@@ -325,7 +352,9 @@ export class AdminPartnerBillController {
|
||||
refIdField: 'id',
|
||||
includeBody: true,
|
||||
})
|
||||
generate(@Body() body: { partnerId: string; year: number; month: number }) {
|
||||
generate(@Body() body: { partnerId: string; weekStartYmd: string }) {
|
||||
if (!body?.weekStartYmd) throw new BadRequestException('请提供 weekStartYmd');
|
||||
if (!body?.partnerId) throw new BadRequestException('请提供 partnerId');
|
||||
return this.settlementService.generatePartnerBill(body);
|
||||
}
|
||||
|
||||
@@ -336,7 +365,8 @@ export class AdminPartnerBillController {
|
||||
batch: true,
|
||||
includeBody: true,
|
||||
})
|
||||
generateAll(@Body() body: { year: number; month: number }) {
|
||||
generateAll(@Body() body: { weekStartYmd: string }) {
|
||||
if (!body?.weekStartYmd) throw new BadRequestException('请提供 weekStartYmd');
|
||||
return this.settlementService.generateAllPartnerBills(body);
|
||||
}
|
||||
|
||||
@@ -349,6 +379,7 @@ export class AdminPartnerBillController {
|
||||
partnerId: query.partnerId,
|
||||
year: query.year ? Number(query.year) : undefined,
|
||||
month: query.month ? Number(query.month) : undefined,
|
||||
weekStartYmd: query.weekStartYmd,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -359,6 +390,7 @@ export class AdminPartnerBillController {
|
||||
status: query.status,
|
||||
year: query.year ? Number(query.year) : undefined,
|
||||
month: query.month ? Number(query.month) : undefined,
|
||||
weekStartYmd: query.weekStartYmd,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -618,6 +650,29 @@ export class PartnerMeController {
|
||||
return this.buildPartnerMe(user.actorId);
|
||||
}
|
||||
|
||||
@Put('bank')
|
||||
async updateBank(@CurrentUser() user: AuthUser, @Body() dto: UpdatePartnerBankDto) {
|
||||
const account = await this.prisma.partnerAccount.findUniqueOrThrow({
|
||||
where: { id: user.actorId },
|
||||
});
|
||||
if (account.isPrimary !== 1) {
|
||||
throw new ForbiddenException('仅主账号可修改收款账户');
|
||||
}
|
||||
const bankAccountName = dto.bankAccountName.trim();
|
||||
const bankAccountNo = dto.bankAccountNo.replace(/\s+/g, '');
|
||||
const bankBranch = dto.bankBranch.trim();
|
||||
if (!bankAccountName) throw new BadRequestException('请填写收款人');
|
||||
if (!/^\d{8,32}$/.test(bankAccountNo)) {
|
||||
throw new BadRequestException('请填写正确的银行账号');
|
||||
}
|
||||
if (!bankBranch) throw new BadRequestException('请填写开户行名称');
|
||||
await this.prisma.partnerAccount.update({
|
||||
where: { id: account.id },
|
||||
data: { bankAccountName, bankAccountNo, bankBranch },
|
||||
});
|
||||
return this.buildPartnerMe(user.actorId);
|
||||
}
|
||||
|
||||
private async buildPartnerMe(actorId: bigint) {
|
||||
let account = await this.prisma.partnerAccount.findUniqueOrThrow({
|
||||
where: { id: actorId },
|
||||
@@ -661,6 +716,14 @@ export class PartnerMeController {
|
||||
wxAvatarUrl: account.wxAvatarUrl ?? undefined,
|
||||
managedWarehouseId: primary.managedWarehouseId?.toString() ?? null,
|
||||
hasWarehouseAccess,
|
||||
bankAccount:
|
||||
account.isPrimary === 1
|
||||
? {
|
||||
bankAccountName: account.bankAccountName,
|
||||
bankAccountNo: account.bankAccountNo,
|
||||
bankBranch: account.bankBranch,
|
||||
}
|
||||
: undefined,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,15 +18,20 @@ import {
|
||||
parseShanghaiYmd,
|
||||
pickPayoutsForWithdrawAmount,
|
||||
previousShanghaiMonth,
|
||||
previousShanghaiWeek,
|
||||
resolveSettlementRate,
|
||||
shanghaiBillPeriodYmds,
|
||||
shanghaiMonthLastInstant,
|
||||
shanghaiMonthRange,
|
||||
shanghaiPeriodYmds,
|
||||
shanghaiPeriodYmdsForWeek,
|
||||
shanghaiT1DayWindow,
|
||||
shanghaiWeekRange,
|
||||
shanghaiWineryPeriodWindow,
|
||||
shanghaiYearMonth,
|
||||
shanghaiYmd,
|
||||
startOfShanghaiDay,
|
||||
nextPartnerBillIssueAt,
|
||||
sumUnbilledPayoutAmount,
|
||||
validateStoreWithdraw,
|
||||
wineryIssueDateFromCompletedAt,
|
||||
@@ -80,7 +85,39 @@ function paymentProofUrlsInput(urls?: string[]): Prisma.InputJsonValue | typeof
|
||||
}
|
||||
|
||||
function withShanghaiPeriod<T extends { periodStart: Date; periodEnd: Date }>(row: T) {
|
||||
return { ...row, ...shanghaiPeriodYmds(row.periodStart) };
|
||||
return { ...row, ...shanghaiBillPeriodYmds(row.periodStart, row.periodEnd) };
|
||||
}
|
||||
|
||||
function resolvePartnerWeekPeriod(weekStartYmd: string): {
|
||||
periodStart: Date;
|
||||
periodEnd: Date;
|
||||
endExclusive: Date;
|
||||
} {
|
||||
const ymd = String(weekStartYmd || '').trim();
|
||||
if (!/^\d{4}-\d{2}-\d{2}$/.test(ymd)) {
|
||||
throw new BadRequestException('请提供周起始日 weekStartYmd(YYYY-MM-DD)');
|
||||
}
|
||||
const start = parseShanghaiYmd(ymd);
|
||||
const week = shanghaiWeekRange(start);
|
||||
if (shanghaiYmd(week.start) !== ymd) {
|
||||
throw new BadRequestException('账期起始日须为周一');
|
||||
}
|
||||
return {
|
||||
periodStart: week.start,
|
||||
periodEnd: new Date(week.endExclusive.getTime() - 1),
|
||||
endExclusive: week.endExclusive,
|
||||
};
|
||||
}
|
||||
|
||||
function partnerSettlementCycleDto(anchor = new Date()) {
|
||||
const { start } = shanghaiWeekRange(anchor);
|
||||
const ymds = shanghaiPeriodYmdsForWeek(start);
|
||||
return {
|
||||
periodStart: ymds.periodStart,
|
||||
periodEnd: ymds.periodEnd,
|
||||
nextIssueAt: nextPartnerBillIssueAt(anchor).toISOString(),
|
||||
cycleLabel: '每周一 08:00 出账',
|
||||
};
|
||||
}
|
||||
|
||||
function toPartnerBillItemDto(row: {
|
||||
@@ -1619,6 +1656,7 @@ export class SettlementService implements OnModuleInit {
|
||||
where: {
|
||||
partnerAccountId: primary.id,
|
||||
status: { in: ['AWAITING_CONFIRM', 'UNPAID', 'PAID', 'REJECTED'] },
|
||||
totalAmount: { gt: 0 },
|
||||
},
|
||||
orderBy: { createdAt: 'desc' },
|
||||
});
|
||||
@@ -1630,6 +1668,58 @@ export class SettlementService implements OnModuleInit {
|
||||
return serializeBigInt(bills.map(withShanghaiPeriod));
|
||||
}
|
||||
|
||||
getPartnerSettlementCycle(anchor = new Date()) {
|
||||
return partnerSettlementCycleDto(anchor);
|
||||
}
|
||||
|
||||
async getPartnerSettlementPreview(partnerAccountId: bigint) {
|
||||
const primary = await this.partnerCityService.resolvePrimaryAccount(partnerAccountId);
|
||||
const { start } = shanghaiWeekRange();
|
||||
const now = new Date();
|
||||
const periodEnd = now.getTime() < start.getTime() ? start : now;
|
||||
const redeemCommissionRate = Number(primary.redeemCommissionRate ?? 0.03);
|
||||
|
||||
const orders = await this.prisma.order.findMany({
|
||||
where: {
|
||||
partnerAccountIdAtPay: primary.id,
|
||||
payStatus: 'PAID',
|
||||
paidAt: { gte: start, lte: periodEnd },
|
||||
},
|
||||
select: { payAmount: true, orderCommissionRateAtPay: true },
|
||||
});
|
||||
const orderCommission = round2(
|
||||
orders.reduce((sum, o) => {
|
||||
const rate = o.orderCommissionRateAtPay != null ? Number(o.orderCommissionRateAtPay) : 0;
|
||||
return sum + Number(o.payAmount) * rate;
|
||||
}, 0),
|
||||
);
|
||||
|
||||
const stores = await this.prisma.store.findMany({
|
||||
where: { partnerAccountId: primary.id },
|
||||
select: { id: true },
|
||||
});
|
||||
const storeIds = stores.map((s) => s.id);
|
||||
const redeems = !storeIds.length
|
||||
? []
|
||||
: await this.prisma.redeemRecord.findMany({
|
||||
where: {
|
||||
storeId: { in: storeIds },
|
||||
createdAt: { gte: start, lte: periodEnd },
|
||||
},
|
||||
select: { amount: true },
|
||||
});
|
||||
const redeemCommission = round2(
|
||||
redeems.reduce((sum, r) => sum + Number(r.amount) * redeemCommissionRate, 0),
|
||||
);
|
||||
|
||||
return {
|
||||
...partnerSettlementCycleDto(),
|
||||
prepaymentAmount: round2(orderCommission + redeemCommission),
|
||||
orderCommissionEstimate: orderCommission,
|
||||
redeemCommissionEstimate: redeemCommission,
|
||||
};
|
||||
}
|
||||
|
||||
async getPartnerBill(partnerAccountId: bigint, billId: bigint) {
|
||||
const primary = await this.partnerCityService.resolvePrimaryAccount(partnerAccountId);
|
||||
const bill = await this.prisma.partnerBill.findFirst({
|
||||
@@ -1637,6 +1727,7 @@ export class SettlementService implements OnModuleInit {
|
||||
include: { items: { orderBy: { occurredAt: 'asc' } } },
|
||||
});
|
||||
if (!bill) throw new NotFoundException('账单不存在');
|
||||
if (Number(bill.totalAmount) <= 0) throw new NotFoundException('账单不存在');
|
||||
this.analyticsService.trackPartnerOneSafe(partnerAccountId, 'PARTNER_H5', {
|
||||
partnerAccountId: primary.id,
|
||||
eventName: 'partner_bill_detail_view',
|
||||
@@ -1659,6 +1750,7 @@ export class SettlementService implements OnModuleInit {
|
||||
partnerId?: string;
|
||||
year?: number;
|
||||
month?: number;
|
||||
weekStartYmd?: string;
|
||||
}) {
|
||||
const page = query.page ?? 1;
|
||||
const pageSize = query.pageSize ?? 20;
|
||||
@@ -1712,18 +1804,22 @@ export class SettlementService implements OnModuleInit {
|
||||
partnerId?: string;
|
||||
year?: number;
|
||||
month?: number;
|
||||
weekStartYmd?: string;
|
||||
}): Prisma.PartnerBillWhereInput {
|
||||
const where: Prisma.PartnerBillWhereInput = {};
|
||||
if (query.status) where.status = query.status as Prisma.EnumPartnerBillStatusFilter['equals'];
|
||||
if (query.partnerId) where.partnerAccountId = BigInt(query.partnerId);
|
||||
if (query.year && query.month) {
|
||||
if (query.weekStartYmd) {
|
||||
const { periodStart, endExclusive } = resolvePartnerWeekPeriod(query.weekStartYmd);
|
||||
where.periodStart = { gte: periodStart, lt: endExclusive };
|
||||
} else if (query.year && query.month) {
|
||||
const { start, endExclusive } = shanghaiMonthRange(query.year, query.month);
|
||||
where.periodStart = { gte: start, lt: endExclusive };
|
||||
}
|
||||
return where;
|
||||
}
|
||||
|
||||
async generateAllPartnerBills(body: { year: number; month: number }) {
|
||||
async generateAllPartnerBills(body: { weekStartYmd: string }) {
|
||||
const partners = await this.prisma.partnerAccount.findMany({
|
||||
where: { isPrimary: 1, status: 'ACTIVE' },
|
||||
select: {
|
||||
@@ -1752,8 +1848,7 @@ export class SettlementService implements OnModuleInit {
|
||||
const bill = await this.generatePartnerBill(
|
||||
{
|
||||
partnerId: p.id.toString(),
|
||||
year: body.year,
|
||||
month: body.month,
|
||||
weekStartYmd: body.weekStartYmd,
|
||||
},
|
||||
{ notify: false },
|
||||
);
|
||||
@@ -1781,10 +1876,7 @@ export class SettlementService implements OnModuleInit {
|
||||
});
|
||||
}
|
||||
}
|
||||
this.notifyPartnerBillDigest(
|
||||
`${body.year}-${String(body.month).padStart(2, '0')}`,
|
||||
created,
|
||||
);
|
||||
this.notifyPartnerBillDigest(body.weekStartYmd, created);
|
||||
return {
|
||||
total: partners.length,
|
||||
success: results.filter((r) => r.ok).length,
|
||||
@@ -1793,10 +1885,15 @@ export class SettlementService implements OnModuleInit {
|
||||
};
|
||||
}
|
||||
|
||||
/** 每月 1 日任务:生成上一自然月账单 */
|
||||
/** 每周一任务:生成上一自然周账单 */
|
||||
async generatePreviousWeekPartnerBills(anchor = new Date()) {
|
||||
const prev = previousShanghaiWeek(anchor);
|
||||
return this.generateAllPartnerBills({ weekStartYmd: shanghaiYmd(prev.start) });
|
||||
}
|
||||
|
||||
/** @deprecated 月账任务已改为周账 */
|
||||
async generatePreviousMonthPartnerBills(anchor = new Date()) {
|
||||
const prev = previousShanghaiMonth(anchor);
|
||||
return this.generateAllPartnerBills({ year: prev.year, month: prev.month });
|
||||
return this.generatePreviousWeekPartnerBills(anchor);
|
||||
}
|
||||
|
||||
async getAdminPartnerBill(id: bigint) {
|
||||
@@ -1814,13 +1911,12 @@ export class SettlementService implements OnModuleInit {
|
||||
}
|
||||
|
||||
async generatePartnerBill(
|
||||
body: { partnerId: string; year: number; month: number },
|
||||
body: { partnerId: string; weekStartYmd: string },
|
||||
opts?: { notify?: boolean },
|
||||
) {
|
||||
const partnerAccountId = BigInt(body.partnerId);
|
||||
const primary = await this.partnerCityService.resolvePrimaryAccount(partnerAccountId);
|
||||
const { start: periodStart, endExclusive } = shanghaiMonthRange(body.year, body.month);
|
||||
const periodEnd = shanghaiMonthLastInstant(body.year, body.month);
|
||||
const { periodStart, periodEnd, endExclusive } = resolvePartnerWeekPeriod(body.weekStartYmd);
|
||||
|
||||
const existing = await this.prisma.partnerBill.findFirst({
|
||||
where: {
|
||||
@@ -1829,7 +1925,7 @@ export class SettlementService implements OnModuleInit {
|
||||
},
|
||||
});
|
||||
if (existing && existing.status !== 'PENDING_REVIEW') {
|
||||
throw new BadRequestException('该月账单已进入审核流程,不可重复生成');
|
||||
throw new BadRequestException('该账期账单已进入审核流程,不可重复生成');
|
||||
}
|
||||
|
||||
if (!primary.cityId) {
|
||||
@@ -1952,7 +2048,7 @@ export class SettlementService implements OnModuleInit {
|
||||
});
|
||||
cityName = city?.name?.trim() || '—';
|
||||
}
|
||||
this.notifyPartnerBillDigest(`${body.year}-${String(body.month).padStart(2, '0')}`, [
|
||||
this.notifyPartnerBillDigest(body.weekStartYmd, [
|
||||
{
|
||||
cityName,
|
||||
partnerLabel: formatPartnerDashLabel(primary.companyName, primary.name),
|
||||
@@ -1981,6 +2077,9 @@ export class SettlementService implements OnModuleInit {
|
||||
async sendPartnerBill(id: bigint) {
|
||||
const bill = await this.prisma.partnerBill.findUnique({ where: { id } });
|
||||
if (!bill) throw new NotFoundException('账单不存在');
|
||||
if (Number(bill.totalAmount) <= 0) {
|
||||
throw new BadRequestException('零元账单不同步给合伙人');
|
||||
}
|
||||
if (bill.status !== 'PENDING_REVIEW' && bill.status !== 'REJECTED') {
|
||||
throw new BadRequestException('仅待审核或已驳回账单可发送');
|
||||
}
|
||||
@@ -2129,6 +2228,7 @@ export class SettlementService implements OnModuleInit {
|
||||
status?: string;
|
||||
year?: number;
|
||||
month?: number;
|
||||
weekStartYmd?: string;
|
||||
}) {
|
||||
const where = this.buildPartnerBillWhere(query);
|
||||
const bills = await this.prisma.partnerBill.findMany({
|
||||
@@ -2172,8 +2272,8 @@ export class SettlementService implements OnModuleInit {
|
||||
csvEscape(b.billNo),
|
||||
csvEscape(b.partnerAccount.companyName ?? ''),
|
||||
csvEscape(b.partnerAccount.phone ?? ''),
|
||||
shanghaiPeriodYmds(b.periodStart).periodStart,
|
||||
shanghaiPeriodYmds(b.periodStart).periodEnd,
|
||||
shanghaiBillPeriodYmds(b.periodStart, b.periodEnd).periodStart,
|
||||
shanghaiBillPeriodYmds(b.periodStart, b.periodEnd).periodEnd,
|
||||
Number(b.orderCommission),
|
||||
Number(b.redeemCommission),
|
||||
Number(b.totalAmount),
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import { Body, Controller, Get, Param, Post, Put, Query, Res, UseGuards } from '@nestjs/common';
|
||||
import type { Response } from 'express';
|
||||
import { IsOptional, IsString, MaxLength } from 'class-validator';
|
||||
import { Transform } from 'class-transformer';
|
||||
import { IsBoolean, IsOptional, IsString, MaxLength } from 'class-validator';
|
||||
import { JwtAuthGuard, AuthUser } from '../../common/guards/jwt-auth.guard';
|
||||
import { OptionalJwtAuthGuard } from '../../common/guards/optional-jwt-auth.guard';
|
||||
import { PartnerPrimaryGuard } from '../../common/guards/partner-primary.guard';
|
||||
import { PartnerPermissionGuard } from '../../common/guards/partner-permission.guard';
|
||||
import { CurrentUser } from '../../common/decorators/current-user.decorator';
|
||||
import { PartnerAssocService } from './partner-assoc.service';
|
||||
|
||||
@@ -16,6 +19,17 @@ class BindPartnerAssocDto {
|
||||
partnerId?: string;
|
||||
}
|
||||
|
||||
class TouchPartnerAssocDto extends BindPartnerAssocDto {
|
||||
@IsOptional()
|
||||
@Transform(({ value }) => {
|
||||
if (value === false || value === 'false' || value === 0 || value === '0') return false;
|
||||
if (value === true || value === 'true' || value === 1 || value === '1') return true;
|
||||
return undefined;
|
||||
})
|
||||
@IsBoolean()
|
||||
countScan?: boolean;
|
||||
}
|
||||
|
||||
class PartnerAssocRemarkDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@@ -23,6 +37,21 @@ class PartnerAssocRemarkDto {
|
||||
remark?: string | null;
|
||||
}
|
||||
|
||||
@Controller('user/partner-assoc')
|
||||
export class UserPartnerAssocPublicController {
|
||||
constructor(private readonly assoc: PartnerAssocService) {}
|
||||
|
||||
@Post('touch')
|
||||
@UseGuards(OptionalJwtAuthGuard)
|
||||
touch(@Body() dto: TouchPartnerAssocDto) {
|
||||
return this.assoc.touchScan({
|
||||
scene: dto.scene,
|
||||
partnerId: dto.partnerId,
|
||||
countScan: dto.countScan,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Controller('user/partner-assoc')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
export class UserPartnerAssocController {
|
||||
@@ -35,7 +64,7 @@ export class UserPartnerAssocController {
|
||||
}
|
||||
|
||||
@Controller('partner/assoc')
|
||||
@UseGuards(JwtAuthGuard, PartnerPrimaryGuard)
|
||||
@UseGuards(JwtAuthGuard, PartnerPermissionGuard)
|
||||
export class PartnerAssocController {
|
||||
constructor(private readonly assoc: PartnerAssocService) {}
|
||||
|
||||
|
||||
@@ -92,6 +92,32 @@ export class PartnerAssocService {
|
||||
};
|
||||
}
|
||||
|
||||
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 shouldCountScan = input.countScan !== false;
|
||||
let scanCount = primary.assocScanCount ?? 0;
|
||||
if (shouldCountScan) {
|
||||
const updated = await this.prisma.partnerAccount.update({
|
||||
where: { id: primary.id },
|
||||
data: { assocScanCount: { increment: 1 } },
|
||||
select: { assocScanCount: true },
|
||||
});
|
||||
scanCount = updated.assocScanCount;
|
||||
}
|
||||
return {
|
||||
partnerId: primary.id.toString(),
|
||||
scanCounted: shouldCountScan,
|
||||
scanCount,
|
||||
};
|
||||
}
|
||||
|
||||
async unbindUser(userId: bigint) {
|
||||
const user = await this.prisma.user.findUnique({ where: { id: userId } });
|
||||
if (!user) throw new NotFoundException('用户不存在');
|
||||
@@ -149,9 +175,10 @@ export class PartnerAssocService {
|
||||
partnerId: primary.id.toString(),
|
||||
qrcodeUrl: ensured.qrcodeUrl,
|
||||
userCount,
|
||||
scanCount: primary.assocScanCount ?? 0,
|
||||
companyName: primary.companyName,
|
||||
name: primary.name,
|
||||
activityPosterId,
|
||||
activityPosterId: partnerAccountId === primary.id ? activityPosterId : null,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@ import {
|
||||
PartnerAssocController,
|
||||
PartnerCommissionController,
|
||||
UserPartnerAssocController,
|
||||
UserPartnerAssocPublicController,
|
||||
} from './partner-assoc.controller';
|
||||
|
||||
@Module({
|
||||
@@ -62,6 +63,7 @@ import {
|
||||
PartnerStoreInfoChangeController,
|
||||
ShopStoreInfoChangeController,
|
||||
AdminStoreInfoChangeController,
|
||||
UserPartnerAssocPublicController,
|
||||
UserPartnerAssocController,
|
||||
PartnerAssocController,
|
||||
PartnerCommissionController,
|
||||
|
||||
Reference in New Issue
Block a user