9.7 KiB
server —— 后端
后端服务(Maven + Spring Boot)。现有代码是从原型 fms-api 迁入的元数据驱动引擎实现:
包名 cn.g3soft.erp,artifactId erp-api,启动类 ErpApiApplication。
构建 mvnw.cmd test;启动 start.cmd(spring-boot:run,默认 8088、context-path /api)。
迁入时未带入三项:流程引擎(
service/workflow、WorkflowController、WorkflowRuntimeController)、数据库迁移脚本(config/migrations、tools/migration)、 本地文件存储数据(data/files)。
已定的技术栈
| 项 | 选型 |
|---|---|
| 框架 | Spring Boot 4.1.0 |
| 语言 | Java 21 |
| Web | spring-boot-starter-webmvc |
| 持久层 | spring-boot-starter-jdbc(无 ORM) |
| 连接池 | Druid 1.2.18 |
| 数据库 | SQL Server(mssql-jdbc) |
| 鉴权 | JJWT 0.12.6 |
| Excel | Apache Fesod 2.0.2-incubating |
| 其他 | ip2region、ua-parser |
| 构建 | Maven |
IP 归属地
登录日志里的 IP 归属地走本地 ip2region 库,不联网。实测:加载 57ms,单次查询 14 微秒,
所以它不构成启动瓶颈(应用自身启动约 1.2~1.7s)。
部署须知:
ip2region.xdb(约 11MB)被.gitignore排除,不在仓库里,部署时必须手工放到src/main/resources/。漏放不会导致启动失败,只会让所有归属地静默变成「未知」。
内网 / 保留地址(RFC1918、回环、链路本地、IPv6 ULA)直接记「内网」,不外呼。
日志
输出与落盘
控制台输出彩色文本;文件输出结构化 JSON(每行一个对象,logstash 格式),默认落在 server/logs/
(相对进程工作目录的 ./logs,根 .gitignore 已忽略)。
| 文件 | 内容 |
|---|---|
logs/erp-api.jsonl |
全量日志 |
logs/erp-api-error.jsonl |
仅 ERROR |
logs/erp-api.<日期>.<序号>.jsonl.gz |
滚动归档(gzip) |
滚动策略:单文件 10MB、保留 30 天;主文件总量上限 1GB、错误文件 256MB。文件写入经 AsyncAppender,
队列满时背压而非丢日志,不阻塞请求线程。
配置在 src/main/resources/logback-spring.xml(appender / 滚动 / 编码器)+ application.yaml 的 logging 块(路径 / 格式 / 级别)。
必须命名为
logback-spring.xml:结构化编码器依赖 Spring 注入的Environment,改成logback.xml会启动失败。
请求追踪
每个请求分配一个 traceId(优先复用上游 X-Request-Id / W3C traceparent),并回写到响应头 X-Request-Id。
通过 MDC 注入,因此该请求的所有日志行都带同一 traceId,按它即可串起完整链路。
入参
X-Request-Id需匹配^[A-Za-z0-9._-]{8,64}$才会被复用,否则静默忽略并自行生成(防日志注入 / 换行伪造)。 若发现上游的 traceId 没串起来,先检查这个格式约束。W3Ctraceparent则要求标准00-<32hex>-<16hex>-<2hex>格式。
访问日志每请求一行(logger_name=ACCESS、message=http_access)。主要字段(均为 JSON 顶层字段):
| 字段 | 说明 |
|---|---|
@timestamp / level / logger_name / thread_name |
标准字段 |
traceId |
请求链路 ID |
method / uri |
uri 仅为 path,不含 query |
status / durationMs / slow |
HTTP 状态 / 耗时 / 是否超过慢请求阈值 |
orgId / userId |
鉴权通过后由 JwtAuthFilter 回写 MDC |
clientIp / userAgent |
客户端信息(userAgent 截断 256 字符) |
params |
请求参数,仅错误或慢请求时出现 |
stack_trace |
异常堆栈,仅错误行 |
注意:本项目异常统一返回 HTTP 200,错误码在响应 body 里。所以判断出错要看
level/erp-api-error.jsonl,不能看status。 另外,被JwtAuthFilter直接拒绝的请求(未登录 / 会话被顶)不会读取 body,因此没有params。
请求参数记录
请求参数(query + body)只在出错或慢请求时记录到 params,正常请求不记,避免刷屏与大 body 常态落盘;
文件上传(multipart / 二进制流)跳过,超出上限按 UTF-8 字节边界截断(不会切断中文)。
erp:
log:
slow-request-ms: 1000 # 超过则视为慢请求,记录 params
max-param-bytes: 2048 # 参数记录上限(字节),同时是请求体缓存上限
exclude-param-paths: # 这些路径完全不记参数(与 getServletPath() 精确匹配)
- /auth/login # 登录接口含明文密码,默认排除
不做脱敏:除
exclude-param-paths外,参数原样写入日志(含业务数据、明文密码)。 上线前请确认日志文件的访问权限与流转范围;如需收口,把对应接口加进排除名单即可。
分级与噪音
ERROR=系统故障、WARN=降级 / 预期内业务竞争、INFO=生命周期与访问、DEBUG=排障。
第三方噪音(Druid、SQL Server JDBC、Spring)默认降到 WARN,可在 application.yaml 的 logging.level.* 调整;
临时排障时可把 cn.g3soft.erp 调到 DEBUG。
级别以
application.yaml为准:logback-spring.xml里也给cn.g3soft.erp/ACCESS各写了一份INFO, 但 Boot 是在加载 logback 配置之后才应用logging.level.*,所以 YAML 会覆盖 XML。改级别请改 YAML, XML 里那两行只是兜底默认值。
查看与分析
- 图形界面:双击
server/log-viewer.html(Edge / Chrome),选择logs(或logs-demo)文件夹即可。 支持.jsonl与轮转.gz,本地解析、不上传。提供概览 / 错误 / 慢请求 / 按机构 / 按接口 / 状态码 / 按小时 / traceId 追踪。 - 命令行:活动文件直接
jq,归档用gzip -dc;Windows 亦可用自带 PowerShell(ConvertFrom-Json),无需安装 jq。 server/logs-demo/是模拟日志(已 gitignore),仅用于演示查看器,可随时删除。
数据访问层(cn.g3soft.erp.database)
database 下分两块,两者零互相引用(一边管"连接怎么来",一边管"拿到连接后怎么读写"):
| 包 / 类 | 职责 |
|---|---|
database/(根) |
通用数据访问,方法都走 JDBC、无 ORM |
database.MetadataReader |
表结构内省:loadTableMetadata / getColumn / describeColumns;列名归一化也在这里 |
database.RowReader |
只读查询与分页:loadData / loadDataBySql / page;唯一持有默认数据源 |
database.RowWriter |
通用写入与条件更新语义:insert / update / delete、$original / $expr / guard、零行原因定位 |
database.model |
纯数据载体,无任何逻辑:TableMetadata / ColumnMetadata / PageResult / GuardCondition / ZeroRowCause |
database.org |
多机构数据库连接:OrgContext(请求级机构/用户上下文)、OrgDatabaseProperties / OrgDatabaseConfig*(配置与热加载)、OrgDataSourceFactory / OrgDataSourceManager / OrgRoutingDataSource(装配与路由) |
common.util.SqlText |
SQL 文本与安全校验:标识符校验与加引号、只读 SQL 校验、排序子句构建;纯静态 |
三条约定:
SqlText不依赖任何人,所以依赖方向固定为service → database → common,不成环。RowReader之外的方法都接收调用方传入的Connection(不自己开事务),事务边界留在服务层。- 认证要按
orgId取该机构连接(登录时机构上下文尚未建立),所以那条查询在service.auth.UserAccountReader里,不在访问层。
分割现状与规划
现状(迁入的原型代码,已按职责归过一轮):
cn.g3soft.erp
├── common/ # 通用返回、异常、纯静态工具
│ ├── ApiResponse.java # HTTP 响应外壳 + 全部错误码常量
│ ├── exception/ # 6 个异常与 GlobalExceptionHandler
│ └── util/ # ParamUtils / SqlText / StringUtils / TimeUtils / RequestParams /
│ # UuidV7Generator + snowflake/(内嵌的雪花 ID 生成器)
├── config/ # AuthProperties / LogProperties / FileStorageProperties /
│ # JacksonConfig / JwtAuthFilter / RequestLoggingFilter
├── controller/ # 6 个 Controller
├── database/ # 数据访问层(见上):RowReader / RowWriter / MetadataReader + model/ + org/
└── service/
├── auth/ # 登录、会话、登录日志、JwtUtils,以及 client/(客户端信息采集)
├── data/ # 通用保存 / 删除 / 查询编排
├── export/ file/ formula/ permission/
规划(新代码按此落位,老代码后续按域重构):
cn.g3soft.erp
├── common/ # 通用返回、异常、工具 —— 已建
├── engine/ # 元数据驱动引擎(查询 / 保存 / 权限)—— 未设计;
│ # 现在的 database + service/{data,formula,permission} 是它的雏形
└── module/
├── ocean/ # 海运
├── air/ # 空运
├── container/ # 箱管
├── finance/ # 财务
├── crm/ # CRM
└── system/ # 系统管理
与前端的关系
唯一契约是 HTTP 接口。 前端不读后端源码,后端不管前端怎么调。
同仓的三条纪律(见根 README.md):
- 独立构建 ——
mvn package不触发前端构建,反之亦然 - 独立版本 —— tag 分开打(
api-v1.2.0/web-v1.2.0) - 接口契约写进
docs/并版本化