cl
4 小时以前 52e09a6b7b7054fc51b9d4bf5f1fbec0a57e60f1
云仓回报调整
20个文件已添加
17个文件已修改
1333 ■■■■■ 已修改文件
pom.xml 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/i18n/en.js 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/i18n/zh.js 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/ResourceContent.js 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/system/httpAuditLog/HttpAuditLogList.jsx 82 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/system/httpAuditLog/HttpAuditLogShow.jsx 59 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/system/httpAuditLog/index.jsx 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-http-audit/pom.xml 45 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-http-audit/src/main/java/com/vincent/rsf/httpaudit/config/HttpAuditAutoConfiguration.java 50 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-http-audit/src/main/java/com/vincent/rsf/httpaudit/entity/HttpAuditLog.java 67 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-http-audit/src/main/java/com/vincent/rsf/httpaudit/mapper/HttpAuditLogMapper.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-http-audit/src/main/java/com/vincent/rsf/httpaudit/props/HttpAuditProperties.java 63 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-http-audit/src/main/java/com/vincent/rsf/httpaudit/service/HttpAuditAsyncRecorder.java 57 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-http-audit/src/main/java/com/vincent/rsf/httpaudit/support/HttpAuditSupport.java 122 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-http-audit/src/main/java/com/vincent/rsf/httpaudit/web/HttpAuditFilter.java 126 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-http-audit/src/main/resources/META-INF/spring.factories 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-open-api/pom.xml 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-open-api/src/main/resources/application-dev.yml 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/pom.xml 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/config/RemotesInfoProperties.java 21 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/controller/CloudWmsMockController.java 75 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/controller/erp/params/DapIlcwmsCompletionLine.java 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/controller/erp/params/DapIlcwmsCompletionRequest.java 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/controller/erp/params/InOutResultReportParam.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/controller/erp/params/InventoryAdjustReportParam.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/feign/CloudWmsErpFeignClient.java 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/feign/fallback/CloudWmsErpFeignClientFallback.java 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/integration/dap/DapIlcwmsResponseNormalizer.java 57 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/service/impl/CloudWmsReportServiceImpl.java 175 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/manager/service/impl/ReviseLogServiceImpl.java 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/manager/service/impl/TaskServiceImpl.java 4 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/controller/HttpAuditLogController.java 43 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/service/HttpAuditLogService.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/service/impl/HttpAuditLogServiceImpl.java 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/resources/application-dev.yml 26 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
version/db/http_audit_menu.sql 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
version/db/sys_http_audit_log.sql 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pom.xml
@@ -19,6 +19,7 @@
    <modules>
        <module>rsf-common</module>
        <module>rsf-framework</module>
        <module>rsf-http-audit</module>
        <module>rsf-server</module>
        <module>rsf-open-api</module>
    </modules>
rsf-admin/src/i18n/en.js
@@ -3,6 +3,8 @@
const customEnglishMessages = {
    ...englishMessages,
    hello: 'Hello World',
    'menu.httpAuditLog': 'HTTP audit',
    'resources.httpAuditLog.name': 'HTTP audit',
    common: {
        response: {
            success: "Success",
@@ -148,6 +150,7 @@
        department: 'Department',
        token: 'Token',
        operation: 'Operation',
        httpAuditLog: 'HTTP audit',
        config: 'Config',
        tenant: 'Tenant',
        userLogin: 'Token',
@@ -336,6 +339,22 @@
                    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",
rsf-admin/src/i18n/zh.js
@@ -3,8 +3,10 @@
const customChineseMessages = {
    ...chineseMessages,
    hello: '你好世界',
    'menu.httpAuditLog': 'HTTP接口审计',
    resources: {
        config: { name: '配置参数' },
        httpAuditLog: { name: 'HTTP接口审计' },
        asnOrderItem: { name: '收货明细' },
        outStockItem: { name: '出库单明细' },
    },
@@ -157,6 +159,7 @@
        department: '部门管理',
        token: '登录日志',
        operation: '操作日志',
        httpAuditLog: 'HTTP接口审计',
        config: '配置参数',
        tenant: '租户管理',
        userLogin: '登录日志',
@@ -368,6 +371,22 @@
                    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",
rsf-admin/src/page/ResourceContent.js
@@ -68,6 +68,7 @@
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) {
@@ -199,6 +200,8 @@
      return rcsTest;
    case "openApiApp":
      return openApiApp;
    case "httpAuditLog":
      return httpAuditLog;
    default:
      return {
        list: ListGuesser,
rsf-admin/src/page/system/httpAuditLog/HttpAuditLogList.jsx
New file
@@ -0,0 +1,82 @@
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;
rsf-admin/src/page/system/httpAuditLog/HttpAuditLogShow.jsx
New file
@@ -0,0 +1,59 @@
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;
rsf-admin/src/page/system/httpAuditLog/index.jsx
New file
@@ -0,0 +1,8 @@
import HttpAuditLogList from "./HttpAuditLogList";
import HttpAuditLogShow from "./HttpAuditLogShow";
export default {
    list: HttpAuditLogList,
    show: HttpAuditLogShow,
    recordRepresentation: (record) => `${record.uri || record.id}`,
};
rsf-http-audit/pom.xml
New file
@@ -0,0 +1,45 @@
<?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>
rsf-http-audit/src/main/java/com/vincent/rsf/httpaudit/config/HttpAuditAutoConfiguration.java
New file
@@ -0,0 +1,50 @@
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;
    }
}
rsf-http-audit/src/main/java/com/vincent/rsf/httpaudit/entity/HttpAuditLog.java
New file
@@ -0,0 +1,67 @@
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;
}
rsf-http-audit/src/main/java/com/vincent/rsf/httpaudit/mapper/HttpAuditLogMapper.java
New file
@@ -0,0 +1,9 @@
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> {
}
rsf-http-audit/src/main/java/com/vincent/rsf/httpaudit/props/HttpAuditProperties.java
New file
@@ -0,0 +1,63 @@
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;
    }
}
rsf-http-audit/src/main/java/com/vincent/rsf/httpaudit/service/HttpAuditAsyncRecorder.java
New file
@@ -0,0 +1,57 @@
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);
            }
        }
    }
}
rsf-http-audit/src/main/java/com/vincent/rsf/httpaudit/support/HttpAuditSupport.java
New file
@@ -0,0 +1,122 @@
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() + ")";
    }
}
rsf-http-audit/src/main/java/com/vincent/rsf/httpaudit/web/HttpAuditFilter.java
New file
@@ -0,0 +1,126 @@
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);
    }
}
rsf-http-audit/src/main/resources/META-INF/spring.factories
New file
@@ -0,0 +1,2 @@
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.vincent.rsf.httpaudit.config.HttpAuditAutoConfiguration
rsf-open-api/pom.xml
@@ -21,6 +21,11 @@
            <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>
rsf-open-api/src/main/resources/application-dev.yml
@@ -72,3 +72,8 @@
    host: http://127.0.0.1
    #端口
    port: 3741
http-audit:
  enabled: true
  query-response-max-chars: 500
  max-response-store-chars: 65535
rsf-server/pom.xml
@@ -26,6 +26,11 @@
            <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>
rsf-server/src/main/java/com/vincent/rsf/server/api/config/RemotesInfoProperties.java
@@ -35,15 +35,30 @@
     */
    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";
rsf-server/src/main/java/com/vincent/rsf/server/api/controller/CloudWmsMockController.java
@@ -1,31 +1,44 @@
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<>();
@@ -35,33 +48,39 @@
        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();
//    }
}
rsf-server/src/main/java/com/vincent/rsf/server/api/controller/erp/params/DapIlcwmsCompletionLine.java
New file
@@ -0,0 +1,30 @@
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;
}
rsf-server/src/main/java/com/vincent/rsf/server/api/controller/erp/params/DapIlcwmsCompletionRequest.java
New file
@@ -0,0 +1,20 @@
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;
}
rsf-server/src/main/java/com/vincent/rsf/server/api/controller/erp/params/InOutResultReportParam.java
@@ -39,4 +39,10 @@
    @ApiModelProperty("批次")
    private String batch;
    @ApiModelProperty(value = "true 入库完成反馈,false 出库完成反馈", required = true)
    private Boolean inbound;
    @ApiModelProperty(value = "条码(云仓必填)")
    private String barcode;
}
rsf-server/src/main/java/com/vincent/rsf/server/api/controller/erp/params/InventoryAdjustReportParam.java
@@ -34,4 +34,22 @@
    @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;
}
rsf-server/src/main/java/com/vincent/rsf/server/api/feign/CloudWmsErpFeignClient.java
@@ -1,7 +1,6 @@
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;
@@ -11,7 +10,7 @@
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(
@@ -21,13 +20,17 @@
)
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)
rsf-server/src/main/java/com/vincent/rsf/server/api/feign/fallback/CloudWmsErpFeignClientFallback.java
@@ -1,7 +1,6 @@
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;
@@ -76,14 +75,20 @@
    }
    @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();
    }
rsf-server/src/main/java/com/vincent/rsf/server/api/integration/dap/DapIlcwmsResponseNormalizer.java
New file
@@ -0,0 +1,57 @@
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;
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/api/service/impl/CloudWmsReportServiceImpl.java
@@ -1,20 +1,26 @@
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
@@ -24,16 +30,7 @@
    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) {
@@ -53,7 +50,17 @@
            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
@@ -65,7 +72,139 @@
            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() {
@@ -86,14 +225,4 @@
        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) { ... }
}
rsf-server/src/main/java/com/vincent/rsf/server/manager/service/impl/ReviseLogServiceImpl.java
@@ -250,7 +250,13 @@
                                .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()
rsf-server/src/main/java/com/vincent/rsf/server/manager/service/impl/TaskServiceImpl.java
@@ -2747,7 +2747,9 @@
                        .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()
rsf-server/src/main/java/com/vincent/rsf/server/system/controller/HttpAuditLogController.java
New file
@@ -0,0 +1,43 @@
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");
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/system/service/HttpAuditLogService.java
New file
@@ -0,0 +1,7 @@
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> {
}
rsf-server/src/main/java/com/vincent/rsf/server/system/service/impl/HttpAuditLogServiceImpl.java
New file
@@ -0,0 +1,11 @@
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 {
}
rsf-server/src/main/resources/application-dev.yml
@@ -81,12 +81,19 @@
    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:
@@ -105,4 +112,15 @@
    #判断是后检验合格后,才允许上架
    flagAvailable: true
    #判断是否校验合格后,才允许收货
    flagReceiving: false
    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": "云仓-取消单据"
version/db/http_audit_menu.sql
New file
@@ -0,0 +1,22 @@
-- 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);
version/db/sys_http_audit_log.sql
New file
@@ -0,0 +1,27 @@
-- 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接口审计';