| | |
| | | <modules> |
| | | <module>rsf-common</module> |
| | | <module>rsf-framework</module> |
| | | <module>rsf-http-audit</module> |
| | | <module>rsf-server</module> |
| | | <module>rsf-open-api</module> |
| | | </modules> |
| | |
| | | const customEnglishMessages = { |
| | | ...englishMessages, |
| | | hello: 'Hello World', |
| | | 'menu.httpAuditLog': 'HTTP audit', |
| | | 'resources.httpAuditLog.name': 'HTTP audit', |
| | | common: { |
| | | response: { |
| | | success: "Success", |
| | |
| | | department: 'Department', |
| | | token: 'Token', |
| | | operation: 'Operation', |
| | | httpAuditLog: 'HTTP audit', |
| | | config: 'Config', |
| | | tenant: 'Tenant', |
| | | userLogin: 'Token', |
| | |
| | | unknown: 'Unknown', |
| | | } |
| | | }, |
| | | httpAuditLog: { |
| | | serviceName: "service", |
| | | scopeType: "scope", |
| | | uri: "uri", |
| | | method: "method", |
| | | functionDesc: "description", |
| | | queryString: "query string", |
| | | requestBody: "request JSON", |
| | | responseBody: "response JSON", |
| | | responseTruncated: "response truncated", |
| | | httpStatus: "HTTP status", |
| | | okFlag: "ok / error", |
| | | spendMs: "spend ms", |
| | | clientIp: "client IP", |
| | | errorMessage: "error", |
| | | }, |
| | | operationRecord: { |
| | | namespace: "namespace", |
| | | url: "url", |
| | |
| | | const customChineseMessages = { |
| | | ...chineseMessages, |
| | | hello: '你好世界', |
| | | 'menu.httpAuditLog': 'HTTP接口审计', |
| | | resources: { |
| | | config: { name: '配置参数' }, |
| | | httpAuditLog: { name: 'HTTP接口审计' }, |
| | | asnOrderItem: { name: '收货明细' }, |
| | | outStockItem: { name: '出库单明细' }, |
| | | }, |
| | |
| | | department: '部门管理', |
| | | token: '登录日志', |
| | | operation: '操作日志', |
| | | httpAuditLog: 'HTTP接口审计', |
| | | config: '配置参数', |
| | | tenant: '租户管理', |
| | | userLogin: '登录日志', |
| | |
| | | unknown: '未知', |
| | | } |
| | | }, |
| | | httpAuditLog: { |
| | | serviceName: "应用", |
| | | scopeType: "内外部", |
| | | uri: "接口路径", |
| | | method: "方法", |
| | | functionDesc: "功能描述", |
| | | queryString: "查询串", |
| | | requestBody: "请求内容(JSON)", |
| | | responseBody: "响应内容(JSON)", |
| | | responseTruncated: "响应已截断", |
| | | httpStatus: "HTTP状态", |
| | | okFlag: "正常/异常", |
| | | spendMs: "耗时(ms)", |
| | | clientIp: "请求IP", |
| | | errorMessage: "异常信息", |
| | | }, |
| | | operationRecord: { |
| | | namespace: "命名空间", |
| | | url: "url", |
| | |
| | | import statisticCount from './statistics/stockStatisticNum'; |
| | | import rcsTest from './rcsTest'; |
| | | import openApiApp from './system/openApiApp'; |
| | | import httpAuditLog from './system/httpAuditLog'; |
| | | |
| | | const ResourceContent = (node) => { |
| | | switch (node.component) { |
| | |
| | | return rcsTest; |
| | | case "openApiApp": |
| | | return openApiApp; |
| | | case "httpAuditLog": |
| | | return httpAuditLog; |
| | | default: |
| | | return { |
| | | list: ListGuesser, |
| New file |
| | |
| | | import React from "react"; |
| | | import { |
| | | List, |
| | | Datagrid, |
| | | TextField, |
| | | DateField, |
| | | TopToolbar, |
| | | FilterButton, |
| | | TextInput, |
| | | SelectInput, |
| | | ShowButton, |
| | | BulkDeleteButton, |
| | | FunctionField, |
| | | } from "react-admin"; |
| | | import { Chip } from "@mui/material"; |
| | | import EmptyData from "@/page/components/EmptyData"; |
| | | import { 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: "内部" }, |
| | | ]} |
| | | />, |
| | | <TextInput source="functionDesc" label="table.field.httpAuditLog.functionDesc" />, |
| | | <TextInput source="method" label="table.field.httpAuditLog.method" />, |
| | | ]; |
| | | |
| | | 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="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" /> |
| | | <DateField source="createTime" label="common.field.createTime" showTime /> |
| | | <ShowButton /> |
| | | </Datagrid> |
| | | </List> |
| | | ); |
| | | |
| | | export default HttpAuditLogList; |
| New file |
| | |
| | | import React from "react"; |
| | | import { Show, SimpleShowLayout, TextField, DateField, FunctionField } from "react-admin"; |
| | | import { Box, Chip } from "@mui/material"; |
| | | |
| | | const JsonBlock = ({ text }) => ( |
| | | <Box component="pre" sx={{ whiteSpace: "pre-wrap", wordBreak: "break-all", m: 0, fontSize: 12 }}> |
| | | {text ?? ""} |
| | | </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="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> |
| | | ); |
| | | |
| | | export default HttpAuditLogShow; |
| New file |
| | |
| | | import HttpAuditLogList from "./HttpAuditLogList"; |
| | | import HttpAuditLogShow from "./HttpAuditLogShow"; |
| | | |
| | | export default { |
| | | list: HttpAuditLogList, |
| | | show: HttpAuditLogShow, |
| | | recordRepresentation: (record) => `${record.uri || record.id}`, |
| | | }; |
| New file |
| | |
| | | <?xml version="1.0" encoding="UTF-8"?> |
| | | <project xmlns="http://maven.apache.org/POM/4.0.0" |
| | | xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" |
| | | xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> |
| | | <modelVersion>4.0.0</modelVersion> |
| | | <parent> |
| | | <groupId>com.vincent</groupId> |
| | | <artifactId>rsf</artifactId> |
| | | <version>1.0.0</version> |
| | | </parent> |
| | | <artifactId>rsf-http-audit</artifactId> |
| | | <packaging>jar</packaging> |
| | | <name>rsf-http-audit</name> |
| | | <description>HTTP 接口审计(引入即注册 Filter 异步落库,不影响业务)</description> |
| | | |
| | | <dependencies> |
| | | <dependency> |
| | | <groupId>org.springframework.boot</groupId> |
| | | <artifactId>spring-boot-starter-web</artifactId> |
| | | </dependency> |
| | | <dependency> |
| | | <groupId>org.springframework.boot</groupId> |
| | | <artifactId>spring-boot-autoconfigure</artifactId> |
| | | </dependency> |
| | | <dependency> |
| | | <groupId>org.springframework.boot</groupId> |
| | | <artifactId>spring-boot-configuration-processor</artifactId> |
| | | <optional>true</optional> |
| | | </dependency> |
| | | <dependency> |
| | | <groupId>com.baomidou</groupId> |
| | | <artifactId>mybatis-plus-boot-starter</artifactId> |
| | | <version>3.4.1</version> |
| | | </dependency> |
| | | <dependency> |
| | | <groupId>org.projectlombok</groupId> |
| | | <artifactId>lombok</artifactId> |
| | | <scope>provided</scope> |
| | | </dependency> |
| | | <dependency> |
| | | <groupId>org.slf4j</groupId> |
| | | <artifactId>slf4j-api</artifactId> |
| | | </dependency> |
| | | </dependencies> |
| | | </project> |
| New file |
| | |
| | | package com.vincent.rsf.httpaudit.config; |
| | | |
| | | import com.vincent.rsf.httpaudit.props.HttpAuditProperties; |
| | | import com.vincent.rsf.httpaudit.service.HttpAuditAsyncRecorder; |
| | | import com.vincent.rsf.httpaudit.web.HttpAuditFilter; |
| | | import org.mybatis.spring.annotation.MapperScan; |
| | | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; |
| | | import org.springframework.boot.context.properties.EnableConfigurationProperties; |
| | | import org.springframework.boot.web.servlet.FilterRegistrationBean; |
| | | import org.springframework.context.annotation.Bean; |
| | | import org.springframework.context.annotation.Configuration; |
| | | import org.springframework.core.Ordered; |
| | | import org.springframework.core.env.Environment; |
| | | import org.springframework.scheduling.annotation.EnableAsync; |
| | | import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; |
| | | |
| | | import java.util.concurrent.Executor; |
| | | |
| | | /** |
| | | * 引入 rsf-http-audit 即生效(可 http-audit.enabled=false 关闭) |
| | | */ |
| | | @Configuration |
| | | @EnableAsync |
| | | @EnableConfigurationProperties(HttpAuditProperties.class) |
| | | @ConditionalOnProperty(prefix = "http-audit", name = "enabled", havingValue = "true", matchIfMissing = true) |
| | | @MapperScan("com.vincent.rsf.httpaudit.mapper") |
| | | public class HttpAuditAutoConfiguration { |
| | | |
| | | @Bean(name = "httpAuditExecutor") |
| | | public Executor httpAuditExecutor() { |
| | | ThreadPoolTaskExecutor ex = new ThreadPoolTaskExecutor(); |
| | | ex.setCorePoolSize(2); |
| | | ex.setMaxPoolSize(8); |
| | | ex.setQueueCapacity(1000); |
| | | ex.setThreadNamePrefix("http-audit-"); |
| | | ex.initialize(); |
| | | return ex; |
| | | } |
| | | |
| | | @Bean |
| | | public FilterRegistrationBean<HttpAuditFilter> httpAuditFilterRegistration( |
| | | HttpAuditAsyncRecorder recorder, HttpAuditProperties props, Environment env) { |
| | | HttpAuditFilter filter = new HttpAuditFilter(recorder, props, env); |
| | | FilterRegistrationBean<HttpAuditFilter> reg = new FilterRegistrationBean<>(); |
| | | reg.setFilter(filter); |
| | | reg.addUrlPatterns("/*"); |
| | | reg.setOrder(Ordered.HIGHEST_PRECEDENCE + 50); |
| | | return reg; |
| | | } |
| | | } |
| 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; |
| | | |
| | | /** |
| | | * HTTP 接口审计记录 |
| | | */ |
| | | @Data |
| | | @Accessors(chain = true) |
| | | @TableName("sys_http_audit_log") |
| | | public class HttpAuditLog implements Serializable { |
| | | |
| | | private static final long serialVersionUID = 1L; |
| | | |
| | | @TableId(type = IdType.AUTO) |
| | | private Long id; |
| | | |
| | | /** 应用名,如 spring.application.name */ |
| | | private String serviceName; |
| | | |
| | | /** EXTERNAL-外部;INTERNAL-内部 */ |
| | | private String scopeType; |
| | | |
| | | /** 请求路径(不含域名) */ |
| | | private String uri; |
| | | |
| | | private String method; |
| | | |
| | | /** 功能说明(来自配置最长前缀匹配) */ |
| | | private String functionDesc; |
| | | |
| | | private String queryString; |
| | | |
| | | /** 请求体 JSON/文本,全量 */ |
| | | private String requestBody; |
| | | |
| | | /** 响应体,查询类或超长时截断 */ |
| | | private String responseBody; |
| | | |
| | | /** 1 表示响应体已按规则截断 */ |
| | | private Integer responseTruncated; |
| | | |
| | | private Integer httpStatus; |
| | | |
| | | /** 1 正常(2xx 且无未捕获异常);0 异常 */ |
| | | private Integer okFlag; |
| | | |
| | | private Integer spendMs; |
| | | |
| | | private String clientIp; |
| | | |
| | | /** 链路上异常摘要 */ |
| | | private String errorMessage; |
| | | |
| | | private Date createTime; |
| | | |
| | | @TableLogic |
| | | private Integer deleted; |
| | | } |
| New file |
| | |
| | | package com.vincent.rsf.httpaudit.mapper; |
| | | |
| | | import com.baomidou.mybatisplus.core.mapper.BaseMapper; |
| | | import com.vincent.rsf.httpaudit.entity.HttpAuditLog; |
| | | import org.apache.ibatis.annotations.Mapper; |
| | | |
| | | @Mapper |
| | | public interface HttpAuditLogMapper extends BaseMapper<HttpAuditLog> { |
| | | } |
| New file |
| | |
| | | package com.vincent.rsf.httpaudit.props; |
| | | |
| | | import lombok.Data; |
| | | import org.springframework.boot.context.properties.ConfigurationProperties; |
| | | |
| | | import java.util.ArrayList; |
| | | import java.util.LinkedHashMap; |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | |
| | | /** |
| | | * HTTP 审计配置 |
| | | */ |
| | | @Data |
| | | @ConfigurationProperties(prefix = "http-audit") |
| | | public class HttpAuditProperties { |
| | | |
| | | private boolean enabled = true; |
| | | |
| | | /** 查询类响应最多保留字符数 */ |
| | | private int queryResponseMaxChars = 500; |
| | | |
| | | /** 非查询类响应最多入库字节(超出截断并标记) */ |
| | | private int maxResponseStoreChars = 65535; |
| | | |
| | | /** 请求体缓存上限(字节) */ |
| | | private int maxRequestCacheBytes = 2 * 1024 * 1024; |
| | | |
| | | /** 响应体缓存上限(字节) */ |
| | | private int maxResponseCacheBytes = 2 * 1024 * 1024; |
| | | |
| | | /** 不落库的路径前缀 */ |
| | | private List<String> excludePathPrefixes = defaultExcludes(); |
| | | |
| | | /** 视为外部调用的路径前缀(其余为内部) */ |
| | | private List<String> externalPathPrefixes = defaultExternal(); |
| | | |
| | | /** 路径 -> 功能描述(按最长路径前缀匹配) */ |
| | | private Map<String, String> pathDescriptions = new LinkedHashMap<>(); |
| | | |
| | | private static List<String> defaultExcludes() { |
| | | List<String> list = new ArrayList<>(); |
| | | list.add("/actuator"); |
| | | list.add("/swagger"); |
| | | list.add("/webjars"); |
| | | list.add("/v2/api-docs"); |
| | | list.add("/v3/api-docs"); |
| | | list.add("/doc.html"); |
| | | list.add("/druid"); |
| | | list.add("/error"); |
| | | list.add("/favicon.ico"); |
| | | list.add("/static/"); |
| | | list.add("/httpAuditLog"); |
| | | return list; |
| | | } |
| | | |
| | | private static List<String> defaultExternal() { |
| | | List<String> list = new ArrayList<>(); |
| | | list.add("/erp"); |
| | | list.add("/cloudwms"); |
| | | return list; |
| | | } |
| | | } |
| New file |
| | |
| | | package com.vincent.rsf.httpaudit.service; |
| | | |
| | | import com.fasterxml.jackson.core.JsonProcessingException; |
| | | import com.fasterxml.jackson.databind.ObjectMapper; |
| | | import com.vincent.rsf.httpaudit.entity.HttpAuditLog; |
| | | import com.vincent.rsf.httpaudit.mapper.HttpAuditLogMapper; |
| | | import lombok.RequiredArgsConstructor; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.springframework.scheduling.annotation.Async; |
| | | import org.springframework.stereotype.Service; |
| | | |
| | | import java.util.LinkedHashMap; |
| | | import java.util.Map; |
| | | |
| | | /** |
| | | * 异步落库;失败时打全量日志,不向业务抛错 |
| | | */ |
| | | @Slf4j |
| | | @Service |
| | | @RequiredArgsConstructor |
| | | public class HttpAuditAsyncRecorder { |
| | | |
| | | private final HttpAuditLogMapper httpAuditLogMapper; |
| | | private final ObjectMapper objectMapper = new ObjectMapper(); |
| | | |
| | | @Async("httpAuditExecutor") |
| | | public void save(HttpAuditLog entity) { |
| | | try { |
| | | httpAuditLogMapper.insert(entity); |
| | | } catch (Throwable t) { |
| | | try { |
| | | Map<String, Object> dump = new LinkedHashMap<>(); |
| | | dump.put("serviceName", entity.getServiceName()); |
| | | dump.put("scopeType", entity.getScopeType()); |
| | | dump.put("uri", entity.getUri()); |
| | | dump.put("method", entity.getMethod()); |
| | | dump.put("functionDesc", entity.getFunctionDesc()); |
| | | dump.put("queryString", entity.getQueryString()); |
| | | dump.put("requestBody", entity.getRequestBody()); |
| | | dump.put("responseBody", entity.getResponseBody()); |
| | | dump.put("httpStatus", entity.getHttpStatus()); |
| | | dump.put("okFlag", entity.getOkFlag()); |
| | | dump.put("spendMs", entity.getSpendMs()); |
| | | dump.put("clientIp", entity.getClientIp()); |
| | | dump.put("errorMessage", entity.getErrorMessage()); |
| | | String json = objectMapper.writeValueAsString(dump); |
| | | log.error("http-audit 落库失败,全量JSON如下:{}", json, t); |
| | | } catch (JsonProcessingException je) { |
| | | log.error("http-audit 落库失败且序列化审计内容失败,requestBody.length={}, responseBody.length={}", |
| | | entity.getRequestBody() == null ? -1 : entity.getRequestBody().length(), |
| | | entity.getResponseBody() == null ? -1 : entity.getResponseBody().length(), |
| | | t); |
| | | log.error("序列化异常", je); |
| | | } |
| | | } |
| | | } |
| | | } |
| New file |
| | |
| | | package com.vincent.rsf.httpaudit.support; |
| | | |
| | | import com.vincent.rsf.httpaudit.props.HttpAuditProperties; |
| | | |
| | | import javax.servlet.http.HttpServletRequest; |
| | | import java.nio.charset.Charset; |
| | | import java.nio.charset.StandardCharsets; |
| | | import java.util.ArrayList; |
| | | import java.util.Comparator; |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | |
| | | /** |
| | | * 内外部判定、路径说明、响应截断 |
| | | */ |
| | | public final class HttpAuditSupport { |
| | | |
| | | private HttpAuditSupport() { |
| | | } |
| | | |
| | | public static String resolveScope(HttpServletRequest request, HttpAuditProperties props) { |
| | | String path = safePath(request); |
| | | for (String p : props.getExternalPathPrefixes()) { |
| | | if (path.startsWith(p)) { |
| | | return "EXTERNAL"; |
| | | } |
| | | } |
| | | return "INTERNAL"; |
| | | } |
| | | |
| | | public static String resolveFunctionDesc(HttpServletRequest request, HttpAuditProperties props) { |
| | | String path = safePath(request); |
| | | Map<String, String> map = props.getPathDescriptions(); |
| | | if (map == null || map.isEmpty()) { |
| | | return null; |
| | | } |
| | | List<String> keys = new ArrayList<>(map.keySet()); |
| | | keys.sort(Comparator.comparingInt(String::length).reversed()); |
| | | for (String k : keys) { |
| | | if (path.startsWith(k)) { |
| | | return map.get(k); |
| | | } |
| | | } |
| | | return null; |
| | | } |
| | | |
| | | public static String safePath(HttpServletRequest request) { |
| | | String ctx = request.getContextPath(); |
| | | String uri = request.getRequestURI(); |
| | | if (ctx != null && !ctx.isEmpty() && uri.startsWith(ctx)) { |
| | | return uri.substring(ctx.length()); |
| | | } |
| | | return uri != null ? uri : ""; |
| | | } |
| | | |
| | | public static boolean shouldExclude(HttpServletRequest request, HttpAuditProperties props) { |
| | | String path = safePath(request); |
| | | for (String p : props.getExcludePathPrefixes()) { |
| | | if (p != null && !p.isEmpty() && path.startsWith(p)) { |
| | | return true; |
| | | } |
| | | } |
| | | String lower = path.toLowerCase(); |
| | | if (lower.endsWith(".js") || lower.endsWith(".css") || lower.endsWith(".ico") |
| | | || lower.endsWith(".png") || lower.endsWith(".jpg") || lower.endsWith(".gif") |
| | | || lower.endsWith(".woff") || lower.endsWith(".woff2") || lower.endsWith(".map")) { |
| | | return true; |
| | | } |
| | | return false; |
| | | } |
| | | |
| | | public static boolean isQueryLike(HttpServletRequest request) { |
| | | String m = request.getMethod(); |
| | | if ("GET".equalsIgnoreCase(m)) { |
| | | return true; |
| | | } |
| | | String path = safePath(request).toLowerCase(); |
| | | return path.contains("/page") || path.contains("/list") || path.contains("/query"); |
| | | } |
| | | |
| | | public static String clientIp(HttpServletRequest request) { |
| | | String xff = request.getHeader("X-Forwarded-For"); |
| | | if (xff != null && !xff.isEmpty()) { |
| | | int i = xff.indexOf(','); |
| | | return i > 0 ? xff.substring(0, i).trim() : xff.trim(); |
| | | } |
| | | String real = request.getHeader("X-Real-IP"); |
| | | if (real != null && !real.isEmpty()) { |
| | | return real.trim(); |
| | | } |
| | | return request.getRemoteAddr(); |
| | | } |
| | | |
| | | public static Charset resolveCharset(HttpServletRequest request) { |
| | | String enc = request.getCharacterEncoding(); |
| | | if (enc == null || enc.isEmpty()) { |
| | | return StandardCharsets.UTF_8; |
| | | } |
| | | try { |
| | | return Charset.forName(enc); |
| | | } catch (Exception e) { |
| | | return StandardCharsets.UTF_8; |
| | | } |
| | | } |
| | | |
| | | public static String bytesToString(byte[] buf, Charset charset) { |
| | | if (buf == null || buf.length == 0) { |
| | | return ""; |
| | | } |
| | | return new String(buf, charset); |
| | | } |
| | | |
| | | public static String truncateForStore(String s, int maxChars) { |
| | | if (s == null) { |
| | | return null; |
| | | } |
| | | if (s.length() <= maxChars) { |
| | | return s; |
| | | } |
| | | return s.substring(0, maxChars) + "...(truncated,len=" + s.length() + ")"; |
| | | } |
| | | } |
| New file |
| | |
| | | package com.vincent.rsf.httpaudit.web; |
| | | |
| | | import com.vincent.rsf.httpaudit.entity.HttpAuditLog; |
| | | import com.vincent.rsf.httpaudit.props.HttpAuditProperties; |
| | | import com.vincent.rsf.httpaudit.service.HttpAuditAsyncRecorder; |
| | | import com.vincent.rsf.httpaudit.support.HttpAuditSupport; |
| | | import lombok.RequiredArgsConstructor; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.springframework.core.env.Environment; |
| | | import org.springframework.web.filter.OncePerRequestFilter; |
| | | import org.springframework.web.util.ContentCachingRequestWrapper; |
| | | import org.springframework.web.util.ContentCachingResponseWrapper; |
| | | |
| | | import javax.servlet.FilterChain; |
| | | import javax.servlet.ServletException; |
| | | import javax.servlet.http.HttpServletRequest; |
| | | import javax.servlet.http.HttpServletResponse; |
| | | import java.io.IOException; |
| | | import java.nio.charset.Charset; |
| | | import java.util.Date; |
| | | |
| | | /** |
| | | * 缓存请求/响应体并异步写审计表 |
| | | */ |
| | | @Slf4j |
| | | @RequiredArgsConstructor |
| | | public class HttpAuditFilter extends OncePerRequestFilter { |
| | | |
| | | private final HttpAuditAsyncRecorder recorder; |
| | | private final HttpAuditProperties props; |
| | | private final Environment environment; |
| | | |
| | | @Override |
| | | protected boolean shouldNotFilter(HttpServletRequest request) { |
| | | return !props.isEnabled() || HttpAuditSupport.shouldExclude(request, props); |
| | | } |
| | | |
| | | @Override |
| | | protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) |
| | | throws ServletException, IOException { |
| | | ContentCachingRequestWrapper reqWrapper = new ContentCachingRequestWrapper(request, props.getMaxRequestCacheBytes()); |
| | | ContentCachingResponseWrapper resWrapper = new ContentCachingResponseWrapper(response); |
| | | long t0 = System.currentTimeMillis(); |
| | | Exception chainError = null; |
| | | try { |
| | | filterChain.doFilter(reqWrapper, resWrapper); |
| | | } catch (IOException | ServletException e) { |
| | | chainError = e; |
| | | throw e; |
| | | } catch (RuntimeException e) { |
| | | chainError = e; |
| | | throw e; |
| | | } finally { |
| | | try { |
| | | record(reqWrapper, resWrapper, t0, chainError); |
| | | } catch (Throwable ignore) { |
| | | log.warn("http-audit record 异常已吞掉:{}", ignore.getMessage()); |
| | | } |
| | | try { |
| | | resWrapper.copyBodyToResponse(); |
| | | } catch (IOException io) { |
| | | log.debug("copyBodyToResponse: {}", io.getMessage()); |
| | | } |
| | | } |
| | | } |
| | | |
| | | private void record(ContentCachingRequestWrapper req, ContentCachingResponseWrapper res, long t0, Exception chainError) { |
| | | Charset charset = HttpAuditSupport.resolveCharset(req); |
| | | String ctReq = req.getContentType(); |
| | | String reqBody; |
| | | if (ctReq != null && ctReq.toLowerCase().startsWith("multipart/")) { |
| | | reqBody = "[multipart omitted]"; |
| | | } else { |
| | | reqBody = HttpAuditSupport.bytesToString(req.getContentAsByteArray(), charset); |
| | | } |
| | | |
| | | String respCt = res.getContentType(); |
| | | String resBodyRaw = HttpAuditSupport.bytesToString(res.getContentAsByteArray(), charset); |
| | | String resBodyToStore; |
| | | int truncated = 0; |
| | | if (respCt != null && (respCt.contains("octet-stream") || respCt.contains("application/pdf"))) { |
| | | resBodyToStore = "[binary response omitted]"; |
| | | truncated = 1; |
| | | } else if (HttpAuditSupport.isQueryLike(req)) { |
| | | resBodyToStore = HttpAuditSupport.truncateForStore(resBodyRaw, props.getQueryResponseMaxChars()); |
| | | if (resBodyRaw != null && resBodyRaw.length() > props.getQueryResponseMaxChars()) { |
| | | truncated = 1; |
| | | } |
| | | } else { |
| | | resBodyToStore = HttpAuditSupport.truncateForStore(resBodyRaw, props.getMaxResponseStoreChars()); |
| | | if (resBodyRaw != null && resBodyRaw.length() > props.getMaxResponseStoreChars()) { |
| | | truncated = 1; |
| | | } |
| | | } |
| | | |
| | | int status = res.getStatus(); |
| | | int ok = (chainError == null && status >= 200 && status < 400) ? 1 : 0; |
| | | String errMsg = null; |
| | | if (chainError != null) { |
| | | String s = chainError.toString(); |
| | | errMsg = s.length() > 4000 ? s.substring(0, 4000) + "..." : s; |
| | | } |
| | | |
| | | String appName = environment.getProperty("spring.application.name", "unknown"); |
| | | |
| | | HttpAuditLog logEntity = new HttpAuditLog() |
| | | .setServiceName(appName) |
| | | .setScopeType(HttpAuditSupport.resolveScope(req, props)) |
| | | .setUri(HttpAuditSupport.safePath(req)) |
| | | .setMethod(req.getMethod()) |
| | | .setFunctionDesc(HttpAuditSupport.resolveFunctionDesc(req, props)) |
| | | .setQueryString(req.getQueryString()) |
| | | .setRequestBody(reqBody) |
| | | .setResponseBody(resBodyToStore) |
| | | .setResponseTruncated(truncated) |
| | | .setHttpStatus(status) |
| | | .setOkFlag(ok) |
| | | .setSpendMs((int) (System.currentTimeMillis() - t0)) |
| | | .setClientIp(HttpAuditSupport.clientIp(req)) |
| | | .setErrorMessage(errMsg) |
| | | .setCreateTime(new Date()) |
| | | .setDeleted(0); |
| | | |
| | | recorder.save(logEntity); |
| | | } |
| | | } |
| New file |
| | |
| | | org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ |
| | | com.vincent.rsf.httpaudit.config.HttpAuditAutoConfiguration |
| | |
| | | <artifactId>rsf-common</artifactId> |
| | | <version>1.0.0</version> |
| | | </dependency> |
| | | <dependency> |
| | | <groupId>com.vincent</groupId> |
| | | <artifactId>rsf-http-audit</artifactId> |
| | | <version>1.0.0</version> |
| | | </dependency> |
| | | <!-- OpenFeign --> |
| | | <dependency> |
| | | <groupId>org.springframework.cloud</groupId> |
| | |
| | | host: http://127.0.0.1 |
| | | #端口 |
| | | port: 3741 |
| | | |
| | | http-audit: |
| | | enabled: true |
| | | query-response-max-chars: 500 |
| | | max-response-store-chars: 65535 |
| | |
| | | <version>1.0.0</version> |
| | | </dependency> |
| | | <dependency> |
| | | <groupId>com.vincent</groupId> |
| | | <artifactId>rsf-http-audit</artifactId> |
| | | <version>1.0.0</version> |
| | | </dependency> |
| | | <dependency> |
| | | <groupId>org.springframework.boot</groupId> |
| | | <artifactId>spring-boot-starter-web</artifactId> |
| | | </dependency> |
| | |
| | | */ |
| | | private String baseUrl; |
| | | |
| | | /** |
| | | * 鼎捷 ilcwmsplus 完成反馈等公共字段(orgNo、单据类别、单位) |
| | | */ |
| | | private Dap dap = new Dap(); |
| | | |
| | | @Data |
| | | public static class Dap { |
| | | private String orgNo = ""; |
| | | private String docTypeIn = ""; |
| | | private String docTypeOut = ""; |
| | | /** 库存调整(9.2)单据类别;移库等 */ |
| | | private String docTypeAdj = ""; |
| | | private String unitNo = "PCS"; |
| | | } |
| | | |
| | | @Data |
| | | @Configuration |
| | | @ConfigurationProperties(prefix = "platform.erp.api") |
| | | public class ApiInfo { |
| | | /** 一键上报质检接口 */ |
| | | private String notifyInspect; |
| | | /** 9.1 入/出库结果上报(立库侧请求云仓) */ |
| | | private String inOutResultPath = "/api/report/inOutResult"; |
| | | /** 9.2 库存调整主动上报(立库侧请求云仓) */ |
| | | /** 已改为 Feign 固定路径 /dapilc/.../cusInventoryCompletionReport、cusOutboundCompletionReport,本项仅作配置占位 */ |
| | | private String inOutResultPath = "/dapilc/restful/service/ilcwmsplus/IKWebService/cusInventoryCompletionReport"; |
| | | /** 9.2 库存调整:仍为 /api/report/inventoryAdjust,报文体与 9.1 一致为 {data:[]} */ |
| | | private String inventoryAdjustPath = "/api/report/inventoryAdjust"; |
| | | /** 物料基础信息同步(立库侧请求云仓) */ |
| | | private String matSyncPath = "/api/mat/sync"; |
| | |
| | | package com.vincent.rsf.server.api.controller; |
| | | |
| | | import com.vincent.rsf.server.api.controller.erp.params.InOutResultReportParam; |
| | | import com.vincent.rsf.server.api.controller.erp.params.InventoryAdjustReportParam; |
| | | import com.vincent.rsf.server.api.controller.erp.params.DapIlcwmsCompletionRequest; |
| | | import io.swagger.annotations.Api; |
| | | import io.swagger.annotations.ApiOperation; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.springframework.http.MediaType; |
| | | import org.springframework.web.bind.annotation.PostMapping; |
| | | import org.springframework.web.bind.annotation.RequestBody; |
| | | import org.springframework.web.bind.annotation.RequestMapping; |
| | | import org.springframework.web.bind.annotation.RestController; |
| | | |
| | | import java.util.HashMap; |
| | | import java.util.Map; |
| | | |
| | | /** |
| | | * 云仓WMS 模拟接口(对接协议 9.1、9.2、物料同步)。 |
| | | * 云仓未提供真实 URL 时,可将 platform.erp.base-url 指向本机该服务(如 http://127.0.0.1:8086/rsf-server), |
| | | * 立库上报请求会打到本接口并返回模拟成功。 |
| | | * 云仓模拟接口。platform.erp.base-url 指向本服务时,Feign 会请求下列 DAP 风格路径。 |
| | | */ |
| | | @Slf4j |
| | | @RestController |
| | | @RequestMapping("/api") |
| | | @Api(value = "云仓模拟接口", tags = "云仓模拟(无真实云仓URL时使用)") |
| | | public class CloudWmsMockController { |
| | | |
| | | private static Map<String, Object> successResponse() { |
| | | private static Map<String, Object> dapOkEnvelope() { |
| | | Map<String, Object> profile = new HashMap<>(); |
| | | profile.put("tenantSid", 1); |
| | | profile.put("userSid", "SYS"); |
| | | Map<String, Object> response = new HashMap<>(); |
| | | response.put("code", -1); |
| | | response.put("success", false); |
| | | response.put("message", ""); |
| | | Map<String, Object> map = new HashMap<>(); |
| | | map.put("duration", 58); |
| | | map.put("statusDescription", "OK"); |
| | | map.put("response", response); |
| | | map.put("profile", profile); |
| | | map.put("uuid", ""); |
| | | map.put("status", 200); |
| | | return map; |
| | | } |
| | | |
| | | private static Map<String, Object> successResponseLegacy() { |
| | | Map<String, Object> data = new HashMap<>(); |
| | | data.put("result", "SUCCESS"); |
| | | Map<String, Object> map = new HashMap<>(); |
| | |
| | | return map; |
| | | } |
| | | |
| | | /** 9.1 入/出库结果上报 - 模拟 */ |
| | | @ApiOperation("入/出库结果上报(模拟)") |
| | | @PostMapping(value = "/report/inOutResult", consumes = MediaType.APPLICATION_JSON_VALUE) |
| | | public Map<String, Object> mockInOutResult(@RequestBody InOutResultReportParam body) { |
| | | log.info("云仓模拟-入/出库结果上报,orderNo={},locId={},matNr={}", |
| | | body != null ? body.getOrderNo() : null, |
| | | body != null ? body.getLocId() : null, |
| | | body != null ? body.getMatNr() : null); |
| | | return successResponse(); |
| | | @ApiOperation("鼎捷-入库完成反馈(模拟)") |
| | | @PostMapping(value = "/dapilc/restful/service/ilcwmsplus/IKWebService/cusInventoryCompletionReport", consumes = MediaType.APPLICATION_JSON_VALUE) |
| | | public Map<String, Object> mockInventoryCompletion(@RequestBody DapIlcwmsCompletionRequest body) { |
| | | log.info("云仓模拟-入库完成反馈,行数={}", body != null && body.getData() != null ? body.getData().size() : 0); |
| | | return dapOkEnvelope(); |
| | | } |
| | | |
| | | /** 9.2 库存调整主动上报 - 模拟 */ |
| | | @ApiOperation("库存调整主动上报(模拟)") |
| | | @PostMapping(value = "/report/inventoryAdjust", consumes = MediaType.APPLICATION_JSON_VALUE) |
| | | public Map<String, Object> mockInventoryAdjust(@RequestBody InventoryAdjustReportParam body) { |
| | | log.info("云仓模拟-库存调整上报,changeType={},wareHouseId={},matNr={}", |
| | | body != null ? body.getChangeType() : null, |
| | | body != null ? body.getWareHouseId() : null, |
| | | body != null ? body.getMatNr() : null); |
| | | return successResponse(); |
| | | @ApiOperation("鼎捷-出库完成反馈(模拟)") |
| | | @PostMapping(value = "/dapilc/restful/service/ilcwmsplus/IKWebService/cusOutboundCompletionReport", consumes = MediaType.APPLICATION_JSON_VALUE) |
| | | public Map<String, Object> mockOutboundCompletion(@RequestBody DapIlcwmsCompletionRequest body) { |
| | | log.info("云仓模拟-出库完成反馈,行数={}", body != null && body.getData() != null ? body.getData().size() : 0); |
| | | return dapOkEnvelope(); |
| | | } |
| | | |
| | | /** 9.2 库存调整主动上报 - 模拟(路径不变,body 为 {data:[]}) */ |
| | | @ApiOperation("库存调整上报(模拟)") |
| | | @PostMapping(value = "/api/report/inventoryAdjust", consumes = MediaType.APPLICATION_JSON_VALUE) |
| | | public Map<String, Object> mockInventoryAdjust(@RequestBody DapIlcwmsCompletionRequest body) { |
| | | log.info("云仓模拟-库存调整上报,行数={}", body != null && body.getData() != null ? body.getData().size() : 0); |
| | | return successResponseLegacy(); |
| | | } |
| | | |
| | | /** 物料基础信息同步 - 模拟 */ |
| | | @ApiOperation("物料同步(模拟)") |
| | | @PostMapping(value = "/mat/sync", consumes = MediaType.APPLICATION_JSON_VALUE) |
| | | @PostMapping(value = "/api/mat/sync", consumes = MediaType.APPLICATION_JSON_VALUE) |
| | | public Map<String, Object> mockMatSync(@RequestBody Object body) { |
| | | log.info("云仓模拟-物料同步,body={}", body != null ? body.toString() : null); |
| | | return successResponse(); |
| | | return successResponseLegacy(); |
| | | } |
| | | |
| | | // /** 9.1 入/出库结果上报 - 模拟(旧路径,已改 DAP) */ |
| | | // @PostMapping(value = "/api/report/inOutResult", consumes = MediaType.APPLICATION_JSON_VALUE) |
| | | // public Map<String, Object> mockInOutResult(@RequestBody InOutResultReportParam body) { |
| | | // return successResponseLegacy(); |
| | | // } |
| | | } |
| New file |
| | |
| | | package com.vincent.rsf.server.api.controller.erp.params; |
| | | |
| | | import com.fasterxml.jackson.annotation.JsonInclude; |
| | | import io.swagger.annotations.ApiModel; |
| | | import lombok.Data; |
| | | import lombok.experimental.Accessors; |
| | | |
| | | /** |
| | | * 鼎捷 ilcwmsplus 立库入/出库完成反馈 - data 中单行 |
| | | */ |
| | | @Data |
| | | @Accessors(chain = true) |
| | | @ApiModel(value = "DapIlcwmsCompletionLine", description = "立库完成反馈明细行") |
| | | @JsonInclude(JsonInclude.Include.NON_NULL) |
| | | public class DapIlcwmsCompletionLine { |
| | | |
| | | private String orgNo; |
| | | private String docType; |
| | | private String docNo; |
| | | private String docSeqNo; |
| | | private String itemNo; |
| | | private Double qty; |
| | | private String unitNo; |
| | | private String inWarehouseNo; |
| | | private String inCellNo; |
| | | private String outWarehouseNo; |
| | | private String outCellNo; |
| | | private String combinationLotNo; |
| | | private String barcode; |
| | | } |
| New file |
| | |
| | | package com.vincent.rsf.server.api.controller.erp.params; |
| | | |
| | | import com.fasterxml.jackson.annotation.JsonInclude; |
| | | import io.swagger.annotations.ApiModel; |
| | | import lombok.Data; |
| | | import lombok.experimental.Accessors; |
| | | |
| | | import java.util.List; |
| | | |
| | | /** |
| | | * 鼎捷 ilcwmsplus 立库入/出库完成反馈 - 请求体 |
| | | */ |
| | | @Data |
| | | @Accessors(chain = true) |
| | | @ApiModel(value = "DapIlcwmsCompletionRequest", description = "立库完成反馈请求") |
| | | @JsonInclude(JsonInclude.Include.NON_NULL) |
| | | public class DapIlcwmsCompletionRequest { |
| | | |
| | | private List<DapIlcwmsCompletionLine> data; |
| | | } |
| | |
| | | |
| | | @ApiModelProperty("批次") |
| | | private String batch; |
| | | |
| | | @ApiModelProperty(value = "true 入库完成反馈,false 出库完成反馈", required = true) |
| | | private Boolean inbound; |
| | | |
| | | @ApiModelProperty(value = "条码(云仓必填)") |
| | | private String barcode; |
| | | } |
| | |
| | | |
| | | @ApiModelProperty("托盘号") |
| | | private String palletId; |
| | | |
| | | @ApiModelProperty("云仓单号") |
| | | private String docNo; |
| | | |
| | | @ApiModelProperty("项次") |
| | | private String docSeqNo; |
| | | |
| | | @ApiModelProperty("单据类别(空则按 changeType 取配置 dap.doc-type-in/out/adj)") |
| | | private String docType; |
| | | |
| | | @ApiModelProperty("批次") |
| | | private String batch; |
| | | |
| | | @ApiModelProperty("条码(云仓必填)") |
| | | private String barcode; |
| | | |
| | | @ApiModelProperty("单位编码(空则取配置 dap.unit-no)") |
| | | private String unitNo; |
| | | } |
| | |
| | | package com.vincent.rsf.server.api.feign; |
| | | |
| | | import com.vincent.rsf.server.api.controller.erp.params.InOutResultReportParam; |
| | | import com.vincent.rsf.server.api.controller.erp.params.InventoryAdjustReportParam; |
| | | import com.vincent.rsf.server.api.controller.erp.params.DapIlcwmsCompletionRequest; |
| | | import com.vincent.rsf.server.api.feign.fallback.CloudWmsErpFeignClientFallbackFactory; |
| | | import org.springframework.cloud.openfeign.FeignClient; |
| | | import org.springframework.http.MediaType; |
| | |
| | | import java.util.Map; |
| | | |
| | | /** |
| | | * 立库侧通过 OpenFeign 调用云仓WMS:入/出库结果上报(9.1)、库存调整上报(9.2)、物料同步。 |
| | | * 立库侧通过 OpenFeign 调用云仓:9.1 鼎捷 ilcwmsplus 入/出库完成反馈(两 URL);9.2 仍为 /api/report/inventoryAdjust,请求体与 9.1 相同 {data:[]}(由 Service 从 InventoryAdjustReportParam 组装)。 |
| | | * 使用 platform.erp.base-url 作为根地址;失败时走 Fallback,统一返回错误响应(不抛异常)。 |
| | | */ |
| | | @FeignClient( |
| | |
| | | ) |
| | | public interface CloudWmsErpFeignClient { |
| | | |
| | | /** 9.1 入/出库结果上报 */ |
| | | @PostMapping(value = "/api/report/inOutResult", consumes = MediaType.APPLICATION_JSON_VALUE) |
| | | Map<String, Object> reportInOutResult(@RequestBody InOutResultReportParam body); |
| | | /**9.1.1 立库入库任务完成反馈 */ |
| | | @PostMapping(value = "/dapilc/restful/service/ilcwmsplus/IKWebService/cusInventoryCompletionReport", consumes = MediaType.APPLICATION_JSON_VALUE) |
| | | Map<String, Object> cusInventoryCompletionReport(@RequestBody DapIlcwmsCompletionRequest body); |
| | | |
| | | /** 9.2 库存调整主动上报 */ |
| | | /**9.1.2 立库出库任务完成反馈 */ |
| | | @PostMapping(value = "/dapilc/restful/service/ilcwmsplus/IKWebService/cusOutboundCompletionReport", consumes = MediaType.APPLICATION_JSON_VALUE) |
| | | Map<String, Object> cusOutboundCompletionReport(@RequestBody DapIlcwmsCompletionRequest body); |
| | | |
| | | /** 9.2 库存调整主动上报(路径不变;body 与入出库一致的 data 数组结构) */ |
| | | @PostMapping(value = "/api/report/inventoryAdjust", consumes = MediaType.APPLICATION_JSON_VALUE) |
| | | Map<String, Object> reportInventoryAdjust(@RequestBody InventoryAdjustReportParam body); |
| | | Map<String, Object> reportInventoryAdjust(@RequestBody DapIlcwmsCompletionRequest body); |
| | | |
| | | /** 物料基础信息同步 */ |
| | | @PostMapping(value = "/api/mat/sync", consumes = MediaType.APPLICATION_JSON_VALUE) |
| | |
| | | package com.vincent.rsf.server.api.feign.fallback; |
| | | |
| | | import com.vincent.rsf.server.api.controller.erp.params.InOutResultReportParam; |
| | | import com.vincent.rsf.server.api.controller.erp.params.InventoryAdjustReportParam; |
| | | import com.vincent.rsf.server.api.controller.erp.params.DapIlcwmsCompletionRequest; |
| | | import com.vincent.rsf.server.api.feign.CloudWmsErpFeignClient; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.springframework.stereotype.Component; |
| | |
| | | } |
| | | |
| | | @Override |
| | | public Map<String, Object> reportInOutResult(InOutResultReportParam body) { |
| | | log.error("调用云仓WMS 入/出库结果上报接口失败,触发降级", cause); |
| | | public Map<String, Object> cusInventoryCompletionReport(DapIlcwmsCompletionRequest body) { |
| | | log.error("调用云仓 入库完成反馈失败,触发降级", cause); |
| | | return errorResponse(); |
| | | } |
| | | |
| | | @Override |
| | | public Map<String, Object> reportInventoryAdjust(InventoryAdjustReportParam body) { |
| | | log.error("调用云仓WMS 库存调整上报接口失败,触发降级", cause); |
| | | public Map<String, Object> cusOutboundCompletionReport(DapIlcwmsCompletionRequest body) { |
| | | log.error("调用云仓 出库完成反馈失败,触发降级", cause); |
| | | return errorResponse(); |
| | | } |
| | | |
| | | @Override |
| | | public Map<String, Object> reportInventoryAdjust(DapIlcwmsCompletionRequest body) { |
| | | log.error("调用云仓 9.2 库存调整上报失败,触发降级", cause); |
| | | return errorResponse(); |
| | | } |
| | | |
| New file |
| | |
| | | package com.vincent.rsf.server.api.integration.dap; |
| | | |
| | | import java.util.HashMap; |
| | | import java.util.Map; |
| | | |
| | | /** |
| | | * 鼎捷 DAP 返回结构转云仓上报内部统一格式(供重试任务判断 code==200) |
| | | */ |
| | | public final class DapIlcwmsResponseNormalizer { |
| | | |
| | | private DapIlcwmsResponseNormalizer() { |
| | | } |
| | | |
| | | public static Map<String, Object> toNotifyFormat(Map<String, Object> dapBody) { |
| | | if (dapBody == null) { |
| | | return resultMap(500, "云仓返回空", null); |
| | | } |
| | | Object st = dapBody.get("status"); |
| | | int status = 0; |
| | | if (st instanceof Number) { |
| | | status = ((Number) st).intValue(); |
| | | } |
| | | if (status == 200) { |
| | | return resultMap(200, "OK", dapBody); |
| | | } |
| | | Object err = dapBody.get("errorMessage"); |
| | | String msg = err != null ? String.valueOf(err) : String.valueOf(dapBody.get("statusDescription")); |
| | | if (msg == null || "null".equals(msg)) { |
| | | msg = "云仓返回失败"; |
| | | } |
| | | return resultMap(500, msg, dapBody); |
| | | } |
| | | |
| | | /** |
| | | * 9.2 等仍走旧路径的接口:若返回无顶层 status,则按原 code/msg 透传(兼容旧云仓 JSON)。 |
| | | */ |
| | | public static Map<String, Object> toNotifyFormatFlexible(Map<String, Object> body) { |
| | | if (body == null) { |
| | | return toNotifyFormat(null); |
| | | } |
| | | if (body.containsKey("status")) { |
| | | return toNotifyFormat(body); |
| | | } |
| | | if (body.containsKey("code")) { |
| | | return body; |
| | | } |
| | | return toNotifyFormat(body); |
| | | } |
| | | |
| | | private static Map<String, Object> resultMap(int code, String msg, Map<String, Object> data) { |
| | | Map<String, Object> map = new HashMap<>(); |
| | | map.put("code", code); |
| | | map.put("msg", msg); |
| | | map.put("data", data); |
| | | return map; |
| | | } |
| | | } |
| | |
| | | package com.vincent.rsf.server.api.service.impl; |
| | | |
| | | import com.vincent.rsf.server.api.config.RemotesInfoProperties; |
| | | import com.vincent.rsf.server.api.controller.erp.params.DapIlcwmsCompletionLine; |
| | | import com.vincent.rsf.server.api.controller.erp.params.DapIlcwmsCompletionRequest; |
| | | import com.vincent.rsf.server.api.controller.erp.params.InOutResultReportParam; |
| | | import com.vincent.rsf.server.api.controller.erp.params.InventoryAdjustReportParam; |
| | | import com.vincent.rsf.server.api.feign.CloudWmsErpFeignClient; |
| | | import com.vincent.rsf.server.api.integration.dap.DapIlcwmsResponseNormalizer; |
| | | import com.vincent.rsf.server.api.service.CloudWmsReportService; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.apache.commons.lang3.StringUtils; |
| | | import org.springframework.beans.factory.annotation.Autowired; |
| | | import org.springframework.stereotype.Service; |
| | | |
| | | import java.util.ArrayList; |
| | | import java.util.Collections; |
| | | import java.util.HashMap; |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | |
| | | /** |
| | | * 立库侧请求云仓:入/出库结果上报(9.1)、库存调整主动上报(9.2)、物料基础信息同步。 |
| | | * 使用 OpenFeign 调用;可选 HttpEntity(RestTemplate) 方式已注释保留。 |
| | | * 立库侧请求云仓:9.1 鼎捷 ilcwmsplus 入/出库两接口;9.2 仍为 /api/report/inventoryAdjust,报文与 9.1 同为 {data:[]}。 |
| | | */ |
| | | @Slf4j |
| | | @Service |
| | |
| | | private RemotesInfoProperties erpApi; |
| | | |
| | | @Autowired |
| | | private RemotesInfoProperties.ApiInfo erpApiInfo; |
| | | |
| | | @Autowired |
| | | private CloudWmsErpFeignClient cloudWmsErpFeignClient; |
| | | |
| | | /** |
| | | * 可选:改用 HttpEntity(RestTemplate) 调用云仓时启用。 |
| | | */ |
| | | // @Autowired |
| | | // private RestTemplate restTemplate; |
| | | |
| | | @Override |
| | | public Map<String, Object> syncMatnrsToCloud(Object body) { |
| | |
| | | log.warn("ErpApi(云仓WMS) 未配置 host,跳过 9.1 入/出库结果上报,订单:{}", param.getOrderNo()); |
| | | return stubSuccess("云仓地址未配置,未实际上报"); |
| | | } |
| | | return cloudWmsErpFeignClient.reportInOutResult(param); |
| | | String err = validateDapBase(); |
| | | if (err != null) { |
| | | return resultMap(400, err, null); |
| | | } |
| | | boolean inbound = param.getInbound() == null || Boolean.TRUE.equals(param.getInbound()); |
| | | DapIlcwmsCompletionRequest req = new DapIlcwmsCompletionRequest() |
| | | .setData(Collections.singletonList(buildInOutLine(param, inbound))); |
| | | Map<String, Object> raw = inbound |
| | | ? cloudWmsErpFeignClient.cusInventoryCompletionReport(req) |
| | | : cloudWmsErpFeignClient.cusOutboundCompletionReport(req); |
| | | return DapIlcwmsResponseNormalizer.toNotifyFormat(raw); |
| | | } |
| | | |
| | | @Override |
| | |
| | | log.warn("ErpApi(云仓WMS) 未配置 host,跳过 9.2 库存调整上报,物料:{}", param.getMatNr()); |
| | | return stubSuccess("云仓地址未配置,未实际上报"); |
| | | } |
| | | return cloudWmsErpFeignClient.reportInventoryAdjust(param); |
| | | String err = validateDapBase(); |
| | | if (err != null) { |
| | | return resultMap(400, err, null); |
| | | } |
| | | Integer changeType = param.getChangeType(); |
| | | if (changeType == null) { |
| | | return resultMap(400, "changeType 不能为空", null); |
| | | } |
| | | DapIlcwmsCompletionRequest req = new DapIlcwmsCompletionRequest(); |
| | | if (changeType == 3) { |
| | | String baseSeq = StringUtils.isNotBlank(param.getDocSeqNo()) ? param.getDocSeqNo() : "1"; |
| | | List<DapIlcwmsCompletionLine> lines = new ArrayList<>(2); |
| | | lines.add(buildAdjustLine(param, false, true, baseSeq + "-O")); |
| | | lines.add(buildAdjustLine(param, true, false, baseSeq + "-I")); |
| | | req.setData(lines); |
| | | } else if (changeType == 1) { |
| | | req.setData(Collections.singletonList(buildAdjustLine(param, true, false, null))); |
| | | } else if (changeType == 2) { |
| | | req.setData(Collections.singletonList(buildAdjustLine(param, false, true, null))); |
| | | } else { |
| | | return resultMap(400, "不支持的 changeType:" + changeType, null); |
| | | } |
| | | Map<String, Object> raw = cloudWmsErpFeignClient.reportInventoryAdjust(req); |
| | | return DapIlcwmsResponseNormalizer.toNotifyFormatFlexible(raw); |
| | | } |
| | | |
| | | private DapIlcwmsCompletionLine buildInOutLine(InOutResultReportParam param, boolean inbound) { |
| | | RemotesInfoProperties.Dap dap = erpApi.getDap(); |
| | | DapIlcwmsCompletionLine line = new DapIlcwmsCompletionLine() |
| | | .setOrgNo(dap.getOrgNo()) |
| | | .setDocType(inbound ? dap.getDocTypeIn() : dap.getDocTypeOut()) |
| | | .setDocNo(param.getOrderNo()) |
| | | .setDocSeqNo(StringUtils.isNotBlank(param.getLineId()) ? param.getLineId() : "1") |
| | | .setItemNo(param.getMatNr()) |
| | | .setQty(parseQty(param.getQty())) |
| | | .setUnitNo(dap.getUnitNo()) |
| | | .setCombinationLotNo(param.getBatch()) |
| | | .setBarcode(resolveBarcode(param)); |
| | | if (inbound) { |
| | | line.setInWarehouseNo(param.getWareHouseId()).setInCellNo(param.getLocId()); |
| | | } else { |
| | | line.setOutWarehouseNo(param.getWareHouseId()).setOutCellNo(param.getLocId()); |
| | | } |
| | | return line; |
| | | } |
| | | |
| | | /** |
| | | * @param fillIn 是否填入库储位 |
| | | * @param fillOut 是否填出库储位 |
| | | * @param docSeqOverride 非空时用作项次(移库第二行等) |
| | | */ |
| | | private DapIlcwmsCompletionLine buildAdjustLine(InventoryAdjustReportParam param, boolean fillIn, boolean fillOut, String docSeqOverride) { |
| | | RemotesInfoProperties.Dap dap = erpApi.getDap(); |
| | | String docType = resolveAdjustDocType(param, dap); |
| | | String docNo = StringUtils.isNotBlank(param.getDocNo()) ? param.getDocNo() : "ADJ"; |
| | | String docSeq = docSeqOverride != null ? docSeqOverride |
| | | : (StringUtils.isNotBlank(param.getDocSeqNo()) ? param.getDocSeqNo() : "1"); |
| | | String unit = StringUtils.isNotBlank(param.getUnitNo()) ? param.getUnitNo() : dap.getUnitNo(); |
| | | DapIlcwmsCompletionLine line = new DapIlcwmsCompletionLine() |
| | | .setOrgNo(dap.getOrgNo()) |
| | | .setDocType(docType) |
| | | .setDocNo(docNo) |
| | | .setDocSeqNo(docSeq) |
| | | .setItemNo(param.getMatNr()) |
| | | .setQty(parseQty(param.getQty())) |
| | | .setUnitNo(unit) |
| | | .setCombinationLotNo(param.getBatch()) |
| | | .setBarcode(resolveAdjustBarcode(param)); |
| | | if (fillIn) { |
| | | line.setInWarehouseNo(param.getWareHouseId()); |
| | | line.setInCellNo(StringUtils.isNotBlank(param.getTargetLocId()) ? param.getTargetLocId() : param.getSourceLocId()); |
| | | } |
| | | if (fillOut) { |
| | | line.setOutWarehouseNo(param.getWareHouseId()); |
| | | line.setOutCellNo(StringUtils.isNotBlank(param.getSourceLocId()) ? param.getSourceLocId() : param.getTargetLocId()); |
| | | } |
| | | return line; |
| | | } |
| | | |
| | | private static String resolveAdjustDocType(InventoryAdjustReportParam param, RemotesInfoProperties.Dap dap) { |
| | | if (StringUtils.isNotBlank(param.getDocType())) { |
| | | return param.getDocType(); |
| | | } |
| | | Integer ct = param.getChangeType(); |
| | | if (ct != null && ct == 2) { |
| | | return dap.getDocTypeOut(); |
| | | } |
| | | if (ct != null && ct == 3) { |
| | | return dap.getDocTypeAdj(); |
| | | } |
| | | return dap.getDocTypeIn(); |
| | | } |
| | | |
| | | private static String resolveBarcode(InOutResultReportParam param) { |
| | | if (StringUtils.isNotBlank(param.getBarcode())) { |
| | | return param.getBarcode(); |
| | | } |
| | | if (StringUtils.isNotBlank(param.getPalletId())) { |
| | | return param.getPalletId(); |
| | | } |
| | | if (param.getMatNr() != null && param.getLocId() != null) { |
| | | return param.getMatNr() + ":" + param.getLocId(); |
| | | } |
| | | return param.getMatNr(); |
| | | } |
| | | |
| | | private static String resolveAdjustBarcode(InventoryAdjustReportParam param) { |
| | | if (StringUtils.isNotBlank(param.getBarcode())) { |
| | | return param.getBarcode(); |
| | | } |
| | | if (StringUtils.isNotBlank(param.getPalletId())) { |
| | | return param.getPalletId(); |
| | | } |
| | | return param.getMatNr(); |
| | | } |
| | | |
| | | private static Double parseQty(String q) { |
| | | if (StringUtils.isBlank(q)) { |
| | | return 0d; |
| | | } |
| | | try { |
| | | return Double.parseDouble(q.trim()); |
| | | } catch (NumberFormatException e) { |
| | | return 0d; |
| | | } |
| | | } |
| | | |
| | | private String validateDapBase() { |
| | | RemotesInfoProperties.Dap d = erpApi.getDap(); |
| | | if (d == null || StringUtils.isBlank(d.getOrgNo())) { |
| | | return "未配置 platform.erp.dap.org-no"; |
| | | } |
| | | return null; |
| | | } |
| | | |
| | | private boolean isCloudWmsConfigured() { |
| | |
| | | map.put("data", data); |
| | | return map; |
| | | } |
| | | |
| | | // ========== 可选:HttpEntity(RestTemplate) 方式(当前未使用) ========== |
| | | // 启用步骤:1)取消上方 restTemplate 的 @Autowired 注入; |
| | | // 2)取消下面整段注释,恢复 buildUrl、postToCloudWms、parseResponse 方法及 OBJECT_MAPPER; |
| | | // 3)在 syncMatnrsToCloud/reportInOutResult/reportInventoryAdjust 中改为:String url = buildUrl(erpApiInfo.getXxxPath()); if (url == null) return stubSuccess(...); return postToCloudWms(url, body); |
| | | // |
| | | // private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); |
| | | // private String buildUrl(String path) { ... } |
| | | // private Map<String, Object> postToCloudWms(String url, Object body) { HttpHeaders headers = ...; HttpEntity<Object> entity = new HttpEntity<>(body, headers); ResponseEntity<String> response = restTemplate.exchange(url, HttpMethod.POST, entity, String.class); return parseResponse(response.getBody()); } |
| | | // private Map<String, Object> parseResponse(String json) { ... } |
| | | } |
| | |
| | | .setSourceLocId(sourceLoc.getCode()) |
| | | .setTargetLocId(finalLoc.getCode()) |
| | | .setMatNr(logItem.getMatnrCode()) |
| | | .setQty(logItem.getReviseQty() != null ? String.valueOf(logItem.getReviseQty()) : "0"); |
| | | .setQty(logItem.getReviseQty() != null ? String.valueOf(logItem.getReviseQty()) : "0") |
| | | .setDocNo(revise.getCode()) |
| | | .setDocSeqNo(logItem.getId() != null ? String.valueOf(logItem.getId()) : "1") |
| | | .setBatch(logItem.getBatch()) |
| | | .setUnitNo(logItem.getUnit()) |
| | | .setBarcode(logItem.getMatnrCode() != null && logItem.getLocCode() != null |
| | | ? logItem.getMatnrCode() + "@" + logItem.getLocCode() : logItem.getMatnrCode()); |
| | | String requestBody = objectMapper.writeValueAsString(param); |
| | | Date now = new Date(); |
| | | CloudWmsNotifyLog notifyLog = new CloudWmsNotifyLog() |
| | |
| | | .setLocId(locId) |
| | | .setMatNr(item.getMatnrCode()) |
| | | .setQty(item.getAnfme() != null ? String.valueOf(item.getAnfme()) : "0") |
| | | .setBatch(item.getBatch()); |
| | | .setBatch(item.getBatch()) |
| | | .setInbound(isInbound) |
| | | .setBarcode(task.getBarcode()); |
| | | try { |
| | | String requestBody = om.writeValueAsString(param); |
| | | CloudWmsNotifyLog notifyLog = new CloudWmsNotifyLog() |
| New file |
| | |
| | | package com.vincent.rsf.server.system.controller; |
| | | |
| | | import com.vincent.rsf.framework.common.R; |
| | | import com.vincent.rsf.httpaudit.entity.HttpAuditLog; |
| | | import com.vincent.rsf.server.common.domain.BaseParam; |
| | | import com.vincent.rsf.server.common.domain.PageParam; |
| | | import com.vincent.rsf.server.system.service.HttpAuditLogService; |
| | | import org.springframework.beans.factory.annotation.Autowired; |
| | | import org.springframework.security.access.prepost.PreAuthorize; |
| | | import org.springframework.web.bind.annotation.*; |
| | | |
| | | import java.util.Arrays; |
| | | import java.util.Map; |
| | | |
| | | @RestController |
| | | public class HttpAuditLogController extends BaseController { |
| | | |
| | | @Autowired |
| | | private HttpAuditLogService httpAuditLogService; |
| | | |
| | | @PreAuthorize("hasAuthority('system:httpAuditLog:list')") |
| | | @PostMapping("/httpAuditLog/page") |
| | | public R page(@RequestBody Map<String, Object> map) { |
| | | BaseParam baseParam = buildParam(map, BaseParam.class); |
| | | PageParam<HttpAuditLog, BaseParam> pageParam = new PageParam<>(baseParam, HttpAuditLog.class); |
| | | return R.ok().add(httpAuditLogService.page(pageParam, pageParam.buildWrapper(true))); |
| | | } |
| | | |
| | | @PreAuthorize("hasAuthority('system:httpAuditLog:list')") |
| | | @GetMapping("/httpAuditLog/{id}") |
| | | public R get(@PathVariable("id") Long id) { |
| | | return R.ok().add(httpAuditLogService.getById(id)); |
| | | } |
| | | |
| | | @PreAuthorize("hasAuthority('system:httpAuditLog:remove')") |
| | | @PostMapping("/httpAuditLog/remove/{ids}") |
| | | public R remove(@PathVariable Long[] ids) { |
| | | if (!httpAuditLogService.removeByIds(Arrays.asList(ids))) { |
| | | return R.error("Delete Fail"); |
| | | } |
| | | return R.ok("Delete Success"); |
| | | } |
| | | } |
| New file |
| | |
| | | package com.vincent.rsf.server.system.service; |
| | | |
| | | import com.baomidou.mybatisplus.extension.service.IService; |
| | | import com.vincent.rsf.httpaudit.entity.HttpAuditLog; |
| | | |
| | | public interface HttpAuditLogService extends IService<HttpAuditLog> { |
| | | } |
| New file |
| | |
| | | package com.vincent.rsf.server.system.service.impl; |
| | | |
| | | import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; |
| | | import com.vincent.rsf.httpaudit.entity.HttpAuditLog; |
| | | import com.vincent.rsf.httpaudit.mapper.HttpAuditLogMapper; |
| | | import com.vincent.rsf.server.system.service.HttpAuditLogService; |
| | | import org.springframework.stereotype.Service; |
| | | |
| | | @Service |
| | | public class HttpAuditLogServiceImpl extends ServiceImpl<HttpAuditLogMapper, HttpAuditLog> implements HttpAuditLogService { |
| | | } |
| | |
| | | port: 8080 |
| | | #接品链接前缀 |
| | | pre-path: "" |
| | | # Feign 调用云仓时的根地址。云仓未提供 URL 时可用本机模拟:http://127.0.0.1:8086/rsf-server(本服务提供的 CloudWmsMockController) |
| | | # Feign 调用云仓时的根地址。本机模拟:http://127.0.0.1:8086/rsf-server(CloudWmsMockController:/dapilc/.../cusInventoryCompletionReport 等) |
| | | base-url: http://127.0.0.1:8086/rsf-server |
| | | #接口明细(立库侧请求云仓时使用的路径) |
| | | # 鼎捷 DAP ilcwmsplus 完成反馈(9.1/9.2 组包用) |
| | | dap: |
| | | org-no: "" |
| | | doc-type-in: "" |
| | | doc-type-out: "" |
| | | doc-type-adj: "" |
| | | unit-no: PCS |
| | | #接口明细(质检等;入出库完成已固定为 /dapilc/restful/service/ilcwmsplus/IKWebService/...) |
| | | api: |
| | | notify-inspect: /report/inspect |
| | | in-out-result-path: /api/report/inOutResult |
| | | in-out-result-path: /dapilc/restful/service/ilcwmsplus/IKWebService/cusInventoryCompletionReport |
| | | inventory-adjust-path: /api/report/inventoryAdjust |
| | | mat-sync-path: /api/mat/sync |
| | | rcs: |
| | |
| | | flagAvailable: true |
| | | #判断是否校验合格后,才允许收货 |
| | | flagReceiving: false |
| | | |
| | | # HTTP 接口审计(rsf-http-audit,引入依赖即生效,可 enabled=false 关闭) |
| | | http-audit: |
| | | enabled: true |
| | | query-response-max-chars: 500 |
| | | max-response-store-chars: 65535 |
| | | path-descriptions: |
| | | "/erp/order": "云仓-订单查询" |
| | | "/erp/order/add": "云仓-单据下发" |
| | | "/erp/order/addAll": "云仓-批量单据下发" |
| | | "/erp/order/cancel": "云仓-取消单据" |
| New file |
| | |
| | | -- HTTP 接口审计菜单(系统管理下,与操作日志同级);执行前请确认 id 210-212 未被占用 |
| | | 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 210, 'menu.httpAuditLog', 1, 'menu.system', '1,210', 'menu.httpAuditLog', '/system/httpAuditLog', 'httpAuditLog', NULL, NULL, 0, NULL, 'Http', 6, NULL, 1, 1, 0, NULL, NULL, NULL, NULL, NULL |
| | | FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `sys_menu` WHERE `id` = 210); |
| | | |
| | | 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 211, 'Query HttpAuditLog', 210, '', '1,210,211', NULL, NULL, NULL, NULL, NULL, 1, 'system:httpAuditLog:list', NULL, 0, NULL, 1, 1, 0, NULL, NULL, NULL, NULL, NULL |
| | | FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `sys_menu` WHERE `id` = 211); |
| | | |
| | | 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 212, 'Delete HttpAuditLog', 210, '', '1,210,212', NULL, NULL, NULL, NULL, NULL, 1, 'system:httpAuditLog:remove', NULL, 1, NULL, 1, 1, 0, NULL, NULL, NULL, NULL, NULL |
| | | FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `sys_menu` WHERE `id` = 212); |
| | | |
| | | -- 超级管理员角色(role_id=1)授权菜单(若已存在则跳过) |
| | | INSERT INTO `sys_role_menu` (`role_id`, `menu_id`) |
| | | SELECT 1, 210 FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `sys_role_menu` WHERE `role_id` = 1 AND `menu_id` = 210); |
| | | INSERT INTO `sys_role_menu` (`role_id`, `menu_id`) |
| | | SELECT 1, 211 FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `sys_role_menu` WHERE `role_id` = 1 AND `menu_id` = 211); |
| | | INSERT INTO `sys_role_menu` (`role_id`, `menu_id`) |
| | | SELECT 1, 212 FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `sys_role_menu` WHERE `role_id` = 1 AND `menu_id` = 212); |
| New file |
| | |
| | | -- HTTP 接口审计(rsf-http-audit 插件写入) |
| | | SET NAMES utf8mb4; |
| | | |
| | | DROP TABLE IF EXISTS `sys_http_audit_log`; |
| | | CREATE TABLE `sys_http_audit_log` ( |
| | | `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键', |
| | | `service_name` varchar(64) DEFAULT NULL COMMENT '应用 spring.application.name', |
| | | `scope_type` varchar(16) NOT NULL COMMENT 'EXTERNAL 外部 / INTERNAL 内部', |
| | | `uri` varchar(512) NOT NULL COMMENT '请求路径', |
| | | `method` varchar(16) DEFAULT NULL COMMENT 'HTTP 方法', |
| | | `function_desc` varchar(255) DEFAULT NULL COMMENT '功能描述', |
| | | `query_string` varchar(2048) DEFAULT NULL COMMENT 'QueryString', |
| | | `request_body` longtext COMMENT '请求体(全量)', |
| | | `response_body` longtext COMMENT '响应体(查询类或超长会截断)', |
| | | `response_truncated` tinyint(4) NOT NULL DEFAULT '0' COMMENT '1 响应已截断', |
| | | `http_status` int(11) DEFAULT NULL COMMENT 'HTTP 状态码', |
| | | `ok_flag` tinyint(4) NOT NULL DEFAULT '0' COMMENT '1 正常 0 异常', |
| | | `spend_ms` int(11) DEFAULT NULL COMMENT '耗时毫秒', |
| | | `client_ip` varchar(64) DEFAULT NULL COMMENT '客户端 IP', |
| | | `error_message` text COMMENT '异常摘要', |
| | | `create_time` datetime DEFAULT NULL COMMENT '创建时间', |
| | | `deleted` tinyint(4) NOT NULL DEFAULT '0' COMMENT '逻辑删除', |
| | | PRIMARY KEY (`id`), |
| | | KEY `idx_create_time` (`create_time`), |
| | | KEY `idx_uri` (`uri`(191)), |
| | | KEY `idx_ok_client` (`ok_flag`,`client_ip`) |
| | | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='HTTP接口审计'; |