增加核销接口

增加hq管理端日志
This commit is contained in:
2026-07-06 19:17:27 +08:00
parent f0b99ea9da
commit 4c38a9dd2b
29 changed files with 904 additions and 37 deletions
@@ -65,3 +65,53 @@ export function orderStatusLogWhere(orderId: bigint): Prisma.CommonEventWhereInp
refId: orderId,
};
}
export function buildHqOperationEvent(data: {
hqAccountId: bigint;
action: string;
refType: string;
refId: bigint;
status?: string;
detail?: Record<string, unknown>;
remark?: string;
}): Prisma.CommonEventCreateInput {
return {
eventType: 'HQ_OPERATION',
refType: data.refType,
refId: data.refId,
actorType: 'HQ',
actorId: data.hqAccountId,
status: data.status,
param1: data.action,
param1Desc: 'action',
param2: data.refType,
param2Desc: 'target_type',
param3: data.refId.toString(),
param3Desc: 'target_id',
remark: data.remark,
extraJson: data.detail as Prisma.InputJsonValue,
};
}
export function hqOperationLogWhere(filters?: {
hqAccountId?: bigint;
action?: string;
refType?: string;
from?: Date;
to?: Date;
}): Prisma.CommonEventWhereInput {
return {
eventType: 'HQ_OPERATION',
...(filters?.hqAccountId ? { actorType: 'HQ' as const, actorId: filters.hqAccountId } : {}),
...(filters?.action ? { param1: filters.action } : {}),
...(filters?.refType ? { param2: filters.refType } : {}),
...(filters?.from || filters?.to
? {
createdAt: {
...(filters.from ? { gte: filters.from } : {}),
...(filters.to ? { lte: filters.to } : {}),
},
}
: {}),
};
}
@@ -0,0 +1,30 @@
import { Injectable } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.module';
import { buildHqOperationEvent } from '../event/event.helpers';
export type LogHqOperationInput = {
hqAccountId: bigint;
action: string;
refType: string;
refId: bigint;
status?: string;
detail?: Record<string, unknown>;
remark?: string;
};
@Injectable()
export class HqOperationLogService {
constructor(private readonly prisma: PrismaService) {}
async log(input: LogHqOperationInput): Promise<void> {
await this.prisma.commonEvent.create({
data: buildHqOperationEvent(input),
});
}
logSafe(input: LogHqOperationInput): void {
void this.log(input).catch((err) => {
console.error('[HqOperationLog] write failed', err);
});
}
}
@@ -0,0 +1,95 @@
/** HQ 后台写操作审计 action(写入 common_event.param1 */
export const HqOperationAction = {
CITY_CREATE: 'CITY_CREATE',
CITY_UPDATE: 'CITY_UPDATE',
PARTNER_CREATE: 'PARTNER_CREATE',
PARTNER_UPDATE: 'PARTNER_UPDATE',
PARTNER_ACCOUNT_CREATE: 'PARTNER_ACCOUNT_CREATE',
PARTNER_ACCOUNT_UPDATE: 'PARTNER_ACCOUNT_UPDATE',
HQ_ACCOUNT_CREATE: 'HQ_ACCOUNT_CREATE',
HQ_ACCOUNT_UPDATE: 'HQ_ACCOUNT_UPDATE',
USER_DELETE: 'USER_DELETE',
USER_BATCH_DELETE: 'USER_BATCH_DELETE',
ORDER_SHIP: 'ORDER_SHIP',
ORDER_STATUS_DEBUG: 'ORDER_STATUS_DEBUG',
ORDER_BATCH_DELETE: 'ORDER_BATCH_DELETE',
STORE_CREATE: 'STORE_CREATE',
STORE_UPDATE: 'STORE_UPDATE',
STORE_STATUS: 'STORE_STATUS',
STORE_AUDIT: 'STORE_AUDIT',
STORE_ACCOUNT_CREATE: 'STORE_ACCOUNT_CREATE',
STORE_ACCOUNT_UPDATE: 'STORE_ACCOUNT_UPDATE',
STORE_MEDIA_CREATE: 'STORE_MEDIA_CREATE',
STORE_MEDIA_UPDATE: 'STORE_MEDIA_UPDATE',
STORE_MEDIA_DELETE: 'STORE_MEDIA_DELETE',
PRODUCT_CREATE: 'PRODUCT_CREATE',
PRODUCT_UPDATE: 'PRODUCT_UPDATE',
PRODUCT_DELETE: 'PRODUCT_DELETE',
PRODUCT_TEMPLATE_CREATE: 'PRODUCT_TEMPLATE_CREATE',
PRODUCT_TEMPLATE_UPDATE: 'PRODUCT_TEMPLATE_UPDATE',
BENEFIT_COUPON_VOID: 'BENEFIT_COUPON_VOID',
DELIVERY_UPDATE: 'DELIVERY_UPDATE',
TICKET_APPROVE: 'TICKET_APPROVE',
TICKET_REJECT: 'TICKET_REJECT',
STORE_PAYOUT_CONFIRM: 'STORE_PAYOUT_CONFIRM',
STORE_PAYOUT_BATCH_CONFIRM: 'STORE_PAYOUT_BATCH_CONFIRM',
PARTNER_BILL_GENERATE: 'PARTNER_BILL_GENERATE',
PARTNER_BILL_CONFIRM: 'PARTNER_BILL_CONFIRM',
PARTNER_BILL_MARK_PAID: 'PARTNER_BILL_MARK_PAID',
REDEEM_DEBUG_CREATE_TOKEN: 'REDEEM_DEBUG_CREATE_TOKEN',
REDEEM_DEBUG_CONFIRM: 'REDEEM_DEBUG_CONFIRM',
} as const;
export type HqOperationActionCode = (typeof HqOperationAction)[keyof typeof HqOperationAction];
export const HQ_OPERATION_ACTION_LABELS: Record<string, string> = {
[HqOperationAction.CITY_CREATE]: '新增开城城市',
[HqOperationAction.CITY_UPDATE]: '编辑开城城市',
[HqOperationAction.PARTNER_CREATE]: '新增城市合伙人',
[HqOperationAction.PARTNER_UPDATE]: '编辑城市合伙人',
[HqOperationAction.PARTNER_ACCOUNT_CREATE]: '新增合伙人账户',
[HqOperationAction.PARTNER_ACCOUNT_UPDATE]: '编辑合伙人账户',
[HqOperationAction.HQ_ACCOUNT_CREATE]: '新增 HQ 管理员',
[HqOperationAction.HQ_ACCOUNT_UPDATE]: '编辑 HQ 管理员/权限',
[HqOperationAction.USER_DELETE]: '删除用户',
[HqOperationAction.USER_BATCH_DELETE]: '批量删除用户',
[HqOperationAction.ORDER_SHIP]: '订单发货',
[HqOperationAction.ORDER_STATUS_DEBUG]: '订单状态调试',
[HqOperationAction.ORDER_BATCH_DELETE]: '批量删除订单',
[HqOperationAction.STORE_CREATE]: '新增门店',
[HqOperationAction.STORE_UPDATE]: '编辑门店',
[HqOperationAction.STORE_STATUS]: '变更门店状态',
[HqOperationAction.STORE_AUDIT]: '门店审核',
[HqOperationAction.STORE_ACCOUNT_CREATE]: '新增门店账户',
[HqOperationAction.STORE_ACCOUNT_UPDATE]: '编辑门店账户',
[HqOperationAction.STORE_MEDIA_CREATE]: '新增门店资源',
[HqOperationAction.STORE_MEDIA_UPDATE]: '编辑门店资源',
[HqOperationAction.STORE_MEDIA_DELETE]: '删除门店资源',
[HqOperationAction.PRODUCT_CREATE]: '新增商品',
[HqOperationAction.PRODUCT_UPDATE]: '编辑商品',
[HqOperationAction.PRODUCT_DELETE]: '删除商品',
[HqOperationAction.PRODUCT_TEMPLATE_CREATE]: '新增详情模板',
[HqOperationAction.PRODUCT_TEMPLATE_UPDATE]: '编辑详情模板',
[HqOperationAction.BENEFIT_COUPON_VOID]: '作废权益券',
[HqOperationAction.DELIVERY_UPDATE]: '编辑配送单',
[HqOperationAction.TICKET_APPROVE]: '工单通过',
[HqOperationAction.TICKET_REJECT]: '工单驳回',
[HqOperationAction.STORE_PAYOUT_CONFIRM]: '门店打款确认',
[HqOperationAction.STORE_PAYOUT_BATCH_CONFIRM]: '批量门店打款',
[HqOperationAction.PARTNER_BILL_GENERATE]: '生成合伙人账单',
[HqOperationAction.PARTNER_BILL_CONFIRM]: '确认合伙人账单',
[HqOperationAction.PARTNER_BILL_MARK_PAID]: '合伙人账单结算',
[HqOperationAction.REDEEM_DEBUG_CREATE_TOKEN]: '核销调试-生成码',
[HqOperationAction.REDEEM_DEBUG_CONFIRM]: '核销调试-确认核销',
STORE_PAYOUT: '门店打款确认',
};
export function resolveHqOperationLabel(action: string | null | undefined, refType?: string | null): string {
if (action && HQ_OPERATION_ACTION_LABELS[action]) {
return HQ_OPERATION_ACTION_LABELS[action];
}
if (refType && HQ_OPERATION_ACTION_LABELS[refType]) {
return HQ_OPERATION_ACTION_LABELS[refType];
}
return action || refType || 'HQ 操作';
}
@@ -0,0 +1,21 @@
import { SetMetadata } from '@nestjs/common';
import type { HqOperationActionCode } from './hq-operation.constants';
export const HQ_OPERATION_KEY = 'hq_operation';
export type HqOperationMeta = {
action: HqOperationActionCode | string;
refType: string;
/** 从路由 params 取 refId */
refIdParam?: string;
/** 从响应体字段取 refId,默认 id */
refIdField?: string;
/** 批量操作无单一 refId 时用 0 */
batch?: boolean;
/** 记录请求体到 extraJson */
includeBody?: boolean;
/** 记录响应体到 extraJson(截断) */
includeResponse?: boolean;
};
export const HqOperation = (meta: HqOperationMeta) => SetMetadata(HQ_OPERATION_KEY, meta);
@@ -0,0 +1,103 @@
import {
CallHandler,
ExecutionContext,
Injectable,
NestInterceptor,
} from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { Observable, tap } from 'rxjs';
import type { AuthUser } from '../guards/jwt-auth.guard';
import { HQ_OPERATION_KEY, type HqOperationMeta } from './hq-operation.decorator';
import { HqOperationLogService } from './hq-operation-log.service';
function pickRefId(value: unknown): bigint | null {
if (value == null || value === '') return null;
try {
return BigInt(String(value));
} catch {
return null;
}
}
function sanitizeBody(body: unknown): unknown {
if (!body || typeof body !== 'object') return body;
const copy = { ...(body as Record<string, unknown>) };
for (const key of Object.keys(copy)) {
if (/password|secret|token/i.test(key)) {
copy[key] = '***';
}
}
return copy;
}
function summarizeResponse(data: unknown): unknown {
if (data == null) return null;
if (typeof data !== 'object') return data;
const obj = data as Record<string, unknown>;
const summary: Record<string, unknown> = {};
for (const key of ['id', 'ok', 'deleted', 'orderNo', 'userNo', 'redeemNo', 'message', 'status']) {
if (obj[key] !== undefined) summary[key] = obj[key];
}
if (Array.isArray(obj.items)) {
summary.itemCount = obj.items.length;
}
if (Array.isArray(obj.orderNos)) {
summary.orderNos = obj.orderNos;
}
return Object.keys(summary).length ? summary : obj;
}
@Injectable()
export class HqOperationInterceptor implements NestInterceptor {
constructor(
private readonly reflector: Reflector,
private readonly logService: HqOperationLogService,
) {}
intercept(context: ExecutionContext, next: CallHandler): Observable<unknown> {
const meta = this.reflector.get<HqOperationMeta | undefined>(
HQ_OPERATION_KEY,
context.getHandler(),
);
if (!meta) return next.handle();
const req = context.switchToHttp().getRequest();
const user = req.user as AuthUser | undefined;
if (!user || user.actorType !== 'HQ') {
return next.handle();
}
return next.handle().pipe(
tap((data) => {
const refId = meta.batch
? 0n
: pickRefId(meta.refIdParam ? req.params?.[meta.refIdParam] : null)
?? pickRefId(
meta.refIdField
? (data as Record<string, unknown> | null)?.[meta.refIdField]
: (data as Record<string, unknown> | null)?.id,
)
?? 0n;
const detail: Record<string, unknown> = {
method: req.method,
path: req.originalUrl ?? req.url,
};
if (meta.includeBody && req.body) {
detail.requestBody = sanitizeBody(req.body);
}
if (meta.includeResponse !== false && data != null) {
detail.response = summarizeResponse(data);
}
this.logService.logSafe({
hqAccountId: user.actorId,
action: meta.action,
refType: meta.refType,
refId,
detail,
});
}),
);
}
}
@@ -0,0 +1,17 @@
import { Module } from '@nestjs/common';
import { APP_INTERCEPTOR } from '@nestjs/core';
import { HqOperationLogService } from './hq-operation-log.service';
import { HqOperationInterceptor } from './hq-operation.interceptor';
@Module({
providers: [
HqOperationLogService,
HqOperationInterceptor,
{
provide: APP_INTERCEPTOR,
useClass: HqOperationInterceptor,
},
],
exports: [HqOperationLogService],
})
export class HqOperationModule {}