269 lines
9.6 KiB
Markdown
269 lines
9.6 KiB
Markdown
# 遗留订单系统 - 项目说明与学习指南
|
||
|
||
> 本项目是一个刻意构造的“遗留系统”代码练习场,供团队内部进行代码审查、重构、安全测试等技能训练使用。
|
||
|
||
---
|
||
|
||
## 一、项目概述
|
||
|
||
这是一个模拟真实环境的电商订单系统,代码风格、架构设计、命名规范均刻意模仿了多年维护、多人经手的遗留项目。系统本身**功能可用**,但代码质量和安全性存在大量隐患。
|
||
|
||
### 技术栈
|
||
|
||
| 组件 | 版本 |
|
||
|------|------|
|
||
| JDK | 1.8 |
|
||
| Spring Boot | 2.1.18.RELEASE |
|
||
| 数据库 | H2 (内存模式) |
|
||
| 数据库访问 | 原生 JDBC |
|
||
| 构建工具 | Maven 3.x |
|
||
|
||
### 快速启动
|
||
|
||
```bash
|
||
mvn spring-boot:run
|
||
```
|
||
|
||
启动后:
|
||
- API 地址:`http://localhost:8080/api/orders`
|
||
- H2 控制台:`http://localhost:8080/h2-console`(JDBC URL: `jdbc:h2:mem:legacydb`,用户名 `sa`,密码 `password123`)
|
||
|
||
### 接口一览
|
||
|
||
| 方法 | 路径 | 说明 |
|
||
|------|------|------|
|
||
| POST | `/api/orders` | 创建订单 |
|
||
| GET | `/api/orders` | 查询所有订单 |
|
||
| GET | `/api/orders/{id}` | 按ID查询订单 |
|
||
| GET | `/api/orders/search?name=xxx` | 按商品名称搜索 |
|
||
| PUT | `/api/orders/{id}/cancel` | 取消订单 |
|
||
| GET | `/api/orders/report` | 订单统计报表 |
|
||
|
||
### 创建订单示例
|
||
|
||
```bash
|
||
curl -X POST http://localhost:8080/api/orders \
|
||
-H "Content-Type: application/json" \
|
||
-d '{
|
||
"kehuMingzi": "张三",
|
||
"items": [
|
||
{"shangpinMingcheng": "机械键盘", "danjia": 399, "shuliang": 2},
|
||
{"shangpinMingcheng": "鼠标垫", "danjia": 29.9, "shuliang": 5}
|
||
]
|
||
}'
|
||
```
|
||
|
||
---
|
||
|
||
## 二、已知问题清单
|
||
|
||
### 2.1 安全漏洞
|
||
|
||
| # | 问题 | 位置 | 严重程度 | 说明 |
|
||
|---|------|------|----------|------|
|
||
| 1 | **SQL 注入** | `OrderDao.findByProductName()` | 严重 | 使用 `Statement` + 字符串拼接构建 SQL,攻击者可注入任意 SQL 语句 |
|
||
|
||
### 2.2 并发缺陷
|
||
|
||
| # | 问题 | 位置 | 说明 |
|
||
|---|------|------|------|
|
||
| 2 | **订单号生成非线程安全** | `OrderService.generateOrderNumber()` 中 `static orderNumberSequence++` | 多线程并发创建订单时可能产生重复订单号 |
|
||
|
||
### 2.3 空指针风险
|
||
|
||
| # | 问题 | 位置 | 说明 |
|
||
|---|------|------|------|
|
||
| 3 | **未判空直接调用** | `OrderService.cancelOrder()` 中 `order.getZhuangtai()` | 查询不存在的订单返回 null 后直接调用,抛出 NullPointerException |
|
||
|
||
### 2.4 数值处理缺陷
|
||
|
||
| # | 问题 | 位置 | 说明 |
|
||
|---|------|------|------|
|
||
| 4 | **整型溢出** | `OrderService.createOrder()` 中 `int intDiscount = (int)discount` | 大金额订单折扣计算可能导致溢出,结果不符合预期 |
|
||
|
||
### 2.5 异常处理不当
|
||
|
||
| # | 问题 | 位置 | 说明 |
|
||
|---|------|------|------|
|
||
| 5 | **异常被吞没** | 全局 `catch(Exception e) { e.printStackTrace(); }` | 数据库异常等关键错误被隐藏,上层无法感知 |
|
||
|
||
---
|
||
|
||
## 三、代码质量问题速查
|
||
|
||
| 问题类型 | 典型位置 | 说明 |
|
||
|----------|----------|------|
|
||
| 上帝类 | `OrderService.java`(350+ 行) | 单个类承载了订单创建、取消、查询、报表等多种职责 |
|
||
| 长方法 | `createOrder()` 方法(200+ 行) | 校验、金额计算、折扣、税额全部耦合在一个方法内 |
|
||
| 重复代码 | `OrderService` 中价格计算逻辑 | 至少 3 处完全相同的价格计算循环 |
|
||
| 硬编码 | 税率 0.17、折扣阶梯、数据库密码 | 业务常量散布在代码中,没有配置化 |
|
||
| 手动 JDBC | 整个 DAO 层 | 未使用 JPA/MyBatis 等 ORM 框架,手写 SQL 和管理连接 |
|
||
| 静态单例 | `DbUtil.java` | 绕过 Spring 容器,自行管理数据库连接池 |
|
||
| 线程不安全 | `SimpleDateFormat`、`static orderNumberSequence` | 多线程环境下共享可变状态 |
|
||
| System.out 日志 | 整个项目 | 没有使用 SLF4J/Logback 等日志框架 |
|
||
| 命名混乱 | `dingdanBianhao`、`kehuMingzi` 等字段名 | 拼音 + 英文混合命名,无统一规范 |
|
||
| 无单元测试 | 整个项目 | 测试覆盖率为零,重构风险极高 |
|
||
| 无 DTO 分层 | Controller 直接暴露实体类 | API 响应结构与数据库模型耦合 |
|
||
|
||
---
|
||
|
||
## 四、推荐练习场景
|
||
|
||
以下场景可按顺序逐步推进,也可根据时间选取重点练习。每个场景均适合使用 AI 辅助完成。
|
||
|
||
### 场景 1:阅读理解(建议 10-15 分钟)
|
||
|
||
**目标**:面对无注释、命名混乱的代码,快速理解业务逻辑。
|
||
|
||
**练习内容**:
|
||
1. 打开 `OrderService.java`,阅读 `createOrder()` 方法
|
||
2. 尝试解释每个方法的业务含义
|
||
3. 为代码添加必要的中文注释
|
||
4. 绘制订单创建流程图
|
||
|
||
**关注点**:
|
||
- 拼音 + 英文混合命名的识别(dingdanBianhao = 订单编号,kehuMingzi = 客户名字)
|
||
- 硬编码税率 0.17 的历史背景
|
||
- 多级 if-else 折扣逻辑的业务规则
|
||
|
||
---
|
||
|
||
### 场景 2:代码重构(建议 15-25 分钟)
|
||
|
||
**目标**:拆分上帝类和大方法,消除重复代码。
|
||
|
||
**练习内容**:
|
||
1. 拆分 `createOrder()` 方法,提取为独立职责的方法:
|
||
- 请求校验 → `validateOrderRequest()`
|
||
- 金额计算 → `computePrices()`
|
||
- 折扣计算 → `applyDiscount()`
|
||
- 税额计算 → `calculateTax()`
|
||
2. 找出并消除 `OrderService` 中 3 处重复的价格计算逻辑
|
||
3. 将硬编码常量提取为配置项
|
||
|
||
**关注点**:
|
||
- 提取方法时确保不改变原有业务逻辑
|
||
- 重复代码的识别标准(不仅仅是完全相同的代码,逻辑等价也要识别)
|
||
|
||
---
|
||
|
||
### 场景 3:补充测试(建议 15-20 分钟)
|
||
|
||
**目标**:为无测试覆盖的遗留系统建立测试保护网。
|
||
|
||
**练习内容**:
|
||
1. 添加 `spring-boot-starter-test` 依赖
|
||
2. 为核心方法编写单元测试:
|
||
- `createOrder()` — 正常场景、边界值、异常场景
|
||
- `cancelOrder()` — 正常取消、不存在订单、已取消订单
|
||
- `calcDiscount()` — 各折扣阶梯
|
||
3. 为 `OrderDao` 编写集成测试(使用 H2 内存数据库)
|
||
|
||
**关注点**:
|
||
- 由于代码耦合度高,编写测试本身就会发现设计问题
|
||
- static 单例和共享状态使测试变得困难,说明可测试性对设计的重要性
|
||
|
||
---
|
||
|
||
### 场景 4:修复缺陷(建议 15-20 分钟)
|
||
|
||
**目标**:审查代码,发现并修复典型缺陷。
|
||
|
||
**练习内容**:
|
||
1. 审查整个项目,找出所有潜在缺陷和安全漏洞
|
||
2. 逐一修复,每种修复完成后运行测试验证
|
||
3. **SQL 注入验证**:构造恶意请求确认漏洞存在,修复后验证已消除
|
||
|
||
**SQL 注入复现**:
|
||
```bash
|
||
# 正常搜索
|
||
curl -G "http://localhost:8080/api/orders/search" --data-urlencode "name=键盘"
|
||
|
||
# 注入验证(返回所有订单,说明漏洞存在)
|
||
curl "http://localhost:8080/api/orders/search?name=' OR '1'='1'--"
|
||
```
|
||
|
||
**修复要点**:
|
||
- SQL 注入:`Statement` → `PreparedStatement`,参数化查询
|
||
- 并发竞态:使用 `AtomicLong` 或数据库序列
|
||
- 空指针:添加 null 检查或使用 `Optional`
|
||
- 整型溢出:使用 `BigDecimal` 处理金额
|
||
- 异常处理:区分可恢复和不可恢复异常,日志记录 + 业务处理
|
||
|
||
---
|
||
|
||
### 场景 5:技术栈升级(建议 15-20 分钟)
|
||
|
||
**目标**:将 Spring Boot 2.1.18 + Java 8 升级到 Spring Boot 3.x + Java 17/21。
|
||
|
||
**涉及变更**:
|
||
1. `pom.xml`:升级版本号
|
||
2. 包名迁移:`javax.*` → `jakarta.*`
|
||
3. 配置迁移:`spring.datasource.initialization-mode` → `spring.sql.init.mode`
|
||
4. H2 版本兼容性处理
|
||
5. 已知 API 变更适配
|
||
|
||
**关注点**:
|
||
- 升级后功能是否正常
|
||
- 哪些废弃 API 需要替换
|
||
|
||
---
|
||
|
||
### 场景 6:添加新功能(建议 20-30 分钟)
|
||
|
||
选择一个方向在现有代码基础上添加新功能。**建议先写测试再写代码**。
|
||
|
||
**方向 A:退款功能**
|
||
1. 新增 `REFUNDED` 订单状态
|
||
2. 实现退款逻辑(已完成订单可退款,退款金额为原单 80%)
|
||
3. 添加 `PUT /api/orders/{id}/refund` API
|
||
|
||
**方向 B:优惠券功能**
|
||
1. 新增优惠券模型(支持满减券和折扣券)
|
||
2. 在 `createOrder()` 中集成优惠券折扣
|
||
3. 处理优惠券与现有折扣的叠加规则
|
||
|
||
**关注点**:
|
||
- 在不破坏原有逻辑的前提下扩展功能
|
||
- 是否应先重构再添加新功能
|
||
|
||
---
|
||
|
||
## 五、项目代码结构
|
||
|
||
```
|
||
legacy-order-system/
|
||
├── pom.xml
|
||
├── README.md
|
||
├── teacher-docs/ # 本目录
|
||
│ └── README.md
|
||
└── src/main/
|
||
├── java/com/legacy/order/
|
||
│ ├── LegacyOrderApplication.java # 启动类
|
||
│ ├── controller/
|
||
│ │ └── OrderController.java # REST 接口
|
||
│ ├── service/
|
||
│ │ └── OrderService.java # 业务逻辑
|
||
│ ├── dao/
|
||
│ │ └── OrderDao.java # 数据访问
|
||
│ ├── model/
|
||
│ │ ├── Order.java # 订单实体
|
||
│ │ ├── OrderItem.java # 订单项实体
|
||
│ │ └── OrderStatus.java # 订单状态枚举
|
||
│ └── util/
|
||
│ └── DbUtil.java # 数据库连接工具
|
||
└── resources/
|
||
├── application.properties # 配置文件
|
||
└── schema.sql # 数据库 DDL
|
||
```
|
||
|
||
---
|
||
|
||
## 六、使用建议
|
||
|
||
1. **代码审查优先**:在对代码进行任何修改之前,先完整阅读和理解现有代码。
|
||
2. **小步迭代**:每次只修改一个问题,修改后立即运行测试验证。
|
||
3. **测试先行**:在重构前先补上关键路径的单元测试,确保重构不破坏功能。
|
||
4. **记录修改**:每次修改后记录改动点、原因和影响范围。
|
||
5. **关注设计**:不仅修 Bug,还要思考为什么会写出这样的 Bug,以及如何在设计层面避免。
|