28个文件已添加
34个文件已修改
1 文件已重命名
3个文件已删除
| | |
| | | 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", |
| | |
| | | edit: "Edit", |
| | | detail: "Details", |
| | | histories: "Histories", |
| | | backToList: "Back to list", |
| | | }, |
| | | field: { |
| | | id: 'ID', |
| | |
| | | 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', |
| | |
| | | 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" }, |
| | |
| | | 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: '出库单明细' }, |
| | | }, |
| | |
| | | edit: "编辑", |
| | | detail: "库存明细", |
| | | histories: "流水记录", |
| | | backToList: "返回列表", |
| | | }, |
| | | field: { |
| | | id: 'ID', |
| | |
| | | department: '部门管理', |
| | | token: '登录日志', |
| | | operation: '操作日志', |
| | | httpAuditLog: 'HTTP接口审计', |
| | | httpAuditLog: 'HTTP审计日志', |
| | | httpAuditRule: 'HTTP审计规则', |
| | | httpAuditSysConfig: 'HTTP接口审计', |
| | | config: '配置参数', |
| | | tenant: '租户管理', |
| | | userLogin: '登录日志', |
| | |
| | | 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: "双向" }, |
| | |
| | | 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) { |
| | |
| | | return httpAuditLog; |
| | | case "httpAuditRule": |
| | | return httpAuditRule; |
| | | case "httpAuditSysConfig": |
| | | return httpAuditSysConfig; |
| | | default: |
| | | return { |
| | | list: ListGuesser, |
| New file |
| | |
| | | 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} />; |
| | | }); |
| | |
| | | title={"menu.taskItemLog"} |
| | | filters={filters} |
| | | empty={false} |
| | | pagination={false} |
| | | filter={{ logId: Number(recodeId) }} |
| | | sort={{ field: "create_time", order: "desc" }} |
| | | actions={( |
| | |
| | | import React, { useState, useRef, useEffect, useMemo, useCallback } from "react"; |
| | | import { useNavigate } from 'react-router-dom'; |
| | | import { |
| | | List, |
| | | DatagridConfigurable, |
| | |
| | | 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': { |
| | |
| | | <StyledDatagrid |
| | | preferenceKey='taskLog' |
| | | bulkActionButtons={false} |
| | | rowClick={'edit'} |
| | | row={<ShowOnDoubleClickDatagridRow />} |
| | | expand={false} |
| | | expandSingle={true} |
| | | omit={['id', 'createTime', 'createBy', 'memo', 'taskId', 'robotCode', 'exceStatus', 'sort', 'expCode']} |
| New file |
| | |
| | | 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; |
| | |
| | | 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}` |
| | | } |
| | |
| | | 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': { |
| | |
| | | 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 |
| | |
| | | )} |
| | | 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']} |
| | |
| | | <DeleteButton sx={{ padding: '1px', fontSize: '.75rem' }} mutationMode={OPERATE_MODE} redirect={location.pathname + '/' + doId} /> |
| | | </WrapperField> |
| | | </StyledDatagrid> |
| | | </ListRowDoubleClickContext.Provider> |
| | | </List> |
| | | <DeliveryItemEdit |
| | | open={editDialog} |
| | |
| | | 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 }) => ({ |
| | |
| | | <StyledDatagrid |
| | | preferenceKey='stock' |
| | | bulkActionButtons={false} |
| | | rowClick='edit' |
| | | row={<EditOnDoubleClickDatagridRow />} |
| | | expandSingle={false} |
| | | omit={['id', 'sourceId', 'memo','statusBool','opt']} |
| | | > |
| | |
| | | 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, |
| | |
| | | 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 }) => ({ |
| | |
| | | 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' }}> |
| | |
| | | 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']} |
| | | > |
| | |
| | | <BillStatusField cellClassName="status" source="exceStatus" label="table.field.outStock.exceStatus" /> |
| | | <TextField source="memo" label="common.field.memo" sortable={false} /> |
| | | </StyledDatagrid> |
| | | </ListRowDoubleClickContext.Provider> |
| | | </List> |
| | | </Card > |
| | | </> |
| | |
| | | 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 }) => ({ |
| | |
| | | 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 ( |
| | | <> |
| | |
| | | )} |
| | | 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" /> |
| | |
| | | <DeleteButton sx={{ padding: '1px', fontSize: '.75rem' }} mutationMode='pessimistic' redirect={"/dictType/" + dictId} /> |
| | | </WrapperField> |
| | | </StyledDatagrid> |
| | | </ListRowDoubleClickContext.Provider> |
| | | </List> |
| | | <DictDataEdit |
| | | open={editDialog} |
| | |
| | | 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': { |
| | |
| | | <StyledDatagrid |
| | | preferenceKey='dictType' |
| | | bulkActionButtons={() => <BulkDeleteButton mutationMode={OPERATE_MODE} />} |
| | | rowClick={'edit'} |
| | | row={<EditOnDoubleClickDatagridRow />} |
| | | omit={['id', 'createTime', 'createBy$', 'memo','statusBool']} |
| | | > |
| | | <NumberField source="id" /> |
| | |
| | | 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; |
| | |
| | | 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> |
| | | ); |
| | | |
| New file |
| | |
| | | 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; |
| New file |
| | |
| | | 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; |
| New file |
| | |
| | | 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; |
| New file |
| | |
| | | import React from "react"; |
| | | import HttpAuditSysConfigList from "./HttpAuditSysConfigList"; |
| | | import HttpAuditSysConfigEdit from "./HttpAuditSysConfigEdit"; |
| | | |
| | | export default { |
| | | list: HttpAuditSysConfigList, |
| | | edit: HttpAuditSysConfigEdit, |
| | | recordRepresentation: (record) => record?.configKey || record?.id || '', |
| | | }; |
| | |
| | | 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': { |
| | |
| | | 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 |
| | |
| | | )} |
| | | 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)} |
| | | > |
| | |
| | | <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')} |
| | |
| | | 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": { |
| | |
| | | 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"> |
| | |
| | | } |
| | | 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" /> |
| | |
| | | /> |
| | | </WrapperField> |
| | | </StyledDatagrid> |
| | | </ListRowDoubleClickContext.Provider> |
| | | </List> |
| | | <SerialRuleItemCreate open={createDialog} setOpen={setCreateDialog} record={dicts} /> |
| | | <SerialRuleItemEdit open={editDialog} setOpen={setEditDialog} record={select} /> |
| | |
| | | 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": { |
| | |
| | | bulkActionButtons={() => ( |
| | | <BulkDeleteButton mutationMode={OPERATE_MODE} /> |
| | | )} |
| | | rowClick={'edit'} |
| | | row={<EditOnDoubleClickDatagridRow />} |
| | | omit={["id", "createTime", "createBy$", "memo",'statusBool']} |
| | | > |
| | | <NumberField source="id" /> |
| | |
| | | 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); |
| | | } |
| | | }); |
| | |
| | | <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> |
| New file |
| | |
| | | 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"); |
| | | } |
| | | } |
| File was renamed from rsf-server/src/main/java/com/vincent/rsf/server/system/controller/HttpAuditRuleController.java |
| | |
| | | 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; |
| | |
| | | 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')") |
| New file |
| | |
| | | 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; |
| | | } |
| | | } |
| New file |
| | |
| | | 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 { |
| | | } |
| | |
| | | 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; |
| | |
| | | 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; |
| | |
| | | @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); |
| | | } |
| New file |
| | |
| | | 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 { |
| | | } |
| New file |
| | |
| | | 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; |
| | | } |
| | |
| | | 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; |
| | | |
| | |
| | | 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(); |
| | | } |
| | | |
| New file |
| | |
| | | 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); |
| | | } |
| | | } |
| | |
| | | |
| | | 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(若有) |
| | | */ |
| | |
| | | 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; |
| | | } |
| | |
| | | list.add("/static/"); |
| | | list.add("/httpAuditLog"); |
| | | list.add("/httpAuditRule"); |
| | | list.add("/httpAuditSysConfig"); |
| | | list.add("/http-audit/"); |
| | | return list; |
| | | } |
| | | |
| New file |
| | |
| | | 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> { |
| | | } |
| New file |
| | |
| | | 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; |
| | | } |
| | | } |
| | |
| | | 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(); |
| | |
| | | .setRequestBody(reqStored) |
| | | .setResponseBody(resStored) |
| | | .setResponseTruncated(truncated) |
| | | .setHttpStatus(httpStatus) |
| | | .setHttpStatus(statusToStore) |
| | | .setOkFlag(ok) |
| | | .setSpendMs((int) Math.min(Integer.MAX_VALUE, System.currentTimeMillis() - startTimeMs)) |
| | | .setClientIp(null) |
| New file |
| | |
| | | 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> { |
| | | } |
| New file |
| | |
| | | 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; |
| | | } |
| | | } |
| | |
| | | |
| | | 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; |
| | |
| | | } |
| | | 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; |
| | | } |
| | | } |
| | |
| | | } |
| | | |
| | | 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) { |
| | |
| | | |
| | | 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; |
| | |
| | | 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()) { |
| New file |
| | |
| | | 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; |
| | | } |
| | | } |
| New file |
| | |
| | | <!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, '&') |
| | | .replace(/</g, '<') |
| | | .replace(/>/g, '>'); |
| | | } |
| | | })(); |
| | | </script> |
| | | </body> |
| | | </html> |
| New file |
| | |
| | | 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(); |
| | | } |
| | | } |
| | | } |
| New file |
| | |
| | | package com.vincent.rsf.openApi.common.datasource; |
| | | |
| | | /** |
| | | * 数据源名称 |
| | | */ |
| | | public interface DataSourceNames { |
| | | |
| | | String PRIMARY = "primary"; |
| | | String JDXAJ_LOG = "jdxaj-log"; |
| | | } |
| New file |
| | |
| | | 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; |
| | | } |
| | | } |
| New file |
| | |
| | | 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; |
| | | } |
| | | } |
| New file |
| | |
| | | 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; |
| | | } |
| New file |
| | |
| | | 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); |
| | | } |
| | | } |
| New file |
| | |
| | | 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(); |
| | | } |
| | | } |
| New file |
| | |
| | | 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); |
| | | } |
| | | } |
| | |
| | | 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 |
| | |
| | | |
| | | http-audit: |
| | | enabled: true |
| | | datasource: primary |
| | | # 审计数据源:primary / jdxaj-log |
| | | datasource: jdxaj-log |
| | |
| | | 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 |
| | |
| | | |
| | | http-audit: |
| | | enabled: true |
| | | # 审计数据源:primary / dj-cloud-wms |
| | | # 审计数据源:primary / jdxaj-log |
| | | datasource: primary |
| | | # 其余审计参数改为 sys_http_audit_config 表配置 |
| | |
| | | |
| | | 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; |
| | |
| | | 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); |
| New file |
| | |
| | | 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(); |
| | | } |
| | | } |
| | |
| | | @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); |
| | |
| | | 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(); |
| | |
| | | |
| | | String PRIMARY = "primary"; |
| | | String DJ_CLOUD_WMS = "dj-cloud-wms"; |
| | | String JDXAJ_LOG = "jdxaj-log"; |
| | | } |
| | | |
| | |
| | | if ("dj-cloud-wms".equalsIgnoreCase(dataSource)) { |
| | | return DataSourceNames.DJ_CLOUD_WMS; |
| | | } |
| | | if ("jdxaj-log".equalsIgnoreCase(dataSource)) { |
| | | return DataSourceNames.JDXAJ_LOG; |
| | | } |
| | | return DataSourceNames.PRIMARY; |
| | | } |
| | | } |
| | |
| | | "/ws/**", |
| | | "/wcs/**", |
| | | "/monitor/**", |
| | | "/mcp/**" |
| | | "/mcp/**", |
| | | "/http-audit/**" |
| | | }; |
| | | |
| | | @Resource |
| | |
| | | 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 |
| | |
| | | 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 |
| | |
| | | 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 |
| | |
| | | # rule-cache-refresh-ms:规则表缓存刷新间隔(毫秒) |
| | | http-audit: |
| | | enabled: true |
| | | # 审计数据源:primary / dj-cloud-wms |
| | | # 审计数据源:primary / dj-cloud-wms / jdxaj-log |
| | | datasource: primary |
| New file |
| | |
| | | -- 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); |