增加核销接口
增加hq管理端日志
This commit is contained in:
@@ -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() {
|
||||
<Route path="/partner-bills" element={<PartnerBillsPage />} />
|
||||
<Route path="/tickets" element={<TicketsPage />} />
|
||||
<Route path="/logs/users" element={<UserLogsPage />} />
|
||||
<Route path="/logs/hq" element={<HqLogsPage />} />
|
||||
<Route path="/logs/third-party" element={<ThirdPartyLogsPage />} />
|
||||
<Route path="/deliveries" element={<DeliveriesPage />} />
|
||||
<Route path="/deliveries/xiaofeixia" element={<XiaofeixiaTestPage />} />
|
||||
|
||||
@@ -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: '第三方日志' },
|
||||
],
|
||||
},
|
||||
|
||||
@@ -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<string, string> = 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;
|
||||
}
|
||||
@@ -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<string, unknown> | null;
|
||||
createdAt: string;
|
||||
};
|
||||
|
||||
export default function HqLogsPage() {
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const [form] = Form.useForm();
|
||||
const [filters, setFilters] = useState<Record<string, string>>(() => ({
|
||||
hqAccountId: searchParams.get('hqAccountId') ?? '',
|
||||
action: searchParams.get('action') ?? '',
|
||||
refType: searchParams.get('refType') ?? '',
|
||||
}));
|
||||
const { data, loading, page, pageSize, setPage, setPageSize, reload } = useAdminList<Row>(
|
||||
'/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<Row | null>(null);
|
||||
const [drawerOpen, setDrawerOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
form.setFieldsValue(filters);
|
||||
}, [form, filters]);
|
||||
|
||||
const columns: ColumnsType<Row> = [
|
||||
{ title: '时间', dataIndex: 'createdAt', width: 160, render: fmtTime },
|
||||
{
|
||||
title: '操作人',
|
||||
width: 160,
|
||||
render: (_, r) => (
|
||||
<div>
|
||||
<div>{r.hqName || '—'}</div>
|
||||
<div style={{ color: '#999', fontSize: 12 }}>
|
||||
{r.hqPhone || ''} {r.hqAccountId ? `(#${r.hqAccountId})` : ''}
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '行为',
|
||||
dataIndex: 'actionLabel',
|
||||
width: 160,
|
||||
render: (v, r) => <Tag color="blue">{v || resolveHqOperationLabel(r.action)}</Tag>,
|
||||
},
|
||||
{ title: '对象类型', dataIndex: 'refType', width: 120, render: (v) => v || '—' },
|
||||
{ title: '对象 ID', dataIndex: 'refId', width: 100, render: (v) => v || '—' },
|
||||
{
|
||||
title: '操作',
|
||||
width: 80,
|
||||
render: (_, row) => (
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
onClick={async () => {
|
||||
const res = await request<Row>(`/admin/logs/hq/${row.id}`);
|
||||
setDetail(res);
|
||||
setDrawerOpen(true);
|
||||
}}
|
||||
>
|
||||
详情
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Typography.Title level={4}>HQ 操作日志</Typography.Title>
|
||||
<Typography.Paragraph type="secondary">
|
||||
记录总部后台写操作(开城、订单、用户、权限、合伙人等),仅追加不删除。
|
||||
</Typography.Paragraph>
|
||||
|
||||
<Form
|
||||
form={form}
|
||||
layout="inline"
|
||||
style={{ marginBottom: 16 }}
|
||||
onFinish={(values) => {
|
||||
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);
|
||||
}}
|
||||
>
|
||||
<Form.Item name="hqAccountId" label="HQ 账户 ID">
|
||||
<Input allowClear style={{ width: 140 }} />
|
||||
</Form.Item>
|
||||
<Form.Item name="action" label="行为">
|
||||
<Select
|
||||
allowClear
|
||||
showSearch
|
||||
optionFilterProp="label"
|
||||
style={{ width: 180 }}
|
||||
options={HQ_OPERATION_ACTION_OPTIONS.map((o) => ({ value: o.value, label: o.label }))}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item name="refType" label="对象类型">
|
||||
<Input allowClear placeholder="ORDER / USER / CITY..." style={{ width: 140 }} />
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Space>
|
||||
<Button type="primary" htmlType="submit">查询</Button>
|
||||
<Button onClick={() => {
|
||||
form.resetFields();
|
||||
setFilters({ hqAccountId: '', action: '', refType: '' });
|
||||
setSearchParams({});
|
||||
setPage(1);
|
||||
void reload();
|
||||
}}
|
||||
>
|
||||
重置
|
||||
</Button>
|
||||
</Space>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
|
||||
<Table
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
columns={columns}
|
||||
dataSource={data?.items ?? []}
|
||||
scroll={{ x: 900 }}
|
||||
pagination={{
|
||||
current: page,
|
||||
pageSize,
|
||||
total: data?.total ?? 0,
|
||||
showSizeChanger: true,
|
||||
onChange: (p, ps) => {
|
||||
setPage(p);
|
||||
setPageSize(ps);
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
<Drawer title="操作详情" width={640} open={drawerOpen} onClose={() => setDrawerOpen(false)}>
|
||||
{detail && (
|
||||
<>
|
||||
<Descriptions column={1} bordered size="small">
|
||||
<Descriptions.Item label="日志 ID">{detail.id}</Descriptions.Item>
|
||||
<Descriptions.Item label="时间">{fmtTime(detail.createdAt)}</Descriptions.Item>
|
||||
<Descriptions.Item label="操作人">
|
||||
{detail.hqName} / {detail.hqPhone} (ID: {detail.hqAccountId})
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="角色">{detail.hqRole || '—'}</Descriptions.Item>
|
||||
<Descriptions.Item label="行为">
|
||||
{detail.actionLabel || resolveHqOperationLabel(detail.action)}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="对象">{detail.refType} / {detail.refId}</Descriptions.Item>
|
||||
<Descriptions.Item label="状态">{detail.status || '—'}</Descriptions.Item>
|
||||
<Descriptions.Item label="备注">{detail.remark || '—'}</Descriptions.Item>
|
||||
</Descriptions>
|
||||
<Typography.Title level={5} style={{ marginTop: 16 }}>请求/响应快照</Typography.Title>
|
||||
<pre style={{
|
||||
margin: 0, padding: 12, background: '#f5f5f5', borderRadius: 4,
|
||||
maxHeight: 400, overflow: 'auto', fontSize: 12,
|
||||
}}
|
||||
>
|
||||
{JSON.stringify(detail.detail, null, 2)}
|
||||
</pre>
|
||||
</>
|
||||
)}
|
||||
</Drawer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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,
|
||||
],
|
||||
})
|
||||
|
||||
@@ -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 {}
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user