cl
4 天以前 450a97460b086663bb07b418b48354b0a3125e85
日志优化
28个文件已添加
34个文件已修改
1 文件已重命名
3个文件已删除
2753 ■■■■ 已修改文件
rsf-admin/src/i18n/en.js 38 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/i18n/zh.js 37 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/ResourceContent.js 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/components/DoubleClickDatagridRows.jsx 62 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/histories/taskLog/TaskItemLogList.jsx 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/histories/taskLog/TaskLogList.jsx 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/histories/taskLog/TaskLogShow.jsx 177 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/histories/taskLog/index.jsx 14 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/orders/delivery/DeliveryItemList.jsx 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/orders/stock/OrderList.jsx 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/orders/transfer/TransferOrders.jsx 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/system/dicts/dictType/DictDataList.jsx 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/system/dicts/dictType/DictTypeList.jsx 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/system/httpAuditLog/HttpAuditLogList.jsx 317 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/system/httpAuditLog/HttpAuditLogShow.jsx 258 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/system/httpAuditSysConfig/HttpAuditSysConfigCreate.jsx 91 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/system/httpAuditSysConfig/HttpAuditSysConfigEdit.jsx 47 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/system/httpAuditSysConfig/HttpAuditSysConfigList.jsx 89 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/system/httpAuditSysConfig/index.jsx 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/system/operationRecord/OperationRecordList.jsx 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/system/serialRule/SerialRuleItemList.jsx 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/system/serialRule/SerialRuleList.jsx 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/utils/common.js 12 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-http-audit/pom.xml 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-http-audit/src/main/java/com/vincent/rsf/httpaudit/admin/HttpAuditLogAdminController.java 98 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-http-audit/src/main/java/com/vincent/rsf/httpaudit/admin/HttpAuditRuleAdminController.java 55 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-http-audit/src/main/java/com/vincent/rsf/httpaudit/admin/HttpAuditSysConfigAdminController.java 153 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-http-audit/src/main/java/com/vincent/rsf/httpaudit/config/HttpAuditAdminApiAutoConfiguration.java 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-http-audit/src/main/java/com/vincent/rsf/httpaudit/config/HttpAuditAutoConfiguration.java 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-http-audit/src/main/java/com/vincent/rsf/httpaudit/config/HttpAuditOpenUiAutoConfiguration.java 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-http-audit/src/main/java/com/vincent/rsf/httpaudit/entity/HttpAuditSysConfig.java 42 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-http-audit/src/main/java/com/vincent/rsf/httpaudit/mapper/HttpAuditConfigMapper.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-http-audit/src/main/java/com/vincent/rsf/httpaudit/open/HttpAuditOpenLogController.java 141 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-http-audit/src/main/java/com/vincent/rsf/httpaudit/props/HttpAuditProperties.java 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-http-audit/src/main/java/com/vincent/rsf/httpaudit/service/HttpAuditLogCrudService.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-http-audit/src/main/java/com/vincent/rsf/httpaudit/service/HttpAuditLogCrudServiceImpl.java 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-http-audit/src/main/java/com/vincent/rsf/httpaudit/service/HttpAuditOutboundRecorder.java 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-http-audit/src/main/java/com/vincent/rsf/httpaudit/service/HttpAuditSysConfigService.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-http-audit/src/main/java/com/vincent/rsf/httpaudit/service/HttpAuditSysConfigServiceImpl.java 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-http-audit/src/main/java/com/vincent/rsf/httpaudit/support/HttpAuditSupport.java 40 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-http-audit/src/main/java/com/vincent/rsf/httpaudit/web/HttpAuditFilter.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-http-audit/src/main/java/com/vincent/rsf/httpaudit/web/OutboundHttpAuditInterceptor.java 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-http-audit/src/main/java/com/vincent/rsf/httpaudit/web/util/HttpAuditAdminQueryHelper.java 83 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-http-audit/src/main/resources/static/http-audit/log-query.html 290 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-open-api/src/main/java/com/vincent/rsf/openApi/common/datasource/DataSourceContextHolder.java 34 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-open-api/src/main/java/com/vincent/rsf/openApi/common/datasource/DataSourceNames.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-open-api/src/main/java/com/vincent/rsf/openApi/common/datasource/HttpAuditDataSourceAspect.java 39 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-open-api/src/main/java/com/vincent/rsf/openApi/common/datasource/RoutingDataSource.java 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-open-api/src/main/java/com/vincent/rsf/openApi/common/datasource/UseDataSource.java 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-open-api/src/main/java/com/vincent/rsf/openApi/common/datasource/UseDataSourceAspect.java 45 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-open-api/src/main/java/com/vincent/rsf/openApi/config/JdxajLogDataSourceConfig.java 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-open-api/src/main/java/com/vincent/rsf/openApi/config/PrimaryDataSourceConfig.java 59 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-open-api/src/main/resources/application-dev.yml 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-open-api/src/main/resources/application-prod.yml 13 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/feign/AuditingFeignClient.java 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/common/config/JdxajLogDataSourceConfig.java 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/common/config/PrimaryDataSourceConfig.java 7 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/common/datasource/DataSourceNames.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/common/datasource/HttpAuditDataSourceAspect.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/common/security/SecurityConfig.java 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/controller/HttpAuditLogController.java 45 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/service/HttpAuditLogService.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/service/impl/HttpAuditLogServiceImpl.java 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/resources/application-dev.yml 34 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/resources/application-prod.yml 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
version/db/http_audit_sysconfig_menu.sql 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/i18n/en.js
@@ -3,11 +3,18 @@
const customEnglishMessages = {
    ...englishMessages,
    hello: 'Hello World',
    'menu.httpAuditLog': 'HTTP audit',
    'menu.httpAuditLog': 'HTTP audit log',
    'menu.httpAuditRule': 'HTTP audit rules',
    'resources.httpAuditLog.name': 'HTTP audit',
    'menu.httpAuditSysConfig': 'HTTP audit settings',
    'common.button.backToList': 'Back to list',
    'httpAuditLog.show.sectionSummary': 'Summary',
    'httpAuditLog.show.sectionRequest': 'Endpoint & description',
    'httpAuditLog.show.sectionPayload': 'Payload & error',
    'resources.httpAuditLog.name': 'HTTP audit log',
    'resources.httpAuditRule.name': 'HTTP audit rules',
    'resources.httpAuditRule.createTitle': 'New audit rule',
    'resources.httpAuditSysConfig.name': 'HTTP audit settings',
    'resources.httpAuditSysConfig.createTitle': 'New audit config',
    common: {
        response: {
            success: "Success",
@@ -18,6 +25,7 @@
            edit: "Edit",
            detail: "Details",
            histories: "Histories",
            backToList: "Back to list",
        },
        field: {
            id: 'ID',
@@ -153,8 +161,9 @@
        department: 'Department',
        token: 'Token',
        operation: 'Operation',
        httpAuditLog: 'HTTP audit',
        httpAuditLog: 'HTTP audit log',
        httpAuditRule: 'HTTP audit rules',
        httpAuditSysConfig: 'HTTP audit settings',
        config: 'Config',
        tenant: 'Tenant',
        userLogin: 'Token',
@@ -347,20 +356,41 @@
            httpAuditLog: {
                serviceName: "service",
                scopeType: "scope",
                scopeExternal: "External",
                scopeInternal: "Internal",
                ioIn: "IN",
                ioOut: "OUT",
                okNormal: "OK",
                okAbnormal: "Error",
                truncatedYes: "Yes",
                truncatedNo: "No",
                uri: "uri",
                method: "method",
                functionDesc: "description",
                requestParams: "request",
                responseParams: "response",
                requestContains: "request contains",
                responseContains: "response contains",
                queryString: "query string",
                requestBody: "request JSON",
                responseBody: "response JSON",
                responseTruncated: "response truncated",
                httpStatus: "HTTP status",
                okFlag: "ok / error",
                spendMs: "spend ms",
                spendMs: "Duration (s)",
                clientIp: "client IP",
                errorMessage: "error",
                ioDirection: "I/O",
            },
            httpAuditSysConfig: {
                configKey: "Config key",
                configVal: "Config value",
                enabled: "Enabled",
                enabledOn: "On",
                enabledOff: "Off",
                sortOrder: "Sort",
                remark: "Remark",
            },
            httpAuditRule: {
                directionLabel: "Direction",
                direction: { IN: "Inbound", OUT: "Outbound", BOTH: "Both" },
rsf-admin/src/i18n/zh.js
@@ -3,12 +3,18 @@
const customChineseMessages = {
    ...chineseMessages,
    hello: '你好世界',
    'menu.httpAuditLog': 'HTTP接口审计',
    'menu.httpAuditLog': 'HTTP审计日志',
    'menu.httpAuditRule': 'HTTP审计规则',
    'menu.httpAuditSysConfig': 'HTTP接口审计',
    'common.button.backToList': '返回列表',
    'httpAuditLog.show.sectionSummary': '概要信息',
    'httpAuditLog.show.sectionRequest': '接口与说明',
    'httpAuditLog.show.sectionPayload': '报文与异常',
    resources: {
        config: { name: '配置参数' },
        httpAuditLog: { name: 'HTTP接口审计' },
        httpAuditLog: { name: 'HTTP审计日志' },
        httpAuditRule: { name: 'HTTP审计规则', createTitle: '新增审计规则' },
        httpAuditSysConfig: { name: 'HTTP接口审计', createTitle: '新增审计配置' },
        asnOrderItem: { name: '收货明细' },
        outStockItem: { name: '出库单明细' },
    },
@@ -25,6 +31,7 @@
            edit: "编辑",
            detail: "库存明细",
            histories: "流水记录",
            backToList: "返回列表",
        },
        field: {
            id: 'ID',
@@ -161,8 +168,9 @@
        department: '部门管理',
        token: '登录日志',
        operation: '操作日志',
        httpAuditLog: 'HTTP接口审计',
        httpAuditLog: 'HTTP审计日志',
        httpAuditRule: 'HTTP审计规则',
        httpAuditSysConfig: 'HTTP接口审计',
        config: '配置参数',
        tenant: '租户管理',
        userLogin: '登录日志',
@@ -378,20 +386,41 @@
            httpAuditLog: {
                serviceName: "应用",
                scopeType: "内外部",
                scopeExternal: "外部",
                scopeInternal: "内部",
                ioIn: "入站",
                ioOut: "出站",
                okNormal: "正常",
                okAbnormal: "异常",
                truncatedYes: "是",
                truncatedNo: "否",
                uri: "接口路径",
                method: "方法",
                functionDesc: "功能描述",
                requestParams: "请求参数",
                responseParams: "返回参数",
                requestContains: "请求参数包含",
                responseContains: "返回参数包含",
                queryString: "查询串",
                requestBody: "请求内容(JSON)",
                responseBody: "响应内容(JSON)",
                responseTruncated: "响应已截断",
                httpStatus: "HTTP状态",
                okFlag: "正常/异常",
                spendMs: "耗时(ms)",
                spendMs: "耗时(s)",
                clientIp: "请求IP",
                errorMessage: "异常信息",
                ioDirection: "方向",
            },
            httpAuditSysConfig: {
                configKey: "配置键",
                configVal: "配置值",
                enabled: "启用",
                enabledOn: "启用",
                enabledOff: "停用",
                sortOrder: "排序",
                remark: "备注",
            },
            httpAuditRule: {
                directionLabel: "作用方向",
                direction: { IN: "入站", OUT: "出站", BOTH: "双向" },
rsf-admin/src/page/ResourceContent.js
@@ -70,6 +70,7 @@
import openApiApp from './system/openApiApp';
import httpAuditLog from './system/httpAuditLog';
import httpAuditRule from './system/httpAuditRule';
import httpAuditSysConfig from './system/httpAuditSysConfig';
const ResourceContent = (node) => {
  switch (node.component) {
@@ -206,6 +207,8 @@
      return httpAuditLog;
    case "httpAuditRule":
      return httpAuditRule;
    case "httpAuditSysConfig":
      return httpAuditSysConfig;
    default:
      return {
        list: ListGuesser,
rsf-admin/src/page/components/DoubleClickDatagridRows.jsx
New file
@@ -0,0 +1,62 @@
import React, { createContext, forwardRef, useCallback, useContext } from "react";
import { useNavigate } from "react-router-dom";
import { DatagridRow } from "ra-ui-materialui";
import { useCreatePath, useRecordContext, useResourceContext } from "react-admin";
/** 列表行双击:由 Provider 注入 (record) => void */
export const ListRowDoubleClickContext = createContext(null);
export const CallbackDoubleClickDatagridRow = forwardRef(function CallbackDoubleClickDatagridRow(props, ref) {
    const record = useRecordContext();
    const onRowDoubleClick = useContext(ListRowDoubleClickContext);
    const handleDoubleClick = useCallback(
        (e) => {
            e.stopPropagation();
            onRowDoubleClick?.(record);
        },
        [record, onRowDoubleClick],
    );
    return <DatagridRow ref={ref} {...props} rowClick={false} onDoubleClick={handleDoubleClick} />;
});
export const EditOnDoubleClickDatagridRow = forwardRef(function EditOnDoubleClickDatagridRow(props, ref) {
    const navigate = useNavigate();
    const createPath = useCreatePath();
    const record = useRecordContext();
    const resource = useResourceContext();
    const handleDoubleClick = useCallback(
        (e) => {
            e.stopPropagation();
            if (record?.id == null) {
                return;
            }
            const path = createPath({ type: "edit", resource, id: record.id });
            navigate(path, { state: { _scrollToTop: true } });
        },
        [createPath, navigate, record, resource],
    );
    return <DatagridRow ref={ref} {...props} rowClick={false} onDoubleClick={handleDoubleClick} />;
});
export const ShowOnDoubleClickDatagridRow = forwardRef(function ShowOnDoubleClickDatagridRow(props, ref) {
    const navigate = useNavigate();
    const createPath = useCreatePath();
    const record = useRecordContext();
    const resource = useResourceContext();
    const handleDoubleClick = useCallback(
        (e) => {
            e.stopPropagation();
            if (record?.id == null) {
                return;
            }
            const path = createPath({ type: "show", resource, id: record.id });
            navigate(path, { state: { _scrollToTop: true } });
        },
        [createPath, navigate, record, resource],
    );
    return <DatagridRow ref={ref} {...props} rowClick={false} onDoubleClick={handleDoubleClick} />;
});
rsf-admin/src/page/histories/taskLog/TaskItemLogList.jsx
@@ -109,6 +109,7 @@
                title={"menu.taskItemLog"}
                filters={filters}
                empty={false}
                pagination={false}
                filter={{ logId: Number(recodeId) }}
                sort={{ field: "create_time", order: "desc" }}
                actions={(
rsf-admin/src/page/histories/taskLog/TaskLogList.jsx
@@ -1,5 +1,4 @@
import React, { useState, useRef, useEffect, useMemo, useCallback } from "react";
import { useNavigate } from 'react-router-dom';
import {
    List,
    DatagridConfigurable,
@@ -43,6 +42,7 @@
import MyField from "../../components/MyField";
import { PAGE_DRAWER_WIDTH, OPERATE_MODE, DEFAULT_PAGE_SIZE } from '@/config/setting';
import * as Common from '@/utils/common';
import { ShowOnDoubleClickDatagridRow } from '@/page/components/DoubleClickDatagridRows';
const StyledDatagrid = styled(DatagridConfigurable)(({ theme }) => ({
    '& .css-1vooibu-MuiSvgIcon-root': {
@@ -123,7 +123,7 @@
                <StyledDatagrid
                    preferenceKey='taskLog'
                    bulkActionButtons={false}
                    rowClick={'edit'}
                    row={<ShowOnDoubleClickDatagridRow />}
                    expand={false}
                    expandSingle={true}
                    omit={['id', 'createTime', 'createBy', 'memo', 'taskId', 'robotCode', 'exceStatus', 'sort', 'expCode']}
rsf-admin/src/page/histories/taskLog/TaskLogShow.jsx
New file
@@ -0,0 +1,177 @@
import React from "react";
import { Show, SimpleForm, useTranslate, TextInput, NumberInput } from "react-admin";
import { Stack, Grid, Typography } from "@mui/material";
import TaskItemLogList from "./TaskItemLogList";
import CustomerTopToolBar from "../../components/EditTopToolBar";
import EditBaseAside from "../../components/EditBaseAside";
const formSx = {
    "& .MuiFormLabel-root.MuiInputLabel-root.Mui-disabled": {
        bgcolor: "white",
        WebkitTextFillColor: "rgba(0, 0, 0)",
    },
    "& .MuiInputBase-input.MuiFilledInput-input.Mui-disabled": {
        bgcolor: "white",
        WebkitTextFillColor: "rgba(0, 0, 0)",
    },
    "& .ra-input": {
        flex: "0 0 auto",
        marginTop: 0,
        marginBottom: 0,
    },
};
/** 主题默认 MuiTextField fullWidth=true,横向排列时每项会独占一行;详情页需与任务管理一致多列排布 */
const fw = false;
const w = { width: 200, maxWidth: "100%" };
const wWide = { width: 280, maxWidth: "100%" };
const wMemo = { width: 420, maxWidth: "100%" };
const TaskLogShow = () => {
    const translate = useTranslate();
    return (
        <Show actions={<CustomerTopToolBar />} aside={<EditBaseAside />}>
            <>
                <SimpleForm
                    shouldUnregister
                    warnWhenUnsavedChanges={false}
                    toolbar={false}
                    mode="onTouched"
                    sx={formSx}
                >
                    <Grid container width={{ xs: "100%", xl: "80%" }} rowSpacing={3} columnSpacing={3}>
                        <Grid item xs={24} md={16}>
                            <Typography variant="h6" gutterBottom>
                                {translate("common.edit.title.main")}
                            </Typography>
                            <Stack direction="row" gap={2} flexWrap="wrap" useFlexGap alignItems="flex-start">
                                <TextInput
                                    label="table.field.task.taskCode"
                                    source="taskCode"
                                    readOnly
                                    fullWidth={fw}
                                    sx={w}
                                    parse={(v) => v}
                                />
                                <TextInput
                                    label="table.field.task.taskStatus"
                                    readOnly
                                    fullWidth={fw}
                                    sx={w}
                                    source="taskStatus$"
                                />
                                <TextInput
                                    label="table.field.task.taskType"
                                    source="taskType$"
                                    readOnly
                                    fullWidth={fw}
                                    sx={w}
                                />
                                <TextInput
                                    label="table.field.task.orgLoc"
                                    source="orgLoc"
                                    readOnly
                                    fullWidth={fw}
                                    sx={w}
                                    parse={(v) => v}
                                />
                                <TextInput
                                    label="table.field.task.targLoc"
                                    source="targLoc"
                                    readOnly
                                    fullWidth={fw}
                                    sx={w}
                                    parse={(v) => v}
                                />
                                <TextInput
                                    label="table.field.task.orgSite"
                                    source="orgSite"
                                    readOnly
                                    fullWidth={fw}
                                    sx={w}
                                    parse={(v) => v}
                                />
                            </Stack>
                            <Stack direction="row" gap={2} flexWrap="wrap" useFlexGap alignItems="flex-start">
                                <TextInput
                                    label="table.field.task.targSite"
                                    source="targSite"
                                    readOnly
                                    fullWidth={fw}
                                    sx={w}
                                    parse={(v) => v}
                                />
                                <TextInput
                                    label="table.field.task.barcode"
                                    source="barcode"
                                    readOnly
                                    fullWidth={fw}
                                    sx={w}
                                    parse={(v) => v}
                                />
                                <NumberInput
                                    label="table.field.task.sort"
                                    source="sort"
                                    readOnly
                                    fullWidth={fw}
                                    sx={w}
                                />
                            </Stack>
                            <Stack direction="row" gap={2} flexWrap="wrap" useFlexGap alignItems="flex-start">
                                <TextInput
                                    label="table.field.task.robotCode"
                                    source="robotCode"
                                    readOnly
                                    fullWidth={fw}
                                    sx={w}
                                    parse={(v) => v}
                                />
                                <NumberInput
                                    label="table.field.task.exceStatus"
                                    source="exceStatus"
                                    readOnly
                                    fullWidth={fw}
                                    sx={w}
                                />
                                <TextInput
                                    label="table.field.task.expDesc"
                                    source="expDesc"
                                    readOnly
                                    fullWidth={fw}
                                    sx={wWide}
                                    parse={(v) => v}
                                />
                                <TextInput
                                    label="table.field.task.expCode"
                                    source="expCode"
                                    readOnly
                                    fullWidth={fw}
                                    sx={w}
                                    parse={(v) => v}
                                />
                            </Stack>
                            <Stack direction="row" gap={2} flexWrap="wrap" useFlexGap alignItems="flex-start">
                                <TextInput
                                    label="common.field.memo"
                                    source="memo"
                                    readOnly
                                    fullWidth={fw}
                                    sx={wMemo}
                                    parse={(v) => v}
                                />
                            </Stack>
                        </Grid>
                    </Grid>
                </SimpleForm>
                <Grid item xs={24} md={16} sx={{ margin: "1em", height: "auto" }}>
                    <Typography variant="h6" gutterBottom>
                        {translate("common.edit.title.common")}
                    </Typography>
                </Grid>
                <TaskItemLogList />
            </>
        </Show>
    );
};
export default TaskLogShow;
rsf-admin/src/page/histories/taskLog/index.jsx
@@ -1,17 +1,11 @@
import React, { useState, useRef, useEffect, useMemo } from "react";
import {
    ListGuesser,
    EditGuesser,
    ShowGuesser,
} from "react-admin";
import TaskLogList from "./TaskLogList";
import TaskLogEdit from "./TaskLogEdit";
import TaskLogShow from "./TaskLogShow";
// import TaskLogEdit from "./TaskLogEdit";
export default {
    list: TaskLogList,
    edit: TaskLogEdit,
    show: ShowGuesser,
    // edit: TaskLogEdit,
    show: TaskLogShow,
    recordRepresentation: (record) => {
        return `${record.id}`
    }
rsf-admin/src/page/orders/delivery/DeliveryItemList.jsx
@@ -42,6 +42,7 @@
import PageDrawer from "../../components/PageDrawer";
import { PAGE_DRAWER_WIDTH, OPERATE_MODE, DEFAULT_ITEM_PAGE_SIZE } from '@/config/setting';
import DeliveryItemEdit from "./DeliveryItemEdit";
import { ListRowDoubleClickContext, CallbackDoubleClickDatagridRow } from '@/page/components/DoubleClickDatagridRows';
const StyledDatagrid = styled(DatagridConfigurable)(({ theme }) => ({
    '& .css-1vooibu-MuiSvgIcon-root': {
@@ -91,6 +92,10 @@
    const location = useLocation();
    const [select, setSelect] = useState({});
    const { data: dicts, isPending, error } = useGetOne('delivery', { id: doId });
    const openItemEdit = useCallback((record) => {
        setSelect(record);
        setEditDialog(true);
    }, []);
    return (
        <Box display="flex">
            <List
@@ -118,13 +123,11 @@
                )}
                perPage={DEFAULT_ITEM_PAGE_SIZE}
            >
                <ListRowDoubleClickContext.Provider value={openItemEdit}>
                <StyledDatagrid
                    preferenceKey='deliveryItem'
                    bulkActionButtons={() => <BulkDeleteButton mutationMode={OPERATE_MODE} />}
                    rowClick={(id, resource, record) => {
                        setSelect(record)
                        setEditDialog(true)
                    }}
                    row={<CallbackDoubleClickDatagridRow />}
                    expand={false}
                    expandSingle={true}
                    omit={['id', 'createTime', 'deliveryId', 'fieldsIndex', 'printQty', 'nromQty', 'createBy', 'memo','statusBool','createBy$','splrCode']}
@@ -153,6 +156,7 @@
                        <DeleteButton sx={{ padding: '1px', fontSize: '.75rem' }} mutationMode={OPERATE_MODE} redirect={location.pathname + '/' + doId} />
                    </WrapperField>
                </StyledDatagrid>
                </ListRowDoubleClickContext.Provider>
            </List>
            <DeliveryItemEdit
                open={editDialog}
rsf-admin/src/page/orders/stock/OrderList.jsx
@@ -44,6 +44,7 @@
import * as Common from '@/utils/common';
import OrderCreate from "./OrderCreate";
import OrderPanel from "./OrderPanel";
import { EditOnDoubleClickDatagridRow } from '@/page/components/DoubleClickDatagridRows';
const StyledDatagrid = styled(DatagridConfigurable)(({ theme }) => ({
@@ -114,7 +115,7 @@
                <StyledDatagrid
                    preferenceKey='stock'
                    bulkActionButtons={false}
                    rowClick='edit'
                    row={<EditOnDoubleClickDatagridRow />}
                    expandSingle={false}
                    omit={['id', 'sourceId', 'memo','statusBool','opt']}
                >
rsf-admin/src/page/orders/transfer/TransferOrders.jsx
@@ -1,4 +1,4 @@
import React, { useState, useRef, useEffect, useMemo } from "react";
import React, { useState, useRef, useEffect, useMemo, useCallback } from "react";
import { Box, Card, CardContent, Grid, Typography, Tooltip } from '@mui/material';
import {
    List,
@@ -18,6 +18,7 @@
import BillStatusField from '../../components/BillStatusField';
import { styled } from '@mui/material/styles';
import * as Common from '@/utils/common.js';
import { ListRowDoubleClickContext, CallbackDoubleClickDatagridRow } from '@/page/components/DoubleClickDatagridRows';
const StyledDatagrid = styled(DatagridConfigurable)(({ theme }) => ({
@@ -45,6 +46,16 @@
    const record = useRecordContext();
    if (!record) return null;
    const translate = useTranslate();
    const onTransferRowDoubleClick = useCallback(
        (row) => {
            if (row.type == 'out') {
                redirct("/outStock");
            } else if (row.type == 'in') {
                redirct("/asnOrder");
            }
        },
        [redirct],
    );
    return (
        <>
            <Card sx={{ margin: 'auto' }}>
@@ -66,17 +77,12 @@
                    actions={false}
                    perPage={DEFAULT_PAGE_SIZE}
                >
                    <ListRowDoubleClickContext.Provider value={onTransferRowDoubleClick}>
                    <StyledDatagrid
                        sx={{ margin: 'auto', width: '100%' }}
                        preferenceKey='outStock'
                        bulkActionButtons={false}
                        rowClick={(id, resource, record) => {
                            if (record.type == 'out') {
                                redirct("/outStock")
                            } else if (record.type == 'in') {
                                redirct("/asnOrder")
                            }
                        }}
                        row={<CallbackDoubleClickDatagridRow />}
                        expandSingle={true}
                        omit={['id', 'memo']}
                    >
@@ -95,6 +101,7 @@
                        <BillStatusField cellClassName="status" source="exceStatus" label="table.field.outStock.exceStatus" />
                        <TextField source="memo" label="common.field.memo" sortable={false} />
                    </StyledDatagrid>
                    </ListRowDoubleClickContext.Provider>
                </List>
            </Card >
        </>
rsf-admin/src/page/system/dicts/dictType/DictDataList.jsx
@@ -46,6 +46,7 @@
import { PAGE_DRAWER_WIDTH, OPERATE_MODE, DEFAULT_PAGE_SIZE, DEFAULT_ITEM_PAGE_SIZE } from '@/config/setting';
import DictDataEdit from "./DictDataEdit";
import { use } from "react";
import { ListRowDoubleClickContext, CallbackDoubleClickDatagridRow } from '@/page/components/DoubleClickDatagridRows';
const StyledDatagrid = styled(DatagridConfigurable)(({ theme }) => ({
@@ -90,6 +91,10 @@
    const [select, setSelect] = useState({});
    const dictId = useGetRecordId();
    const { data: dicts, isPending, error } = useGetOne('dictType', { id: dictId });
    const openItemEdit = useCallback((record) => {
        setSelect(record);
        setEditDialog(true);
    }, []);
    return (
        <>
@@ -119,12 +124,10 @@
                    )}
                    perPage={DEFAULT_ITEM_PAGE_SIZE}
                >
                    <ListRowDoubleClickContext.Provider value={openItemEdit}>
                    <StyledDatagrid
                        bulkActionButtons={() => <BulkDeleteButton mutationMode={OPERATE_MODE} />}
                        rowClick={(id, resource, record) => {
                            setSelect(record)
                            setEditDialog(true)
                        }}
                        row={<CallbackDoubleClickDatagridRow />}
                        omit={['id', 'createTime', 'createBy$', 'memo', 'statusBool']}
                    >
                        <NumberField source="id" />
@@ -145,6 +148,7 @@
                            <DeleteButton sx={{ padding: '1px', fontSize: '.75rem' }} mutationMode='pessimistic' redirect={"/dictType/" + dictId} />
                        </WrapperField>
                    </StyledDatagrid>
                    </ListRowDoubleClickContext.Provider>
                </List>
                <DictDataEdit
                    open={editDialog}
rsf-admin/src/page/system/dicts/dictType/DictTypeList.jsx
@@ -40,6 +40,7 @@
import MyExportButton from '../../../components/MyExportButton';
import { PAGE_DRAWER_WIDTH, OPERATE_MODE, DEFAULT_PAGE_SIZE } from '@/config/setting';
import { width } from "@mui/system";
import { EditOnDoubleClickDatagridRow } from '@/page/components/DoubleClickDatagridRows';
const StyledDatagrid = styled(DatagridConfigurable)(({ theme }) => ({
    '& .css-1vooibu-MuiSvgIcon-root': {
@@ -118,7 +119,7 @@
                <StyledDatagrid
                    preferenceKey='dictType'
                    bulkActionButtons={() => <BulkDeleteButton mutationMode={OPERATE_MODE} />}
                    rowClick={'edit'}
                    row={<EditOnDoubleClickDatagridRow />}
                    omit={['id', 'createTime', 'createBy$', 'memo','statusBool']}
                >
                    <NumberField source="id" />
rsf-admin/src/page/system/httpAuditLog/HttpAuditLogList.jsx
@@ -1,91 +1,258 @@
import React from "react";
import React, { useMemo } from "react";
import {
    List,
    Datagrid,
    DatagridConfigurable,
    SearchInput,
    TopToolbar,
    SelectColumnsButton,
    FilterButton,
    TextField,
    DateField,
    TopToolbar,
    FilterButton,
    NumberField,
    TextInput,
    DateInput,
    SelectInput,
    WrapperField,
    FunctionField,
    ShowButton,
    BulkDeleteButton,
    FunctionField,
    useTranslate,
    Pagination,
} from "react-admin";
import { Chip } from "@mui/material";
import { Box, Chip } from "@mui/material";
import { styled } from "@mui/material/styles";
import EmptyData from "@/page/components/EmptyData";
import { DEFAULT_PAGE_SIZE } from "@/config/setting";
import { OPERATE_MODE, DEFAULT_PAGE_SIZE } from "@/config/setting";
const filters = [
    <TextInput source="uri" label="table.field.httpAuditLog.uri" alwaysOn />,
    <TextInput source="clientIp" label="table.field.httpAuditLog.clientIp" />,
    <SelectInput
        source="okFlag"
        label="table.field.httpAuditLog.okFlag"
        choices={[
            { id: 1, name: "正常" },
            { id: 0, name: "异常" },
        ]}
    />,
    <TextInput source="serviceName" label="table.field.httpAuditLog.serviceName" />,
    <SelectInput
        source="scopeType"
        label="table.field.httpAuditLog.scopeType"
        choices={[
            { id: "EXTERNAL", name: "外部" },
            { id: "INTERNAL", name: "内部" },
        ]}
    />,
    <SelectInput
        source="ioDirection"
        label="table.field.httpAuditLog.ioDirection"
        choices={[
            { id: "IN", name: "IN" },
            { id: "OUT", name: "OUT" },
        ]}
    />,
    <TextInput source="functionDesc" label="table.field.httpAuditLog.functionDesc" />,
    <TextInput source="method" label="table.field.httpAuditLog.method" />,
];
const httpAuditLogPagination = <Pagination rowsPerPageOptions={[10, 25, 50, 100, 200]} />;
const HttpAuditLogList = () => (
    <List
        title="menu.httpAuditLog"
        filters={filters}
        sort={{ field: "create_time", order: "DESC" }}
        perPage={DEFAULT_PAGE_SIZE}
        empty={<EmptyData />}
        actions={
            <TopToolbar>
                <FilterButton />
            </TopToolbar>
        }
    >
        <Datagrid bulkActionButtons={<BulkDeleteButton />}>
            <TextField source="id" />
            <TextField source="serviceName" label="table.field.httpAuditLog.serviceName" />
            <TextField source="scopeType" label="table.field.httpAuditLog.scopeType" />
            <TextField source="uri" label="table.field.httpAuditLog.uri" />
            <TextField source="ioDirection" label="table.field.httpAuditLog.ioDirection" />
            <TextField source="method" label="table.field.httpAuditLog.method" />
            <TextField source="functionDesc" label="table.field.httpAuditLog.functionDesc" />
            <TextField source="clientIp" label="table.field.httpAuditLog.clientIp" />
            <FunctionField
const StyledDatagrid = styled(DatagridConfigurable)(() => ({
    "& .RaDatagrid-row": { cursor: "default" },
    "& .column-uri": {
        maxWidth: "18em",
        overflow: "hidden",
        textOverflow: "ellipsis",
        whiteSpace: "nowrap",
    },
    "& .column-requestBody": {
        maxWidth: "18em",
        overflow: "hidden",
        textOverflow: "ellipsis",
        whiteSpace: "nowrap",
    },
    "& .column-responseBody": {
        maxWidth: "18em",
        overflow: "hidden",
        textOverflow: "ellipsis",
        whiteSpace: "nowrap",
    },
    "& .opt": { width: 140 },
}));
function joinRequestParams(record) {
    const parts = [];
    if (record.queryString) {
        parts.push(record.queryString);
    }
    if (record.requestBody) {
        parts.push(record.requestBody);
    }
    return parts.join("\n");
}
const HttpAuditLogList = () => {
    const translate = useTranslate();
    const filters = useMemo(
        () => [
            <SearchInput source="condition" alwaysOn />,
            <DateInput source="timeStart" label="common.time.after" />,
            <DateInput source="timeEnd" label="common.time.before" />,
            <TextInput source="uri" label="table.field.httpAuditLog.uri" />,
            <TextInput source="clientIp" label="table.field.httpAuditLog.clientIp" />,
            <SelectInput
                source="okFlag"
                label="table.field.httpAuditLog.okFlag"
                render={(record) =>
                    record.okFlag === 1 ? (
                        <Chip label="正常" color="success" size="small" variant="outlined" />
                    ) : (
                        <Chip label="异常" color="error" size="small" variant="outlined" />
                    )
                choices={[
                    { id: 1, name: translate("table.field.httpAuditLog.okNormal") },
                    { id: 0, name: translate("table.field.httpAuditLog.okAbnormal") },
                ]}
            />,
            <TextInput source="serviceName" label="table.field.httpAuditLog.serviceName" />,
            <SelectInput
                source="scopeType"
                label="table.field.httpAuditLog.scopeType"
                choices={[
                    { id: "EXTERNAL", name: translate("table.field.httpAuditLog.scopeExternal") },
                    { id: "INTERNAL", name: translate("table.field.httpAuditLog.scopeInternal") },
                ]}
            />,
            <SelectInput
                source="ioDirection"
                label="table.field.httpAuditLog.ioDirection"
                choices={[
                    { id: "IN", name: translate("table.field.httpAuditLog.ioIn") },
                    { id: "OUT", name: translate("table.field.httpAuditLog.ioOut") },
                ]}
            />,
            <TextInput source="functionDesc" label="table.field.httpAuditLog.functionDesc" />,
            <TextInput source="method" label="table.field.httpAuditLog.method" />,
            <TextInput source="requestContains" label="table.field.httpAuditLog.requestContains" />,
            <TextInput source="responseContains" label="table.field.httpAuditLog.responseContains" />,
        ],
        [translate],
    );
    return (
        <Box display="flex">
            <List
                title="menu.httpAuditLog"
                filters={filters}
                sort={{ field: "create_time", order: "DESC" }}
                perPage={DEFAULT_PAGE_SIZE}
                pagination={httpAuditLogPagination}
                empty={<EmptyData />}
                actions={
                    <TopToolbar>
                        <FilterButton />
                        <SelectColumnsButton preferenceKey="httpAuditLog" />
                    </TopToolbar>
                }
            />
            <TextField source="httpStatus" label="table.field.httpAuditLog.httpStatus" />
            <TextField source="spendMs" label="table.field.httpAuditLog.spendMs" />
            <DateField source="createTime" label="common.field.createTime" showTime />
            <ShowButton />
        </Datagrid>
    </List>
);
            >
                <StyledDatagrid
                    preferenceKey="httpAuditLog"
                    rowClick={false}
                    bulkActionButtons={() => <BulkDeleteButton mutationMode={OPERATE_MODE} />}
                >
                    <NumberField source="id" label="common.field.id" />
                    <TextField source="serviceName" label="table.field.httpAuditLog.serviceName" />
                    <FunctionField
                        source="scopeType"
                        label="table.field.httpAuditLog.scopeType"
                        render={(record) =>
                            record.scopeType === "EXTERNAL"
                                ? translate("table.field.httpAuditLog.scopeExternal")
                                : record.scopeType === "INTERNAL"
                                  ? translate("table.field.httpAuditLog.scopeInternal")
                                  : (record.scopeType ?? "")
                        }
                    />
                    <TextField source="uri" label="table.field.httpAuditLog.uri" />
                    <FunctionField
                        source="ioDirection"
                        label="table.field.httpAuditLog.ioDirection"
                        render={(record) =>
                            record.ioDirection === "IN"
                                ? translate("table.field.httpAuditLog.ioIn")
                                : record.ioDirection === "OUT"
                                  ? translate("table.field.httpAuditLog.ioOut")
                                  : (record.ioDirection ?? "")
                        }
                    />
                    <TextField source="method" label="table.field.httpAuditLog.method" />
                    <TextField source="functionDesc" label="table.field.httpAuditLog.functionDesc" />
                    <FunctionField
                        source="requestBody"
                        label="table.field.httpAuditLog.requestParams"
                        sortable={false}
                        render={(record) => {
                            const full = joinRequestParams(record);
                            if (!full) {
                                return "";
                            }
                            return (
                                <Box
                                    component="span"
                                    title={full}
                                    sx={{
                                        maxWidth: "inherit",
                                        display: "inline-block",
                                        overflow: "hidden",
                                        textOverflow: "ellipsis",
                                        whiteSpace: "nowrap",
                                        verticalAlign: "bottom",
                                        width: "100%",
                                    }}
                                >
                                    {full}
                                </Box>
                            );
                        }}
                    />
                    <FunctionField
                        source="responseBody"
                        label="table.field.httpAuditLog.responseParams"
                        sortable={false}
                        render={(record) => {
                            const full = record.responseBody ?? "";
                            if (!full) {
                                return "";
                            }
                            return (
                                <Box
                                    component="span"
                                    title={full}
                                    sx={{
                                        maxWidth: "inherit",
                                        display: "inline-block",
                                        overflow: "hidden",
                                        textOverflow: "ellipsis",
                                        whiteSpace: "nowrap",
                                        verticalAlign: "bottom",
                                        width: "100%",
                                    }}
                                >
                                    {full}
                                </Box>
                            );
                        }}
                    />
                    <TextField source="clientIp" label="table.field.httpAuditLog.clientIp" />
                    <FunctionField
                        source="okFlag"
                        label="table.field.httpAuditLog.okFlag"
                        render={(record) =>
                            record.okFlag === 1 ? (
                                <Chip
                                    label={translate("table.field.httpAuditLog.okNormal")}
                                    color="success"
                                    size="small"
                                    variant="outlined"
                                />
                            ) : (
                                <Chip
                                    label={translate("table.field.httpAuditLog.okAbnormal")}
                                    color="error"
                                    size="small"
                                    variant="outlined"
                                />
                            )
                        }
                    />
                    <NumberField source="httpStatus" label="table.field.httpAuditLog.httpStatus" />
                    <FunctionField
                        source="spendMs"
                        label="table.field.httpAuditLog.spendMs"
                        render={(record) =>
                            record.spendMs == null ? "" : String(Number((Number(record.spendMs) / 1000).toFixed(3)))
                        }
                    />
                    <FunctionField
                        source="responseTruncated"
                        label="table.field.httpAuditLog.responseTruncated"
                        render={(record) =>
                            record.responseTruncated === 1
                                ? translate("table.field.httpAuditLog.truncatedYes")
                                : translate("table.field.httpAuditLog.truncatedNo")
                        }
                    />
                    <DateField source="createTime" label="common.field.createTime" showTime />
                    <WrapperField label="common.field.opt" cellClassName="opt">
                        <ShowButton label="toolbar.detail" sx={{ padding: "1px", fontSize: ".75rem" }} />
                    </WrapperField>
                </StyledDatagrid>
            </List>
        </Box>
    );
};
export default HttpAuditLogList;
rsf-admin/src/page/system/httpAuditLog/HttpAuditLogShow.jsx
@@ -1,59 +1,221 @@
import React from "react";
import { Show, SimpleShowLayout, TextField, DateField, FunctionField } from "react-admin";
import { Box, Chip } from "@mui/material";
import { Link } from "react-router-dom";
import ArrowBackIcon from "@mui/icons-material/ArrowBack";
import { Show, TopToolbar, useRecordContext, useTranslate, useCreatePath, useResourceContext } from "react-admin";
import { Box, Button, Chip, Divider, Paper, Stack, Typography } from "@mui/material";
const HttpAuditLogShowActions = () => {
    const translate = useTranslate();
    const resource = useResourceContext();
    const createPath = useCreatePath();
    const listPath = createPath({ resource, type: "list" });
    return (
        <TopToolbar>
            <Button
                component={Link}
                to={listPath}
                variant="outlined"
                size="small"
                startIcon={<ArrowBackIcon fontSize="small" />}
                sx={{ textTransform: "none" }}
            >
                {translate("common.button.backToList")}
            </Button>
        </TopToolbar>
    );
};
const JsonBlock = ({ text }) => (
    <Box component="pre" sx={{ whiteSpace: "pre-wrap", wordBreak: "break-all", m: 0, fontSize: 12 }}>
    <Box
        component="pre"
        sx={{
            whiteSpace: "pre-wrap",
            wordBreak: "break-all",
            m: 0,
            fontSize: 12,
            fontFamily: "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace",
            bgcolor: (t) => (t.palette.mode === "dark" ? "grey.900" : "grey.50"),
            color: "text.primary",
            border: 1,
            borderColor: "divider",
            borderRadius: 1,
            p: 1.5,
            maxHeight: 360,
            overflow: "auto",
        }}
    >
        {text ?? ""}
    </Box>
);
const CompactItem = ({ labelKey, children }) => {
    const translate = useTranslate();
    return (
        <Box
            sx={{
                minWidth: 108,
                maxWidth: 260,
                flex: "0 1 auto",
                px: 1.25,
                py: 1,
                borderRadius: 1,
                bgcolor: (t) => (t.palette.mode === "dark" ? "action.hover" : "grey.50"),
                border: 1,
                borderColor: "divider",
            }}
        >
            <Typography variant="caption" color="text.secondary" component="div" sx={{ lineHeight: 1.4 }}>
                {translate(labelKey)}
            </Typography>
            <Box sx={{ mt: 0.5 }}>{children}</Box>
        </Box>
    );
};
const BlockItem = ({ labelKey, children }) => {
    const translate = useTranslate();
    return (
        <Box sx={{ width: "100%" }}>
            <Typography
                variant="subtitle2"
                color="text.secondary"
                component="div"
                sx={{ mb: 0.75, fontWeight: 600 }}
            >
                {translate(labelKey)}
            </Typography>
            <Box>{children}</Box>
        </Box>
    );
};
const SectionTitle = ({ labelKey }) => {
    const translate = useTranslate();
    return (
        <Typography variant="subtitle1" sx={{ fontWeight: 600, color: "text.primary", mb: 1.5 }}>
            {translate(labelKey)}
        </Typography>
    );
};
const HttpAuditLogShowContent = () => {
    const record = useRecordContext();
    const translate = useTranslate();
    if (!record) {
        return null;
    }
    const spendSec =
        record.spendMs == null ? "" : String(Number((Number(record.spendMs) / 1000).toFixed(3)));
    const truncatedLabel =
        record.responseTruncated === 1
            ? translate("table.field.httpAuditLog.truncatedYes")
            : translate("table.field.httpAuditLog.truncatedNo");
    return (
        <Box sx={{ p: 2, pb: 4, maxWidth: 1200, mx: "auto" }}>
            <Paper variant="outlined" sx={{ p: 2.5, mb: 2, borderRadius: 2 }}>
                <SectionTitle labelKey="httpAuditLog.show.sectionSummary" />
                <Stack direction="row" flexWrap="wrap" useFlexGap spacing={1.5} sx={{ alignItems: "stretch" }}>
                    <CompactItem labelKey="common.field.id">
                        <Typography variant="body2" fontWeight={500}>
                            {record.id ?? ""}
                        </Typography>
                    </CompactItem>
                    <CompactItem labelKey="table.field.httpAuditLog.serviceName">
                        <Typography variant="body2" sx={{ wordBreak: "break-all" }}>
                            {record.serviceName ?? ""}
                        </Typography>
                    </CompactItem>
                    <CompactItem labelKey="table.field.httpAuditLog.scopeType">
                        <Typography variant="body2">{record.scopeType ?? ""}</Typography>
                    </CompactItem>
                    <CompactItem labelKey="table.field.httpAuditLog.ioDirection">
                        <Typography variant="body2">{record.ioDirection ?? ""}</Typography>
                    </CompactItem>
                    <CompactItem labelKey="table.field.httpAuditLog.method">
                        <Typography variant="body2" fontWeight={500}>
                            {record.method ?? ""}
                        </Typography>
                    </CompactItem>
                    <CompactItem labelKey="table.field.httpAuditLog.clientIp">
                        <Typography variant="body2">{record.clientIp ?? ""}</Typography>
                    </CompactItem>
                    <CompactItem labelKey="table.field.httpAuditLog.okFlag">
                        {record.okFlag === 1 ? (
                            <Chip
                                label={translate("table.field.httpAuditLog.okNormal")}
                                color="success"
                                size="small"
                                variant="outlined"
                            />
                        ) : (
                            <Chip
                                label={translate("table.field.httpAuditLog.okAbnormal")}
                                color="error"
                                size="small"
                                variant="outlined"
                            />
                        )}
                    </CompactItem>
                    <CompactItem labelKey="table.field.httpAuditLog.httpStatus">
                        <Typography variant="body2">{record.httpStatus ?? ""}</Typography>
                    </CompactItem>
                    <CompactItem labelKey="table.field.httpAuditLog.spendMs">
                        <Typography variant="body2">{spendSec}</Typography>
                    </CompactItem>
                    <CompactItem labelKey="table.field.httpAuditLog.responseTruncated">
                        <Typography variant="body2">{truncatedLabel}</Typography>
                    </CompactItem>
                    <CompactItem labelKey="common.field.createTime">
                        <Typography variant="body2" sx={{ wordBreak: "break-all" }}>
                            {record.createTime != null ? String(record.createTime) : ""}
                        </Typography>
                    </CompactItem>
                </Stack>
            </Paper>
            <Paper variant="outlined" sx={{ p: 2.5, mb: 2, borderRadius: 2 }}>
                <SectionTitle labelKey="httpAuditLog.show.sectionRequest" />
                <Stack spacing={2}>
                    <BlockItem labelKey="table.field.httpAuditLog.uri">
                        <Typography variant="body2" sx={{ wordBreak: "break-all", lineHeight: 1.6 }}>
                            {record.uri ?? ""}
                        </Typography>
                    </BlockItem>
                    <BlockItem labelKey="table.field.httpAuditLog.functionDesc">
                        <Typography variant="body2" sx={{ wordBreak: "break-all", lineHeight: 1.6 }}>
                            {record.functionDesc ?? ""}
                        </Typography>
                    </BlockItem>
                </Stack>
            </Paper>
            <Paper variant="outlined" sx={{ p: 2.5, borderRadius: 2 }}>
                <SectionTitle labelKey="httpAuditLog.show.sectionPayload" />
                <Stack spacing={2.5} divider={<Divider flexItem />}>
                    <BlockItem labelKey="table.field.httpAuditLog.queryString">
                        <JsonBlock text={record.queryString} />
                    </BlockItem>
                    <BlockItem labelKey="table.field.httpAuditLog.requestBody">
                        <JsonBlock text={record.requestBody} />
                    </BlockItem>
                    <BlockItem labelKey="table.field.httpAuditLog.responseBody">
                        <JsonBlock text={record.responseBody} />
                    </BlockItem>
                    <BlockItem labelKey="table.field.httpAuditLog.errorMessage">
                        <JsonBlock text={record.errorMessage} />
                    </BlockItem>
                </Stack>
            </Paper>
        </Box>
    );
};
const HttpAuditLogShow = () => (
    <Show>
        <SimpleShowLayout>
            <TextField source="id" />
            <TextField source="serviceName" label="table.field.httpAuditLog.serviceName" />
            <TextField source="scopeType" label="table.field.httpAuditLog.scopeType" />
            <TextField source="uri" label="table.field.httpAuditLog.uri" />
            <TextField source="ioDirection" label="table.field.httpAuditLog.ioDirection" />
            <TextField source="method" label="table.field.httpAuditLog.method" />
            <TextField source="functionDesc" label="table.field.httpAuditLog.functionDesc" />
            <TextField source="clientIp" label="table.field.httpAuditLog.clientIp" />
            <FunctionField
                label="table.field.httpAuditLog.okFlag"
                render={(record) =>
                    record.okFlag === 1 ? (
                        <Chip label="正常" color="success" size="small" variant="outlined" />
                    ) : (
                        <Chip label="异常" color="error" size="small" variant="outlined" />
                    )
                }
            />
            <TextField source="httpStatus" label="table.field.httpAuditLog.httpStatus" />
            <TextField source="spendMs" label="table.field.httpAuditLog.spendMs" />
            <TextField source="responseTruncated" label="table.field.httpAuditLog.responseTruncated" />
            <DateField source="createTime" label="common.field.createTime" showTime />
            <FunctionField
                source="queryString"
                label="table.field.httpAuditLog.queryString"
                render={(record) => <JsonBlock text={record.queryString} />}
            />
            <FunctionField
                source="requestBody"
                label="table.field.httpAuditLog.requestBody"
                render={(record) => <JsonBlock text={record.requestBody} />}
            />
            <FunctionField
                source="responseBody"
                label="table.field.httpAuditLog.responseBody"
                render={(record) => <JsonBlock text={record.responseBody} />}
            />
            <FunctionField
                source="errorMessage"
                label="table.field.httpAuditLog.errorMessage"
                render={(record) => <JsonBlock text={record.errorMessage} />}
            />
        </SimpleShowLayout>
    <Show actions={<HttpAuditLogShowActions />}>
        <HttpAuditLogShowContent />
    </Show>
);
rsf-admin/src/page/system/httpAuditSysConfig/HttpAuditSysConfigCreate.jsx
New file
@@ -0,0 +1,91 @@
import React from "react";
import {
    CreateBase,
    TextInput,
    NumberInput,
    SelectInput,
    Toolbar,
    Form,
    SaveButton,
    required,
    useNotify,
    useTranslate,
} from 'react-admin';
import { Dialog, DialogContent, DialogTitle, Grid, Box } from '@mui/material';
import DialogCloseButton from "@/page/components/DialogCloseButton";
const HttpAuditSysConfigCreate = (props) => {
    const { open, setOpen } = props;
    const notify = useNotify();
    const translate = useTranslate();
    const handleClose = (event, reason) => {
        if (reason !== "backdropClick") setOpen(false);
    };
    const handleSuccess = () => {
        setOpen(false);
        notify('新增成功');
    };
    const handleError = (error) => {
        notify(error?.message || '新增失败', { type: 'error' });
    };
    return (
        <CreateBase
            resource="httpAuditSysConfig"
            record={{ enabled: 1, sortOrder: 0 }}
            mutationOptions={{ onSuccess: handleSuccess, onError: handleError }}
        >
            <Dialog
                open={open}
                onClose={handleClose}
                fullWidth
                disableRestoreFocus
                maxWidth="md"
            >
                <Form>
                    <DialogTitle sx={{ position: 'sticky', top: 0, backgroundColor: 'background.paper', zIndex: 1000 }}>
                        {translate('resources.httpAuditSysConfig.createTitle')}
                        <Box sx={{ position: 'absolute', top: 8, right: 8, zIndex: 1001 }}>
                            <DialogCloseButton onClose={handleClose} />
                        </Box>
                    </DialogTitle>
                    <DialogContent sx={{ mt: 2 }}>
                        <Grid container rowSpacing={2} columnSpacing={2}>
                            <Grid item xs={12}>
                                <TextInput source="configKey" label="table.field.httpAuditSysConfig.configKey" validate={required()} fullWidth />
                            </Grid>
                            <Grid item xs={12}>
                                <TextInput source="configVal" label="table.field.httpAuditSysConfig.configVal" fullWidth multiline minRows={4} />
                            </Grid>
                            <Grid item xs={12} sm={6}>
                                <SelectInput
                                    source="enabled"
                                    label="table.field.httpAuditSysConfig.enabled"
                                    choices={[
                                        { id: 1, name: translate('table.field.httpAuditSysConfig.enabledOn') },
                                        { id: 0, name: translate('table.field.httpAuditSysConfig.enabledOff') },
                                    ]}
                                    fullWidth
                                />
                            </Grid>
                            <Grid item xs={12} sm={6}>
                                <NumberInput source="sortOrder" label="table.field.httpAuditSysConfig.sortOrder" fullWidth />
                            </Grid>
                            <Grid item xs={12}>
                                <TextInput source="remark" label="table.field.httpAuditSysConfig.remark" fullWidth multiline minRows={2} />
                            </Grid>
                        </Grid>
                    </DialogContent>
                    <Toolbar sx={{ justifyContent: 'flex-end', px: 2, pb: 2 }}>
                        <SaveButton label="ra.action.save" type="button" />
                    </Toolbar>
                </Form>
            </Dialog>
        </CreateBase>
    );
};
export default HttpAuditSysConfigCreate;
rsf-admin/src/page/system/httpAuditSysConfig/HttpAuditSysConfigEdit.jsx
New file
@@ -0,0 +1,47 @@
import React from "react";
import {
    Edit,
    SimpleForm,
    TextInput,
    NumberInput,
    SelectInput,
    Toolbar,
    SaveButton,
    DeleteButton,
    useTranslate,
} from 'react-admin';
import { OPERATE_MODE } from '@/config/setting';
const HttpAuditToolbar = () => (
    <Toolbar>
        <SaveButton />
        <DeleteButton mutationMode={OPERATE_MODE} />
    </Toolbar>
);
const HttpAuditSysConfigEdit = () => {
    const translate = useTranslate();
    return (
        <Edit
            mutationMode={OPERATE_MODE}
            resource="httpAuditSysConfig"
        >
            <SimpleForm toolbar={<HttpAuditToolbar />}>
                <TextInput source="configKey" label="table.field.httpAuditSysConfig.configKey" fullWidth />
                <TextInput source="configVal" label="table.field.httpAuditSysConfig.configVal" fullWidth multiline minRows={4} />
                <SelectInput
                    source="enabled"
                    label="table.field.httpAuditSysConfig.enabled"
                    choices={[
                        { id: 1, name: translate('table.field.httpAuditSysConfig.enabledOn') },
                        { id: 0, name: translate('table.field.httpAuditSysConfig.enabledOff') },
                    ]}
                />
                <NumberInput source="sortOrder" label="table.field.httpAuditSysConfig.sortOrder" fullWidth />
                <TextInput source="remark" label="table.field.httpAuditSysConfig.remark" fullWidth multiline minRows={2} />
            </SimpleForm>
        </Edit>
    );
};
export default HttpAuditSysConfigEdit;
rsf-admin/src/page/system/httpAuditSysConfig/HttpAuditSysConfigList.jsx
New file
@@ -0,0 +1,89 @@
import React, { useMemo, useState } from "react";
import {
    List,
    DatagridConfigurable,
    SearchInput,
    TopToolbar,
    SelectColumnsButton,
    EditButton,
    FilterButton,
    TextField,
    FunctionField,
    SelectInput,
    WrapperField,
    DeleteButton,
    useTranslate,
} from 'react-admin';
import { Box } from '@mui/material';
import { styled } from '@mui/material/styles';
import HttpAuditSysConfigCreate from "./HttpAuditSysConfigCreate";
import EmptyData from "@/page/components/EmptyData";
import MyCreateButton from "@/page/components/MyCreateButton";
import { OPERATE_MODE, DEFAULT_PAGE_SIZE } from '@/config/setting';
const StyledDatagrid = styled(DatagridConfigurable)(() => ({
    '& .RaDatagrid-row': { cursor: 'auto' },
    '& .opt': { width: 140 },
}));
const HttpAuditSysConfigList = () => {
    const [createDialog, setCreateDialog] = useState(false);
    const translate = useTranslate();
    const filters = useMemo(() => [
        <SearchInput source="condition" alwaysOn />,
        <SelectInput
            source="enabled"
            label="table.field.httpAuditSysConfig.enabled"
            choices={[
                { id: 1, name: translate('table.field.httpAuditSysConfig.enabledOn') },
                { id: 0, name: translate('table.field.httpAuditSysConfig.enabledOff') },
            ]}
        />,
    ], [translate]);
    return (
        <Box display="flex">
            <List
                title="menu.httpAuditSysConfig"
                empty={<EmptyData onClick={() => setCreateDialog(true)} />}
                filters={filters}
                sort={{ field: "sortOrder", order: "asc" }}
                actions={(
                    <TopToolbar>
                        <FilterButton />
                        <MyCreateButton onClick={() => setCreateDialog(true)} />
                        <SelectColumnsButton preferenceKey="httpAuditSysConfig" />
                    </TopToolbar>
                )}
                perPage={DEFAULT_PAGE_SIZE}
            >
                <StyledDatagrid
                    preferenceKey="httpAuditSysConfig"
                    bulkActionButtons={() => <DeleteButton mutationMode={OPERATE_MODE} />}
                    rowClick={false}
                >
                    <TextField source="id" label="common.field.id" />
                    <TextField source="configKey" label="table.field.httpAuditSysConfig.configKey" />
                    <TextField source="configVal" label="table.field.httpAuditSysConfig.configVal" />
                    <FunctionField
                        source="enabled"
                        label="table.field.httpAuditSysConfig.enabled"
                        render={(r) => (r.enabled === 1
                            ? translate('table.field.httpAuditSysConfig.enabledOn')
                            : translate('table.field.httpAuditSysConfig.enabledOff'))}
                    />
                    <TextField source="sortOrder" label="table.field.httpAuditSysConfig.sortOrder" />
                    <TextField source="remark" label="table.field.httpAuditSysConfig.remark" />
                    <WrapperField cellClassName="opt" label="common.field.opt">
                        <EditButton sx={{ padding: '1px', fontSize: '.75rem' }} />
                        <DeleteButton sx={{ padding: '1px', fontSize: '.75rem' }} mutationMode={OPERATE_MODE} />
                    </WrapperField>
                </StyledDatagrid>
            </List>
            <HttpAuditSysConfigCreate open={createDialog} setOpen={setCreateDialog} />
        </Box>
    );
};
export default HttpAuditSysConfigList;
rsf-admin/src/page/system/httpAuditSysConfig/index.jsx
New file
@@ -0,0 +1,9 @@
import React from "react";
import HttpAuditSysConfigList from "./HttpAuditSysConfigList";
import HttpAuditSysConfigEdit from "./HttpAuditSysConfigEdit";
export default {
    list: HttpAuditSysConfigList,
    edit: HttpAuditSysConfigEdit,
    recordRepresentation: (record) => record?.configKey || record?.id || '',
};
rsf-admin/src/page/system/operationRecord/OperationRecordList.jsx
@@ -44,6 +44,7 @@
import { format } from 'date-fns';
import OperationDetail from './OperationDetail'
import { width } from "@mui/system";
import { ListRowDoubleClickContext, CallbackDoubleClickDatagridRow } from '@/page/components/DoubleClickDatagridRows';
const StyledDatagrid = styled(DatagridConfigurable)(({ theme }) => ({
    '& .css-1vooibu-MuiSvgIcon-root': {
@@ -114,6 +115,10 @@
    const [createDialog, setCreateDialog] = useState(false);
    const [drawerVal, setDrawerVal] = useState(false);
    const toggleOperationDetail = useCallback((record) => {
        setDrawerVal((prev) => (prev && prev === record ? null : record));
    }, []);
    return (
        <Box display="flex">
            <List
@@ -138,13 +143,11 @@
                )}
                perPage={DEFAULT_PAGE_SIZE}
            >
                <ListRowDoubleClickContext.Provider value={toggleOperationDetail}>
                <StyledDatagrid
                    preferenceKey='operationRecord'
                    bulkActionButtons={false}
                    rowClick={(id, resource, record) => {
                        setDrawerVal(!!drawerVal && drawerVal === record ? null : record);
                        return false;
                    }}
                    row={<CallbackDoubleClickDatagridRow />}
                    omit={['appkey', 'statusBool', 'err', 'updateTime', 'createTime', 'memo']}
                    rowSx={rowSx(drawerVal || null)}
                >
@@ -167,6 +170,7 @@
                    <BooleanField source="statusBool" label="common.field.status" sortable={false} />
                    <TextField source="memo" label="common.field.memo" sortable={false} />
                </StyledDatagrid>
                </ListRowDoubleClickContext.Provider>
            </List>
            <PageDrawer
                title={translate('table.field.operationRecord.detail')}
rsf-admin/src/page/system/serialRule/SerialRuleItemList.jsx
@@ -57,6 +57,7 @@
import * as Common from "@/utils/common";
import CustomerTopToolBar from "../../components/EditTopToolBar";
import SerialRuleItemEdit from "./SerialRuleItemEdit";
import { ListRowDoubleClickContext, CallbackDoubleClickDatagridRow } from "@/page/components/DoubleClickDatagridRows";
const StyledDatagrid = styled(DatagridConfigurable)(({ theme }) => ({
  "& .css-1vooibu-MuiSvgIcon-root": {
@@ -106,6 +107,10 @@
  const [select, setSelect] = useState({});
  const ruleId = useGetRecordId();
  const { data: dicts, isPending, error } = useGetOne('serialRule', { id: ruleId });
  const openItemEdit = useCallback((record) => {
    setSelect(record);
    setEditDialog(true);
  }, []);
  return (
    <>
      <Box display="flex">
@@ -138,15 +143,13 @@
          }
          perPage={DEFAULT_PAGE_SIZE}
        >
          <ListRowDoubleClickContext.Provider value={openItemEdit}>
          <StyledDatagrid
            preferenceKey="serialRuleItem"
            bulkActionButtons={() => (
              <BulkDeleteButton mutationMode={OPERATE_MODE} />
            )}
            rowClick={(id, resource, record) => {
              setSelect(record)
              setEditDialog(true)
            }}
            row={<CallbackDoubleClickDatagridRow />}
            omit={["id", "ruleId", "createTime", "createBy$", "memo",'statusBool']}
          >
            <NumberField source="id" />
@@ -212,6 +215,7 @@
              />
            </WrapperField>
          </StyledDatagrid>
          </ListRowDoubleClickContext.Provider>
        </List>
        <SerialRuleItemCreate open={createDialog} setOpen={setCreateDialog} record={dicts} />
        <SerialRuleItemEdit open={editDialog} setOpen={setEditDialog} record={select} />
rsf-admin/src/page/system/serialRule/SerialRuleList.jsx
@@ -52,6 +52,7 @@
  DEFAULT_PAGE_SIZE,
} from "@/config/setting";
import * as Common from "@/utils/common";
import { EditOnDoubleClickDatagridRow } from "@/page/components/DoubleClickDatagridRows";
const StyledDatagrid = styled(DatagridConfigurable)(({ theme }) => ({
  "& .css-1vooibu-MuiSvgIcon-root": {
@@ -142,7 +143,7 @@
          bulkActionButtons={() => (
            <BulkDeleteButton mutationMode={OPERATE_MODE} />
          )}
          rowClick={'edit'}
          row={<EditOnDoubleClickDatagridRow />}
          omit={["id", "createTime", "createBy$", "memo",'statusBool']}
        >
          <NumberField source="id" />
rsf-admin/src/utils/common.js
@@ -39,16 +39,16 @@
        return;
    }
    const navMenus = [];
    const seen = new Set();
    const traverse = (nodes) => {
        nodes.forEach((node) => {
            // 叶子:无子或 children 为空数组;仅收集有 component 的节点(页面资源)
            const children = node.children;
            const hasChildren = Array.isArray(children) && children.length > 0;
            if (!hasChildren) {
                if (node.component) {
                    navMenus.push(node);
                }
            } else {
            if (node.component && !seen.has(node.component)) {
                navMenus.push(node);
                seen.add(node.component);
            }
            if (hasChildren) {
                traverse(children);
            }
        });
rsf-http-audit/pom.xml
@@ -41,5 +41,35 @@
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
        </dependency>
        <dependency>
            <groupId>com.vincent</groupId>
            <artifactId>rsf-framework</artifactId>
            <version>1.0.0</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-core</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-config</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>9</source>
                    <target>9</target>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>
rsf-http-audit/src/main/java/com/vincent/rsf/httpaudit/admin/HttpAuditLogAdminController.java
New file
@@ -0,0 +1,98 @@
package com.vincent.rsf.httpaudit.admin;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.vincent.rsf.framework.common.Cools;
import com.vincent.rsf.framework.common.R;
import com.vincent.rsf.httpaudit.entity.HttpAuditLog;
import com.vincent.rsf.httpaudit.service.HttpAuditLogCrudService;
import com.vincent.rsf.httpaudit.web.util.HttpAuditAdminQueryHelper;
import org.apache.commons.lang3.StringUtils;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import java.util.Arrays;
import java.util.Map;
@RestController
public class HttpAuditLogAdminController {
    private final HttpAuditLogCrudService httpAuditLogCrudService;
    public HttpAuditLogAdminController(HttpAuditLogCrudService httpAuditLogCrudService) {
        this.httpAuditLogCrudService = httpAuditLogCrudService;
    }
    @PreAuthorize("hasAuthority('system:httpAuditLog:list')")
    @PostMapping("/httpAuditLog/page")
    public R page(@RequestBody Map<String, Object> body) {
        Map<String, Object> map = HttpAuditAdminQueryHelper.normalizeBody(body);
        Page<HttpAuditLog> page = HttpAuditAdminQueryHelper.extractPage(map);
        String orderBy = HttpAuditAdminQueryHelper.extractOrderBy(map);
        String condition = HttpAuditAdminQueryHelper.extractCondition(map);
        Object timeStart = map.remove("timeStart");
        Object timeEnd = map.remove("timeEnd");
        QueryWrapper<HttpAuditLog> qw = new QueryWrapper<>();
        HttpAuditAdminQueryHelper.applyCreateTimeRange(qw, timeStart, timeEnd);
        if (!Cools.isEmpty(map.get("uri"))) {
            qw.like("uri", map.get("uri"));
        }
        if (!Cools.isEmpty(map.get("clientIp"))) {
            qw.eq("client_ip", map.get("clientIp"));
        }
        if (!Cools.isEmpty(map.get("okFlag"))) {
            qw.eq("ok_flag", map.get("okFlag"));
        }
        if (!Cools.isEmpty(map.get("serviceName"))) {
            qw.like("service_name", map.get("serviceName"));
        }
        if (!Cools.isEmpty(map.get("scopeType"))) {
            qw.eq("scope_type", map.get("scopeType"));
        }
        if (!Cools.isEmpty(map.get("ioDirection"))) {
            qw.eq("io_direction", map.get("ioDirection"));
        }
        if (!Cools.isEmpty(map.get("functionDesc"))) {
            qw.like("function_desc", map.get("functionDesc"));
        }
        if (!Cools.isEmpty(map.get("method"))) {
            qw.eq("method", map.get("method"));
        }
        if (!Cools.isEmpty(map.get("requestContains"))) {
            String v = String.valueOf(map.get("requestContains")).trim();
            qw.and(w -> w.like("query_string", v).or().like("request_body", v));
        }
        if (!Cools.isEmpty(map.get("responseContains"))) {
            qw.like("response_body", map.get("responseContains"));
        }
        if (StringUtils.isNotBlank(condition)) {
            qw.and(w -> w.like("uri", condition)
                    .or().like("service_name", condition)
                    .or().like("method", condition)
                    .or().like("client_ip", condition)
                    .or().like("function_desc", condition));
        }
        HttpAuditAdminQueryHelper.applySafeOrder(qw, orderBy, "ORDER BY create_time DESC");
        return R.ok().add(httpAuditLogCrudService.page(page, qw));
    }
    @PreAuthorize("hasAuthority('system:httpAuditLog:list')")
    @GetMapping("/httpAuditLog/{id}")
    public R get(@PathVariable("id") Long id) {
        return R.ok().add(httpAuditLogCrudService.getById(id));
    }
    @PreAuthorize("hasAuthority('system:httpAuditLog:remove')")
    @PostMapping("/httpAuditLog/remove/{ids}")
    public R remove(@PathVariable Long[] ids) {
        if (!httpAuditLogCrudService.removeByIds(Arrays.asList(ids))) {
            return R.error("Delete Fail");
        }
        return R.ok("Delete Success");
    }
}
rsf-http-audit/src/main/java/com/vincent/rsf/httpaudit/admin/HttpAuditRuleAdminController.java
File was renamed from rsf-server/src/main/java/com/vincent/rsf/server/system/controller/HttpAuditRuleController.java
@@ -1,18 +1,19 @@
package com.vincent.rsf.server.system.controller;
package com.vincent.rsf.httpaudit.admin;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.vincent.rsf.framework.common.Cools;
import com.vincent.rsf.framework.common.R;
import com.vincent.rsf.httpaudit.entity.HttpAuditRule;
import com.vincent.rsf.httpaudit.service.HttpAuditRuleService;
import com.vincent.rsf.server.common.domain.BaseParam;
import com.vincent.rsf.server.common.domain.PageParam;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import com.vincent.rsf.httpaudit.web.util.HttpAuditAdminQueryHelper;
import org.apache.commons.lang3.StringUtils;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import java.util.Arrays;
import java.util.Date;
@@ -21,27 +22,41 @@
import java.util.Set;
@RestController
@ConditionalOnProperty(prefix = "http-audit", name = "enabled", havingValue = "true", matchIfMissing = true)
public class HttpAuditRuleController extends BaseController {
public class HttpAuditRuleAdminController {
    private static final Set<String> RULE_TYPES = new HashSet<>(Arrays.asList(
            HttpAuditRule.TYPE_URI, HttpAuditRule.TYPE_IP, HttpAuditRule.TYPE_REQUEST_BODY));
    private static final Set<String> MATCH_MODES = new HashSet<>(Arrays.asList(
            HttpAuditRule.MODE_EQUAL, HttpAuditRule.MODE_PREFIX, HttpAuditRule.MODE_CONTAINS, HttpAuditRule.MODE_REGEX));
    @Autowired
    private HttpAuditRuleService httpAuditRuleService;
    private final HttpAuditRuleService httpAuditRuleService;
    public HttpAuditRuleAdminController(HttpAuditRuleService httpAuditRuleService) {
        this.httpAuditRuleService = httpAuditRuleService;
    }
    @PreAuthorize("hasAuthority('system:httpAuditRule:list')")
    @PostMapping("/httpAuditRule/page")
    public R page(@RequestBody Map<String, Object> map) {
        BaseParam baseParam = buildParam(map, BaseParam.class);
        PageParam<HttpAuditRule, BaseParam> pageParam = new PageParam<>(baseParam, HttpAuditRule.class);
        QueryWrapper<HttpAuditRule> wrapper = pageParam.buildWrapper(true, qw -> {
            qw.orderByAsc("sort_order").orderByAsc("id");
        }, "create_time");
        Page<HttpAuditRule> page = httpAuditRuleService.page(pageParam, wrapper);
        return R.ok().add(page);
    public R page(@RequestBody Map<String, Object> body) {
        Map<String, Object> map = HttpAuditAdminQueryHelper.normalizeBody(body);
        Page<HttpAuditRule> page = HttpAuditAdminQueryHelper.extractPage(map);
        String orderBy = HttpAuditAdminQueryHelper.extractOrderBy(map);
        String condition = HttpAuditAdminQueryHelper.extractCondition(map);
        QueryWrapper<HttpAuditRule> qw = new QueryWrapper<>();
        if (!Cools.isEmpty(map.get("ruleType"))) {
            qw.eq("rule_type", map.get("ruleType"));
        }
        if (!Cools.isEmpty(map.get("enabled"))) {
            qw.eq("enabled", map.get("enabled"));
        }
        if (!Cools.isEmpty(map.get("direction"))) {
            qw.eq("direction", map.get("direction"));
        }
        if (StringUtils.isNotBlank(condition)) {
            qw.and(w -> w.like("pattern", condition).or().like("remark", condition));
        }
        HttpAuditAdminQueryHelper.applySafeOrder(qw, orderBy, "ORDER BY sort_order ASC, id ASC");
        return R.ok().add(httpAuditRuleService.page(page, qw));
    }
    @PreAuthorize("hasAuthority('system:httpAuditRule:list')")
rsf-http-audit/src/main/java/com/vincent/rsf/httpaudit/admin/HttpAuditSysConfigAdminController.java
New file
@@ -0,0 +1,153 @@
package com.vincent.rsf.httpaudit.admin;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.vincent.rsf.framework.common.Cools;
import com.vincent.rsf.framework.common.R;
import com.vincent.rsf.httpaudit.entity.HttpAuditSysConfig;
import com.vincent.rsf.httpaudit.service.HttpAuditDbConfigService;
import com.vincent.rsf.httpaudit.service.HttpAuditSysConfigService;
import com.vincent.rsf.httpaudit.web.util.HttpAuditAdminQueryHelper;
import org.apache.commons.lang3.StringUtils;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import java.util.Arrays;
import java.util.Date;
import java.util.Map;
@RestController
public class HttpAuditSysConfigAdminController {
    private final HttpAuditSysConfigService httpAuditSysConfigService;
    private final HttpAuditDbConfigService httpAuditDbConfigService;
    public HttpAuditSysConfigAdminController(HttpAuditSysConfigService httpAuditSysConfigService,
                                            HttpAuditDbConfigService httpAuditDbConfigService) {
        this.httpAuditSysConfigService = httpAuditSysConfigService;
        this.httpAuditDbConfigService = httpAuditDbConfigService;
    }
    @PreAuthorize("hasAuthority('system:httpAuditSysConfig:list')")
    @PostMapping("/httpAuditSysConfig/page")
    public R page(@RequestBody Map<String, Object> body) {
        Map<String, Object> map = HttpAuditAdminQueryHelper.normalizeBody(body);
        Page<HttpAuditSysConfig> page = HttpAuditAdminQueryHelper.extractPage(map);
        String orderBy = HttpAuditAdminQueryHelper.extractOrderBy(map);
        String condition = HttpAuditAdminQueryHelper.extractCondition(map);
        QueryWrapper<HttpAuditSysConfig> qw = new QueryWrapper<>();
        if (!Cools.isEmpty(map.get("configKey"))) {
            qw.like("config_key", map.get("configKey"));
        }
        if (!Cools.isEmpty(map.get("enabled"))) {
            qw.eq("enabled", map.get("enabled"));
        }
        if (StringUtils.isNotBlank(condition)) {
            qw.and(w -> w.like("config_key", condition)
                    .or().like("config_val", condition)
                    .or().like("remark", condition));
        }
        HttpAuditAdminQueryHelper.applySafeOrder(qw, orderBy, "ORDER BY sort_order ASC, id ASC");
        return R.ok().add(httpAuditSysConfigService.page(page, qw));
    }
    @PreAuthorize("hasAuthority('system:httpAuditSysConfig:list')")
    @GetMapping("/httpAuditSysConfig/{id}")
    public R get(@PathVariable Long id) {
        return R.ok().add(httpAuditSysConfigService.getById(id));
    }
    @PreAuthorize("hasAuthority('system:httpAuditSysConfig:save')")
    @PostMapping("/httpAuditSysConfig/save")
    public R save(@RequestBody HttpAuditSysConfig row) {
        R err = validate(row);
        if (err != null) {
            return err;
        }
        if (existsKey(row.getConfigKey(), null)) {
            return R.error("configKey exists");
        }
        Date now = new Date();
        if (row.getEnabled() == null) {
            row.setEnabled(1);
        }
        if (row.getSortOrder() == null) {
            row.setSortOrder(0);
        }
        row.setCreateTime(now);
        row.setUpdateTime(now);
        if (httpAuditSysConfigService.save(row)) {
            httpAuditDbConfigService.refresh();
            return R.ok("Save Success").add(row);
        }
        return R.error("Save Fail");
    }
    @PreAuthorize("hasAuthority('system:httpAuditSysConfig:update')")
    @PostMapping("/httpAuditSysConfig/update")
    public R update(@RequestBody HttpAuditSysConfig row) {
        if (row.getId() == null) {
            return R.error("id required");
        }
        HttpAuditSysConfig old = httpAuditSysConfigService.getById(row.getId());
        if (old == null) {
            return R.error("not found");
        }
        R err = validate(row);
        if (err != null) {
            return err;
        }
        if (!old.getConfigKey().equals(row.getConfigKey()) && existsKey(row.getConfigKey(), row.getId())) {
            return R.error("configKey exists");
        }
        if (row.getEnabled() == null) {
            row.setEnabled(1);
        }
        if (row.getSortOrder() == null) {
            row.setSortOrder(0);
        }
        row.setUpdateTime(new Date());
        if (httpAuditSysConfigService.updateById(row)) {
            httpAuditDbConfigService.refresh();
            return R.ok("Update Success").add(row);
        }
        return R.error("Update Fail");
    }
    @PreAuthorize("hasAuthority('system:httpAuditSysConfig:remove')")
    @PostMapping("/httpAuditSysConfig/remove/{ids}")
    public R remove(@PathVariable Long[] ids) {
        if (httpAuditSysConfigService.removeByIds(Arrays.asList(ids))) {
            httpAuditDbConfigService.refresh();
            return R.ok("Remove Success");
        }
        return R.error("Remove Fail");
    }
    private boolean existsKey(String key, Long excludeId) {
        if (StringUtils.isBlank(key)) {
            return false;
        }
        LambdaQueryWrapper<HttpAuditSysConfig> q = new LambdaQueryWrapper<HttpAuditSysConfig>()
                .eq(HttpAuditSysConfig::getConfigKey, key.trim());
        if (excludeId != null) {
            q.ne(HttpAuditSysConfig::getId, excludeId);
        }
        return httpAuditSysConfigService.count(q) > 0;
    }
    private static R validate(HttpAuditSysConfig row) {
        if (row == null) {
            return R.error("body required");
        }
        if (StringUtils.isBlank(row.getConfigKey())) {
            return R.error("configKey required");
        }
        return null;
    }
}
rsf-http-audit/src/main/java/com/vincent/rsf/httpaudit/config/HttpAuditAdminApiAutoConfiguration.java
New file
@@ -0,0 +1,17 @@
package com.vincent.rsf.httpaudit.config;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
/**
 * 管理端 CRUD(需 classpath 含方法级安全;无 Spring Security 时不注册,避免开放无鉴权接口)
 */
@Configuration
@ConditionalOnClass(EnableGlobalMethodSecurity.class)
@ConditionalOnProperty(prefix = "http-audit", name = "admin-api-enabled", havingValue = "true", matchIfMissing = true)
@ComponentScan(basePackages = "com.vincent.rsf.httpaudit.admin")
public class HttpAuditAdminApiAutoConfiguration {
}
rsf-http-audit/src/main/java/com/vincent/rsf/httpaudit/config/HttpAuditAutoConfiguration.java
@@ -8,8 +8,12 @@
import com.vincent.rsf.httpaudit.service.HttpAuditCleanupService;
import com.vincent.rsf.httpaudit.service.HttpAuditDbConfigService;
import com.vincent.rsf.httpaudit.service.HttpAuditOutboundRecorder;
import com.vincent.rsf.httpaudit.service.HttpAuditLogCrudService;
import com.vincent.rsf.httpaudit.service.HttpAuditLogCrudServiceImpl;
import com.vincent.rsf.httpaudit.service.HttpAuditRuleService;
import com.vincent.rsf.httpaudit.service.HttpAuditRuleServiceImpl;
import com.vincent.rsf.httpaudit.service.HttpAuditSysConfigService;
import com.vincent.rsf.httpaudit.service.HttpAuditSysConfigServiceImpl;
import com.vincent.rsf.httpaudit.web.HttpAuditFilter;
import com.vincent.rsf.httpaudit.web.OutboundHttpAuditInterceptor;
import org.mybatis.spring.annotation.MapperScan;
@@ -18,6 +22,7 @@
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.core.Ordered;
import org.springframework.core.env.Environment;
import org.springframework.scheduling.annotation.EnableAsync;
@@ -35,9 +40,20 @@
@EnableConfigurationProperties(HttpAuditProperties.class)
@ConditionalOnProperty(prefix = "http-audit", name = "enabled", havingValue = "true", matchIfMissing = true)
@MapperScan("com.vincent.rsf.httpaudit.mapper")
@Import({HttpAuditAdminApiAutoConfiguration.class, HttpAuditOpenUiAutoConfiguration.class})
public class HttpAuditAutoConfiguration {
    @Bean
    public HttpAuditSysConfigService httpAuditSysConfigService(HttpAuditConfigMapper httpAuditConfigMapper) {
        return new HttpAuditSysConfigServiceImpl(httpAuditConfigMapper);
    }
    @Bean
    public HttpAuditLogCrudService httpAuditLogCrudService(HttpAuditLogMapper httpAuditLogMapper) {
        return new HttpAuditLogCrudServiceImpl(httpAuditLogMapper);
    }
    @Bean
    public HttpAuditRuleService httpAuditRuleService(HttpAuditRuleMapper mapper, HttpAuditProperties props) {
        return new HttpAuditRuleServiceImpl(mapper, props);
    }
rsf-http-audit/src/main/java/com/vincent/rsf/httpaudit/config/HttpAuditOpenUiAutoConfiguration.java
New file
@@ -0,0 +1,14 @@
package com.vincent.rsf.httpaudit.config;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
/**
 * 简易日志查询页与开放查询接口(默认开启;simple-ui-enabled=false 关闭)
 */
@Configuration
@ConditionalOnProperty(prefix = "http-audit", name = "simple-ui-enabled", havingValue = "true", matchIfMissing = true)
@ComponentScan(basePackages = "com.vincent.rsf.httpaudit.open")
public class HttpAuditOpenUiAutoConfiguration {
}
rsf-http-audit/src/main/java/com/vincent/rsf/httpaudit/entity/HttpAuditSysConfig.java
New file
@@ -0,0 +1,42 @@
package com.vincent.rsf.httpaudit.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.experimental.Accessors;
import java.io.Serializable;
import java.util.Date;
/**
 * sys_http_audit_config 行
 */
@Data
@Accessors(chain = true)
@TableName("sys_http_audit_config")
public class HttpAuditSysConfig implements Serializable {
    private static final long serialVersionUID = 1L;
    @TableId(type = IdType.AUTO)
    private Long id;
    private String configKey;
    private String configVal;
    private Integer enabled;
    private Integer sortOrder;
    private String remark;
    private Date createTime;
    private Date updateTime;
    @TableLogic
    private Integer deleted;
}
rsf-http-audit/src/main/java/com/vincent/rsf/httpaudit/mapper/HttpAuditConfigMapper.java
@@ -1,5 +1,7 @@
package com.vincent.rsf.httpaudit.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.vincent.rsf.httpaudit.entity.HttpAuditSysConfig;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
@@ -7,9 +9,8 @@
import java.util.Map;
@Mapper
public interface HttpAuditConfigMapper {
public interface HttpAuditConfigMapper extends BaseMapper<HttpAuditSysConfig> {
    @Select("SELECT config_key, config_val FROM sys_http_audit_config WHERE deleted = 0 AND enabled = 1")
    List<Map<String, Object>> listEnabledConfig();
}
rsf-http-audit/src/main/java/com/vincent/rsf/httpaudit/open/HttpAuditOpenLogController.java
New file
@@ -0,0 +1,141 @@
package com.vincent.rsf.httpaudit.open;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.vincent.rsf.framework.common.Cools;
import com.vincent.rsf.framework.common.R;
import com.vincent.rsf.httpaudit.entity.HttpAuditLog;
import com.vincent.rsf.httpaudit.props.HttpAuditProperties;
import com.vincent.rsf.httpaudit.service.HttpAuditLogCrudService;
import com.vincent.rsf.httpaudit.web.util.HttpAuditAdminQueryHelper;
import org.apache.commons.lang3.StringUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.Map;
/**
 * 简易查询(simple-ui-token 非空时校验请求头)
 */
@RestController
public class HttpAuditOpenLogController {
    public static final String TOKEN_HEADER = "X-Http-Audit-Ui-Token";
    private final HttpAuditLogCrudService httpAuditLogCrudService;
    private final HttpAuditProperties props;
    public HttpAuditOpenLogController(HttpAuditLogCrudService httpAuditLogCrudService, HttpAuditProperties props) {
        this.httpAuditLogCrudService = httpAuditLogCrudService;
        this.props = props;
    }
    /** 是否要求 Token;不校验本接口,供静态页决定是否展示 Token 输入区 */
    @GetMapping("/http-audit/open/ui-meta")
    public R uiMeta() {
        Map<String, Object> data = new HashMap<>();
        data.put("tokenRequired", StringUtils.isNotBlank(props.getSimpleUiToken()));
        return R.ok().add(data);
    }
    @PostMapping("/http-audit/open/log/page")
    public R pagePost(@RequestHeader(value = TOKEN_HEADER, required = false) String token,
                      @RequestBody(required = false) Map<String, Object> body) {
        return doPage(token, HttpAuditAdminQueryHelper.normalizeBody(body != null ? body : Map.of()));
    }
    @GetMapping("/http-audit/open/log/page")
    public R pageGet(@RequestHeader(value = TOKEN_HEADER, required = false) String token,
                     @RequestParam(required = false) Integer current,
                     @RequestParam(required = false) Integer pageSize,
                     @RequestParam(required = false) String uri,
                     @RequestParam(required = false) String clientIp,
                     @RequestParam(required = false) String condition,
                     @RequestParam(required = false) String orderBy,
                     @RequestParam(required = false) String timeStart,
                     @RequestParam(required = false) String timeEnd,
                     @RequestParam(required = false) String requestContains,
                     @RequestParam(required = false) String responseContains) {
        Map<String, Object> map = new HashMap<>();
        if (current != null) {
            map.put("current", current);
        }
        if (pageSize != null) {
            map.put("pageSize", pageSize);
        }
        if (uri != null) {
            map.put("uri", uri);
        }
        if (clientIp != null) {
            map.put("clientIp", clientIp);
        }
        if (condition != null) {
            map.put("condition", condition);
        }
        if (orderBy != null) {
            map.put("orderBy", orderBy);
        }
        if (timeStart != null) {
            map.put("timeStart", timeStart);
        }
        if (timeEnd != null) {
            map.put("timeEnd", timeEnd);
        }
        if (requestContains != null) {
            map.put("requestContains", requestContains);
        }
        if (responseContains != null) {
            map.put("responseContains", responseContains);
        }
        return doPage(token, map);
    }
    private R doPage(String token, Map<String, Object> map) {
        if (!tokenMatches(token)) {
            return R.error("unauthorized");
        }
        Object timeStart = map.remove("timeStart");
        Object timeEnd = map.remove("timeEnd");
        Page<HttpAuditLog> page = HttpAuditAdminQueryHelper.extractPage(map);
        String orderBy = HttpAuditAdminQueryHelper.extractOrderBy(map);
        String condition = HttpAuditAdminQueryHelper.extractCondition(map);
        QueryWrapper<HttpAuditLog> qw = new QueryWrapper<>();
        HttpAuditAdminQueryHelper.applyCreateTimeRange(qw, timeStart, timeEnd);
        if (!Cools.isEmpty(map.get("uri"))) {
            qw.like("uri", map.get("uri"));
        }
        if (!Cools.isEmpty(map.get("clientIp"))) {
            qw.eq("client_ip", map.get("clientIp"));
        }
        if (StringUtils.isNotBlank(condition)) {
            qw.and(w -> w.like("uri", condition)
                    .or().like("service_name", condition)
                    .or().like("method", condition)
                    .or().like("client_ip", condition)
                    .or().like("function_desc", condition));
        }
        if (!Cools.isEmpty(map.get("requestContains"))) {
            String v = String.valueOf(map.get("requestContains")).trim();
            qw.and(w -> w.like("query_string", v).or().like("request_body", v));
        }
        if (!Cools.isEmpty(map.get("responseContains"))) {
            qw.like("response_body", map.get("responseContains"));
        }
        HttpAuditAdminQueryHelper.applySafeOrder(qw, orderBy, "ORDER BY create_time DESC");
        return R.ok().add(httpAuditLogCrudService.page(page, qw));
    }
    private boolean tokenMatches(String token) {
        String expected = props.getSimpleUiToken();
        if (StringUtils.isBlank(expected)) {
            return true;
        }
        return expected.equals(token);
    }
}
rsf-http-audit/src/main/java/com/vincent/rsf/httpaudit/props/HttpAuditProperties.java
@@ -17,6 +17,15 @@
    private boolean enabled = true;
    /** 是否注册 /httpAuditRule、/httpAuditLog、/httpAuditSysConfig 等管理接口 */
    private boolean adminApiEnabled = true;
    /** 是否提供静态查询页与 /http-audit/open/log/page(默认 true;可 simple-ui-enabled=false 关闭) */
    private boolean simpleUiEnabled = true;
    /** 非空时要求请求头 X-Http-Audit-Ui-Token 与本值一致;留空则不校验(公网建议配置) */
    private String simpleUiToken = "";
    /**
     * true:入站/出站是否落库由 {@code sys_http_audit_rule} 决定(含 record_all=1 全量、方向 IN/OUT/BOTH、截断长度);false:排除路径外入站与全部出站均记录,截断用本配置 + 规则中「全量」行的 request/response_max_chars(若有)
     */
@@ -59,7 +68,7 @@
    public List<String> getEffectiveExcludePrefixes() {
        List<String> list = excludePathPrefixes == null ? new ArrayList<>() : new ArrayList<>(excludePathPrefixes);
        if (!isExcludeAuditSelfPaths()) {
            list.removeIf(p -> "/httpAuditLog".equals(p) || "/httpAuditRule".equals(p));
            list.removeIf(p -> "/httpAuditLog".equals(p) || "/httpAuditRule".equals(p) || "/httpAuditSysConfig".equals(p));
        }
        return list;
    }
@@ -120,6 +129,8 @@
        list.add("/static/");
        list.add("/httpAuditLog");
        list.add("/httpAuditRule");
        list.add("/httpAuditSysConfig");
        list.add("/http-audit/");
        return list;
    }
rsf-http-audit/src/main/java/com/vincent/rsf/httpaudit/service/HttpAuditLogCrudService.java
New file
@@ -0,0 +1,7 @@
package com.vincent.rsf.httpaudit.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.vincent.rsf.httpaudit.entity.HttpAuditLog;
public interface HttpAuditLogCrudService extends IService<HttpAuditLog> {
}
rsf-http-audit/src/main/java/com/vincent/rsf/httpaudit/service/HttpAuditLogCrudServiceImpl.java
New file
@@ -0,0 +1,13 @@
package com.vincent.rsf.httpaudit.service;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.vincent.rsf.httpaudit.entity.HttpAuditLog;
import com.vincent.rsf.httpaudit.mapper.HttpAuditLogMapper;
public class HttpAuditLogCrudServiceImpl extends ServiceImpl<HttpAuditLogMapper, HttpAuditLog>
        implements HttpAuditLogCrudService {
    public HttpAuditLogCrudServiceImpl(HttpAuditLogMapper mapper) {
        this.baseMapper = mapper;
    }
}
rsf-http-audit/src/main/java/com/vincent/rsf/httpaudit/service/HttpAuditOutboundRecorder.java
@@ -41,7 +41,11 @@
            String reqStored = HttpAuditSupport.storeWithCharLimit(reqText, reqMax);
            String resStored = HttpAuditSupport.storeWithCharLimit(resText, resMax);
            int truncated = HttpAuditSupport.overCharLimit(resText, resMax) ? 1 : 0;
            int ok = (ex == null && httpStatus != null && httpStatus >= 200 && httpStatus < 400) ? 1 : 0;
            Integer statusToStore = httpStatus;
            if (statusToStore == null && ex != null) {
                statusToStore = HttpAuditSupport.inferHttpStatusFromThrowable(ex);
            }
            int ok = (ex == null && statusToStore != null && statusToStore >= 200 && statusToStore < 400) ? 1 : 0;
            String errMsg = null;
            if (ex != null) {
                String s = ex.toString();
@@ -60,7 +64,7 @@
                    .setRequestBody(reqStored)
                    .setResponseBody(resStored)
                    .setResponseTruncated(truncated)
                    .setHttpStatus(httpStatus)
                    .setHttpStatus(statusToStore)
                    .setOkFlag(ok)
                    .setSpendMs((int) Math.min(Integer.MAX_VALUE, System.currentTimeMillis() - startTimeMs))
                    .setClientIp(null)
rsf-http-audit/src/main/java/com/vincent/rsf/httpaudit/service/HttpAuditSysConfigService.java
New file
@@ -0,0 +1,7 @@
package com.vincent.rsf.httpaudit.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.vincent.rsf.httpaudit.entity.HttpAuditSysConfig;
public interface HttpAuditSysConfigService extends IService<HttpAuditSysConfig> {
}
rsf-http-audit/src/main/java/com/vincent/rsf/httpaudit/service/HttpAuditSysConfigServiceImpl.java
New file
@@ -0,0 +1,13 @@
package com.vincent.rsf.httpaudit.service;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.vincent.rsf.httpaudit.entity.HttpAuditSysConfig;
import com.vincent.rsf.httpaudit.mapper.HttpAuditConfigMapper;
public class HttpAuditSysConfigServiceImpl extends ServiceImpl<HttpAuditConfigMapper, HttpAuditSysConfig>
        implements HttpAuditSysConfigService {
    public HttpAuditSysConfigServiceImpl(HttpAuditConfigMapper mapper) {
        this.baseMapper = mapper;
    }
}
rsf-http-audit/src/main/java/com/vincent/rsf/httpaudit/support/HttpAuditSupport.java
@@ -2,9 +2,14 @@
import com.vincent.rsf.httpaudit.props.HttpAuditProperties;
import org.springframework.web.context.request.async.AsyncRequestTimeoutException;
import javax.servlet.http.HttpServletRequest;
import java.net.ConnectException;
import java.net.SocketTimeoutException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.TimeoutException;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
@@ -144,4 +149,39 @@
        }
        return uri.substring(0, maxLen - 3) + "...";
    }
    /** 无 HTTP 状态码时由异常推断落库状态;超时类为 504 */
    public static Integer inferHttpStatusFromThrowable(Throwable ex) {
        if (ex == null) {
            return null;
        }
        for (Throwable t = ex; t != null; t = t.getCause()) {
            if (t instanceof SocketTimeoutException || t instanceof TimeoutException) {
                return 504;
            }
            if (t instanceof AsyncRequestTimeoutException) {
                return 504;
            }
            if (t instanceof ConnectException) {
                String m = t.getMessage();
                if (m != null && m.toLowerCase().contains("timed out")) {
                    return 504;
                }
            }
            if ("feign.RetryableException".equals(t.getClass().getName())) {
                Throwable c = t.getCause();
                if (c instanceof SocketTimeoutException) {
                    return 504;
                }
                String m = t.getMessage();
                if (m != null) {
                    String low = m.toLowerCase();
                    if (low.contains("timed out") || low.contains("timeout")) {
                        return 504;
                    }
                }
            }
        }
        return null;
    }
}
rsf-http-audit/src/main/java/com/vincent/rsf/httpaudit/web/HttpAuditFilter.java
@@ -109,6 +109,10 @@
        }
        int status = res.getStatus();
        Integer timeoutStatus = chainError == null ? null : HttpAuditSupport.inferHttpStatusFromThrowable(chainError);
        if (timeoutStatus != null) {
            status = timeoutStatus;
        }
        int ok = (chainError == null && status >= 200 && status < 400) ? 1 : 0;
        String errMsg = null;
        if (chainError != null) {
rsf-http-audit/src/main/java/com/vincent/rsf/httpaudit/web/OutboundHttpAuditInterceptor.java
@@ -2,6 +2,7 @@
import com.vincent.rsf.httpaudit.model.HttpAuditDecision;
import com.vincent.rsf.httpaudit.service.HttpAuditOutboundRecorder;
import com.vincent.rsf.httpaudit.support.HttpAuditSupport;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpRequest;
@@ -45,7 +46,12 @@
        try {
            respBytes = StreamUtils.copyToByteArray(raw.getBody());
        } catch (IOException e) {
            outboundRecorder.saveOutbound(FN_REST, url, method, reqText, dec, raw.getRawStatusCode(), null, t0, e);
            int code = raw.getRawStatusCode();
            Integer inferred = HttpAuditSupport.inferHttpStatusFromThrowable(e);
            if (inferred != null) {
                code = inferred;
            }
            outboundRecorder.saveOutbound(FN_REST, url, method, reqText, dec, code, null, t0, e);
            throw e;
        }
        if (!dec.isAudit()) {
rsf-http-audit/src/main/java/com/vincent/rsf/httpaudit/web/util/HttpAuditAdminQueryHelper.java
New file
@@ -0,0 +1,83 @@
package com.vincent.rsf.httpaudit.web.util;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.vincent.rsf.framework.common.Cools;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Pattern;
/**
 * 管理端分页请求解析(与 rsf-admin MyDataProvider 入参一致)
 */
public final class HttpAuditAdminQueryHelper {
    private static final Pattern SAFE_ORDER = Pattern.compile(
            "^[a-zA-Z0-9_]+\\s+(asc|ASC|desc|DESC)(\\s*,\\s*[a-zA-Z0-9_]+\\s+(asc|ASC|desc|DESC))*$");
    private HttpAuditAdminQueryHelper() {
    }
    public static Map<String, Object> normalizeBody(Map<String, Object> body) {
        Map<String, Object> m = new HashMap<>(body != null ? body : Map.of());
        Object meta = m.get("meta");
        if (meta instanceof Map) {
            for (Map.Entry<?, ?> e : ((Map<?, ?>) meta).entrySet()) {
                m.put(String.valueOf(e.getKey()), e.getValue());
            }
            m.remove("meta");
        }
        return m;
    }
    public static <T> Page<T> extractPage(Map<String, Object> m) {
        long cur = 1L;
        long size = 10L;
        if (m.get("current") != null) {
            cur = Long.parseLong(String.valueOf(m.get("current")));
        }
        if (m.get("pageSize") != null) {
            size = Long.parseLong(String.valueOf(m.get("pageSize")));
        }
        m.remove("current");
        m.remove("pageSize");
        return new Page<>(cur, size);
    }
    public static String extractOrderBy(Map<String, Object> m) {
        Object ob = m.remove("orderBy");
        return ob == null ? null : String.valueOf(ob).trim();
    }
    public static String extractCondition(Map<String, Object> m) {
        Object c = m.remove("condition");
        if (Cools.isEmpty(c)) {
            return null;
        }
        return String.valueOf(c).trim();
    }
    public static void applySafeOrder(QueryWrapper<?> qw, String orderBy, String defaultOrderBySql) {
        if (orderBy != null && !orderBy.isEmpty() && SAFE_ORDER.matcher(orderBy).matches()) {
            qw.last("ORDER BY " + orderBy);
        } else if (defaultOrderBySql != null) {
            qw.last(defaultOrderBySql);
        }
    }
    /** create_time 区间;入参可为日期或含 T 的日期时间字符串 */
    public static void applyCreateTimeRange(QueryWrapper<?> qw, Object timeStart, Object timeEnd) {
        if (!Cools.isEmpty(timeStart)) {
            qw.ge("create_time", normalizeDateTimeParam(timeStart));
        }
        if (!Cools.isEmpty(timeEnd)) {
            qw.le("create_time", normalizeDateTimeParam(timeEnd));
        }
    }
    private static Object normalizeDateTimeParam(Object raw) {
        String s = String.valueOf(raw).trim();
        return s.indexOf('T') >= 0 ? s.replace('T', ' ') : raw;
    }
}
rsf-http-audit/src/main/resources/static/http-audit/log-query.html
New file
@@ -0,0 +1,290 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8"/>
  <meta name="viewport" content="width=device-width, initial-scale=1"/>
  <title>HTTP 审计日志</title>
  <style>
    body { font-family: system-ui, sans-serif; margin: 16px; background: #f5f5f5; }
    h1 { font-size: 1.1rem; }
    .row { margin: 8px 0; display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
    label { font-size: 0.85rem; color: #444; }
    input, button { padding: 6px 10px; font-size: 0.9rem; }
    table { border-collapse: collapse; width: 100%; background: #fff; margin-top: 12px; font-size: 0.8rem; }
    th, td { border: 1px solid #ddd; padding: 6px 8px; vertical-align: top; }
    th { background: #eee; text-align: left; }
    tr:nth-child(even) { background: #fafafa; }
    .muted { color: #888; font-size: 0.8rem; }
    .err { color: #c62828; margin-top: 8px; }
    pre { white-space: pre-wrap; word-break: break-all; max-height: 100px; overflow: auto; margin: 0; }
    .col-req-resp pre { max-height: 80px; font-size: 0.75rem; }
    .pager { display: none; flex-wrap: wrap; gap: 8px; align-items: center; margin-top: 10px; padding: 8px; background: #fff; border: 1px solid #ddd; font-size: 0.85rem; }
    .pager.visible { display: flex; }
    .pager button:disabled { opacity: 0.45; cursor: not-allowed; }
    .pager input[type="number"] { width: 3.5rem; }
  </style>
</head>
<body>
<h1>HTTP 审计日志</h1>
<p id="introLine" class="muted" style="display:none"></p>
<div id="tokenRow" class="row" style="display:none">
  <label>Token <input type="password" id="token" size="36" autocomplete="off" placeholder="与 http-audit.simple-ui-token 一致"/></label>
  <button type="button" id="saveTok">记住 Token(本地)</button>
</div>
<div class="row">
  <label>每页 <input type="number" id="pageSize" value="20" min="1" max="500" style="width:4rem"/></label>
  <button type="button" id="savePageSize">记住每页条数</button>
  <label>URI 含 <input type="text" id="uri" size="28"/></label>
  <button type="button" id="btn" disabled>查询</button>
</div>
<div class="row">
  <label>开始时间 <input type="datetime-local" id="timeStart" step="1"/></label>
  <label>结束时间 <input type="datetime-local" id="timeEnd" step="1"/></label>
</div>
<div class="row">
  <label>请求参数含 <input type="text" id="requestContains" size="32"/></label>
  <label>返回参数含 <input type="text" id="responseContains" size="32"/></label>
</div>
<div id="err" class="err"></div>
<div id="pager" class="pager" aria-live="polite">
  <span id="pagerInfo"></span>
  <button type="button" id="pgFirst">首页</button>
  <button type="button" id="pgPrev">上一页</button>
  <button type="button" id="pgNext">下一页</button>
  <button type="button" id="pgLast">末页</button>
  <label>跳转到 <input type="number" id="pgGoto" min="1" step="1"/> 页 <button type="button" id="pgGotoBtn">跳转</button></label>
</div>
<table id="tbl" style="display:none">
  <thead>
  <tr>
    <th>id</th>
    <th>时间</th>
    <th>方向</th>
    <th>URI</th>
    <th>方法</th>
    <th>状态</th>
    <th>耗时(s)</th>
    <th>请求参数</th>
    <th>返回参数</th>
    <th>说明</th>
  </tr>
  </thead>
  <tbody></tbody>
</table>
<script>
(function () {
  const TOKEN_KEY = 'httpAuditUiToken';
  const PAGE_SIZE_KEY = 'httpAuditLogPageSize';
  const tokenEl = document.getElementById('token');
  const pageSizeEl = document.getElementById('pageSize');
  const btnQuery = document.getElementById('btn');
  var tokenRequired = false;
  var savedPs = localStorage.getItem(PAGE_SIZE_KEY);
  if (savedPs) {
    var n = parseInt(savedPs, 10);
    if (n >= 1 && n <= 500) pageSizeEl.value = String(n);
  }
  var lastPageMeta = { current: 1, pages: 1, total: 0, size: 20 };
  const apiBase = (function () {
    const p = location.pathname;
    const m = p.match(/^(.+)\/http-audit\//);
    return m ? m[1] : '';
  })();
  const apiUrl = apiBase + '/http-audit/open/log/page';
  const apiMeta = apiBase + '/http-audit/open/ui-meta';
  function applyTokenUi() {
    const intro = document.getElementById('introLine');
    const tokenRow = document.getElementById('tokenRow');
    if (tokenRequired) {
      intro.style.display = 'block';
      intro.textContent = '已启用 Token:须填写正确值后才能查询(请求头 X-Http-Audit-Ui-Token)。';
      tokenRow.style.display = 'flex';
      tokenEl.value = localStorage.getItem(TOKEN_KEY) || '';
    } else {
      intro.style.display = 'none';
      intro.textContent = '';
      tokenRow.style.display = 'none';
      tokenEl.value = '';
    }
  }
  function loadUiMeta() {
    return fetch(apiMeta).then(function (res) {
      return res.json();
    }).then(function (json) {
      if (json.code === 200 && json.data) {
        tokenRequired = !!json.data.tokenRequired;
      }
      applyTokenUi();
    });
  }
  document.getElementById('saveTok').onclick = function () {
    localStorage.setItem(TOKEN_KEY, tokenEl.value.trim());
    alert('已保存到浏览器本地');
  };
  document.getElementById('savePageSize').onclick = function () {
    var ps = parseInt(pageSizeEl.value, 10) || 20;
    if (ps < 1) ps = 1;
    if (ps > 500) ps = 500;
    pageSizeEl.value = String(ps);
    localStorage.setItem(PAGE_SIZE_KEY, String(ps));
    alert('每页条数已记住');
  };
  function buildBody(pageNum) {
    const uri = document.getElementById('uri').value.trim();
    const pageSize = parseInt(pageSizeEl.value, 10) || 20;
    const ts = document.getElementById('timeStart').value;
    const te = document.getElementById('timeEnd').value;
    const reqC = document.getElementById('requestContains').value.trim();
    const resC = document.getElementById('responseContains').value.trim();
    const body = { current: pageNum, pageSize: Math.min(500, Math.max(1, pageSize)) };
    if (uri) body.uri = uri;
    if (ts) body.timeStart = ts;
    if (te) body.timeEnd = te;
    if (reqC) body.requestContains = reqC;
    if (resC) body.responseContains = resC;
    return body;
  }
  function renderRows(records) {
    const tb = document.querySelector('#tbl tbody');
    tb.innerHTML = '';
    records.forEach(function (r) {
      const tr = document.createElement('tr');
      tr.innerHTML =
        '<td>' + (r.id ?? '') + '</td>' +
        '<td>' + (r.createTime ?? '') + '</td>' +
        '<td>' + (r.ioDirection ?? '') + '</td>' +
        '<td><pre>' + escapeHtml(r.uri || '') + '</pre></td>' +
        '<td>' + escapeHtml(r.method || '') + '</td>' +
        '<td>' + (r.httpStatus ?? '') + '</td>' +
        '<td>' + spendMsToSec(r.spendMs) + '</td>' +
        '<td class="col-req-resp"><pre>' + escapeHtml(joinRequestPreview(r)) + '</pre></td>' +
        '<td class="col-req-resp"><pre>' + escapeHtml(r.responseBody || '') + '</pre></td>' +
        '<td>' + escapeHtml(r.functionDesc || '') + '</td>';
      tb.appendChild(tr);
    });
  }
  function updatePager(page) {
    const pager = document.getElementById('pager');
    const info = document.getElementById('pagerInfo');
    const total = page.total != null ? Number(page.total) : 0;
    const size = page.size != null ? Number(page.size) : (parseInt(pageSizeEl.value, 10) || 20);
    var pages = page.pages != null ? Number(page.pages) : (total > 0 ? Math.ceil(total / size) : 1);
    var current = page.current != null ? Number(page.current) : 1;
    if (pages < 1) pages = 1;
    if (current < 1) current = 1;
    if (current > pages) current = pages;
    lastPageMeta = { current: current, pages: pages, total: total, size: size };
    info.textContent = '共 ' + total + ' 条,每页 ' + size + ' 条,第 ' + current + ' / ' + pages + ' 页';
    document.getElementById('pgFirst').disabled = current <= 1;
    document.getElementById('pgPrev').disabled = current <= 1;
    document.getElementById('pgNext').disabled = current >= pages || total === 0;
    document.getElementById('pgLast').disabled = current >= pages || total === 0;
    document.getElementById('pgGoto').max = String(Math.max(1, pages));
    document.getElementById('pgGoto').value = String(current);
    pager.classList.add('visible');
  }
  async function fetchPage(pageNum) {
    const err = document.getElementById('err');
    err.textContent = '';
    const tok = tokenEl.value.trim();
    if (tokenRequired && !tok) {
      err.textContent = '请填写 Token';
      return;
    }
    const body = buildBody(pageNum);
    try {
      const headers = { 'Content-Type': 'application/json' };
      if (tok) headers['X-Http-Audit-Ui-Token'] = tok;
      const res = await fetch(apiUrl, {
        method: 'POST',
        headers: headers,
        body: JSON.stringify(body)
      });
      const json = await res.json();
      if (json.code !== 200) {
        err.textContent = json.msg || '请求失败';
        document.getElementById('pager').classList.remove('visible');
        return;
      }
      const page = json.data;
      const records = page.records || [];
      renderRows(records);
      document.getElementById('tbl').style.display = records.length ? 'table' : 'none';
      updatePager(page);
      if (!records.length) err.textContent = '无数据';
    } catch (e) {
      err.textContent = String(e);
      document.getElementById('pager').classList.remove('visible');
    }
  }
  document.getElementById('btn').onclick = function () {
    fetchPage(1);
  };
  document.getElementById('pgFirst').onclick = function () {
    fetchPage(1);
  };
  document.getElementById('pgPrev').onclick = function () {
    if (lastPageMeta.current > 1) fetchPage(lastPageMeta.current - 1);
  };
  document.getElementById('pgNext').onclick = function () {
    if (lastPageMeta.current < lastPageMeta.pages) fetchPage(lastPageMeta.current + 1);
  };
  document.getElementById('pgLast').onclick = function () {
    fetchPage(lastPageMeta.pages);
  };
  document.getElementById('pgGotoBtn').onclick = function () {
    var p = parseInt(document.getElementById('pgGoto').value, 10);
    if (isNaN(p) || p < 1) p = 1;
    if (p > lastPageMeta.pages) p = lastPageMeta.pages;
    fetchPage(p);
  };
  document.getElementById('pgGoto').addEventListener('keydown', function (e) {
    if (e.key === 'Enter') document.getElementById('pgGotoBtn').click();
  });
  loadUiMeta().then(function () {
    btnQuery.disabled = false;
  }).catch(function () {
    applyTokenUi();
    btnQuery.disabled = false;
    document.getElementById('introLine').style.display = 'block';
    document.getElementById('introLine').textContent = '无法加载页面配置(' + apiMeta + '),请检查网络或刷新。';
  });
  function joinRequestPreview(r) {
    var parts = [];
    if (r.queryString) parts.push(r.queryString);
    if (r.requestBody) parts.push(r.requestBody);
    return parts.join('\n');
  }
  function spendMsToSec(ms) {
    if (ms == null || ms === '') return '';
    var n = Number(ms);
    if (isNaN(n)) return '';
    return String(Number((n / 1000).toFixed(3)));
  }
  function escapeHtml(s) {
    return String(s)
      .replace(/&/g, '&amp;')
      .replace(/</g, '&lt;')
      .replace(/>/g, '&gt;');
  }
})();
</script>
</body>
</html>
rsf-open-api/src/main/java/com/vincent/rsf/openApi/common/datasource/DataSourceContextHolder.java
New file
@@ -0,0 +1,34 @@
package com.vincent.rsf.openApi.common.datasource;
import java.util.ArrayDeque;
import java.util.Deque;
/**
 * 数据源上下文
 */
public final class DataSourceContextHolder {
    private static final ThreadLocal<Deque<String>> CONTEXT = ThreadLocal.withInitial(ArrayDeque::new);
    private DataSourceContextHolder() {
    }
    public static void push(String dataSource) {
        CONTEXT.get().push(dataSource);
    }
    public static String peek() {
        Deque<String> deque = CONTEXT.get();
        return deque.isEmpty() ? null : deque.peek();
    }
    public static void poll() {
        Deque<String> deque = CONTEXT.get();
        if (!deque.isEmpty()) {
            deque.pop();
        }
        if (deque.isEmpty()) {
            CONTEXT.remove();
        }
    }
}
rsf-open-api/src/main/java/com/vincent/rsf/openApi/common/datasource/DataSourceNames.java
New file
@@ -0,0 +1,10 @@
package com.vincent.rsf.openApi.common.datasource;
/**
 * 数据源名称
 */
public interface DataSourceNames {
    String PRIMARY = "primary";
    String JDXAJ_LOG = "jdxaj-log";
}
rsf-open-api/src/main/java/com/vincent/rsf/openApi/common/datasource/HttpAuditDataSourceAspect.java
New file
@@ -0,0 +1,39 @@
package com.vincent.rsf.openApi.common.datasource;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
/**
 * http-audit 数据源切换
 */
@Aspect
@Component
@Order(Ordered.HIGHEST_PRECEDENCE + 10)
public class HttpAuditDataSourceAspect {
    @Value("${http-audit.datasource:primary}")
    private String dataSource;
    @Around("execution(* com.vincent.rsf.httpaudit..*(..))")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        String selected = resolveDataSource();
        DataSourceContextHolder.push(selected);
        try {
            return joinPoint.proceed();
        } finally {
            DataSourceContextHolder.poll();
        }
    }
    private String resolveDataSource() {
        if ("jdxaj-log".equalsIgnoreCase(dataSource)) {
            return DataSourceNames.JDXAJ_LOG;
        }
        return DataSourceNames.PRIMARY;
    }
}
rsf-open-api/src/main/java/com/vincent/rsf/openApi/common/datasource/RoutingDataSource.java
New file
@@ -0,0 +1,15 @@
package com.vincent.rsf.openApi.common.datasource;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
/**
 * 动态路由数据源
 */
public class RoutingDataSource extends AbstractRoutingDataSource {
    @Override
    protected Object determineCurrentLookupKey() {
        String dataSource = DataSourceContextHolder.peek();
        return dataSource == null ? DataSourceNames.PRIMARY : dataSource;
    }
}
rsf-open-api/src/main/java/com/vincent/rsf/openApi/common/datasource/UseDataSource.java
New file
@@ -0,0 +1,14 @@
package com.vincent.rsf.openApi.common.datasource;
import java.lang.annotation.*;
/**
 * 指定数据源
 */
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface UseDataSource {
    String value() default DataSourceNames.PRIMARY;
}
rsf-open-api/src/main/java/com/vincent/rsf/openApi/common/datasource/UseDataSourceAspect.java
New file
@@ -0,0 +1,45 @@
package com.vincent.rsf.openApi.common.datasource;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
/**
 * 注解数据源切换
 */
@Aspect
@Component
@Order(Ordered.HIGHEST_PRECEDENCE + 20)
public class UseDataSourceAspect {
    @Around("@annotation(com.vincent.rsf.openApi.common.datasource.UseDataSource) || @within(com.vincent.rsf.openApi.common.datasource.UseDataSource)")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        UseDataSource useDataSource = resolveAnnotation(joinPoint);
        if (useDataSource == null) {
            return joinPoint.proceed();
        }
        DataSourceContextHolder.push(useDataSource.value());
        try {
            return joinPoint.proceed();
        } finally {
            DataSourceContextHolder.poll();
        }
    }
    private UseDataSource resolveAnnotation(ProceedingJoinPoint joinPoint) {
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        UseDataSource annotation = method.getAnnotation(UseDataSource.class);
        if (annotation != null) {
            return annotation;
        }
        Class<?> targetClass = joinPoint.getTarget().getClass();
        return targetClass.getAnnotation(UseDataSource.class);
    }
}
rsf-open-api/src/main/java/com/vincent/rsf/openApi/config/JdxajLogDataSourceConfig.java
New file
@@ -0,0 +1,23 @@
package com.vincent.rsf.openApi.config;
import com.alibaba.druid.pool.DruidDataSource;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.sql.DataSource;
/**
 * 日志库副数据源
 */
@Configuration
@ConditionalOnProperty(prefix = "spring.datasource.jdxaj-log", name = "url")
public class JdxajLogDataSourceConfig {
    @Bean(name = "jdxajLogDataSource")
    @ConfigurationProperties(prefix = "spring.datasource.jdxaj-log")
    public DataSource jdxajLogDataSource() {
        return new DruidDataSource();
    }
}
rsf-open-api/src/main/java/com/vincent/rsf/openApi/config/PrimaryDataSourceConfig.java
New file
@@ -0,0 +1,59 @@
package com.vincent.rsf.openApi.config;
import com.vincent.rsf.openApi.common.datasource.DataSourceNames;
import com.vincent.rsf.openApi.common.datasource.RoutingDataSource;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.jdbc.core.JdbcTemplate;
import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;
/**
 * 主数据源配置
 */
@Configuration
public class PrimaryDataSourceConfig {
    @Bean(name = "primaryDataSourceProperties")
    @ConfigurationProperties(prefix = "spring.datasource")
    public DataSourceProperties primaryDataSourceProperties() {
        return new DataSourceProperties();
    }
    @Bean(name = "primaryDataSource")
    public DataSource primaryDataSource(@Qualifier("primaryDataSourceProperties") DataSourceProperties properties) {
        return properties.initializeDataSourceBuilder().build();
    }
    @Bean(name = "dataSource")
    @Primary
    public DataSource dataSource(
            @Qualifier("primaryDataSource") DataSource primaryDataSource,
            @Qualifier("jdxajLogDataSource") ObjectProvider<DataSource> jdxajLogDataSourceProvider) {
        RoutingDataSource routingDataSource = new RoutingDataSource();
        Map<Object, Object> map = new HashMap<>();
        map.put(DataSourceNames.PRIMARY, primaryDataSource);
        DataSource jdxajLogDataSource = jdxajLogDataSourceProvider.getIfAvailable();
        if (jdxajLogDataSource != null) {
            map.put(DataSourceNames.JDXAJ_LOG, jdxajLogDataSource);
        }
        routingDataSource.setDefaultTargetDataSource(primaryDataSource);
        routingDataSource.setTargetDataSources(map);
        routingDataSource.afterPropertiesSet();
        return routingDataSource;
    }
    @Bean(name = "jdbcTemplate")
    @Primary
    public JdbcTemplate jdbcTemplate(@Qualifier("dataSource") DataSource dataSource) {
        return new JdbcTemplate(dataSource);
    }
}
rsf-open-api/src/main/resources/application-dev.yml
@@ -47,6 +47,16 @@
        login-username: admin
        login-password: admin
        enabled: true
    jdxaj-log:
      type: com.alibaba.druid.pool.DruidDataSource
      driver-class-name: com.mysql.cj.jdbc.Driver
      username: root
      password: 12345
      url: jdbc:mysql://127.0.0.1:3306/rsf_jdxaj_log?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai
      initial-size: 1
      min-idle: 2
      max-active: 12
      max-wait: 10000
  servlet:
    multipart:
      maxFileSize: 100MB
@@ -105,4 +115,5 @@
http-audit:
  enabled: true
  datasource: primary
  # 审计数据源:primary / jdxaj-log
  datasource: jdxaj-log
rsf-open-api/src/main/resources/application-prod.yml
@@ -47,6 +47,16 @@
        login-username: admin
        login-password: admin
        enabled: true
    jdxaj-log:
      type: com.alibaba.druid.pool.DruidDataSource
      driver-class-name: com.mysql.cj.jdbc.Driver
      username: root
      password: xltys1995
      url: jdbc:mysql://127.0.0.1:3306/rsf_jdxaj_log?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai
      initial-size: 1
      min-idle: 2
      max-active: 12
      max-wait: 10000
  servlet:
    multipart:
      maxFileSize: 100MB
@@ -105,6 +115,5 @@
http-audit:
  enabled: true
  # 审计数据源:primary / dj-cloud-wms
  # 审计数据源:primary / jdxaj-log
  datasource: primary
  # 其余审计参数改为 sys_http_audit_config 表配置
rsf-server/src/main/java/com/vincent/rsf/server/api/feign/AuditingFeignClient.java
@@ -2,6 +2,7 @@
import com.vincent.rsf.httpaudit.model.HttpAuditDecision;
import com.vincent.rsf.httpaudit.service.HttpAuditOutboundRecorder;
import com.vincent.rsf.httpaudit.support.HttpAuditSupport;
import feign.Client;
import feign.Request;
import feign.Response;
@@ -56,7 +57,12 @@
        try {
            respBytes = Util.toByteArray(resp.body().asInputStream());
        } catch (IOException e) {
            outboundRecorder.saveOutbound(FN_FEIGN, url, method, reqText, dec, resp.status(), null, t0, e);
            Integer st = resp.status();
            Integer inferred = HttpAuditSupport.inferHttpStatusFromThrowable(e);
            if (inferred != null) {
                st = inferred;
            }
            outboundRecorder.saveOutbound(FN_FEIGN, url, method, reqText, dec, st, null, t0, e);
            throw e;
        }
        String resText = new String(respBytes, StandardCharsets.UTF_8);
rsf-server/src/main/java/com/vincent/rsf/server/common/config/JdxajLogDataSourceConfig.java
New file
@@ -0,0 +1,23 @@
package com.vincent.rsf.server.common.config;
import com.alibaba.druid.pool.DruidDataSource;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.sql.DataSource;
/**
 * 日志库副数据源
 */
@Configuration
@ConditionalOnProperty(prefix = "spring.datasource.jdxaj-log", name = "url")
public class JdxajLogDataSourceConfig {
    @Bean(name = "jdxajLogDataSource")
    @ConfigurationProperties(prefix = "spring.datasource.jdxaj-log")
    public DataSource jdxajLogDataSource() {
        return new DruidDataSource();
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/common/config/PrimaryDataSourceConfig.java
@@ -36,7 +36,8 @@
    @Primary
    public DataSource dataSource(
            @Qualifier("primaryDataSource") DataSource primaryDataSource,
            @Qualifier("cusItemSyncDataSource") ObjectProvider<DataSource> cusItemSyncDataSourceProvider) {
            @Qualifier("cusItemSyncDataSource") ObjectProvider<DataSource> cusItemSyncDataSourceProvider,
            @Qualifier("jdxajLogDataSource") ObjectProvider<DataSource> jdxajLogDataSourceProvider) {
        RoutingDataSource routingDataSource = new RoutingDataSource();
        Map<Object, Object> map = new HashMap<>();
        map.put(DataSourceNames.PRIMARY, primaryDataSource);
@@ -44,6 +45,10 @@
        if (cusItemSyncDataSource != null) {
            map.put(DataSourceNames.DJ_CLOUD_WMS, cusItemSyncDataSource);
        }
        DataSource jdxajLogDataSource = jdxajLogDataSourceProvider.getIfAvailable();
        if (jdxajLogDataSource != null) {
            map.put(DataSourceNames.JDXAJ_LOG, jdxajLogDataSource);
        }
        routingDataSource.setDefaultTargetDataSource(primaryDataSource);
        routingDataSource.setTargetDataSources(map);
        routingDataSource.afterPropertiesSet();
rsf-server/src/main/java/com/vincent/rsf/server/common/datasource/DataSourceNames.java
@@ -7,5 +7,6 @@
    String PRIMARY = "primary";
    String DJ_CLOUD_WMS = "dj-cloud-wms";
    String JDXAJ_LOG = "jdxaj-log";
}
rsf-server/src/main/java/com/vincent/rsf/server/common/datasource/HttpAuditDataSourceAspect.java
@@ -34,6 +34,9 @@
        if ("dj-cloud-wms".equalsIgnoreCase(dataSource)) {
            return DataSourceNames.DJ_CLOUD_WMS;
        }
        if ("jdxaj-log".equalsIgnoreCase(dataSource)) {
            return DataSourceNames.JDXAJ_LOG;
        }
        return DataSourceNames.PRIMARY;
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/common/security/SecurityConfig.java
@@ -56,7 +56,8 @@
            "/ws/**",
            "/wcs/**",
            "/monitor/**",
            "/mcp/**"
            "/mcp/**",
            "/http-audit/**"
    };
    @Resource
rsf-server/src/main/java/com/vincent/rsf/server/system/controller/HttpAuditLogController.java
File was deleted
rsf-server/src/main/java/com/vincent/rsf/server/system/service/HttpAuditLogService.java
File was deleted
rsf-server/src/main/java/com/vincent/rsf/server/system/service/impl/HttpAuditLogServiceImpl.java
File was deleted
rsf-server/src/main/resources/application-dev.yml
@@ -54,16 +54,16 @@
      min-idle: 2
      max-active: 12
      max-wait: 10000
#    erp:
#      type: com.alibaba.druid.pool.DruidDataSource
#      driver-class-name: com.mysql.cj.jdbc.Driver
#      username: root
#      password: 12345
#      url: jdbc:mysql://127.0.0.1:3306/rsf_jdxaj?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai
#      initial-size: 1
#      min-idle: 2
#      max-active: 12
#      max-wait: 10000
    jdxaj-log:
      type: com.alibaba.druid.pool.DruidDataSource
      driver-class-name: com.mysql.cj.jdbc.Driver
      username: root
      password: 12345
      url: jdbc:mysql://127.0.0.1:3306/rsf_jdxaj_log?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai
      initial-size: 1
      min-idle: 2
      max-active: 12
      max-wait: 10000
  servlet:
    multipart:
      maxFileSize: 100MB
@@ -138,11 +138,11 @@
      sync-cron: "0/3 * * * * ?"
      recover-cron: "0/5 * * * * ?"
# HTTP 接口审计(rsf-http-audit,引入依赖即生效,可 enabled=false 关闭)
# whitelist-only=true:仅 sys_http_audit_rule 命中规则才写审计;无规则时不落库。false:排除路径外全量记录。
# rule-cache-refresh-ms:规则表缓存刷新间隔(毫秒)
# HTTP 接口审计(rsf-http-audit,不引入依赖则无审计;enabled=false 关闭 Filter 与管理接口)
# admin-api-enabled:是否注册 /httpAuditRule、/httpAuditLog、/httpAuditSysConfig
# 简易页默认开启;simple-ui-token 非空则校验请求头(公网建议配置)
http-audit:
  # enabled: true
  enabled: false
  # 审计数据源:primary / dj-cloud-wms
  datasource: primary
  enabled: true
#  enabled: false
  # 审计数据源:primary / dj-cloud-wms / jdxaj-log
  datasource: jdxaj-log
rsf-server/src/main/resources/application-prod.yml
@@ -63,6 +63,16 @@
      min-idle: 2
      max-active: 12
      max-wait: 10000
    jdxaj-log:
      type: com.alibaba.druid.pool.DruidDataSource
      driver-class-name: com.mysql.cj.jdbc.Driver
      username: root
      password: xltys1995
      url: jdbc:mysql://127.0.0.1:3306/rsf_jdxaj_log?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai
      initial-size: 1
      min-idle: 2
      max-active: 12
      max-wait: 10000
  servlet:
    multipart:
      maxFileSize: 100MB
@@ -144,5 +154,5 @@
# rule-cache-refresh-ms:规则表缓存刷新间隔(毫秒)
http-audit:
  enabled: true
  # 审计数据源:primary / dj-cloud-wms
  # 审计数据源:primary / dj-cloud-wms / jdxaj-log
  datasource: primary
version/db/http_audit_sysconfig_menu.sql
New file
@@ -0,0 +1,36 @@
-- HTTP 接口审计(sys_http_audit_config)菜单;执行前请确认 id 398-402 未被占用
SET NAMES utf8mb4;
INSERT INTO `sys_menu` (`id`, `name`, `parent_id`, `parent_name`, `path`, `path_name`, `route`, `component`, `brief`, `code`, `type`, `authority`, `icon`, `sort`, `meta`, `tenant_id`, `status`, `deleted`, `create_time`, `create_by`, `update_time`, `update_by`, `memo`)
SELECT 398, 'menu.httpAuditSysConfig', 1, 'menu.system', '1,398', 'menu.httpAuditSysConfig', '/system/httpAuditSysConfig', 'httpAuditSysConfig', NULL, NULL, 0, NULL, 'Tune', 5, NULL, 1, 1, 0, NULL, NULL, NULL, NULL, NULL
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `sys_menu` WHERE `id` = 398);
INSERT INTO `sys_menu` (`id`, `name`, `parent_id`, `parent_name`, `path`, `path_name`, `route`, `component`, `brief`, `code`, `type`, `authority`, `icon`, `sort`, `meta`, `tenant_id`, `status`, `deleted`, `create_time`, `create_by`, `update_time`, `update_by`, `memo`)
SELECT 399, 'Query HttpAuditSysConfig', 398, '', '1,398,399', NULL, NULL, NULL, NULL, NULL, 1, 'system:httpAuditSysConfig:list', NULL, 0, NULL, 1, 1, 0, NULL, NULL, NULL, NULL, NULL
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `sys_menu` WHERE `id` = 399);
INSERT INTO `sys_menu` (`id`, `name`, `parent_id`, `parent_name`, `path`, `path_name`, `route`, `component`, `brief`, `code`, `type`, `authority`, `icon`, `sort`, `meta`, `tenant_id`, `status`, `deleted`, `create_time`, `create_by`, `update_time`, `update_by`, `memo`)
SELECT 400, 'Save HttpAuditSysConfig', 398, '', '1,398,400', NULL, NULL, NULL, NULL, NULL, 1, 'system:httpAuditSysConfig:save', NULL, 1, NULL, 1, 1, 0, NULL, NULL, NULL, NULL, NULL
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `sys_menu` WHERE `id` = 400);
INSERT INTO `sys_menu` (`id`, `name`, `parent_id`, `parent_name`, `path`, `path_name`, `route`, `component`, `brief`, `code`, `type`, `authority`, `icon`, `sort`, `meta`, `tenant_id`, `status`, `deleted`, `create_time`, `create_by`, `update_time`, `update_by`, `memo`)
SELECT 401, 'Update HttpAuditSysConfig', 398, '', '1,398,401', NULL, NULL, NULL, NULL, NULL, 1, 'system:httpAuditSysConfig:update', NULL, 2, NULL, 1, 1, 0, NULL, NULL, NULL, NULL, NULL
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `sys_menu` WHERE `id` = 401);
INSERT INTO `sys_menu` (`id`, `name`, `parent_id`, `parent_name`, `path`, `path_name`, `route`, `component`, `brief`, `code`, `type`, `authority`, `icon`, `sort`, `meta`, `tenant_id`, `status`, `deleted`, `create_time`, `create_by`, `update_time`, `update_by`, `memo`)
SELECT 402, 'Delete HttpAuditSysConfig', 398, '', '1,398,402', NULL, NULL, NULL, NULL, NULL, 1, 'system:httpAuditSysConfig:remove', NULL, 3, NULL, 1, 1, 0, NULL, NULL, NULL, NULL, NULL
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `sys_menu` WHERE `id` = 402);
UPDATE `sys_menu` SET `sort` = 6 WHERE `id` = 390 AND EXISTS (SELECT 1 FROM `sys_menu` WHERE `id` = 398);
UPDATE `sys_menu` SET `sort` = 7 WHERE `id` = 393 AND EXISTS (SELECT 1 FROM `sys_menu` WHERE `id` = 398);
INSERT INTO `sys_role_menu` (`role_id`, `menu_id`)
SELECT 1, 398 FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `sys_role_menu` WHERE `role_id` = 1 AND `menu_id` = 398);
INSERT INTO `sys_role_menu` (`role_id`, `menu_id`)
SELECT 1, 399 FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `sys_role_menu` WHERE `role_id` = 1 AND `menu_id` = 399);
INSERT INTO `sys_role_menu` (`role_id`, `menu_id`)
SELECT 1, 400 FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `sys_role_menu` WHERE `role_id` = 1 AND `menu_id` = 400);
INSERT INTO `sys_role_menu` (`role_id`, `menu_id`)
SELECT 1, 401 FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `sys_role_menu` WHERE `role_id` = 1 AND `menu_id` = 401);
INSERT INTO `sys_role_menu` (`role_id`, `menu_id`)
SELECT 1, 402 FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `sys_role_menu` WHERE `role_id` = 1 AND `menu_id` = 402);