feat: 技术支持工单/企微权限/开发版本管理/消息推送等迭代

This commit is contained in:
2026-08-04 21:32:13 +08:00
parent c8ea5a3119
commit 9d96c73246
1341 changed files with 0 additions and 195605 deletions
@@ -1,30 +0,0 @@
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);
});
}
}
@@ -1,219 +0,0 @@
/** HQ 后台写操作审计 action(写入 common_event.param1 */
export const HqOperationAction = {
CITY_CREATE: 'CITY_CREATE',
CITY_UPDATE: 'CITY_UPDATE',
CITY_DELETE: 'CITY_DELETE',
CITY_PARTNER_BIND: 'CITY_PARTNER_BIND',
CITY_PARTNER_UPDATE: 'CITY_PARTNER_UPDATE',
CITY_PARTNER_UNBIND: 'CITY_PARTNER_UNBIND',
WAREHOUSE_CREATE: 'WAREHOUSE_CREATE',
WAREHOUSE_UPDATE: 'WAREHOUSE_UPDATE',
WAREHOUSE_DELETE: 'WAREHOUSE_DELETE',
PARTNER_CREATE: 'PARTNER_CREATE',
PARTNER_UPDATE: 'PARTNER_UPDATE',
PARTNER_ACCOUNT_CREATE: 'PARTNER_ACCOUNT_CREATE',
PARTNER_ACCOUNT_UPDATE: 'PARTNER_ACCOUNT_UPDATE',
PARTNER_ACCOUNT_DELETE: 'PARTNER_ACCOUNT_DELETE',
HQ_ACCOUNT_CREATE: 'HQ_ACCOUNT_CREATE',
HQ_ACCOUNT_UPDATE: 'HQ_ACCOUNT_UPDATE',
HQ_PERMISSION_UPDATE: 'HQ_PERMISSION_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',
ORDER_DELETE: 'ORDER_DELETE',
ORDER_PROXY_CREATE: 'ORDER_PROXY_CREATE',
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_ACCOUNT_STAFF_DELETE: 'STORE_ACCOUNT_STAFF_DELETE',
STORE_MEDIA_CREATE: 'STORE_MEDIA_CREATE',
STORE_MEDIA_UPDATE: 'STORE_MEDIA_UPDATE',
STORE_MEDIA_DELETE: 'STORE_MEDIA_DELETE',
STORE_CATEGORY_CREATE: 'STORE_CATEGORY_CREATE',
STORE_CATEGORY_UPDATE: 'STORE_CATEGORY_UPDATE',
STORE_CATEGORY_DELETE: 'STORE_CATEGORY_DELETE',
STORE_CATEGORY_ENSURE: 'STORE_CATEGORY_ENSURE',
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',
BENEFIT_COUPON_GRANT: 'BENEFIT_COUPON_GRANT',
DELIVERY_UPDATE: 'DELIVERY_UPDATE',
TICKET_APPROVE: 'TICKET_APPROVE',
TICKET_REJECT: 'TICKET_REJECT',
TICKET_CREATE: 'TICKET_CREATE',
SUPPORT_TICKET_CREATE: 'SUPPORT_TICKET_CREATE',
SUPPORT_TICKET_APPROVE: 'SUPPORT_TICKET_APPROVE',
SUPPORT_TICKET_REJECT: 'SUPPORT_TICKET_REJECT',
SUPPORT_TICKET_START_TESTING: 'SUPPORT_TICKET_START_TESTING',
SUPPORT_TICKET_PASS: 'SUPPORT_TICKET_PASS',
INVOICE_CREATE: 'INVOICE_CREATE',
INVOICE_ISSUE: 'INVOICE_ISSUE',
INVOICE_REJECT: 'INVOICE_REJECT',
STORE_PAYOUT_CONFIRM: 'STORE_PAYOUT_CONFIRM',
STORE_PAYOUT_BATCH_CONFIRM: 'STORE_PAYOUT_BATCH_CONFIRM',
STORE_BILL_CONFIRM: 'STORE_BILL_CONFIRM',
STORE_BILL_BATCH_CONFIRM: 'STORE_BILL_BATCH_CONFIRM',
STORE_WITHDRAW_APPROVE: 'STORE_WITHDRAW_APPROVE',
STORE_WITHDRAW_REJECT: 'STORE_WITHDRAW_REJECT',
PARTNER_BILL_GENERATE: 'PARTNER_BILL_GENERATE',
PARTNER_BILL_SEND: 'PARTNER_BILL_SEND',
PARTNER_BILL_BATCH_SEND: 'PARTNER_BILL_BATCH_SEND',
PARTNER_BILL_CONFIRM: 'PARTNER_BILL_CONFIRM',
PARTNER_BILL_MARK_PAID: 'PARTNER_BILL_MARK_PAID',
PARTNER_BILL_BATCH_MARK_PAID: 'PARTNER_BILL_BATCH_MARK_PAID',
PARTNER_BILL_REJECT: 'PARTNER_BILL_REJECT',
WINERY_BILL_CONFIRM: 'WINERY_BILL_CONFIRM',
WINERY_BILL_BATCH_CONFIRM: 'WINERY_BILL_BATCH_CONFIRM',
LOGISTICS_BILL_CONFIRM: 'LOGISTICS_BILL_CONFIRM',
LOGISTICS_BILL_BATCH_CONFIRM: 'LOGISTICS_BILL_BATCH_CONFIRM',
LOGISTICS_PROVIDER_RECHARGE: 'LOGISTICS_PROVIDER_RECHARGE',
REDEEM_DEBUG_CREATE_TOKEN: 'REDEEM_DEBUG_CREATE_TOKEN',
REDEEM_DEBUG_CONFIRM: 'REDEEM_DEBUG_CONFIRM',
PROMO_CODE_CREATE: 'PROMO_CODE_CREATE',
PROMO_CODE_UPDATE: 'PROMO_CODE_UPDATE',
PROMO_CODE_UPDATE_STATUS: 'PROMO_CODE_UPDATE_STATUS',
WECOM_BOT_CREATE: 'WECOM_BOT_CREATE',
WECOM_BOT_UPDATE: 'WECOM_BOT_UPDATE',
WECOM_BOT_DELETE: 'WECOM_BOT_DELETE',
WECOM_BOT_RELOAD: 'WECOM_BOT_RELOAD',
LLM_CONFIG_CREATE: 'LLM_CONFIG_CREATE',
LLM_CONFIG_UPDATE: 'LLM_CONFIG_UPDATE',
LLM_CONFIG_DELETE: 'LLM_CONFIG_DELETE',
LLM_CONFIG_TEST: 'LLM_CONFIG_TEST',
KNOWLEDGE_BASE_CREATE: 'KNOWLEDGE_BASE_CREATE',
KNOWLEDGE_BASE_UPDATE: 'KNOWLEDGE_BASE_UPDATE',
KNOWLEDGE_BASE_DELETE: 'KNOWLEDGE_BASE_DELETE',
KNOWLEDGE_DOC_CREATE: 'KNOWLEDGE_DOC_CREATE',
KNOWLEDGE_DOC_DELETE: 'KNOWLEDGE_DOC_DELETE',
REDEEM_PENDING_COMPLETE: 'REDEEM_PENDING_COMPLETE',
REDEEM_PENDING_REJECT: 'REDEEM_PENDING_REJECT',
DEPLOY_TRIGGER: 'DEPLOY_TRIGGER',
SYSTEM_CONFIG_UPDATE: 'SYSTEM_CONFIG_UPDATE',
SYSTEM_CONFIG_SYNC_ENV: 'SYSTEM_CONFIG_SYNC_ENV',
SYSTEM_CONFIG_IMPORT_ENV: 'SYSTEM_CONFIG_IMPORT_ENV',
WECOM_ALERT_TEST: 'WECOM_ALERT_TEST',
} 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.CITY_DELETE]: '删除开城城市',
[HqOperationAction.CITY_PARTNER_BIND]: '绑定城市合伙人',
[HqOperationAction.CITY_PARTNER_UPDATE]: '编辑城市合伙人绑定',
[HqOperationAction.CITY_PARTNER_UNBIND]: '解绑城市合伙人',
[HqOperationAction.WAREHOUSE_CREATE]: '新增城市仓库',
[HqOperationAction.WAREHOUSE_UPDATE]: '编辑城市仓库',
[HqOperationAction.WAREHOUSE_DELETE]: '删除城市仓库',
[HqOperationAction.PARTNER_CREATE]: '新增城市合伙人',
[HqOperationAction.PARTNER_UPDATE]: '编辑城市合伙人',
[HqOperationAction.PARTNER_ACCOUNT_CREATE]: '新增合伙人账户',
[HqOperationAction.PARTNER_ACCOUNT_UPDATE]: '编辑合伙人账户',
[HqOperationAction.PARTNER_ACCOUNT_DELETE]: '删除合伙人子账号',
[HqOperationAction.HQ_ACCOUNT_CREATE]: '新增 HQ 管理员',
[HqOperationAction.HQ_ACCOUNT_UPDATE]: '编辑 HQ 管理员',
[HqOperationAction.HQ_PERMISSION_UPDATE]: '配置 HQ 权限',
[HqOperationAction.USER_DELETE]: '删除用户',
[HqOperationAction.USER_BATCH_DELETE]: '批量删除用户',
[HqOperationAction.ORDER_SHIP]: '订单发货',
[HqOperationAction.ORDER_STATUS_DEBUG]: '订单状态调试',
[HqOperationAction.ORDER_BATCH_DELETE]: '批量删除订单',
[HqOperationAction.ORDER_DELETE]: '删除订单',
[HqOperationAction.ORDER_PROXY_CREATE]: '总部代下单',
[HqOperationAction.STORE_CREATE]: '新增门店',
[HqOperationAction.STORE_UPDATE]: '编辑门店',
[HqOperationAction.STORE_STATUS]: '变更门店状态',
[HqOperationAction.STORE_AUDIT]: '门店审核',
[HqOperationAction.STORE_ACCOUNT_CREATE]: '新增门店账户',
[HqOperationAction.STORE_ACCOUNT_UPDATE]: '编辑门店账户',
[HqOperationAction.STORE_ACCOUNT_STAFF_DELETE]: '删除门店子账号',
[HqOperationAction.STORE_MEDIA_CREATE]: '新增门店资源',
[HqOperationAction.STORE_MEDIA_UPDATE]: '编辑门店资源',
[HqOperationAction.STORE_MEDIA_DELETE]: '删除门店资源',
[HqOperationAction.STORE_CATEGORY_CREATE]: '新增门店分类',
[HqOperationAction.STORE_CATEGORY_UPDATE]: '编辑门店分类',
[HqOperationAction.STORE_CATEGORY_DELETE]: '删除门店分类',
[HqOperationAction.STORE_CATEGORY_ENSURE]: '初始化默认门店分类',
[HqOperationAction.PRODUCT_CREATE]: '新增商品',
[HqOperationAction.PRODUCT_UPDATE]: '编辑商品',
[HqOperationAction.PRODUCT_DELETE]: '删除商品',
[HqOperationAction.PRODUCT_TEMPLATE_CREATE]: '新增详情模板',
[HqOperationAction.PRODUCT_TEMPLATE_UPDATE]: '编辑详情模板',
[HqOperationAction.BENEFIT_COUPON_VOID]: '作废权益券',
[HqOperationAction.BENEFIT_COUPON_GRANT]: '手动发放权益',
[HqOperationAction.DELIVERY_UPDATE]: '编辑配送单',
[HqOperationAction.TICKET_APPROVE]: '工单通过',
[HqOperationAction.TICKET_REJECT]: '工单驳回',
[HqOperationAction.TICKET_CREATE]: '创建工单',
[HqOperationAction.SUPPORT_TICKET_CREATE]: '创建技术支持工单',
[HqOperationAction.SUPPORT_TICKET_APPROVE]: '技术支持评审通过',
[HqOperationAction.SUPPORT_TICKET_REJECT]: '技术支持评审驳回',
[HqOperationAction.SUPPORT_TICKET_START_TESTING]: '技术支持转入测试',
[HqOperationAction.SUPPORT_TICKET_PASS]: '技术支持测试通过',
[HqOperationAction.INVOICE_CREATE]: '创建发票申请',
[HqOperationAction.INVOICE_ISSUE]: '开具发票',
[HqOperationAction.INVOICE_REJECT]: '驳回发票',
[HqOperationAction.STORE_PAYOUT_CONFIRM]: '门店打款确认',
[HqOperationAction.STORE_PAYOUT_BATCH_CONFIRM]: '批量门店打款',
[HqOperationAction.STORE_BILL_CONFIRM]: '门店对账单确认打款',
[HqOperationAction.STORE_BILL_BATCH_CONFIRM]: '批量门店对账单打款',
[HqOperationAction.STORE_WITHDRAW_APPROVE]: '门店提现审核通过',
[HqOperationAction.STORE_WITHDRAW_REJECT]: '门店提现驳回',
[HqOperationAction.PARTNER_BILL_GENERATE]: '生成合伙人账单',
[HqOperationAction.PARTNER_BILL_SEND]: '发送合伙人账单',
[HqOperationAction.PARTNER_BILL_BATCH_SEND]: '批量发送合伙人账单',
[HqOperationAction.PARTNER_BILL_CONFIRM]: '确认合伙人账单',
[HqOperationAction.PARTNER_BILL_MARK_PAID]: '合伙人账单结算',
[HqOperationAction.PARTNER_BILL_BATCH_MARK_PAID]: '批量合伙人账单结算',
[HqOperationAction.PARTNER_BILL_REJECT]: '驳回合伙人打款申请',
[HqOperationAction.WINERY_BILL_CONFIRM]: '酒厂对账单确认打款',
[HqOperationAction.WINERY_BILL_BATCH_CONFIRM]: '批量酒厂对账单打款',
[HqOperationAction.LOGISTICS_BILL_CONFIRM]: '物流对账单确认结算',
[HqOperationAction.LOGISTICS_BILL_BATCH_CONFIRM]: '批量物流对账单结算',
[HqOperationAction.LOGISTICS_PROVIDER_RECHARGE]: '物流承运商充值',
[HqOperationAction.REDEEM_DEBUG_CREATE_TOKEN]: '核销调试-生成码',
[HqOperationAction.REDEEM_DEBUG_CONFIRM]: '核销调试-确认核销',
[HqOperationAction.PROMO_CODE_CREATE]: '创建推广码',
[HqOperationAction.PROMO_CODE_UPDATE]: '编辑推广码',
[HqOperationAction.PROMO_CODE_UPDATE_STATUS]: '推广码启停',
[HqOperationAction.WECOM_BOT_CREATE]: '创建企微机器人',
[HqOperationAction.WECOM_BOT_UPDATE]: '编辑企微机器人',
[HqOperationAction.WECOM_BOT_DELETE]: '删除企微机器人',
[HqOperationAction.WECOM_BOT_RELOAD]: '重载企微机器人连接',
[HqOperationAction.LLM_CONFIG_CREATE]: '创建语言模型配置',
[HqOperationAction.LLM_CONFIG_UPDATE]: '更新语言模型配置',
[HqOperationAction.LLM_CONFIG_DELETE]: '删除语言模型配置',
[HqOperationAction.LLM_CONFIG_TEST]: '测试语言模型配置',
[HqOperationAction.KNOWLEDGE_BASE_CREATE]: '创建知识库',
[HqOperationAction.KNOWLEDGE_BASE_UPDATE]: '更新知识库',
[HqOperationAction.KNOWLEDGE_BASE_DELETE]: '删除知识库',
[HqOperationAction.KNOWLEDGE_DOC_CREATE]: '上传知识库文档',
[HqOperationAction.KNOWLEDGE_DOC_DELETE]: '删除知识库文档',
[HqOperationAction.REDEEM_PENDING_COMPLETE]: '弱网待处理单-补核销',
[HqOperationAction.REDEEM_PENDING_REJECT]: '弱网待处理单-驳回',
[HqOperationAction.DEPLOY_TRIGGER]: '触发系统发布',
[HqOperationAction.SYSTEM_CONFIG_UPDATE]: '更新系统配置',
[HqOperationAction.SYSTEM_CONFIG_SYNC_ENV]: '同步系统配置到 env 文件',
[HqOperationAction.SYSTEM_CONFIG_IMPORT_ENV]: '从当前环境导入配置',
[HqOperationAction.WECOM_ALERT_TEST]: '测试企微运营告警',
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 操作';
}
@@ -1,21 +0,0 @@
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);
@@ -1,113 +0,0 @@
import {
CallHandler,
ExecutionContext,
Injectable,
NestInterceptor,
} from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { Observable, catchError, tap, throwError } from 'rxjs';
import type { AuthUser } from '../guards/jwt-auth.guard';
import type { RequestWithId } from '../logging/request-id.middleware';
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<RequestWithId & { user?: AuthUser }>();
const user = req.user as AuthUser | undefined;
if (!user || user.actorType !== 'HQ') {
return next.handle();
}
const writeLog = (status: 'SUCCESS' | 'FAILED', data?: unknown, errorMessage?: string) => {
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,
requestId: req.requestId,
};
if (meta.includeBody && req.body) {
detail.requestBody = sanitizeBody(req.body);
}
if (status === 'SUCCESS' && meta.includeResponse !== false && data != null) {
detail.response = summarizeResponse(data);
}
if (errorMessage) detail.error = errorMessage;
this.logService.logSafe({
hqAccountId: user.actorId,
action: meta.action,
refType: meta.refType,
refId,
status,
detail,
});
};
return next.handle().pipe(
tap((data) => writeLog('SUCCESS', data)),
catchError((err: { message?: string }) => {
writeLog('FAILED', undefined, err?.message ?? '操作失败');
return throwError(() => err);
}),
);
}
}
@@ -1,17 +0,0 @@
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 {}