182 lines
9.7 KiB
Markdown
182 lines
9.7 KiB
Markdown
# 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` 库**,不联网。实测:加载 5~7ms,单次查询 1~4 微秒,
|
||
所以它不构成启动瓶颈(应用自身启动约 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 字节边界截断(不会切断中文)。
|
||
|
||
```yaml
|
||
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/`** 并版本化
|