增加核销接口
增加hq管理端日志
This commit is contained in:
@@ -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 {}
|
||||
Reference in New Issue
Block a user