Files
workspace/code/g3soft-erp/server
2026-10-13 22:32:50 +08:00
..
u
2026-10-13 22:32:50 +08:00
u
2026-10-13 17:29:55 +08:00
u
2026-10-13 22:32:50 +08:00
u
2026-10-13 17:29:55 +08:00
u
2026-10-13 17:29:55 +08:00

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 没串起来,先检查这个格式约束。W3C traceparent 则要求标准 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 校验、排序子句构建;纯静态

三条约定:

  1. SqlText 不依赖任何人,所以依赖方向固定为 service → database → common,不成环。
  2. RowReader 之外的方法都接收调用方传入的 Connection(不自己开事务),事务边界留在服务层。
  3. 认证要按 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):

  1. 独立构建 —— mvn package 不触发前端构建,反之亦然
  2. 独立版本 —— tag 分开打(api-v1.2.0 / web-v1.2.0)
  3. 接口契约写进 docs/ 并版本化