webadmin增加oss日志
This commit is contained in:
@@ -29,6 +29,7 @@ import ResourcesPage from './pages/ResourcesPage';
|
|||||||
import StoreBillsPage from './pages/StoreBillsPage';
|
import StoreBillsPage from './pages/StoreBillsPage';
|
||||||
import PartnerBillsPage from './pages/PartnerBillsPage';
|
import PartnerBillsPage from './pages/PartnerBillsPage';
|
||||||
import TicketsPage from './pages/TicketsPage';
|
import TicketsPage from './pages/TicketsPage';
|
||||||
|
import OssUploadLogsPage from './pages/OssUploadLogsPage';
|
||||||
import UserLogsPage from './pages/UserLogsPage';
|
import UserLogsPage from './pages/UserLogsPage';
|
||||||
import HqLogsPage from './pages/HqLogsPage';
|
import HqLogsPage from './pages/HqLogsPage';
|
||||||
import ThirdPartyLogsPage from './pages/ThirdPartyLogsPage';
|
import ThirdPartyLogsPage from './pages/ThirdPartyLogsPage';
|
||||||
@@ -86,6 +87,7 @@ export default function App() {
|
|||||||
<Route path="/logs/stores" element={<StoreLogsPage />} />
|
<Route path="/logs/stores" element={<StoreLogsPage />} />
|
||||||
<Route path="/logs/partners" element={<PartnerLogsPage />} />
|
<Route path="/logs/partners" element={<PartnerLogsPage />} />
|
||||||
<Route path="/logs/hq" element={<HqLogsPage />} />
|
<Route path="/logs/hq" element={<HqLogsPage />} />
|
||||||
|
<Route path="/logs/oss" element={<OssUploadLogsPage />} />
|
||||||
<Route path="/logs/third-party" element={<ThirdPartyLogsPage />} />
|
<Route path="/logs/third-party" element={<ThirdPartyLogsPage />} />
|
||||||
<Route path="/deliveries" element={<DeliveriesPage />} />
|
<Route path="/deliveries" element={<DeliveriesPage />} />
|
||||||
<Route path="/deliveries/xiaofeixia" element={<XiaofeixiaTestPage />} />
|
<Route path="/deliveries/xiaofeixia" element={<XiaofeixiaTestPage />} />
|
||||||
|
|||||||
@@ -90,6 +90,7 @@ const MENU_ITEMS: MenuProps['items'] = [
|
|||||||
{ key: '/logs/stores', label: '商户日志' },
|
{ key: '/logs/stores', label: '商户日志' },
|
||||||
{ key: '/logs/partners', label: '合伙人日志' },
|
{ key: '/logs/partners', label: '合伙人日志' },
|
||||||
{ key: '/logs/hq', label: 'HQ 操作日志' },
|
{ key: '/logs/hq', label: 'HQ 操作日志' },
|
||||||
|
{ key: '/logs/oss', label: 'OSS 上传日志' },
|
||||||
{ key: '/logs/third-party', label: '第三方日志' },
|
{ key: '/logs/third-party', label: '第三方日志' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,247 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { Button, Descriptions, Drawer, Form, Input, Select, Table, Tag, Typography } from 'antd';
|
||||||
|
import type { ColumnsType } from 'antd/es/table';
|
||||||
|
import { request } from '../lib/api';
|
||||||
|
import { fmtTime } from '../lib/constants';
|
||||||
|
import { useAdminList } from '../lib/useAdminList';
|
||||||
|
|
||||||
|
type Row = {
|
||||||
|
id: string;
|
||||||
|
scene: string;
|
||||||
|
status: string;
|
||||||
|
actorType: string | null;
|
||||||
|
actorId: string | null;
|
||||||
|
clientApp: string | null;
|
||||||
|
bizType: string | null;
|
||||||
|
mediaType: string | null;
|
||||||
|
fileName: string | null;
|
||||||
|
fileSize: number | null;
|
||||||
|
mimeType: string | null;
|
||||||
|
ossKey: string | null;
|
||||||
|
url: string | null;
|
||||||
|
bucket: string | null;
|
||||||
|
mock: boolean | null;
|
||||||
|
errorMessage?: string | null;
|
||||||
|
createdAt: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const SCENE_OPTIONS = [
|
||||||
|
{ value: 'UPLOAD_PUT_OBJECT', label: '服务端上传' },
|
||||||
|
{ value: 'UPLOAD_TOKEN', label: '直传凭证' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const STATUS_OPTIONS = [
|
||||||
|
{ value: 'SUCCESS', label: 'SUCCESS' },
|
||||||
|
{ value: 'FAILED', label: 'FAILED' },
|
||||||
|
{ value: 'PENDING', label: 'PENDING' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const CLIENT_APP_OPTIONS = [
|
||||||
|
{ value: 'HQ_WEB', label: 'HQ WebAdmin' },
|
||||||
|
{ value: 'USER_H5', label: 'C 端 H5' },
|
||||||
|
{ value: 'USER_MINI', label: 'C 端小程序' },
|
||||||
|
{ value: 'SHOP_H5', label: '门店 H5' },
|
||||||
|
{ value: 'PARTNER_H5', label: '合伙人 H5' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const STATUS_COLOR: Record<string, string> = {
|
||||||
|
SUCCESS: 'success',
|
||||||
|
FAILED: 'error',
|
||||||
|
PENDING: 'processing',
|
||||||
|
};
|
||||||
|
|
||||||
|
function formatFileSize(size: number | null | undefined) {
|
||||||
|
if (size == null || size <= 0) return '—';
|
||||||
|
if (size < 1024) return `${size} B`;
|
||||||
|
if (size < 1024 * 1024) return `${(size / 1024).toFixed(1)} KB`;
|
||||||
|
return `${(size / 1024 / 1024).toFixed(2)} MB`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function JsonBlock({ value }: { value: unknown }) {
|
||||||
|
if (value == null) return <span>—</span>;
|
||||||
|
return (
|
||||||
|
<pre style={{ margin: 0, whiteSpace: 'pre-wrap', wordBreak: 'break-all', fontSize: 12 }}>
|
||||||
|
{JSON.stringify(value, null, 2)}
|
||||||
|
</pre>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function OssUploadLogsPage() {
|
||||||
|
const [filters, setFilters] = useState<Record<string, string>>({});
|
||||||
|
const { data, loading, page, pageSize, setPage, setPageSize } = useAdminList<Row>(
|
||||||
|
'/admin/logs/oss',
|
||||||
|
() => {
|
||||||
|
const qs = new URLSearchParams();
|
||||||
|
if (filters.scene) qs.set('scene', filters.scene);
|
||||||
|
if (filters.status) qs.set('status', filters.status);
|
||||||
|
if (filters.bizType) qs.set('bizType', filters.bizType);
|
||||||
|
if (filters.clientApp) qs.set('clientApp', filters.clientApp);
|
||||||
|
if (filters.refId) qs.set('refId', filters.refId);
|
||||||
|
return qs;
|
||||||
|
},
|
||||||
|
[filters],
|
||||||
|
);
|
||||||
|
const [detail, setDetail] = useState<Record<string, unknown> | null>(null);
|
||||||
|
const [drawerOpen, setDrawerOpen] = useState(false);
|
||||||
|
|
||||||
|
const columns: ColumnsType<Row> = [
|
||||||
|
{ title: '时间', dataIndex: 'createdAt', width: 160, render: fmtTime },
|
||||||
|
{
|
||||||
|
title: '场景',
|
||||||
|
dataIndex: 'scene',
|
||||||
|
width: 120,
|
||||||
|
render: (v: string) => SCENE_OPTIONS.find((o) => o.value === v)?.label ?? v,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '状态',
|
||||||
|
dataIndex: 'status',
|
||||||
|
width: 90,
|
||||||
|
render: (v: string) => <Tag color={STATUS_COLOR[v] ?? 'default'}>{v}</Tag>,
|
||||||
|
},
|
||||||
|
{ title: '端', dataIndex: 'clientApp', width: 110, render: (v) => v || '—' },
|
||||||
|
{
|
||||||
|
title: '操作者',
|
||||||
|
width: 120,
|
||||||
|
render: (_, r) => (r.actorType && r.actorId ? `${r.actorType}#${r.actorId}` : '—'),
|
||||||
|
},
|
||||||
|
{ title: '业务类型', dataIndex: 'bizType', width: 120, render: (v) => v || '—' },
|
||||||
|
{ title: '文件名', dataIndex: 'fileName', ellipsis: true, render: (v) => v || '—' },
|
||||||
|
{
|
||||||
|
title: '大小',
|
||||||
|
dataIndex: 'fileSize',
|
||||||
|
width: 90,
|
||||||
|
render: (v: number | null) => formatFileSize(v),
|
||||||
|
},
|
||||||
|
{ title: 'OSS Key', dataIndex: 'ossKey', ellipsis: true, width: 160, render: (v) => v || '—' },
|
||||||
|
{
|
||||||
|
title: 'Mock',
|
||||||
|
dataIndex: 'mock',
|
||||||
|
width: 70,
|
||||||
|
render: (v: boolean | null) => (v == null ? '—' : v ? '是' : '否'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '错误',
|
||||||
|
dataIndex: 'errorMessage',
|
||||||
|
ellipsis: true,
|
||||||
|
render: (v: string | null | undefined) => v || '—',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
width: 80,
|
||||||
|
fixed: 'right',
|
||||||
|
render: (_, row) => (
|
||||||
|
<Button
|
||||||
|
type="link"
|
||||||
|
size="small"
|
||||||
|
onClick={async () => {
|
||||||
|
setDetail(await request(`/admin/logs/oss/${row.id}`));
|
||||||
|
setDrawerOpen(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
详情
|
||||||
|
</Button>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Typography.Title level={4}>OSS 上传日志</Typography.Title>
|
||||||
|
<Typography.Paragraph type="secondary">
|
||||||
|
记录各端经 <code>/common/resources/upload</code> 的文件上传与直传凭证申请。
|
||||||
|
</Typography.Paragraph>
|
||||||
|
<Form
|
||||||
|
layout="inline"
|
||||||
|
style={{ marginBottom: 16 }}
|
||||||
|
onFinish={(v) => {
|
||||||
|
setFilters(v);
|
||||||
|
setPage(1);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Form.Item name="scene" label="场景">
|
||||||
|
<Select allowClear placeholder="全部" style={{ width: 140 }} options={SCENE_OPTIONS} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item name="status" label="状态">
|
||||||
|
<Select allowClear placeholder="全部" style={{ width: 120 }} options={STATUS_OPTIONS} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item name="bizType" label="业务类型">
|
||||||
|
<Input allowClear placeholder="COVER / STORE_TITLE" style={{ width: 150 }} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item name="clientApp" label="端">
|
||||||
|
<Select allowClear placeholder="全部" style={{ width: 140 }} options={CLIENT_APP_OPTIONS} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item name="refId" label="操作者ID">
|
||||||
|
<Input allowClear style={{ width: 120 }} />
|
||||||
|
</Form.Item>
|
||||||
|
<Button type="primary" htmlType="submit">
|
||||||
|
筛选
|
||||||
|
</Button>
|
||||||
|
</Form>
|
||||||
|
<Table
|
||||||
|
rowKey="id"
|
||||||
|
loading={loading}
|
||||||
|
columns={columns}
|
||||||
|
dataSource={data?.items ?? []}
|
||||||
|
scroll={{ x: 1400 }}
|
||||||
|
pagination={{
|
||||||
|
current: page,
|
||||||
|
pageSize,
|
||||||
|
total: data?.total ?? 0,
|
||||||
|
showSizeChanger: true,
|
||||||
|
onChange: (p, ps) => {
|
||||||
|
setPage(p);
|
||||||
|
setPageSize(ps);
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Drawer title="OSS 上传日志详情" width={640} open={drawerOpen} onClose={() => setDrawerOpen(false)}>
|
||||||
|
{detail && (
|
||||||
|
<Descriptions column={1} bordered size="small">
|
||||||
|
<Descriptions.Item label="ID">{String(detail.id)}</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="场景">{String(detail.scene)}</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="状态">
|
||||||
|
<Tag color={STATUS_COLOR[String(detail.status)] ?? 'default'}>{String(detail.status)}</Tag>
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="端">{String(detail.clientApp ?? '—')}</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="操作者">
|
||||||
|
{detail.actorType ? `${String(detail.actorType)}#${String(detail.actorId)}` : '—'}
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="业务类型">{String(detail.bizType ?? '—')}</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="媒体类型">{String(detail.mediaType ?? '—')}</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="文件名">{String(detail.fileName ?? '—')}</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="大小">{formatFileSize(detail.fileSize as number | null)}</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="MIME">{String(detail.mimeType ?? '—')}</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="Bucket">{String(detail.bucket ?? '—')}</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="OSS Key">{String(detail.ossKey ?? '—')}</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="URL">
|
||||||
|
{detail.url ? (
|
||||||
|
<Typography.Link href={String(detail.url)} target="_blank" rel="noreferrer">
|
||||||
|
{String(detail.url)}
|
||||||
|
</Typography.Link>
|
||||||
|
) : (
|
||||||
|
'—'
|
||||||
|
)}
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="Mock">{detail.mock == null ? '—' : detail.mock ? '是' : '否'}</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="错误信息">
|
||||||
|
{detail.errorMessage ? (
|
||||||
|
<Typography.Text type="danger" style={{ whiteSpace: 'pre-wrap', wordBreak: 'break-all' }}>
|
||||||
|
{String(detail.errorMessage)}
|
||||||
|
</Typography.Text>
|
||||||
|
) : (
|
||||||
|
'—'
|
||||||
|
)}
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="时间">{fmtTime(String(detail.createdAt))}</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="请求体">
|
||||||
|
<JsonBlock value={detail.requestBody} />
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="响应体">
|
||||||
|
<JsonBlock value={detail.responseBody} />
|
||||||
|
</Descriptions.Item>
|
||||||
|
</Descriptions>
|
||||||
|
)}
|
||||||
|
</Drawer>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -25,6 +25,7 @@ const PROVIDER_OPTIONS = [
|
|||||||
{ value: 'WECHAT_AUTH', label: 'WECHAT_AUTH' },
|
{ value: 'WECHAT_AUTH', label: 'WECHAT_AUTH' },
|
||||||
{ value: 'WECHAT_PAY', label: 'WECHAT_PAY' },
|
{ value: 'WECHAT_PAY', label: 'WECHAT_PAY' },
|
||||||
{ value: 'WECHAT_MAP', label: 'WECHAT_MAP' },
|
{ value: 'WECHAT_MAP', label: 'WECHAT_MAP' },
|
||||||
|
{ value: 'ALIYUN_OSS', label: 'ALIYUN_OSS' },
|
||||||
{ value: 'ALIYUN_SMS', label: 'ALIYUN_SMS' },
|
{ value: 'ALIYUN_SMS', label: 'ALIYUN_SMS' },
|
||||||
{ value: 'MOCK_SMS', label: 'MOCK_SMS' },
|
{ value: 'MOCK_SMS', label: 'MOCK_SMS' },
|
||||||
{ value: 'XFX', label: '小飞侠 (XFX)' },
|
{ value: 'XFX', label: '小飞侠 (XFX)' },
|
||||||
|
|||||||
@@ -243,6 +243,7 @@ enum ThirdPartyProvider {
|
|||||||
WECHAT_REFUND
|
WECHAT_REFUND
|
||||||
WECHAT_AUTH
|
WECHAT_AUTH
|
||||||
WECHAT_MAP
|
WECHAT_MAP
|
||||||
|
ALIYUN_OSS
|
||||||
XFX
|
XFX
|
||||||
SMS
|
SMS
|
||||||
LOGISTICS
|
LOGISTICS
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import type { PrismaService } from '../../common/prisma/prisma.module';
|
||||||
|
|
||||||
|
export type OssActorRef = {
|
||||||
|
refType: string;
|
||||||
|
refId: bigint;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function ossActorRefFromAuth(
|
||||||
|
actorType?: string,
|
||||||
|
actorId?: bigint,
|
||||||
|
): OssActorRef | undefined {
|
||||||
|
if (!actorType || actorId == null) return undefined;
|
||||||
|
return { refType: actorType, refId: actorId };
|
||||||
|
}
|
||||||
|
|
||||||
|
type LogOssUploadInput = {
|
||||||
|
scene: 'UPLOAD_PUT_OBJECT' | 'UPLOAD_TOKEN';
|
||||||
|
requestBody?: Record<string, unknown>;
|
||||||
|
responseBody?: Record<string, unknown>;
|
||||||
|
externalNo?: string;
|
||||||
|
status: 'SUCCESS' | 'FAILED';
|
||||||
|
errorMessage?: string;
|
||||||
|
actorRef?: OssActorRef;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function logOssUpload(prisma: PrismaService, input: LogOssUploadInput) {
|
||||||
|
const row = await prisma.logThirdParty.create({
|
||||||
|
data: {
|
||||||
|
provider: 'ALIYUN_OSS',
|
||||||
|
scene: input.scene,
|
||||||
|
refType: input.actorRef?.refType,
|
||||||
|
refId: input.actorRef?.refId,
|
||||||
|
requestBody: input.requestBody as never,
|
||||||
|
responseBody: input.responseBody as never,
|
||||||
|
externalNo: input.externalNo?.slice(0, 128),
|
||||||
|
status: input.status,
|
||||||
|
errorMessage: input.errorMessage?.slice(0, 512),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return row.id;
|
||||||
|
}
|
||||||
@@ -12,21 +12,31 @@ import {
|
|||||||
UseInterceptors,
|
UseInterceptors,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { FileInterceptor } from '@nestjs/platform-express';
|
import { FileInterceptor } from '@nestjs/platform-express';
|
||||||
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
|
import { JwtAuthGuard, type AuthUser } from '../../common/guards/jwt-auth.guard';
|
||||||
import { ResourceService } from './resource.service';
|
import { CurrentUser } from '../../common/decorators/current-user.decorator';
|
||||||
|
import { ResourceService, type OssUploadActor } from './resource.service';
|
||||||
import { ResourceListQueryDto } from './dto/common-query.dto';
|
import { ResourceListQueryDto } from './dto/common-query.dto';
|
||||||
import { RegisterResourceDto, UpdateResourceDto, UploadFileDto, UploadTokenDto } from './dto/common-mutate.dto';
|
import { RegisterResourceDto, UpdateResourceDto, UploadFileDto, UploadTokenDto } from './dto/common-mutate.dto';
|
||||||
|
|
||||||
const DEFAULT_MAX_BYTES = 10 * 1024 * 1024;
|
const DEFAULT_MAX_BYTES = 10 * 1024 * 1024;
|
||||||
|
|
||||||
|
function resolveUploadActor(user?: AuthUser): OssUploadActor | undefined {
|
||||||
|
if (!user) return undefined;
|
||||||
|
return {
|
||||||
|
refType: user.actorType,
|
||||||
|
refId: user.actorId,
|
||||||
|
clientApp: user.clientApp,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
@Controller('common/resources')
|
@Controller('common/resources')
|
||||||
@UseGuards(JwtAuthGuard)
|
@UseGuards(JwtAuthGuard)
|
||||||
export class ResourceController {
|
export class ResourceController {
|
||||||
constructor(private readonly service: ResourceService) {}
|
constructor(private readonly service: ResourceService) {}
|
||||||
|
|
||||||
@Post('upload-token')
|
@Post('upload-token')
|
||||||
uploadToken(@Body() dto: UploadTokenDto) {
|
uploadToken(@CurrentUser() user: AuthUser, @Body() dto: UploadTokenDto) {
|
||||||
return this.service.getUploadToken(dto);
|
return this.service.getUploadToken(dto, resolveUploadActor(user));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('upload')
|
@Post('upload')
|
||||||
@@ -35,8 +45,12 @@ export class ResourceController {
|
|||||||
limits: { fileSize: Number(process.env.OSS_MAX_UPLOAD_BYTES ?? DEFAULT_MAX_BYTES) },
|
limits: { fileSize: Number(process.env.OSS_MAX_UPLOAD_BYTES ?? DEFAULT_MAX_BYTES) },
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
upload(@UploadedFile() file: Express.Multer.File, @Body() dto: UploadFileDto) {
|
upload(
|
||||||
return this.service.uploadFile(file, dto);
|
@CurrentUser() user: AuthUser,
|
||||||
|
@UploadedFile() file: Express.Multer.File,
|
||||||
|
@Body() dto: UploadFileDto,
|
||||||
|
) {
|
||||||
|
return this.service.uploadFile(file, dto, resolveUploadActor(user));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post()
|
@Post()
|
||||||
|
|||||||
@@ -5,11 +5,16 @@ import { PrismaService } from '../../common/prisma/prisma.module';
|
|||||||
import { serializeBigInt } from '../../common/decorators/current-user.decorator';
|
import { serializeBigInt } from '../../common/decorators/current-user.decorator';
|
||||||
import { OSS_PROVIDER } from '../../integrations/integrations.constants';
|
import { OSS_PROVIDER } from '../../integrations/integrations.constants';
|
||||||
import type { IOssProvider } from '../../integrations/oss/oss.interface';
|
import type { IOssProvider } from '../../integrations/oss/oss.interface';
|
||||||
|
import { logOssUpload, type OssActorRef } from '../../integrations/oss/oss-log.util';
|
||||||
import type { ResourceListQueryDto } from './dto/common-query.dto';
|
import type { ResourceListQueryDto } from './dto/common-query.dto';
|
||||||
import type { RegisterResourceDto, UpdateResourceDto, UploadFileDto, UploadTokenDto } from './dto/common-mutate.dto';
|
import type { RegisterResourceDto, UpdateResourceDto, UploadFileDto, UploadTokenDto } from './dto/common-mutate.dto';
|
||||||
|
|
||||||
const DEFAULT_MAX_BYTES = 10 * 1024 * 1024;
|
const DEFAULT_MAX_BYTES = 10 * 1024 * 1024;
|
||||||
|
|
||||||
|
export type OssUploadActor = OssActorRef & {
|
||||||
|
clientApp?: string;
|
||||||
|
};
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ResourceService {
|
export class ResourceService {
|
||||||
constructor(
|
constructor(
|
||||||
@@ -17,25 +22,120 @@ export class ResourceService {
|
|||||||
@Inject(OSS_PROVIDER) private readonly oss: IOssProvider,
|
@Inject(OSS_PROVIDER) private readonly oss: IOssProvider,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
getUploadToken(dto: UploadTokenDto) {
|
getUploadToken(dto: UploadTokenDto, actor?: OssUploadActor) {
|
||||||
return this.oss.getUploadToken(dto);
|
try {
|
||||||
|
const result = this.oss.getUploadToken(dto);
|
||||||
|
void logOssUpload(this.prisma, {
|
||||||
|
scene: 'UPLOAD_TOKEN',
|
||||||
|
actorRef: actor,
|
||||||
|
requestBody: {
|
||||||
|
bizType: dto.bizType,
|
||||||
|
mediaType: dto.mediaType,
|
||||||
|
fileName: dto.fileName,
|
||||||
|
clientApp: actor?.clientApp,
|
||||||
|
},
|
||||||
|
responseBody: {
|
||||||
|
bucket: result.bucket,
|
||||||
|
ossKey: result.ossKey,
|
||||||
|
url: result.url,
|
||||||
|
mock: result.mock ?? false,
|
||||||
|
},
|
||||||
|
externalNo: result.ossKey,
|
||||||
|
status: 'SUCCESS',
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
} catch (err) {
|
||||||
|
void logOssUpload(this.prisma, {
|
||||||
|
scene: 'UPLOAD_TOKEN',
|
||||||
|
actorRef: actor,
|
||||||
|
requestBody: {
|
||||||
|
bizType: dto.bizType,
|
||||||
|
mediaType: dto.mediaType,
|
||||||
|
fileName: dto.fileName,
|
||||||
|
clientApp: actor?.clientApp,
|
||||||
|
},
|
||||||
|
status: 'FAILED',
|
||||||
|
errorMessage: err instanceof Error ? err.message : String(err),
|
||||||
|
});
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async uploadFile(file: Express.Multer.File | undefined, dto: UploadFileDto) {
|
async uploadFile(
|
||||||
if (!file) {
|
file: Express.Multer.File | undefined,
|
||||||
throw new BadRequestException('请选择要上传的文件');
|
dto: UploadFileDto,
|
||||||
}
|
actor?: OssUploadActor,
|
||||||
const maxUploadBytes = Number(process.env.OSS_MAX_UPLOAD_BYTES ?? DEFAULT_MAX_BYTES);
|
) {
|
||||||
if (file.size > maxUploadBytes) {
|
const baseRequest = {
|
||||||
throw new BadRequestException(`文件不能超过 ${Math.floor(maxUploadBytes / 1024 / 1024)}MB`);
|
|
||||||
}
|
|
||||||
return this.oss.putObject({
|
|
||||||
bizType: dto.bizType,
|
bizType: dto.bizType,
|
||||||
mediaType: dto.mediaType,
|
mediaType: dto.mediaType,
|
||||||
|
clientApp: actor?.clientApp,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!file) {
|
||||||
|
await logOssUpload(this.prisma, {
|
||||||
|
scene: 'UPLOAD_PUT_OBJECT',
|
||||||
|
actorRef: actor,
|
||||||
|
requestBody: baseRequest,
|
||||||
|
status: 'FAILED',
|
||||||
|
errorMessage: '请选择要上传的文件',
|
||||||
|
});
|
||||||
|
throw new BadRequestException('请选择要上传的文件');
|
||||||
|
}
|
||||||
|
|
||||||
|
const maxUploadBytes = Number(process.env.OSS_MAX_UPLOAD_BYTES ?? DEFAULT_MAX_BYTES);
|
||||||
|
const requestBody = {
|
||||||
|
...baseRequest,
|
||||||
fileName: file.originalname || 'upload.bin',
|
fileName: file.originalname || 'upload.bin',
|
||||||
buffer: file.buffer,
|
fileSize: file.size,
|
||||||
mimeType: file.mimetype,
|
mimeType: file.mimetype,
|
||||||
});
|
};
|
||||||
|
|
||||||
|
if (file.size > maxUploadBytes) {
|
||||||
|
const message = `文件不能超过 ${Math.floor(maxUploadBytes / 1024 / 1024)}MB`;
|
||||||
|
await logOssUpload(this.prisma, {
|
||||||
|
scene: 'UPLOAD_PUT_OBJECT',
|
||||||
|
actorRef: actor,
|
||||||
|
requestBody,
|
||||||
|
status: 'FAILED',
|
||||||
|
errorMessage: message,
|
||||||
|
});
|
||||||
|
throw new BadRequestException(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await this.oss.putObject({
|
||||||
|
bizType: dto.bizType,
|
||||||
|
mediaType: dto.mediaType,
|
||||||
|
fileName: file.originalname || 'upload.bin',
|
||||||
|
buffer: file.buffer,
|
||||||
|
mimeType: file.mimetype,
|
||||||
|
});
|
||||||
|
await logOssUpload(this.prisma, {
|
||||||
|
scene: 'UPLOAD_PUT_OBJECT',
|
||||||
|
actorRef: actor,
|
||||||
|
requestBody,
|
||||||
|
responseBody: {
|
||||||
|
bucket: result.bucket,
|
||||||
|
region: result.region,
|
||||||
|
ossKey: result.ossKey,
|
||||||
|
url: result.url,
|
||||||
|
mock: result.mock ?? false,
|
||||||
|
},
|
||||||
|
externalNo: result.ossKey,
|
||||||
|
status: 'SUCCESS',
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
} catch (err) {
|
||||||
|
await logOssUpload(this.prisma, {
|
||||||
|
scene: 'UPLOAD_PUT_OBJECT',
|
||||||
|
actorRef: actor,
|
||||||
|
requestBody,
|
||||||
|
status: 'FAILED',
|
||||||
|
errorMessage: err instanceof Error ? err.message : String(err),
|
||||||
|
});
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async register(dto: RegisterResourceDto) {
|
async register(dto: RegisterResourceDto) {
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { Controller, Get, Param, Query, UseGuards } from '@nestjs/common';
|
||||||
|
import { HqAuthGuard } from '../../common/guards/hq-auth.guard';
|
||||||
|
import { AdminOssLogsService } from './admin-oss-logs.service';
|
||||||
|
import { AdminOssLogsQueryDto } from './dto/admin-query.dto';
|
||||||
|
|
||||||
|
@Controller('admin/logs/oss')
|
||||||
|
@UseGuards(HqAuthGuard)
|
||||||
|
export class AdminOssLogsController {
|
||||||
|
constructor(private readonly service: AdminOssLogsService) {}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
list(@Query() query: AdminOssLogsQueryDto) {
|
||||||
|
return this.service.list(query);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get(':id')
|
||||||
|
detail(@Param('id') id: string) {
|
||||||
|
return this.service.detail(BigInt(id));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||||
|
import { Prisma } from '@prisma/client';
|
||||||
|
import { PrismaService } from '../../common/prisma/prisma.module';
|
||||||
|
import { serializeBigInt } from '../../common/decorators/current-user.decorator';
|
||||||
|
import type { AdminOssLogsQueryDto } from './dto/admin-query.dto';
|
||||||
|
|
||||||
|
function mapOssLogRow(row: {
|
||||||
|
id: bigint;
|
||||||
|
scene: string;
|
||||||
|
refType: string | null;
|
||||||
|
refId: bigint | null;
|
||||||
|
requestBody: unknown;
|
||||||
|
responseBody: unknown;
|
||||||
|
externalNo: string | null;
|
||||||
|
status: string;
|
||||||
|
errorMessage: string | null;
|
||||||
|
createdAt: Date;
|
||||||
|
}) {
|
||||||
|
const req = (row.requestBody ?? {}) as Record<string, unknown>;
|
||||||
|
const res = (row.responseBody ?? {}) as Record<string, unknown>;
|
||||||
|
return {
|
||||||
|
id: row.id,
|
||||||
|
scene: row.scene,
|
||||||
|
status: row.status,
|
||||||
|
actorType: row.refType,
|
||||||
|
actorId: row.refId,
|
||||||
|
clientApp: (req.clientApp as string | undefined) ?? null,
|
||||||
|
bizType: (req.bizType as string | undefined) ?? null,
|
||||||
|
mediaType: (req.mediaType as string | undefined) ?? null,
|
||||||
|
fileName: (req.fileName as string | undefined) ?? null,
|
||||||
|
fileSize: (req.fileSize as number | undefined) ?? null,
|
||||||
|
mimeType: (req.mimeType as string | undefined) ?? null,
|
||||||
|
ossKey: (res.ossKey as string | undefined) ?? row.externalNo ?? null,
|
||||||
|
url: (res.url as string | undefined) ?? null,
|
||||||
|
bucket: (res.bucket as string | undefined) ?? null,
|
||||||
|
mock: (res.mock as boolean | undefined) ?? null,
|
||||||
|
errorMessage: row.errorMessage,
|
||||||
|
createdAt: row.createdAt,
|
||||||
|
requestBody: row.requestBody,
|
||||||
|
responseBody: row.responseBody,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AdminOssLogsService {
|
||||||
|
constructor(private readonly prisma: PrismaService) {}
|
||||||
|
|
||||||
|
async list(query: AdminOssLogsQueryDto) {
|
||||||
|
const page = query.page ?? 1;
|
||||||
|
const pageSize = query.pageSize ?? 20;
|
||||||
|
const where: Prisma.LogThirdPartyWhereInput = {
|
||||||
|
provider: 'ALIYUN_OSS',
|
||||||
|
};
|
||||||
|
if (query.scene) where.scene = query.scene;
|
||||||
|
if (query.status) where.status = query.status as Prisma.EnumThirdPartyLogStatusFilter['equals'];
|
||||||
|
if (query.refType) where.refType = query.refType;
|
||||||
|
if (query.refId) where.refId = BigInt(query.refId);
|
||||||
|
|
||||||
|
const andFilters: Prisma.LogThirdPartyWhereInput[] = [];
|
||||||
|
if (query.bizType) {
|
||||||
|
andFilters.push({
|
||||||
|
requestBody: { string_contains: `"bizType":"${query.bizType}"` },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (query.clientApp) {
|
||||||
|
andFilters.push({
|
||||||
|
requestBody: { string_contains: `"clientApp":"${query.clientApp}"` },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (andFilters.length) {
|
||||||
|
where.AND = andFilters;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [rows, total] = await Promise.all([
|
||||||
|
this.prisma.logThirdParty.findMany({
|
||||||
|
where,
|
||||||
|
orderBy: { createdAt: 'desc' },
|
||||||
|
skip: (page - 1) * pageSize,
|
||||||
|
take: pageSize,
|
||||||
|
}),
|
||||||
|
this.prisma.logThirdParty.count({ where }),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return serializeBigInt({
|
||||||
|
items: rows.map(mapOssLogRow),
|
||||||
|
total,
|
||||||
|
page,
|
||||||
|
pageSize,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async detail(id: bigint) {
|
||||||
|
const row = await this.prisma.logThirdParty.findFirst({
|
||||||
|
where: { id, provider: 'ALIYUN_OSS' },
|
||||||
|
});
|
||||||
|
if (!row) throw new NotFoundException('OSS 上传日志不存在');
|
||||||
|
return serializeBigInt(mapOssLogRow(row));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -399,6 +399,32 @@ export class AdminHqLogsQueryDto extends PaginationQueryDto {
|
|||||||
to?: string;
|
to?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class AdminOssLogsQueryDto extends PaginationQueryDto {
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
scene?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsIn(['SUCCESS', 'FAILED', 'PENDING'])
|
||||||
|
status?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
bizType?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
clientApp?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
refType?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
refId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export class AdminStoreMediaQueryDto extends PaginationQueryDto {
|
export class AdminStoreMediaQueryDto extends PaginationQueryDto {
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsString()
|
@IsString()
|
||||||
|
|||||||
@@ -31,6 +31,8 @@ import { AdminPartnerLogsController } from './admin-partner-logs.controller';
|
|||||||
import { AdminPartnerLogsService } from './admin-partner-logs.service';
|
import { AdminPartnerLogsService } from './admin-partner-logs.service';
|
||||||
import { AdminHqLogsController } from './admin-hq-logs.controller';
|
import { AdminHqLogsController } from './admin-hq-logs.controller';
|
||||||
import { AdminHqLogsService } from './admin-hq-logs.service';
|
import { AdminHqLogsService } from './admin-hq-logs.service';
|
||||||
|
import { AdminOssLogsController } from './admin-oss-logs.controller';
|
||||||
|
import { AdminOssLogsService } from './admin-oss-logs.service';
|
||||||
import { AdminTicketsController } from './admin-tickets.controller';
|
import { AdminTicketsController } from './admin-tickets.controller';
|
||||||
import { AdminTicketsService } from './admin-tickets.service';
|
import { AdminTicketsService } from './admin-tickets.service';
|
||||||
import { SuperAdminGuard } from '../../common/guards/super-admin.guard';
|
import { SuperAdminGuard } from '../../common/guards/super-admin.guard';
|
||||||
@@ -77,6 +79,7 @@ import { AdminDeployService } from './admin-deploy.service';
|
|||||||
AdminStoreLogsController,
|
AdminStoreLogsController,
|
||||||
AdminPartnerLogsController,
|
AdminPartnerLogsController,
|
||||||
AdminHqLogsController,
|
AdminHqLogsController,
|
||||||
|
AdminOssLogsController,
|
||||||
AdminTicketsController,
|
AdminTicketsController,
|
||||||
AdminXiaofeixiaController,
|
AdminXiaofeixiaController,
|
||||||
AdminProductDetailTemplatesController,
|
AdminProductDetailTemplatesController,
|
||||||
@@ -101,6 +104,7 @@ import { AdminDeployService } from './admin-deploy.service';
|
|||||||
AdminStoreLogsService,
|
AdminStoreLogsService,
|
||||||
AdminPartnerLogsService,
|
AdminPartnerLogsService,
|
||||||
AdminHqLogsService,
|
AdminHqLogsService,
|
||||||
|
AdminOssLogsService,
|
||||||
AdminTicketsService,
|
AdminTicketsService,
|
||||||
AdminXiaofeixiaService,
|
AdminXiaofeixiaService,
|
||||||
AdminProductDetailTemplatesService,
|
AdminProductDetailTemplatesService,
|
||||||
|
|||||||
Reference in New Issue
Block a user