diff --git a/apps/admin-web/src/App.tsx b/apps/admin-web/src/App.tsx index f098bcf..0f3d4e7 100644 --- a/apps/admin-web/src/App.tsx +++ b/apps/admin-web/src/App.tsx @@ -25,6 +25,7 @@ import StorePayoutsPage from './pages/StorePayoutsPage'; import PartnerBillsPage from './pages/PartnerBillsPage'; import TicketsPage from './pages/TicketsPage'; import UserLogsPage from './pages/UserLogsPage'; +import HqLogsPage from './pages/HqLogsPage'; import ThirdPartyLogsPage from './pages/ThirdPartyLogsPage'; function RequireAuth({ children }: { children: React.ReactNode }) { @@ -63,6 +64,7 @@ export default function App() { } /> } /> } /> + } /> } /> } /> } /> diff --git a/apps/admin-web/src/layouts/AdminLayout.tsx b/apps/admin-web/src/layouts/AdminLayout.tsx index 8ea19ca..6f6a7da 100644 --- a/apps/admin-web/src/layouts/AdminLayout.tsx +++ b/apps/admin-web/src/layouts/AdminLayout.tsx @@ -73,6 +73,7 @@ const MENU_ITEMS: MenuProps['items'] = [ label: '日志', children: [ { key: '/logs/users', label: '用户日志' }, + { key: '/logs/hq', label: 'HQ 操作日志' }, { key: '/logs/third-party', label: '第三方日志' }, ], }, diff --git a/apps/admin-web/src/lib/hq-log.ts b/apps/admin-web/src/lib/hq-log.ts new file mode 100644 index 0000000..62bbbe1 --- /dev/null +++ b/apps/admin-web/src/lib/hq-log.ts @@ -0,0 +1,42 @@ +export const HQ_OPERATION_ACTION_OPTIONS = [ + { value: 'CITY_CREATE', label: '新增开城城市' }, + { value: 'CITY_UPDATE', label: '编辑开城城市' }, + { value: 'PARTNER_CREATE', label: '新增城市合伙人' }, + { value: 'PARTNER_UPDATE', label: '编辑城市合伙人' }, + { value: 'PARTNER_ACCOUNT_CREATE', label: '新增合伙人账户' }, + { value: 'PARTNER_ACCOUNT_UPDATE', label: '编辑合伙人账户' }, + { value: 'HQ_ACCOUNT_CREATE', label: '新增 HQ 管理员' }, + { value: 'HQ_ACCOUNT_UPDATE', label: '编辑 HQ 管理员/权限' }, + { value: 'USER_DELETE', label: '删除用户' }, + { value: 'USER_BATCH_DELETE', label: '批量删除用户' }, + { value: 'ORDER_SHIP', label: '订单发货' }, + { value: 'ORDER_STATUS_DEBUG', label: '订单状态调试' }, + { value: 'ORDER_BATCH_DELETE', label: '批量删除订单' }, + { value: 'STORE_CREATE', label: '新增门店' }, + { value: 'STORE_UPDATE', label: '编辑门店' }, + { value: 'STORE_STATUS', label: '变更门店状态' }, + { value: 'STORE_AUDIT', label: '门店审核' }, + { value: 'STORE_ACCOUNT_CREATE', label: '新增门店账户' }, + { value: 'STORE_ACCOUNT_UPDATE', label: '编辑门店账户' }, + { value: 'PRODUCT_CREATE', label: '新增商品' }, + { value: 'PRODUCT_UPDATE', label: '编辑商品' }, + { value: 'PRODUCT_DELETE', label: '删除商品' }, + { value: 'BENEFIT_COUPON_VOID', label: '作废权益券' }, + { value: 'DELIVERY_UPDATE', label: '编辑配送单' }, + { value: 'TICKET_APPROVE', label: '工单通过' }, + { value: 'TICKET_REJECT', label: '工单驳回' }, + { value: 'STORE_PAYOUT_CONFIRM', label: '门店打款确认' }, + { value: 'STORE_PAYOUT_BATCH_CONFIRM', label: '批量门店打款' }, + { value: 'PARTNER_BILL_GENERATE', label: '生成合伙人账单' }, + { value: 'PARTNER_BILL_CONFIRM', label: '确认合伙人账单' }, + { value: 'PARTNER_BILL_MARK_PAID', label: '合伙人账单结算' }, +] as const; + +export const HQ_OPERATION_ACTION_LABELS: Record = Object.fromEntries( + HQ_OPERATION_ACTION_OPTIONS.map((o) => [o.value, o.label]), +); + +export function resolveHqOperationLabel(action: string | null | undefined): string { + if (!action) return '—'; + return HQ_OPERATION_ACTION_LABELS[action] || action; +} diff --git a/apps/admin-web/src/pages/HqLogsPage.tsx b/apps/admin-web/src/pages/HqLogsPage.tsx new file mode 100644 index 0000000..16c32b9 --- /dev/null +++ b/apps/admin-web/src/pages/HqLogsPage.tsx @@ -0,0 +1,196 @@ +import { useEffect, useState } from 'react'; +import { + Button, Descriptions, Drawer, Form, Input, Select, Space, Table, Tag, Typography, +} from 'antd'; +import type { ColumnsType } from 'antd/es/table'; +import { useSearchParams } from 'react-router-dom'; +import { request } from '../lib/api'; +import { fmtTime } from '../lib/constants'; +import { HQ_OPERATION_ACTION_OPTIONS, resolveHqOperationLabel } from '../lib/hq-log'; +import { useAdminList } from '../lib/useAdminList'; + +type Row = { + id: string; + hqAccountId: string | null; + hqName: string | null; + hqPhone: string | null; + hqRole: string | null; + action: string | null; + actionLabel: string; + refType: string | null; + refId: string | null; + status: string | null; + remark: string | null; + detail: Record | null; + createdAt: string; +}; + +export default function HqLogsPage() { + const [searchParams, setSearchParams] = useSearchParams(); + const [form] = Form.useForm(); + const [filters, setFilters] = useState>(() => ({ + hqAccountId: searchParams.get('hqAccountId') ?? '', + action: searchParams.get('action') ?? '', + refType: searchParams.get('refType') ?? '', + })); + const { data, loading, page, pageSize, setPage, setPageSize, reload } = useAdminList( + '/admin/logs/hq', + () => { + const qs = new URLSearchParams(); + if (filters.hqAccountId) qs.set('hqAccountId', filters.hqAccountId); + if (filters.action) qs.set('action', filters.action); + if (filters.refType) qs.set('refType', filters.refType); + return qs; + }, + [filters], + ); + const [detail, setDetail] = useState(null); + const [drawerOpen, setDrawerOpen] = useState(false); + + useEffect(() => { + form.setFieldsValue(filters); + }, [form, filters]); + + const columns: ColumnsType = [ + { title: '时间', dataIndex: 'createdAt', width: 160, render: fmtTime }, + { + title: '操作人', + width: 160, + render: (_, r) => ( +
+
{r.hqName || '—'}
+
+ {r.hqPhone || ''} {r.hqAccountId ? `(#${r.hqAccountId})` : ''} +
+
+ ), + }, + { + title: '行为', + dataIndex: 'actionLabel', + width: 160, + render: (v, r) => {v || resolveHqOperationLabel(r.action)}, + }, + { title: '对象类型', dataIndex: 'refType', width: 120, render: (v) => v || '—' }, + { title: '对象 ID', dataIndex: 'refId', width: 100, render: (v) => v || '—' }, + { + title: '操作', + width: 80, + render: (_, row) => ( + + ), + }, + ]; + + return ( +
+ HQ 操作日志 + + 记录总部后台写操作(开城、订单、用户、权限、合伙人等),仅追加不删除。 + + +
{ + setFilters(values); + setPage(1); + const qs = new URLSearchParams(); + if (values.hqAccountId) qs.set('hqAccountId', values.hqAccountId); + if (values.action) qs.set('action', values.action); + if (values.refType) qs.set('refType', values.refType); + setSearchParams(qs); + }} + > + + + + + + + + + + + + +
+ + { + setPage(p); + setPageSize(ps); + }, + }} + /> + + setDrawerOpen(false)}> + {detail && ( + <> + + {detail.id} + {fmtTime(detail.createdAt)} + + {detail.hqName} / {detail.hqPhone} (ID: {detail.hqAccountId}) + + {detail.hqRole || '—'} + + {detail.actionLabel || resolveHqOperationLabel(detail.action)} + + {detail.refType} / {detail.refId} + {detail.status || '—'} + {detail.remark || '—'} + + 请求/响应快照 +
+              {JSON.stringify(detail.detail, null, 2)}
+            
+ + )} +
+ + ); +} diff --git a/server/dukang-api/src/app.module.ts b/server/dukang-api/src/app.module.ts index 416659f..43ac719 100644 --- a/server/dukang-api/src/app.module.ts +++ b/server/dukang-api/src/app.module.ts @@ -16,6 +16,7 @@ import { AnalyticsModule } from './modules/analytics/analytics.module'; import { JobsModule } from './jobs/jobs.module'; import { OpsModule } from './modules/ops/ops.module'; import { CommonModule } from './modules/common/common.module'; +import { HqOperationModule } from './common/hq-operation/hq-operation.module'; import { CallbacksModule } from './callbacks/callbacks.module'; @Module({ @@ -41,6 +42,7 @@ import { CallbacksModule } from './callbacks/callbacks.module'; JobsModule, OpsModule, CommonModule, + HqOperationModule, CallbacksModule, ], }) diff --git a/server/dukang-api/src/common/event/event.helpers.ts b/server/dukang-api/src/common/event/event.helpers.ts index cb2f091..7e99ca5 100644 --- a/server/dukang-api/src/common/event/event.helpers.ts +++ b/server/dukang-api/src/common/event/event.helpers.ts @@ -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; + 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 } : {}), + }, + } + : {}), + }; +} diff --git a/server/dukang-api/src/common/hq-operation/hq-operation-log.service.ts b/server/dukang-api/src/common/hq-operation/hq-operation-log.service.ts new file mode 100644 index 0000000..46dd212 --- /dev/null +++ b/server/dukang-api/src/common/hq-operation/hq-operation-log.service.ts @@ -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; + remark?: string; +}; + +@Injectable() +export class HqOperationLogService { + constructor(private readonly prisma: PrismaService) {} + + async log(input: LogHqOperationInput): Promise { + await this.prisma.commonEvent.create({ + data: buildHqOperationEvent(input), + }); + } + + logSafe(input: LogHqOperationInput): void { + void this.log(input).catch((err) => { + console.error('[HqOperationLog] write failed', err); + }); + } +} diff --git a/server/dukang-api/src/common/hq-operation/hq-operation.constants.ts b/server/dukang-api/src/common/hq-operation/hq-operation.constants.ts new file mode 100644 index 0000000..40669e0 --- /dev/null +++ b/server/dukang-api/src/common/hq-operation/hq-operation.constants.ts @@ -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 = { + [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 操作'; +} diff --git a/server/dukang-api/src/common/hq-operation/hq-operation.decorator.ts b/server/dukang-api/src/common/hq-operation/hq-operation.decorator.ts new file mode 100644 index 0000000..3210608 --- /dev/null +++ b/server/dukang-api/src/common/hq-operation/hq-operation.decorator.ts @@ -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); diff --git a/server/dukang-api/src/common/hq-operation/hq-operation.interceptor.ts b/server/dukang-api/src/common/hq-operation/hq-operation.interceptor.ts new file mode 100644 index 0000000..8577875 --- /dev/null +++ b/server/dukang-api/src/common/hq-operation/hq-operation.interceptor.ts @@ -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) }; + 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; + const summary: Record = {}; + 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 { + const meta = this.reflector.get( + 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 | null)?.[meta.refIdField] + : (data as Record | null)?.id, + ) + ?? 0n; + + const detail: Record = { + 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, + }); + }), + ); + } +} diff --git a/server/dukang-api/src/common/hq-operation/hq-operation.module.ts b/server/dukang-api/src/common/hq-operation/hq-operation.module.ts new file mode 100644 index 0000000..7418c1c --- /dev/null +++ b/server/dukang-api/src/common/hq-operation/hq-operation.module.ts @@ -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 {} diff --git a/server/dukang-api/src/modules/ops/admin-benefit.controller.ts b/server/dukang-api/src/modules/ops/admin-benefit.controller.ts index 72dcb7a..167fb2f 100644 --- a/server/dukang-api/src/modules/ops/admin-benefit.controller.ts +++ b/server/dukang-api/src/modules/ops/admin-benefit.controller.ts @@ -1,5 +1,7 @@ import { Controller, Get, Param, Post, Query, UseGuards } from '@nestjs/common'; import { HqAuthGuard } from '../../common/guards/hq-auth.guard'; +import { HqOperation } from '../../common/hq-operation/hq-operation.decorator'; +import { HqOperationAction } from '../../common/hq-operation/hq-operation.constants'; import { AdminBenefitService } from './admin-benefit.service'; import { AdminBenefitCouponsQueryDto, AdminBenefitLedgersQueryDto } from './dto/admin-query.dto'; @@ -19,6 +21,7 @@ export class AdminBenefitCouponsController { } @Post(':id/void') + @HqOperation({ action: HqOperationAction.BENEFIT_COUPON_VOID, refType: 'BENEFIT_COUPON', refIdParam: 'id' }) voidCoupon(@Param('id') id: string) { return this.service.voidCoupon(BigInt(id)); } diff --git a/server/dukang-api/src/modules/ops/admin-cities.controller.ts b/server/dukang-api/src/modules/ops/admin-cities.controller.ts index 80992ae..f970e8f 100644 --- a/server/dukang-api/src/modules/ops/admin-cities.controller.ts +++ b/server/dukang-api/src/modules/ops/admin-cities.controller.ts @@ -1,9 +1,10 @@ -import { Controller, Get, Param, Post, Put, Query, UseGuards } from '@nestjs/common'; +import { Body, Controller, Get, Param, Post, Put, Query, UseGuards } from '@nestjs/common'; import { HqAuthGuard } from '../../common/guards/hq-auth.guard'; +import { HqOperation } from '../../common/hq-operation/hq-operation.decorator'; +import { HqOperationAction } from '../../common/hq-operation/hq-operation.constants'; import { AdminCitiesService } from './admin-cities.service'; import { AdminCitiesQueryDto } from './dto/admin-query.dto'; import { CreateCityDto, UpdateCityDto } from './dto/admin-mutate.dto'; -import { Body } from '@nestjs/common'; @Controller('admin/cities') @UseGuards(HqAuthGuard) @@ -21,11 +22,18 @@ export class AdminCitiesController { } @Post() + @HqOperation({ action: HqOperationAction.CITY_CREATE, refType: 'CITY', refIdField: 'id', includeBody: true }) create(@Body() dto: CreateCityDto) { return this.service.create(dto); } @Put(':id') + @HqOperation({ + action: HqOperationAction.CITY_UPDATE, + refType: 'CITY', + refIdParam: 'id', + includeBody: true, + }) update(@Param('id') id: string, @Body() dto: UpdateCityDto) { return this.service.update(BigInt(id), dto); } diff --git a/server/dukang-api/src/modules/ops/admin-hq-accounts.controller.ts b/server/dukang-api/src/modules/ops/admin-hq-accounts.controller.ts index eb2b0a0..3146723 100644 --- a/server/dukang-api/src/modules/ops/admin-hq-accounts.controller.ts +++ b/server/dukang-api/src/modules/ops/admin-hq-accounts.controller.ts @@ -1,6 +1,8 @@ import { Body, Controller, Get, Param, Post, Put, Query, UseGuards } from '@nestjs/common'; import { HqAuthGuard } from '../../common/guards/hq-auth.guard'; import { SuperAdminGuard } from '../../common/guards/super-admin.guard'; +import { HqOperation } from '../../common/hq-operation/hq-operation.decorator'; +import { HqOperationAction } from '../../common/hq-operation/hq-operation.constants'; import { AdminHqAccountsService } from './admin-hq-accounts.service'; import { AdminHqAccountsQueryDto } from './dto/admin-query.dto'; import { CreateHqAccountDto, UpdateHqAccountDto } from './dto/admin-mutate.dto'; @@ -22,12 +24,24 @@ export class AdminHqAccountsController { @Post() @UseGuards(SuperAdminGuard) + @HqOperation({ + action: HqOperationAction.HQ_ACCOUNT_CREATE, + refType: 'HQ_ACCOUNT', + refIdField: 'id', + includeBody: true, + }) create(@Body() dto: CreateHqAccountDto) { return this.service.create(dto); } @Put(':id') @UseGuards(SuperAdminGuard) + @HqOperation({ + action: HqOperationAction.HQ_ACCOUNT_UPDATE, + refType: 'HQ_ACCOUNT', + refIdParam: 'id', + includeBody: true, + }) update(@Param('id') id: string, @Body() dto: UpdateHqAccountDto) { return this.service.update(BigInt(id), dto); } diff --git a/server/dukang-api/src/modules/ops/admin-hq-logs.controller.ts b/server/dukang-api/src/modules/ops/admin-hq-logs.controller.ts new file mode 100644 index 0000000..16cd2ae --- /dev/null +++ b/server/dukang-api/src/modules/ops/admin-hq-logs.controller.ts @@ -0,0 +1,20 @@ +import { Controller, Get, Param, Query, UseGuards } from '@nestjs/common'; +import { HqAuthGuard } from '../../common/guards/hq-auth.guard'; +import { AdminHqLogsService } from './admin-hq-logs.service'; +import { AdminHqLogsQueryDto } from './dto/admin-query.dto'; + +@Controller('admin/logs/hq') +@UseGuards(HqAuthGuard) +export class AdminHqLogsController { + constructor(private readonly service: AdminHqLogsService) {} + + @Get() + list(@Query() query: AdminHqLogsQueryDto) { + return this.service.list(query); + } + + @Get(':id') + detail(@Param('id') id: string) { + return this.service.detail(BigInt(id)); + } +} diff --git a/server/dukang-api/src/modules/ops/admin-hq-logs.service.ts b/server/dukang-api/src/modules/ops/admin-hq-logs.service.ts new file mode 100644 index 0000000..2168821 --- /dev/null +++ b/server/dukang-api/src/modules/ops/admin-hq-logs.service.ts @@ -0,0 +1,96 @@ +import { Injectable, NotFoundException } from '@nestjs/common'; +import { PrismaService } from '../../common/prisma/prisma.module'; +import { serializeBigInt } from '../../common/decorators/current-user.decorator'; +import { hqOperationLogWhere } from '../../common/event/event.helpers'; +import { resolveHqOperationLabel } from '../../common/hq-operation/hq-operation.constants'; +import type { AdminHqLogsQueryDto } from './dto/admin-query.dto'; + +@Injectable() +export class AdminHqLogsService { + constructor(private readonly prisma: PrismaService) {} + + async list(query: AdminHqLogsQueryDto) { + const page = query.page ?? 1; + const pageSize = query.pageSize ?? 20; + const where = hqOperationLogWhere({ + hqAccountId: query.hqAccountId ? BigInt(query.hqAccountId) : undefined, + action: query.action, + refType: query.refType, + from: query.from ? new Date(query.from) : undefined, + to: query.to ? new Date(query.to) : undefined, + }); + + const [rows, total] = await Promise.all([ + this.prisma.commonEvent.findMany({ + where, + orderBy: { createdAt: 'desc' }, + skip: (page - 1) * pageSize, + take: pageSize, + }), + this.prisma.commonEvent.count({ where }), + ]); + + const hqIds = [...new Set(rows.map((r) => r.actorId).filter((id): id is bigint => id != null))]; + const hqAccounts = hqIds.length + ? await this.prisma.hqAccount.findMany({ + where: { id: { in: hqIds } }, + select: { id: true, name: true, phone: true, adminRole: true }, + }) + : []; + const hqMap = new Map(hqAccounts.map((a) => [a.id.toString(), a])); + + return serializeBigInt({ + items: rows.map((row) => { + const hq = row.actorId ? hqMap.get(row.actorId.toString()) : undefined; + const action = row.param1Desc === 'action' ? row.param1 : row.param1; + return { + id: row.id, + hqAccountId: row.actorId, + hqName: hq?.name ?? null, + hqPhone: hq?.phone ?? null, + hqRole: hq?.adminRole ?? null, + action, + actionLabel: resolveHqOperationLabel(action, row.refType), + refType: row.param2Desc === 'target_type' ? row.param2 : row.refType, + refId: row.param3Desc === 'target_id' ? row.param3 : row.refId.toString(), + status: row.status, + remark: row.remark, + detail: row.extraJson, + createdAt: row.createdAt, + }; + }), + total, + page, + pageSize, + }); + } + + async detail(id: bigint) { + const row = await this.prisma.commonEvent.findFirst({ + where: { id, eventType: 'HQ_OPERATION' }, + }); + if (!row) throw new NotFoundException('操作日志不存在'); + + const hq = row.actorId + ? await this.prisma.hqAccount.findUnique({ + where: { id: row.actorId }, + select: { id: true, name: true, phone: true, adminRole: true }, + }) + : null; + + const action = row.param1Desc === 'action' ? row.param1 : row.param1; + return serializeBigInt({ + id: row.id, + hqAccountId: row.actorId, + hqAccount: hq, + action, + actionLabel: resolveHqOperationLabel(action, row.refType), + refType: row.param2Desc === 'target_type' ? row.param2 : row.refType, + refId: row.param3Desc === 'target_id' ? row.param3 : row.refId.toString(), + status: row.status, + remark: row.remark, + detail: row.extraJson, + createdAt: row.createdAt, + }); + } +} diff --git a/server/dukang-api/src/modules/ops/admin-orders.controller.ts b/server/dukang-api/src/modules/ops/admin-orders.controller.ts index f6f74bc..036007c 100644 --- a/server/dukang-api/src/modules/ops/admin-orders.controller.ts +++ b/server/dukang-api/src/modules/ops/admin-orders.controller.ts @@ -1,6 +1,8 @@ import { Body, Controller, Get, Param, Post, Put, Query, UseGuards } from '@nestjs/common'; import { HqAuthGuard } from '../../common/guards/hq-auth.guard'; import { SuperAdminGuard } from '../../common/guards/super-admin.guard'; +import { HqOperation } from '../../common/hq-operation/hq-operation.decorator'; +import { HqOperationAction } from '../../common/hq-operation/hq-operation.constants'; import { AdminOrdersService } from './admin-orders.service'; import { AdminShipOrderDto, BatchDeleteOrdersDto, UpdateOrderStatusDto } from './dto/admin-mutate.dto'; import { AdminOrdersQueryDto } from './dto/admin-query.dto'; @@ -17,6 +19,12 @@ export class AdminOrdersController { @Post('batch-delete') @UseGuards(SuperAdminGuard) + @HqOperation({ + action: HqOperationAction.ORDER_BATCH_DELETE, + refType: 'ORDER', + batch: true, + includeBody: true, + }) batchDelete(@Body() dto: BatchDeleteOrdersDto) { return this.ordersService.batchDeleteOrders(dto.ids.map((id) => BigInt(id))); } @@ -33,12 +41,24 @@ export class AdminOrdersController { /** HQ 发货:调用小飞侠创建运单并更新配送信息 */ @Post(':id/ship') + @HqOperation({ + action: HqOperationAction.ORDER_SHIP, + refType: 'ORDER', + refIdParam: 'id', + includeBody: true, + }) ship(@Param('id') id: string, @Body() dto: AdminShipOrderDto) { return this.ordersService.shipOrder(BigInt(id), dto); } /** preV1 调试:直接改订单状态,不走业务校验 */ @Put(':id/status') + @HqOperation({ + action: HqOperationAction.ORDER_STATUS_DEBUG, + refType: 'ORDER', + refIdParam: 'id', + includeBody: true, + }) updateStatus(@Param('id') id: string, @Body() dto: UpdateOrderStatusDto) { return this.ordersService.updateStatusDebug(BigInt(id), dto.status); } diff --git a/server/dukang-api/src/modules/ops/admin-partners.controller.ts b/server/dukang-api/src/modules/ops/admin-partners.controller.ts index 762f883..bbf8edc 100644 --- a/server/dukang-api/src/modules/ops/admin-partners.controller.ts +++ b/server/dukang-api/src/modules/ops/admin-partners.controller.ts @@ -1,5 +1,7 @@ import { Body, Controller, Get, Param, Post, Put, Query, UseGuards } from '@nestjs/common'; import { HqAuthGuard } from '../../common/guards/hq-auth.guard'; +import { HqOperation } from '../../common/hq-operation/hq-operation.decorator'; +import { HqOperationAction } from '../../common/hq-operation/hq-operation.constants'; import { AdminPartnersService } from './admin-partners.service'; import { AdminPartnerAccountsQueryDto, AdminPartnersQueryDto } from './dto/admin-query.dto'; import { @@ -25,11 +27,23 @@ export class AdminPartnersController { } @Post() + @HqOperation({ + action: HqOperationAction.PARTNER_CREATE, + refType: 'PARTNER', + refIdField: 'id', + includeBody: true, + }) create(@Body() dto: CreatePartnerDto) { return this.service.createPartner(dto); } @Put(':id') + @HqOperation({ + action: HqOperationAction.PARTNER_UPDATE, + refType: 'PARTNER', + refIdParam: 'id', + includeBody: true, + }) update(@Param('id') id: string, @Body() dto: UpdatePartnerDto) { return this.service.updatePartner(BigInt(id), dto); } @@ -51,11 +65,23 @@ export class AdminPartnerAccountsController { } @Post() + @HqOperation({ + action: HqOperationAction.PARTNER_ACCOUNT_CREATE, + refType: 'PARTNER_ACCOUNT', + refIdField: 'id', + includeBody: true, + }) create(@Body() dto: CreatePartnerAccountDto) { return this.service.createPartnerAccount(dto); } @Put(':id') + @HqOperation({ + action: HqOperationAction.PARTNER_ACCOUNT_UPDATE, + refType: 'PARTNER_ACCOUNT', + refIdParam: 'id', + includeBody: true, + }) update(@Param('id') id: string, @Body() dto: UpdatePartnerAccountDto) { return this.service.updatePartnerAccount(BigInt(id), dto); } diff --git a/server/dukang-api/src/modules/ops/admin-product-detail-templates.controller.ts b/server/dukang-api/src/modules/ops/admin-product-detail-templates.controller.ts index 4ba9985..0c92aa1 100644 --- a/server/dukang-api/src/modules/ops/admin-product-detail-templates.controller.ts +++ b/server/dukang-api/src/modules/ops/admin-product-detail-templates.controller.ts @@ -1,5 +1,7 @@ import { Body, Controller, Get, Param, Post, Put, Query, UseGuards } from '@nestjs/common'; import { HqAuthGuard } from '../../common/guards/hq-auth.guard'; +import { HqOperation } from '../../common/hq-operation/hq-operation.decorator'; +import { HqOperationAction } from '../../common/hq-operation/hq-operation.constants'; import { AdminProductDetailTemplatesService } from './admin-product-detail-templates.service'; import { AdminProductDetailTemplatesQueryDto } from './dto/admin-query.dto'; import { @@ -23,11 +25,23 @@ export class AdminProductDetailTemplatesController { } @Post() + @HqOperation({ + action: HqOperationAction.PRODUCT_TEMPLATE_CREATE, + refType: 'PRODUCT_TEMPLATE', + refIdField: 'id', + includeBody: true, + }) create(@Body() dto: CreateProductDetailTemplateDto) { return this.service.create(dto); } @Put(':id') + @HqOperation({ + action: HqOperationAction.PRODUCT_TEMPLATE_UPDATE, + refType: 'PRODUCT_TEMPLATE', + refIdParam: 'id', + includeBody: true, + }) update(@Param('id') id: string, @Body() dto: UpdateProductDetailTemplateDto) { return this.service.update(BigInt(id), dto); } diff --git a/server/dukang-api/src/modules/ops/admin-products.controller.ts b/server/dukang-api/src/modules/ops/admin-products.controller.ts index 9bf3dd6..a09e251 100644 --- a/server/dukang-api/src/modules/ops/admin-products.controller.ts +++ b/server/dukang-api/src/modules/ops/admin-products.controller.ts @@ -1,5 +1,7 @@ import { Body, Controller, Delete, Get, Param, Post, Put, Query, UseGuards } from '@nestjs/common'; import { HqAuthGuard } from '../../common/guards/hq-auth.guard'; +import { HqOperation } from '../../common/hq-operation/hq-operation.decorator'; +import { HqOperationAction } from '../../common/hq-operation/hq-operation.constants'; import { AdminProductsService } from './admin-products.service'; import { AdminProductsQueryDto } from './dto/admin-query.dto'; import { CreateProductDto, UpdateProductDto } from './dto/admin-mutate.dto'; @@ -20,16 +22,19 @@ export class AdminProductsController { } @Post() + @HqOperation({ action: HqOperationAction.PRODUCT_CREATE, refType: 'PRODUCT', refIdField: 'id', includeBody: true }) create(@Body() dto: CreateProductDto) { return this.service.create(dto); } @Put(':id') + @HqOperation({ action: HqOperationAction.PRODUCT_UPDATE, refType: 'PRODUCT', refIdParam: 'id', includeBody: true }) update(@Param('id') id: string, @Body() dto: UpdateProductDto) { return this.service.update(BigInt(id), dto); } @Delete(':id') + @HqOperation({ action: HqOperationAction.PRODUCT_DELETE, refType: 'PRODUCT', refIdParam: 'id' }) remove(@Param('id') id: string) { return this.service.remove(BigInt(id)); } diff --git a/server/dukang-api/src/modules/ops/admin-redeem-debug.controller.ts b/server/dukang-api/src/modules/ops/admin-redeem-debug.controller.ts index b560da5..9c40450 100644 --- a/server/dukang-api/src/modules/ops/admin-redeem-debug.controller.ts +++ b/server/dukang-api/src/modules/ops/admin-redeem-debug.controller.ts @@ -1,5 +1,7 @@ import { Body, Controller, Post, UseGuards } from '@nestjs/common'; import { HqAuthGuard } from '../../common/guards/hq-auth.guard'; +import { HqOperation } from '../../common/hq-operation/hq-operation.decorator'; +import { HqOperationAction } from '../../common/hq-operation/hq-operation.constants'; import { AdminRedeemDebugService } from './admin-redeem-debug.service'; import type { AdminRedeemDebugCreateTokenDto, @@ -13,6 +15,12 @@ export class AdminRedeemDebugController { /** preV1 调试:为用户生成核销码 */ @Post('create-token') + @HqOperation({ + action: HqOperationAction.REDEEM_DEBUG_CREATE_TOKEN, + refType: 'REDEEM_DEBUG', + batch: true, + includeBody: true, + }) createToken(@Body() dto: AdminRedeemDebugCreateTokenDto) { return this.service.createToken(dto); } @@ -25,6 +33,12 @@ export class AdminRedeemDebugController { /** preV1 调试:门店侧确认核销 */ @Post('confirm') + @HqOperation({ + action: HqOperationAction.REDEEM_DEBUG_CONFIRM, + refType: 'REDEEM_DEBUG', + batch: true, + includeBody: true, + }) confirm(@Body() dto: AdminRedeemDebugStoreTokenDto) { return this.service.confirm(dto); } diff --git a/server/dukang-api/src/modules/ops/admin-redeem.controller.ts b/server/dukang-api/src/modules/ops/admin-redeem.controller.ts index 702733c..5741851 100644 --- a/server/dukang-api/src/modules/ops/admin-redeem.controller.ts +++ b/server/dukang-api/src/modules/ops/admin-redeem.controller.ts @@ -1,5 +1,7 @@ import { Body, Controller, Get, Param, Put, Query, UseGuards } from '@nestjs/common'; import { HqAuthGuard } from '../../common/guards/hq-auth.guard'; +import { HqOperation } from '../../common/hq-operation/hq-operation.decorator'; +import { HqOperationAction } from '../../common/hq-operation/hq-operation.constants'; import { AdminRedeemService, AdminDeliveriesService } from './admin-redeem.service'; import { AdminDeliveriesQueryDto, AdminRedeemRecordsQueryDto } from './dto/admin-query.dto'; import { UpdateDeliveryDto } from './dto/admin-mutate.dto'; @@ -36,6 +38,12 @@ export class AdminDeliveriesController { } @Put(':id') + @HqOperation({ + action: HqOperationAction.DELIVERY_UPDATE, + refType: 'DELIVERY', + refIdParam: 'id', + includeBody: true, + }) update(@Param('id') id: string, @Body() dto: UpdateDeliveryDto) { return this.service.update(BigInt(id), dto); } diff --git a/server/dukang-api/src/modules/ops/admin-stores.controller.ts b/server/dukang-api/src/modules/ops/admin-stores.controller.ts index 259c930..9c0762d 100644 --- a/server/dukang-api/src/modules/ops/admin-stores.controller.ts +++ b/server/dukang-api/src/modules/ops/admin-stores.controller.ts @@ -1,5 +1,7 @@ import { Body, Controller, Delete, Get, Param, Post, Put, Query, UseGuards } from '@nestjs/common'; import { HqAuthGuard } from '../../common/guards/hq-auth.guard'; +import { HqOperation } from '../../common/hq-operation/hq-operation.decorator'; +import { HqOperationAction } from '../../common/hq-operation/hq-operation.constants'; import { AdminStoresService } from './admin-stores.service'; import { AdminStoreAccountsQueryDto, @@ -32,21 +34,25 @@ export class AdminStoresController { } @Post() + @HqOperation({ action: HqOperationAction.STORE_CREATE, refType: 'STORE', refIdField: 'id', includeBody: true }) create(@Body() dto: CreateStoreDto) { return this.service.createStore(dto); } @Put(':id') + @HqOperation({ action: HqOperationAction.STORE_UPDATE, refType: 'STORE', refIdParam: 'id', includeBody: true }) update(@Param('id') id: string, @Body() dto: UpdateStoreDto) { return this.service.updateStore(BigInt(id), dto); } @Put(':id/status') + @HqOperation({ action: HqOperationAction.STORE_STATUS, refType: 'STORE', refIdParam: 'id', includeBody: true }) updateStatus(@Param('id') id: string, @Body() dto: UpdateStoreStatusDto) { return this.service.updateStoreStatus(BigInt(id), dto); } @Put(':id/audit') + @HqOperation({ action: HqOperationAction.STORE_AUDIT, refType: 'STORE', refIdParam: 'id', includeBody: true }) audit(@Param('id') id: string, @Body() body: { approved: boolean; remark?: string }) { return this.service.auditStore(BigInt(id), body); } @@ -68,11 +74,23 @@ export class AdminStoreAccountsController { } @Post() + @HqOperation({ + action: HqOperationAction.STORE_ACCOUNT_CREATE, + refType: 'STORE_ACCOUNT', + refIdField: 'id', + includeBody: true, + }) create(@Body() dto: CreateStoreAccountDto) { return this.service.createStoreAccount(dto); } @Put(':id') + @HqOperation({ + action: HqOperationAction.STORE_ACCOUNT_UPDATE, + refType: 'STORE_ACCOUNT', + refIdParam: 'id', + includeBody: true, + }) update(@Param('id') id: string, @Body() dto: UpdateStoreAccountDto) { return this.service.updateStoreAccount(BigInt(id), dto); } @@ -89,16 +107,29 @@ export class AdminStoreMediaController { } @Post() + @HqOperation({ + action: HqOperationAction.STORE_MEDIA_CREATE, + refType: 'STORE_MEDIA', + refIdField: 'id', + includeBody: true, + }) create(@Body() dto: CreateStoreMediaDto) { return this.service.createStoreMedia(dto); } @Put(':id') + @HqOperation({ + action: HqOperationAction.STORE_MEDIA_UPDATE, + refType: 'STORE_MEDIA', + refIdParam: 'id', + includeBody: true, + }) update(@Param('id') id: string, @Body() dto: UpdateStoreMediaDto) { return this.service.updateStoreMedia(BigInt(id), dto); } @Delete(':id') + @HqOperation({ action: HqOperationAction.STORE_MEDIA_DELETE, refType: 'STORE_MEDIA', refIdParam: 'id' }) remove(@Param('id') id: string) { return this.service.deleteStoreMedia(BigInt(id)); } diff --git a/server/dukang-api/src/modules/ops/admin-tickets.controller.ts b/server/dukang-api/src/modules/ops/admin-tickets.controller.ts index ab86cfb..bed325e 100644 --- a/server/dukang-api/src/modules/ops/admin-tickets.controller.ts +++ b/server/dukang-api/src/modules/ops/admin-tickets.controller.ts @@ -1,5 +1,7 @@ import { Body, Controller, Get, Param, Post, Query, UseGuards } from '@nestjs/common'; import { HqAuthGuard } from '../../common/guards/hq-auth.guard'; +import { HqOperation } from '../../common/hq-operation/hq-operation.decorator'; +import { HqOperationAction } from '../../common/hq-operation/hq-operation.constants'; import { AdminTicketsService } from './admin-tickets.service'; import { TicketListQueryDto } from '../common/dto/common-query.dto'; @@ -19,11 +21,23 @@ export class AdminTicketsController { } @Post(':id/approve') + @HqOperation({ + action: HqOperationAction.TICKET_APPROVE, + refType: 'TICKET', + refIdParam: 'id', + includeBody: true, + }) approve(@Param('id') id: string, @Body() body: { remark?: string }) { return this.service.approve(BigInt(id), body.remark); } @Post(':id/reject') + @HqOperation({ + action: HqOperationAction.TICKET_REJECT, + refType: 'TICKET', + refIdParam: 'id', + includeBody: true, + }) reject(@Param('id') id: string, @Body() body: { remark?: string }) { return this.service.reject(BigInt(id), body.remark); } diff --git a/server/dukang-api/src/modules/ops/admin-users.controller.ts b/server/dukang-api/src/modules/ops/admin-users.controller.ts index 54848ea..c34a6ad 100644 --- a/server/dukang-api/src/modules/ops/admin-users.controller.ts +++ b/server/dukang-api/src/modules/ops/admin-users.controller.ts @@ -1,6 +1,8 @@ import { Body, Controller, Delete, Get, Param, Post, Query, UseGuards } from '@nestjs/common'; import { HqAuthGuard } from '../../common/guards/hq-auth.guard'; import { SuperAdminGuard } from '../../common/guards/super-admin.guard'; +import { HqOperation } from '../../common/hq-operation/hq-operation.decorator'; +import { HqOperationAction } from '../../common/hq-operation/hq-operation.constants'; import { AdminUsersService } from './admin-users.service'; import { AdminUsersQueryDto } from './dto/admin-query.dto'; import { BatchDeleteUsersConfirmDto, BatchDeleteUsersDto } from './dto/admin-mutate.dto'; @@ -23,6 +25,12 @@ export class AdminUsersController { @Post('batch-delete') @UseGuards(SuperAdminGuard) + @HqOperation({ + action: HqOperationAction.USER_BATCH_DELETE, + refType: 'USER', + batch: true, + includeBody: true, + }) batchDelete(@Body() dto: BatchDeleteUsersConfirmDto) { return this.usersService.batchDeleteUsers( dto.ids.map((id) => BigInt(id)), @@ -37,6 +45,11 @@ export class AdminUsersController { @Delete(':id') @UseGuards(SuperAdminGuard) + @HqOperation({ + action: HqOperationAction.USER_DELETE, + refType: 'USER', + refIdParam: 'id', + }) remove(@Param('id') id: string) { return this.usersService.deleteUser(BigInt(id)); } diff --git a/server/dukang-api/src/modules/ops/dto/admin-query.dto.ts b/server/dukang-api/src/modules/ops/dto/admin-query.dto.ts index 78e32a1..2f6f833 100644 --- a/server/dukang-api/src/modules/ops/dto/admin-query.dto.ts +++ b/server/dukang-api/src/modules/ops/dto/admin-query.dto.ts @@ -275,6 +275,28 @@ export class AdminUserLogsQueryDto extends PaginationQueryDto { to?: string; } +export class AdminHqLogsQueryDto extends PaginationQueryDto { + @IsOptional() + @IsString() + hqAccountId?: string; + + @IsOptional() + @IsString() + action?: string; + + @IsOptional() + @IsString() + refType?: string; + + @IsOptional() + @IsString() + from?: string; + + @IsOptional() + @IsString() + to?: string; +} + export class AdminStoreMediaQueryDto extends PaginationQueryDto { @IsOptional() @IsString() diff --git a/server/dukang-api/src/modules/ops/ops.module.ts b/server/dukang-api/src/modules/ops/ops.module.ts index c32711f..138a0a0 100644 --- a/server/dukang-api/src/modules/ops/ops.module.ts +++ b/server/dukang-api/src/modules/ops/ops.module.ts @@ -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 { AdminHqLogsController } from './admin-hq-logs.controller'; +import { AdminHqLogsService } from './admin-hq-logs.service'; import { AdminTicketsController } from './admin-tickets.controller'; import { AdminTicketsService } from './admin-tickets.service'; import { SuperAdminGuard } from '../../common/guards/super-admin.guard'; @@ -56,6 +58,7 @@ import { AdminRedeemDebugService } from './admin-redeem-debug.service'; AdminHqAccountsController, AdminProductsController, AdminUserLogsController, + AdminHqLogsController, AdminTicketsController, AdminXiaofeixiaController, AdminProductDetailTemplatesController, @@ -74,6 +77,7 @@ import { AdminRedeemDebugService } from './admin-redeem-debug.service'; AdminHqAccountsService, AdminProductsService, AdminUserLogsService, + AdminHqLogsService, AdminTicketsService, AdminXiaofeixiaService, AdminProductDetailTemplatesService, diff --git a/server/dukang-api/src/modules/settlement/settlement.controller.ts b/server/dukang-api/src/modules/settlement/settlement.controller.ts index add2313..90343a5 100644 --- a/server/dukang-api/src/modules/settlement/settlement.controller.ts +++ b/server/dukang-api/src/modules/settlement/settlement.controller.ts @@ -3,6 +3,8 @@ import { SettlementService } from './settlement.service'; import { JwtAuthGuard, AuthUser } from '../../common/guards/jwt-auth.guard'; import { CurrentUser } from '../../common/decorators/current-user.decorator'; import { HqAuthGuard } from '../../common/guards/hq-auth.guard'; +import { HqOperation } from '../../common/hq-operation/hq-operation.decorator'; +import { HqOperationAction } from '../../common/hq-operation/hq-operation.constants'; import { PrismaService } from '../../common/prisma/prisma.module'; @Controller('partner/settlement') @@ -52,11 +54,23 @@ export class AdminStorePayoutController { } @Post(':id/confirm') + @HqOperation({ + action: HqOperationAction.STORE_PAYOUT_CONFIRM, + refType: 'STORE_PAYOUT', + refIdParam: 'id', + includeBody: true, + }) confirm(@Param('id') id: string, @Body() body: { paymentRef?: string; batchNo?: string; remark?: string }) { return this.settlementService.confirmStorePayout(BigInt(id), body); } @Post('batch-confirm') + @HqOperation({ + action: HqOperationAction.STORE_PAYOUT_BATCH_CONFIRM, + refType: 'STORE_PAYOUT', + batch: true, + includeBody: true, + }) batchConfirm(@Body() body: { ids: string[]; batchNo?: string }) { return this.settlementService.batchConfirmStorePayouts(body.ids ?? [], body); } @@ -73,6 +87,12 @@ export class AdminPartnerBillController { constructor(private readonly settlementService: SettlementService) {} @Post('generate') + @HqOperation({ + action: HqOperationAction.PARTNER_BILL_GENERATE, + refType: 'PARTNER_BILL', + refIdField: 'id', + includeBody: true, + }) generate(@Body() body: { partnerId: string; year: number; month: number }) { return this.settlementService.generatePartnerBill(body); } @@ -101,11 +121,22 @@ export class AdminPartnerBillController { } @Post(':id/confirm') + @HqOperation({ + action: HqOperationAction.PARTNER_BILL_CONFIRM, + refType: 'PARTNER_BILL', + refIdParam: 'id', + }) confirm(@Param('id') id: string) { return this.settlementService.confirmPartnerBill(BigInt(id)); } @Post(':id/mark-paid') + @HqOperation({ + action: HqOperationAction.PARTNER_BILL_MARK_PAID, + refType: 'PARTNER_BILL', + refIdParam: 'id', + includeBody: true, + }) markPaid(@Param('id') id: string, @Body() body: { paymentRef?: string }) { return this.settlementService.markPartnerBillPaid(BigInt(id), body); } diff --git a/server/dukang-api/src/modules/settlement/settlement.service.ts b/server/dukang-api/src/modules/settlement/settlement.service.ts index 0049f19..5051c2f 100644 --- a/server/dukang-api/src/modules/settlement/settlement.service.ts +++ b/server/dukang-api/src/modules/settlement/settlement.service.ts @@ -106,18 +106,6 @@ export class SettlementService { }, }); - await this.prisma.commonEvent.create({ - data: { - eventType: 'HQ_OPERATION', - refType: 'STORE_PAYOUT', - refId: id, - actorType: 'HQ', - status: 'PAID', - param1: dto.paymentRef ?? '', - remark: dto.remark ?? '门店 T+1 打款确认', - }, - }); - return serializeBigInt(updated); } @@ -269,17 +257,6 @@ export class SettlementService { data: { status: 'CONFIRMED', confirmedAt: new Date() }, }); - await this.prisma.commonEvent.create({ - data: { - eventType: 'HQ_OPERATION', - refType: 'PARTNER_BILL', - refId: id, - actorType: 'HQ', - status: 'CONFIRMED', - amount1: Number(updated.totalAmount), - }, - }); - return serializeBigInt(updated); } @@ -293,18 +270,6 @@ export class SettlementService { data: { status: 'PAID', paidAt: new Date() }, }); - await this.prisma.commonEvent.create({ - data: { - eventType: 'HQ_OPERATION', - refType: 'PARTNER_BILL', - refId: id, - actorType: 'HQ', - status: 'PAID', - param1: dto.paymentRef ?? '', - amount1: Number(updated.totalAmount), - }, - }); - return serializeBigInt(updated); }