webadmin端增加商铺日志

This commit is contained in:
2026-07-07 12:12:12 +08:00
parent e25b208545
commit 6572799264
20 changed files with 919 additions and 5 deletions
+17
View File
@@ -603,4 +603,21 @@ CREATE TABLE log_user_analytics (
KEY idx_log_user_analytics_ref (ref_type, ref_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户行为埋点日志';
DROP TABLE IF EXISTS log_store_analytics;
CREATE TABLE log_store_analytics (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
store_account_id BIGINT UNSIGNED DEFAULT NULL COMMENT '门店账号ID,系统/HQ操作可为NULL',
store_id BIGINT UNSIGNED NOT NULL COMMENT '门店ID',
event_name VARCHAR(64) NOT NULL COMMENT '门店行为事件名',
client_app VARCHAR(32) DEFAULT NULL COMMENT 'SHOP_H5|HQ_WEB|PARTNER_H5',
ref_type VARCHAR(32) DEFAULT NULL COMMENT 'REDEEM_RECORD|STORE_PAYOUT|...',
ref_id BIGINT UNSIGNED DEFAULT NULL,
extra_json JSON DEFAULT NULL,
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
KEY idx_log_store_analytics_store_created (store_id, created_at),
KEY idx_log_store_analytics_account_created (store_account_id, created_at),
KEY idx_log_store_analytics_event_created (event_name, created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='门店商户行为日志';
SET FOREIGN_KEY_CHECKS = 1;
+17
View File
@@ -872,3 +872,20 @@ model LogUserAnalytics {
@@index([refType, refId])
@@map("log_user_analytics")
}
model LogStoreAnalytics {
id BigInt @id @default(autoincrement()) @db.UnsignedBigInt
storeAccountId BigInt? @map("store_account_id") @db.UnsignedBigInt
storeId BigInt @map("store_id") @db.UnsignedBigInt
eventName String @map("event_name") @db.VarChar(64)
clientApp ClientApp? @map("client_app")
refType String? @map("ref_type") @db.VarChar(32)
refId BigInt? @map("ref_id") @db.UnsignedBigInt
extraJson Json? @map("extra_json")
createdAt DateTime @default(now()) @map("created_at") @db.DateTime(3)
@@index([storeId, createdAt])
@@index([storeAccountId, createdAt])
@@index([eventName, createdAt])
@@map("log_store_analytics")
}
@@ -10,6 +10,11 @@ export type TrackEventInput = {
extraJson?: Record<string, unknown>;
};
export type TrackStoreEventInput = TrackEventInput & {
storeAccountId?: bigint;
storeId: bigint;
};
@Injectable()
export class AnalyticsService {
constructor(private readonly prisma: PrismaService) {}
@@ -42,6 +47,16 @@ export class AnalyticsService {
void this.trackOne(userId, clientApp, event).catch(() => {});
}
async trackStoreOne(storeAccountId: bigint | undefined, clientApp: ClientApp | string, event: TrackStoreEventInput) {
await this.prisma.logStoreAnalytics.create({
data: this.toStoreRow(storeAccountId, clientApp, event),
});
}
trackStoreOneSafe(storeAccountId: bigint | undefined, clientApp: ClientApp | string, event: TrackStoreEventInput) {
void this.trackStoreOne(storeAccountId, clientApp, event).catch(() => {});
}
private toRow(userId: bigint, clientApp: ClientApp | string, event: TrackEventInput) {
return {
userId,
@@ -53,4 +68,20 @@ export class AnalyticsService {
extraJson: event.extraJson as never,
};
}
private toStoreRow(
storeAccountId: bigint | undefined,
clientApp: ClientApp | string,
event: TrackStoreEventInput,
) {
return {
storeAccountId,
storeId: event.storeId,
eventName: event.eventName,
clientApp: clientApp as ClientApp,
refType: event.refType,
refId: event.refId,
extraJson: event.extraJson as never,
};
}
}
@@ -150,6 +150,23 @@ export class AuthService {
});
}
private trackStoreEvent(
storeAccountId: bigint | undefined,
storeId: bigint,
clientApp: ClientApp | string,
eventName: string,
extraJson?: Record<string, unknown>,
ref?: { refType?: string; refId?: bigint },
) {
this.analyticsService.trackStoreOneSafe(storeAccountId, clientApp, {
storeId,
eventName,
refType: ref?.refType,
refId: ref?.refId,
extraJson,
});
}
private async assertSmsSendAllowed(phone: string, scene: SmsScene) {
if (scene === SmsScene.STORE_LOGIN) {
const account = await this.prisma.storeAccount.findUnique({ where: { phone } });
@@ -222,6 +239,19 @@ export class AuthService {
if (!result.ok) {
throw new BadRequestException(result.errorMessage ?? '短信发送失败');
}
if (scene === SmsScene.STORE_LOGIN && actorRef?.refType === 'STORE') {
const storeAccount = await this.prisma.storeAccount.findUnique({
where: { id: actorRef.refId },
select: { id: true, storeId: true },
});
if (storeAccount) {
this.trackStoreEvent(storeAccount.id, storeAccount.storeId, clientApp, 'store_sms_send', {
scene,
phone: this.maskPhone(normalizedPhone),
status: 'success',
});
}
}
} catch (err) {
if (err instanceof BadRequestException) throw err;
const message = err instanceof Error ? err.message : '短信发送失败';
@@ -435,7 +465,18 @@ export class AuthService {
async loginStore(phone: string, code: string, clientApp: ClientApp) {
const normalizedPhone = this.assertMobilePhone(phone);
await this.smsProvider.verify(normalizedPhone, code, SmsScene.STORE_LOGIN);
try {
await this.smsProvider.verify(normalizedPhone, code, SmsScene.STORE_LOGIN);
} catch (err) {
const account = await this.prisma.storeAccount.findUnique({ where: { phone: normalizedPhone } });
if (account) {
this.trackStoreEvent(account.id, account.storeId, clientApp, 'store_sms_verify_fail', {
phone: this.maskPhone(normalizedPhone),
reason: err instanceof BadRequestException ? err.message : '验证码错误',
});
}
throw err;
}
const account = await this.prisma.storeAccount.findUnique({
where: { phone: normalizedPhone },
include: { store: true },
@@ -446,6 +487,12 @@ export class AuthService {
where: { id: account.id },
data: { lastLoginAt: new Date() },
});
this.trackStoreEvent(account.id, account.storeId, clientApp, 'store_sms_login', {
phone: this.maskPhone(normalizedPhone),
});
this.trackStoreEvent(account.id, account.storeId, clientApp, 'store_login_success', {
method: 'sms',
});
return this.issueToken('STORE', account.id, clientApp, false, undefined, {
id: account.id.toString(),
storeId: account.storeId.toString(),
@@ -828,6 +875,12 @@ export class AuthService {
include: { store: true },
});
this.trackStoreEvent(account.id, account.storeId, clientApp, 'store_wechat_login', { platform });
this.trackStoreEvent(account.id, account.storeId, clientApp, 'store_login_success', {
method: 'wechat',
platform,
});
return this.issueToken('STORE', account.id, clientApp, false, undefined, {
id: account.id.toString(),
storeId: account.storeId.toString(),
@@ -872,6 +925,8 @@ export class AuthService {
include: { store: true },
});
this.trackStoreEvent(updated.id, updated.storeId, clientApp, 'store_wechat_bind', { platform });
return this.issueToken('STORE', updated.id, clientApp, false, undefined, {
id: updated.id.toString(),
storeId: updated.storeId.toString(),
@@ -0,0 +1,20 @@
import { Controller, Get, Param, Query, UseGuards } from '@nestjs/common';
import { HqAuthGuard } from '../../common/guards/hq-auth.guard';
import { AdminStoreLogsService } from './admin-store-logs.service';
import { AdminStoreLogsQueryDto } from './dto/admin-query.dto';
@Controller('admin/logs/stores')
@UseGuards(HqAuthGuard)
export class AdminStoreLogsController {
constructor(private readonly service: AdminStoreLogsService) {}
@Get()
list(@Query() query: AdminStoreLogsQueryDto) {
return this.service.list(query);
}
@Get(':source/:rawId')
detail(@Param('source') source: string, @Param('rawId') rawId: string) {
return this.service.detail(`${source}:${rawId}`);
}
}
@@ -0,0 +1,381 @@
import { Injectable, NotFoundException } from '@nestjs/common';
import { Prisma } from '@prisma/client';
import {
eventNamesForStoreLogCategory,
resolveStoreLogCategory,
} from '@dukang/shared-types';
import { PrismaService } from '../../common/prisma/prisma.module';
import { serializeBigInt } from '../../common/decorators/current-user.decorator';
import type { AdminStoreLogsQueryDto } from './dto/admin-query.dto';
type StoreLogItem = {
id: string;
source: 'analytics' | 'redeem_record' | 'store_payout';
storeId: string;
storeAccountId: string | null;
storeName: string | null;
accountName: string | null;
accountPhone: string | null;
category: ReturnType<typeof resolveStoreLogCategory>;
eventName: string;
clientApp: string | null;
refType: string | null;
refId: string | null;
extraJson: Record<string, unknown> | null;
createdAt: Date;
};
@Injectable()
export class AdminStoreLogsService {
constructor(private readonly prisma: PrismaService) {}
async list(query: AdminStoreLogsQueryDto) {
const page = query.page ?? 1;
const pageSize = query.pageSize ?? 20;
const storeIds = await this.resolveStoreIds(query);
if (storeIds && storeIds.length === 0) {
return { items: [], total: 0, page, pageSize };
}
const dateFilter = this.buildDateFilter(query);
const categoryEvents = query.eventName
? [query.eventName]
: query.category
? eventNamesForStoreLogCategory(query.category)
: undefined;
const includeRedeem = !query.category || query.category === 'redeem';
const includePayout = !query.category || query.category === 'payout';
const fetchLimit = page * pageSize;
const [analyticsRows, redeemRows, payoutRows] = await Promise.all([
this.fetchAnalyticsRows({
storeIds,
categoryEvents,
dateFilter,
limit: fetchLimit,
}),
includeRedeem
? this.fetchRedeemRows({ storeIds, dateFilter, limit: fetchLimit })
: Promise.resolve([]),
includePayout
? this.fetchPayoutRows({ storeIds, dateFilter, limit: fetchLimit, category: query.category })
: Promise.resolve([]),
]);
const merged = [...analyticsRows, ...redeemRows, ...payoutRows]
.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
const items = merged.slice((page - 1) * pageSize, page * pageSize);
const total = await this.countTotal({
storeIds,
categoryEvents,
dateFilter,
includeRedeem,
includePayout,
});
return serializeBigInt({ items, total, page, pageSize });
}
async detail(compositeId: string) {
const [source, rawId] = compositeId.split(':');
if (!source || !rawId) throw new NotFoundException('日志不存在');
if (source === 'analytics') {
const row = await this.prisma.logStoreAnalytics.findUnique({ where: { id: BigInt(rawId) } });
if (!row) throw new NotFoundException('日志不存在');
const enriched = await this.enrichAnalyticsRows([row]);
return serializeBigInt(enriched[0]);
}
if (source === 'redeem_record') {
const row = await this.prisma.redeemRecord.findUnique({
where: { id: BigInt(rawId) },
include: { store: true, user: { select: { id: true, userNo: true, phone: true, nickname: true } } },
});
if (!row) throw new NotFoundException('日志不存在');
return serializeBigInt(this.redeemToItem(row));
}
if (source === 'store_payout') {
const row = await this.prisma.storePayout.findUnique({
where: { id: BigInt(rawId) },
include: { store: true, redeemRecord: { select: { redeemNo: true, amount: true } } },
});
if (!row) throw new NotFoundException('日志不存在');
return serializeBigInt(this.payoutToItem(row));
}
throw new NotFoundException('日志不存在');
}
private buildDateFilter(query: AdminStoreLogsQueryDto): Prisma.DateTimeFilter | undefined {
if (!query.from && !query.to) return undefined;
return {
...(query.from ? { gte: new Date(query.from) } : {}),
...(query.to ? { lte: new Date(query.to) } : {}),
};
}
private async resolveStoreIds(query: AdminStoreLogsQueryDto): Promise<bigint[] | undefined> {
if (query.storeId) return [BigInt(query.storeId)];
const storeWhere: Prisma.StoreWhereInput = {};
if (query.storeName) storeWhere.name = { contains: query.storeName };
if (query.storeAccountId || query.phone) {
const accountWhere: Prisma.StoreAccountWhereInput = {};
if (query.storeAccountId) accountWhere.id = BigInt(query.storeAccountId);
if (query.phone) accountWhere.phone = { contains: query.phone };
const accounts = await this.prisma.storeAccount.findMany({
where: accountWhere,
select: { storeId: true },
take: 100,
});
if (accounts.length === 0) return [];
const ids = [...new Set(accounts.map((a) => a.storeId))];
if (storeWhere.name) {
const stores = await this.prisma.store.findMany({
where: { id: { in: ids }, ...storeWhere },
select: { id: true },
});
return stores.map((s) => s.id);
}
return ids;
}
if (query.storeName) {
const stores = await this.prisma.store.findMany({
where: storeWhere,
select: { id: true },
take: 100,
});
return stores.map((s) => s.id);
}
return undefined;
}
private async fetchAnalyticsRows(input: {
storeIds?: bigint[];
categoryEvents?: string[];
dateFilter?: Prisma.DateTimeFilter;
limit: number;
}) {
const where: Prisma.LogStoreAnalyticsWhereInput = {};
if (input.storeIds) where.storeId = { in: input.storeIds };
if (input.categoryEvents?.length) where.eventName = { in: input.categoryEvents };
if (input.dateFilter) where.createdAt = input.dateFilter;
const rows = await this.prisma.logStoreAnalytics.findMany({
where,
orderBy: { createdAt: 'desc' },
take: input.limit,
});
return this.enrichAnalyticsRows(rows);
}
private async enrichAnalyticsRows(
rows: Array<{
id: bigint;
storeAccountId: bigint | null;
storeId: bigint;
eventName: string;
clientApp: string | null;
refType: string | null;
refId: bigint | null;
extraJson: unknown;
createdAt: Date;
}>,
): Promise<StoreLogItem[]> {
const storeIds = [...new Set(rows.map((r) => r.storeId))];
const accountIds = [...new Set(rows.map((r) => r.storeAccountId).filter((id): id is bigint => id != null))];
const [stores, accounts] = await Promise.all([
storeIds.length
? this.prisma.store.findMany({ where: { id: { in: storeIds } }, select: { id: true, name: true } })
: Promise.resolve([]),
accountIds.length
? this.prisma.storeAccount.findMany({
where: { id: { in: accountIds } },
select: { id: true, name: true, phone: true },
})
: Promise.resolve([]),
]);
const storeMap = new Map(stores.map((s) => [s.id.toString(), s] as const));
const accountMap = new Map(accounts.map((a) => [a.id.toString(), a] as const));
return rows.map((row) => {
const store = storeMap.get(row.storeId.toString());
const account = row.storeAccountId ? accountMap.get(row.storeAccountId.toString()) : undefined;
return {
id: `analytics:${row.id}`,
source: 'analytics' as const,
storeId: row.storeId.toString(),
storeAccountId: row.storeAccountId?.toString() ?? null,
storeName: store?.name ?? null,
accountName: account?.name ?? null,
accountPhone: account?.phone ?? null,
category: resolveStoreLogCategory(row.eventName),
eventName: row.eventName,
clientApp: row.clientApp,
refType: row.refType,
refId: row.refId?.toString() ?? null,
extraJson: (row.extraJson as Record<string, unknown> | null) ?? null,
createdAt: row.createdAt,
};
});
}
private async fetchRedeemRows(input: {
storeIds?: bigint[];
dateFilter?: Prisma.DateTimeFilter;
limit: number;
}) {
const where: Prisma.RedeemRecordWhereInput = {};
if (input.storeIds) where.storeId = { in: input.storeIds };
if (input.dateFilter) where.createdAt = input.dateFilter;
const rows = await this.prisma.redeemRecord.findMany({
where,
orderBy: { createdAt: 'desc' },
take: input.limit,
include: {
store: { select: { id: true, name: true } },
user: { select: { id: true, userNo: true, phone: true, nickname: true } },
},
});
return rows.map((row) => this.redeemToItem(row));
}
private redeemToItem(row: {
id: bigint;
storeId: bigint;
amount: unknown;
redeemNo: string;
createdAt: Date;
store?: { name: string } | null;
user?: { id: bigint; userNo: string | null; phone: string | null; nickname: string | null } | null;
}): StoreLogItem {
return {
id: `redeem_record:${row.id}`,
source: 'redeem_record',
storeId: row.storeId.toString(),
storeAccountId: null,
storeName: row.store?.name ?? null,
accountName: null,
accountPhone: null,
category: 'redeem',
eventName: 'store_redeem_confirm',
clientApp: 'SHOP_H5',
refType: 'REDEEM_RECORD',
refId: row.id.toString(),
extraJson: {
redeemNo: row.redeemNo,
amount: Number(row.amount),
userId: row.user?.id.toString(),
userNo: row.user?.userNo,
userPhone: row.user?.phone,
userNickname: row.user?.nickname,
legacy: true,
},
createdAt: row.createdAt,
};
}
private async fetchPayoutRows(input: {
storeIds?: bigint[];
dateFilter?: Prisma.DateTimeFilter;
limit: number;
category?: string;
}) {
const where: Prisma.StorePayoutWhereInput = {};
if (input.storeIds) where.storeId = { in: input.storeIds };
if (input.dateFilter) where.createdAt = input.dateFilter;
if (input.category === 'payout') {
// include all payout statuses
}
const rows = await this.prisma.storePayout.findMany({
where,
orderBy: { createdAt: 'desc' },
take: input.limit,
include: {
store: { select: { id: true, name: true } },
redeemRecord: { select: { redeemNo: true, amount: true } },
},
});
return rows.map((row) => this.payoutToItem(row));
}
private payoutToItem(row: {
id: bigint;
storeId: bigint;
status: string;
payoutAmount: unknown;
redeemAmount: unknown;
paidAt: Date | null;
createdAt: Date;
store?: { name: string } | null;
redeemRecord?: { redeemNo: string; amount: unknown } | null;
}): StoreLogItem {
const paid = row.status === 'PAID';
return {
id: `store_payout:${row.id}`,
source: 'store_payout',
storeId: row.storeId.toString(),
storeAccountId: null,
storeName: row.store?.name ?? null,
accountName: null,
accountPhone: null,
category: 'payout',
eventName: paid ? 'store_payout_paid' : 'store_payout_created',
clientApp: paid ? 'HQ_WEB' : null,
refType: 'STORE_PAYOUT',
refId: row.id.toString(),
extraJson: {
status: row.status,
payoutAmount: Number(row.payoutAmount),
redeemAmount: Number(row.redeemAmount),
redeemNo: row.redeemRecord?.redeemNo,
paidAt: row.paidAt?.toISOString() ?? null,
legacy: true,
},
createdAt: paid && row.paidAt ? row.paidAt : row.createdAt,
};
}
private async countTotal(input: {
storeIds?: bigint[];
categoryEvents?: string[];
dateFilter?: Prisma.DateTimeFilter;
includeRedeem: boolean;
includePayout: boolean;
}) {
const redeemWhere: Prisma.RedeemRecordWhereInput = {};
const payoutWhere: Prisma.StorePayoutWhereInput = {};
const analyticsWhere: Prisma.LogStoreAnalyticsWhereInput = {};
if (input.storeIds) {
redeemWhere.storeId = { in: input.storeIds };
payoutWhere.storeId = { in: input.storeIds };
analyticsWhere.storeId = { in: input.storeIds };
}
if (input.dateFilter) {
redeemWhere.createdAt = input.dateFilter;
payoutWhere.createdAt = input.dateFilter;
analyticsWhere.createdAt = input.dateFilter;
}
if (input.categoryEvents?.length) analyticsWhere.eventName = { in: input.categoryEvents };
const [analyticsCount, redeemCount, payoutCount] = await Promise.all([
this.prisma.logStoreAnalytics.count({ where: analyticsWhere }),
input.includeRedeem ? this.prisma.redeemRecord.count({ where: redeemWhere }) : 0,
input.includePayout ? this.prisma.storePayout.count({ where: payoutWhere }) : 0,
]);
return analyticsCount + redeemCount + payoutCount;
}
}
@@ -275,6 +275,40 @@ export class AdminUserLogsQueryDto extends PaginationQueryDto {
to?: string;
}
export class AdminStoreLogsQueryDto extends PaginationQueryDto {
@IsOptional()
@IsString()
storeId?: string;
@IsOptional()
@IsString()
storeAccountId?: string;
@IsOptional()
@IsString()
phone?: string;
@IsOptional()
@IsString()
storeName?: string;
@IsOptional()
@IsString()
category?: string;
@IsOptional()
@IsString()
eventName?: string;
@IsOptional()
@IsString()
from?: string;
@IsOptional()
@IsString()
to?: string;
}
export class AdminHqLogsQueryDto extends PaginationQueryDto {
@IsOptional()
@IsString()
@@ -23,6 +23,8 @@ import { AdminProductsController } from './admin-products.controller';
import { AdminProductsService } from './admin-products.service';
import { AdminUserLogsController } from './admin-user-logs.controller';
import { AdminUserLogsService } from './admin-user-logs.service';
import { AdminStoreLogsController } from './admin-store-logs.controller';
import { AdminStoreLogsService } from './admin-store-logs.service';
import { AdminHqLogsController } from './admin-hq-logs.controller';
import { AdminHqLogsService } from './admin-hq-logs.service';
import { AdminTicketsController } from './admin-tickets.controller';
@@ -58,6 +60,7 @@ import { AdminRedeemDebugService } from './admin-redeem-debug.service';
AdminHqAccountsController,
AdminProductsController,
AdminUserLogsController,
AdminStoreLogsController,
AdminHqLogsController,
AdminTicketsController,
AdminXiaofeixiaController,
@@ -77,6 +80,7 @@ import { AdminRedeemDebugService } from './admin-redeem-debug.service';
AdminHqAccountsService,
AdminProductsService,
AdminUserLogsService,
AdminStoreLogsService,
AdminHqLogsService,
AdminTicketsService,
AdminXiaofeixiaService,
@@ -161,6 +161,17 @@ export class RedeemService {
const ttl = await this.redis.ttl(`redeem:token:${token}`);
this.analyticsService.trackStoreOneSafe(storeAccountId, 'SHOP_H5', {
storeId: account.storeId,
eventName: 'store_redeem_preview',
extraJson: {
tokenSuffix: token.slice(-8),
amount: cached.amount,
userId: cached.userId,
redeemType: cached.allocations && cached.allocations.length > 1 ? 'DIRECT' : cached.couponId ? 'COUPON' : 'DIRECT',
},
});
return serializeBigInt({
token,
amount: cached.amount,
@@ -288,6 +299,17 @@ export class RedeemService {
storeId: account.storeId.toString(),
amount,
};
this.analyticsService.trackStoreOneSafe(storeAccountId, 'SHOP_H5', {
storeId: account.storeId,
eventName: 'store_redeem_confirm',
refType: 'REDEEM_RECORD',
refId: record.id,
extraJson: {
redeemNo: record.redeemNo,
amount,
userId: cached.userId,
},
});
this.analyticsService.trackOneSafe(BigInt(cached.userId), 'SHOP_H5', {
eventName: 'benefit_redeem_success',
refType: 'STORE',
@@ -1,5 +1,6 @@
import { Module } from '@nestjs/common';
import { IamModule } from '../iam/iam.module';
import { AnalyticsModule } from '../analytics/analytics.module';
import { SettlementService } from './settlement.service';
import {
AdminPartnerBillController,
@@ -10,7 +11,7 @@ import {
} from './settlement.controller';
@Module({
imports: [IamModule],
imports: [IamModule, AnalyticsModule],
controllers: [
SettlementController,
PartnerMeController,
@@ -2,6 +2,7 @@ import { BadRequestException, Injectable, NotFoundException } from '@nestjs/comm
import { Prisma } from '@prisma/client';
import { PrismaService } from '../../common/prisma/prisma.module';
import { serializeBigInt } from '../../common/decorators/current-user.decorator';
import { AnalyticsService } from '../analytics/analytics.service';
function generateBillNo() {
return `PB${Date.now()}${Math.floor(Math.random() * 900 + 100)}`;
@@ -9,7 +10,10 @@ function generateBillNo() {
@Injectable()
export class SettlementService {
constructor(private readonly prisma: PrismaService) {}
constructor(
private readonly prisma: PrismaService,
private readonly analyticsService: AnalyticsService,
) {}
async createStorePayout(
redeemRecordId: bigint,
@@ -33,6 +37,18 @@ export class SettlementService {
expectedPayAt,
},
});
this.analyticsService.trackStoreOneSafe(undefined, 'SHOP_H5', {
storeId,
eventName: 'store_payout_created',
refType: 'STORE_PAYOUT',
refId: payout.id,
extraJson: {
redeemRecordId: redeemRecordId.toString(),
payoutAmount,
redeemAmount,
settlementRate,
},
});
return serializeBigInt(payout);
}
@@ -108,6 +124,18 @@ export class SettlementService {
},
});
this.analyticsService.trackStoreOneSafe(undefined, 'HQ_WEB', {
storeId: payout.storeId,
eventName: 'store_payout_paid',
refType: 'STORE_PAYOUT',
refId: id,
extraJson: {
batchNo: dto.batchNo ?? payout.batchNo,
paymentRef: dto.paymentRef,
payoutAmount: Number(payout.payoutAmount),
},
});
return serializeBigInt(updated);
}
@@ -1,6 +1,7 @@
import { Module, forwardRef } from '@nestjs/common';
import { IamModule } from '../iam/iam.module';
import { RedeemModule } from '../redeem/redeem.module';
import { AnalyticsModule } from '../analytics/analytics.module';
import { StoreService } from './store.service';
import {
PartnerDashboardController,
@@ -11,7 +12,7 @@ import {
} from './store.controller';
@Module({
imports: [IamModule, forwardRef(() => RedeemModule)],
imports: [IamModule, AnalyticsModule, forwardRef(() => RedeemModule)],
controllers: [
PublicStoreController,
PartnerStoreController,
@@ -7,12 +7,16 @@ import { loadAppConfig } from '@dukang/shared-types';
import { PrismaService } from '../../common/prisma/prisma.module';
import { serializeBigInt } from '../../common/decorators/current-user.decorator';
import { mapStoreCompat } from '../../common/compat/v31-compat';
import { AnalyticsService } from '../analytics/analytics.service';
@Injectable()
export class StoreService {
private readonly config = loadAppConfig();
constructor(private readonly prisma: PrismaService) {}
constructor(
private readonly prisma: PrismaService,
private readonly analyticsService: AnalyticsService,
) {}
async listOpenStores(cityCode?: string) {
const where: Record<string, unknown> = { status: 'OPEN' };
@@ -213,6 +217,16 @@ export class StoreService {
data: { status },
include: { coverResource: true },
});
this.analyticsService.trackStoreOneSafe(undefined, 'PARTNER_H5', {
storeId,
eventName: 'store_status_change',
extraJson: {
status,
previousStatus: store.status,
actor: 'PARTNER',
partnerAccountId: partnerAccountId.toString(),
},
});
return serializeBigInt(mapStoreCompat(updated));
}
@@ -267,11 +281,22 @@ export class StoreService {
async updateShopStatus(storeAccountId: bigint, status: 'OPEN' | 'PAUSED') {
const account = await this.prisma.storeAccount.findUniqueOrThrow({
where: { id: storeAccountId },
include: { store: true },
});
const previousStatus = account.store.status;
const store = await this.prisma.store.update({
where: { id: account.storeId },
data: { status },
});
this.analyticsService.trackStoreOneSafe(storeAccountId, 'SHOP_H5', {
storeId: account.storeId,
eventName: 'store_status_change',
extraJson: {
status,
previousStatus,
actor: 'STORE',
},
});
return serializeBigInt(store);
}