+
+
+
+
+ 操作审计
+
+
+ 所有写操作(含被拒绝的请求)自动留痕,共 {total} 条
+
+
+
} onClick={() => void load()} loading={loading}>
+ 刷新
+
+
+
+ {/* 筛选区 */}
+
+ }
+ value={userInput}
+ allowClear
+ style={{ width: 180 }}
+ onChange={(e) => setUserInput(e.target.value)}
+ onPressEnter={() => {
+ setPage(1);
+ setUserId(userInput.trim());
+ }}
+ onBlur={() => {
+ setPage(1);
+ setUserId(userInput.trim());
+ }}
+ />
+
+
+ {error && (
+
+ )}
+
+
+ rowKey="id"
+ columns={columns}
+ dataSource={rows}
+ loading={loading && { indicator: }}
+ size="small"
+ scroll={{ x: 1000 }}
+ pagination={{
+ current: page,
+ pageSize: PAGE_SIZE,
+ total,
+ showSizeChanger: false,
+ showTotal: (t) => `共 ${t} 条`,
+ onChange: setPage,
+ }}
+ />
+
+ {/* 详情抽屉:完整 URL / UA / request_id / error_message 都在这里 */}
+ setDetail(null)}
+ >
+ {detail && (
+
+
+ {dayjs(detail.created_at).format("YYYY-MM-DD HH:mm:ss")}
+
+
+ {detail.user_id ? `${detail.display_name || ""} (${detail.user_id})` : "未认证"}
+
+ {detail.role || "-"}
+
+ {detail.module_label || detail.module} / {detail.action_label || detail.action}
+
+
+ {detail.target_id ? (
+ <>
+ {detail.target_name || detail.target_id}
+ ({detail.target_type})
+ >
+ ) : (
+ "-"
+ )}
+
+
+
+ {detail.method} {detail.url}
+
+
+ {detail.status_code ?? "-"}
+ {detail.error_message && (
+
+ {detail.error_message}
+
+ )}
+
+ {detail.ip_address || "-"}
+
+
+ {detail.user_agent || "-"}
+
+
+ {detail.request_id ? (
+
+ {detail.request_id}
+
+ ) : (
+ "-"
+ )}
+
+ {detail.details && (
+
+
+ {JSON.stringify(detail.details, null, 2)}
+
+
+ )}
+
+ )}
+
+
+ );
+}
diff --git a/frontend/src/pages/admin/AdminProductsPage.tsx b/frontend/src/pages/admin/AdminProductsPage.tsx
index 901f044..2cca6ad 100644
--- a/frontend/src/pages/admin/AdminProductsPage.tsx
+++ b/frontend/src/pages/admin/AdminProductsPage.tsx
@@ -13,7 +13,7 @@ import {
} from "../../services/printApi";
import { useToast } from "../../components/ui/Toast";
import { useAuth } from "../../contexts/AuthContext";
-import { getStatusConfig, lifecycleBadge } from "../../constants/task";
+import { getStatusConfig, lifecycleBadge, isAdminRole } from "../../constants/task";
import { extractErrorMessage } from "../../utils/errorMessage";
const QR_BASE = "/api/v1/products/qrcode";
@@ -31,7 +31,7 @@ interface ProductGroup { groupKey: string; products: ProductResponse[]; allInWar
export default function AdminProductsPage() {
const { toast } = useToast();
const { user: authUser } = useAuth();
- const isAdmin = authUser?.role === "SUPER_ADMIN" || authUser?.role === "SUPERVISOR";
+ const isAdmin = isAdminRole(authUser?.role);
const [products, setProducts] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
diff --git a/frontend/src/services/auditApi.ts b/frontend/src/services/auditApi.ts
new file mode 100644
index 0000000..195cab7
--- /dev/null
+++ b/frontend/src/services/auditApi.ts
@@ -0,0 +1,77 @@
+/** 操作审计日志 API */
+import api from "./api";
+
+export interface AuditLogItem {
+ id: string;
+ user_id: string | null;
+ display_name: string | null;
+ role: string | null;
+
+ action: string;
+ /** 服务端补的中文标签,前端不再各自维护枚举映射 */
+ action_label: string | null;
+ module: string;
+ module_label: string | null;
+
+ target_type: string | null;
+ target_id: string | null;
+ target_name: string | null;
+ details: Record | null;
+
+ ip_address: string | null;
+ user_agent: string | null;
+ method: string | null;
+ url: string | null;
+ status_code: number | null;
+ error_message: string | null;
+
+ /** 拿着它可在后端结构化日志中定位同一次请求 */
+ request_id: string | null;
+
+ created_at: string;
+}
+
+export interface AuditLogListResponse {
+ items: AuditLogItem[];
+ total: number;
+}
+
+export interface AuditOption {
+ value: string;
+ label: string;
+}
+
+export interface AuditOptionsResponse {
+ modules: AuditOption[];
+ actions: AuditOption[];
+}
+
+export interface AuditLogQuery {
+ user_id?: string;
+ module?: string;
+ action?: string;
+ target_id?: string;
+ request_id?: string;
+ status_code?: number;
+ /** YYYY-MM-DD */
+ start_date?: string;
+ /** YYYY-MM-DD(含当天) */
+ end_date?: string;
+ page?: number;
+ page_size?: number;
+}
+
+/** 分页查询审计日志(按时间倒序) */
+export async function fetchAuditLogs(q: AuditLogQuery = {}): Promise {
+ const params = Object.fromEntries(
+ Object.entries(q).filter(([, v]) => v !== undefined && v !== null && v !== "")
+ );
+ const { data } = await api.get("/audit/logs", { params });
+ return data;
+}
+
+/** 获取模块/动作筛选项 */
+export async function fetchAuditOptions(): Promise {
+ const { data } = await api.get("/audit/options");
+ return data;
+}