commit caffc89686026292fa530002bbb24b601b516985 Author: micrograils Date: Sat Jun 13 16:53:08 2026 +0800 init: 遗留订单系统初始版本 diff --git a/.codebuddy/plans/legacy-ecommerce-demo-repo_99b9a35c.md b/.codebuddy/plans/legacy-ecommerce-demo-repo_99b9a35c.md new file mode 100644 index 0000000..8917989 --- /dev/null +++ b/.codebuddy/plans/legacy-ecommerce-demo-repo_99b9a35c.md @@ -0,0 +1,175 @@ +--- +name: legacy-ecommerce-demo-repo +overview: 创建一个 Java Spring Boot 电商订单系统的"烂代码"演示仓库,涵盖理解老代码、重构、补测试、修Bug、技术栈升级、添加新功能等6大AI辅助场景,约12-15个文件,适合30-45分钟培训实战。 +todos: + - id: create-project-skeleton + content: 创建Maven项目骨架:pom.xml(Spring Boot 2.1.18 + H2 + JDBC依赖)、application.properties、schema.sql + status: completed + - id: create-models + content: 创建数据模型层:Order.java(java.util.Date、无Lombok)、OrderItem.java、OrderStatus.java枚举 + status: completed + dependencies: + - create-project-skeleton + - id: create-db-util + content: 创建DbUtil.java工具类:硬编码连接串、手动管理Connection、带明显设计缺陷的单例模式 + status: completed + dependencies: + - create-project-skeleton + - id: create-dao-layer + content: 创建OrderDao.java:原始JDBC操作、SQL字符串拼接(含SQL注入风险)、无连接池、无try-with-resources + status: completed + dependencies: + - create-models + - create-db-util + - id: create-god-service + content: 创建OrderService.java上帝类:400+行、static可变计数器并发Bug、3处重复价格计算、200+行长方法createOrder、System.out日志、无异常处理 + status: completed + dependencies: + - create-dao-layer + - id: create-controller + content: 创建OrderController.java:混合校验/业务/DAO调用、无DTO分层、硬编码错误消息、search接口暴露SQL注入 + status: completed + dependencies: + - create-god-service + - id: create-main-and-readme + content: 创建LegacyOrderApplication.java主启动类,以及README.md培训指南(含6大场景操作步骤和学员任务说明) + status: completed + dependencies: + - create-controller + - id: verify-and-test + content: 使用 [subagent:code-explorer] 验证项目结构完整性,确认可编译、H2自动建表、5个预埋Bug存在、所有API可访问 + status: completed + dependencies: + - create-main-and-readme +--- + +## 用户需求 + +为一线开发团队制作一个用于AI编码培训的"传统烂代码"演示仓库,让学员在培训过程中跟着实操,学习如何用AI辅助老旧项目维护。 + +## 产品概述 + +一个刻意设计为"遗留系统"风格的Java Spring Boot电商订单管理后端,包含订单创建、查询、取消等基本功能。代码刻意引入典型遗留系统问题:上帝类、长函数、重复代码、SQL注入风险、硬编码、无测试、无异常处理等。支持一键启动(H2内存数据库),学员无需配置环境即可运行。 + +## 核心功能 + +- 订单CRUD REST API(创建、查询、取消、列表) +- H2内存数据库,零配置启动 +- 预留6大AI辅助实操场景 + +## 六大培训场景设计 + +1. **理解老代码+添加注释**:学员面对无注释、命名混乱的代码,用AI快速理解业务逻辑并生成文档 +2. **重构(拆分大函数/消除重复)**:OrderService中故意放置200+行方法和3处重复的价格计算逻辑 +3. **补单元测试**:项目零测试覆盖,学员用AI为关键路径生成JUnit测试 +4. **修Bug(埋典型bug)**:预埋SQL注入、并发竞态、空指针、整型溢出、日期比较错误共5个bug +5. **技术栈升级**:从Spring Boot 2.1.x + Java 8 迁移到 Spring Boot 3.x + Java 17(或更高) +6. **添加新功能**:在烂代码基础上增加订单退款、优惠券等新需求 + +## 技术栈选择 + +- **语言**: Java 8(刻意使用旧版本,为升级场景留空间) +- **框架**: Spring Boot 2.1.18.RELEASE +- **构建工具**: Maven 3.6+ +- **数据库**: H2(内存模式,零配置) +- **数据访问**: 原始JDBC + 手动连接管理(不使用任何ORM) +- **依赖**: spring-boot-starter-web, h2, spring-boot-starter-jdbc + +## 实现方案 + +### 整体策略 + +创建一个刻意劣化的Spring Boot应用,每个文件都包含典型遗留代码特征。关键设计原则: + +- **不用ORM**:直接JDBC操作,手动管理连接,SQL字符串拼接 +- **不分层**:Controller直接调用DAO,Service是上帝类 +- **不写测试**:整个项目零测试文件 +- **不用现代API**:java.util.Date、旧式for循环、避免Stream/Lambda +- **全局状态**:使用static可变字段管理订单号序列 + +### 遗留代码特征清单 + +| 特征 | 体现位置 | 培训价值 | +| --- | --- | --- | +| 上帝类 | OrderService.java (400+行) | 演示AI拆分大类的过程 | +| 长方法 | createOrder() 200+行 | 演示AI提取方法 | +| 重复代码 | 价格计算在3处重复 | 演示AI消除重复 | +| SQL拼接 | OrderDao.java | 演示AI发现安全漏洞 | +| 硬编码 | 税率0.17、连接串 | 演示AI提取配置 | +| 无日志 | 全部System.out | 演示AI添加日志框架 | +| 无异常处理 | 整个项目 | 演示AI添加异常处理 | +| 静态可变状态 | orderNumberSequence | 演示AI发现并发问题 | + + +### 预埋Bug清单 + +1. **SQL注入**:OrderDao.getOrderByName()使用字符串拼接用户输入 +2. **并发竞态**:OrderService中static int orderNumberSequence++无同步 +3. **空指针异常**:computeTotalPrice()未检查OrderItem列表是否为空 +4. **整型溢出**:折扣计算 int discount = price * discountPercent / 100(price可为大值) +5. **日期比较错误**:使用java.util.Date的before/after而非推荐方式,跨年场景有边界问题 + +### 项目结构 + +``` +legacy-order-system/ +├── pom.xml # [NEW] Maven配置,Spring Boot 2.1.18 + H2 +├── README.md # [NEW] 培训指南,含6大场景操作说明 +└── src/main/ + ├── java/com/legacy/order/ + │ ├── LegacyOrderApplication.java # [NEW] 主启动类,无任何配置分离 + │ ├── controller/ + │ │ └── OrderController.java # [NEW] REST控制器,600+行,混合了校验、业务、DAO调用 + │ ├── service/ + │ │ └── OrderService.java # [NEW] 上帝类,400+行,包含所有业务逻辑 + │ ├── dao/ + │ │ └── OrderDao.java # [NEW] 原始JDBC操作,SQL拼接,连接手动管理 + │ ├── model/ + │ │ ├── Order.java # [NEW] 订单实体,java.util.Date,无Lombok + │ │ ├── OrderItem.java # [NEW] 订单项实体 + │ │ └── OrderStatus.java # [NEW] 订单状态枚举 + │ └── util/ + │ └── DbUtil.java # [NEW] 硬编码连接工具类,单例模式 + └── resources/ + ├── application.properties # [NEW] 最小化配置,H2内存库 + └── schema.sql # [NEW] 建表DDL,自动执行 +``` + +### 数据流架构 + +```mermaid +graph LR + A[HTTP Request] --> B[OrderController] + B --> C[OrderService
上帝类/全局状态] + C --> D[OrderDao
原始JDBC/SQL拼接] + D --> E[H2内存数据库] + C -.-> F[DbUtil
硬编码连接] + F -.-> E +``` + +### API设计 + +| 方法 | 路径 | 说明 | +| --- | --- | --- | +| POST | /api/orders | 创建订单 | +| GET | /api/orders/{id} | 查询单个订单 | +| GET | /api/orders | 列出所有订单 | +| PUT | /api/orders/{id}/cancel | 取消订单 | +| GET | /api/orders/search?name=xxx | 按商品名搜索(含SQL注入风险) | + + +## 实现注意事项 + +- **性能**:H2内存库保证启动速度(<2秒),学员无需安装任何外部依赖 +- **可移植性**:纯Maven项目,克隆后mvn spring-boot:run即可启动 +- **日志**:刻意使用System.out.println模拟旧项目无日志框架的情况 +- **编码规范**:刻意使用坏命名(如tmp、data、list、flag等)、魔法数字、无注释 +- **向后兼容**:使用Java 8语法,避免模块化系统,兼容JDK 8-17 + +## 使用的扩展 + +### SubAgent + +- **code-explorer** +- 用途:验证生成的仓库文件结构完整性和代码一致性 +- 预期结果:确认所有文件存在、包引用正确、Maven配置有效 \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ac44a5a --- /dev/null +++ b/.gitignore @@ -0,0 +1,14 @@ +# === Maven === +target/ + +# === IDE === +.idea/ +*.iml +.vscode/ +.settings/ +.project +.classpath + +# === OS === +.DS_Store +Thumbs.db diff --git a/README.md b/README.md new file mode 100644 index 0000000..5866df5 --- /dev/null +++ b/README.md @@ -0,0 +1,64 @@ +# 订单管理系统 + +> 内部订单管理系统,用于处理日常订单业务。 + +## 环境要求 + +- JDK 1.8+ +- Maven 3.x + +## 快速启动 + +```bash +mvn spring-boot:run +``` + +启动后访问 `http://localhost:8080` + +## 接口说明 + +``` +POST /api/orders 创建订单 +GET /api/orders/{id} 查询订单 +GET /api/orders 订单列表 +PUT /api/orders/{id}/cancel 取消订单 +GET /api/orders/search 商品搜索 +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} + ] + }' +``` + +### 查询订单 + +```bash +# 查询所有订单 +curl http://localhost:8080/api/orders + +# 按ID查询 +curl http://localhost:8080/api/orders/1 + +# 按商品名搜索 +curl -G "http://localhost:8080/api/orders/search" --data-urlencode "name=键盘" +``` + +## 注意事项 + +- 项目使用 H2 内存数据库,重启后数据会丢失 +- H2 控制台:http://localhost:8080/h2-console(用户名sa,密码password123) + +## 练习指南 + +详细的项目分析、已知问题和练习场景请参阅 [teacher-docs/README.md](teacher-docs/README.md)。 + diff --git a/mytest.txt b/mytest.txt new file mode 100644 index 0000000..87b9939 --- /dev/null +++ b/mytest.txt @@ -0,0 +1,65 @@ +测试插入 + +# ==================== 创建订单 ==================== + +# 创建订单1 +curl -X POST http://localhost:8080/api/orders \ + -H "Content-Type: application/json" \ + -d '{ + "kehuMingzi": "张三", + "items": [ + {"shangpinMingcheng": "苹果", "danjia": 5.5, "shuliang": 3}, + {"shangpinMingcheng": "香蕉", "danjia": 3.0, "shuliang": 5} + ] + }' + +# 创建订单2 +curl -X POST http://localhost:8080/api/orders \ + -H "Content-Type: application/json" \ + -d '{ + "kehuMingzi": "李四", + "items": [ + {"shangpinMingcheng": "西瓜", "danjia": 20.0, "shuliang": 1}, + {"shangpinMingcheng": "葡萄", "danjia": 12.5, "shuliang": 2} + ] + }' + +# 创建订单3 +curl -X POST http://localhost:8080/api/orders \ + -H "Content-Type: application/json" \ + -d '{ + "kehuMingzi": "王五", + "items": [ + {"shangpinMingcheng": "苹果汁", "danjia": 8.0, "shuliang": 4}, + {"shangpinMingcheng": "橙汁", "danjia": 6.5, "shuliang": 6} + ] + }' + +# ==================== 查询订单 ==================== + +# 查询所有订单 +curl http://localhost:8080/api/orders + +# 按ID查询订单(假设ID为1) +curl http://localhost:8080/api/orders/1 + +# ==================== 搜索订单(按商品名) ==================== + +# 搜索含"苹果"的订单 +curl "http://localhost:8080/api/orders/search?name=苹果" + +curl -G "http://localhost:8080/api/orders/search" --data-urlencode "name=苹果" +curl "http://localhost:8080/api/orders/search?name=%E8%8B%B9%E6%9E%9C" + +curl "http://localhost:8080/api/orders/search?name=%25%27%20OR%201%3D1%20--%20" + + +# ==================== 取消订单 ==================== + +# 取消订单(假设ID为1) +curl -X PUT http://localhost:8080/api/orders/1/cancel + +# ==================== 统计报表 ==================== + +# 查看订单统计报表 +curl http://localhost:8080/api/orders/report diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..ae76a53 --- /dev/null +++ b/pom.xml @@ -0,0 +1,53 @@ + + + 4.0.0 + + com.legacy + legacy-order-system + 1.0.0 + jar + + order-system + 订单管理系统 + + + org.springframework.boot + spring-boot-starter-parent + 2.1.18.RELEASE + + + + + 1.8 + UTF-8 + + + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-starter-jdbc + + + + com.h2database + h2 + runtime + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + diff --git a/src/main/java/com/legacy/order/LegacyOrderApplication.java b/src/main/java/com/legacy/order/LegacyOrderApplication.java new file mode 100644 index 0000000..d9878b4 --- /dev/null +++ b/src/main/java/com/legacy/order/LegacyOrderApplication.java @@ -0,0 +1,15 @@ +package com.legacy.order; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class LegacyOrderApplication { + + public static void main(String[] args) { + System.out.println("===================================="); + System.out.println(" 订单管理系统 v1.0"); + System.out.println("===================================="); + SpringApplication.run(LegacyOrderApplication.class, args); + } +} diff --git a/src/main/java/com/legacy/order/controller/OrderController.java b/src/main/java/com/legacy/order/controller/OrderController.java new file mode 100644 index 0000000..ae910cd --- /dev/null +++ b/src/main/java/com/legacy/order/controller/OrderController.java @@ -0,0 +1,190 @@ +package com.legacy.order.controller; + +import com.legacy.order.model.Order; +import com.legacy.order.model.OrderItem; +import com.legacy.order.service.OrderService; + +import java.util.*; + +@org.springframework.web.bind.annotation.RestController +@org.springframework.web.bind.annotation.RequestMapping("/api/orders") +public class OrderController { + + private OrderService orderService = new OrderService(); + + /** + * 创建订单 + */ + @org.springframework.web.bind.annotation.PostMapping("") + public Object createOrder(@org.springframework.web.bind.annotation.RequestBody OrderRequest req) { + System.out.println("[Controller] 收到创建订单请求"); + + if (req.getKehuMingzi() == null || req.getKehuMingzi().trim().isEmpty()) { + Map err = new HashMap<>(); + err.put("code", -1); + err.put("msg", "客户名称不能为空!"); + return err; + } + + if (req.getItems() == null || req.getItems().isEmpty()) { + Map err = new HashMap<>(); + err.put("code", -1); + err.put("msg", "订单项不能为空!"); + return err; + } + + List items = new ArrayList<>(); + for (int i = 0; i < req.getItems().size(); i++) { + ItemRequest ir = req.getItems().get(i); + if (ir.getShangpinMingcheng() == null || ir.getShangpinMingcheng().trim().isEmpty()) { + Map err = new HashMap<>(); + err.put("code", -1); + err.put("msg", "第" + (i + 1) + "个商品的名称为空!"); + return err; + } + if (ir.getDanjia() == null || ir.getDanjia() <= 0) { + Map err = new HashMap<>(); + err.put("code", -1); + err.put("msg", "第" + (i + 1) + "个商品的单价不合法!"); + return err; + } + if (ir.getShuliang() == null || ir.getShuliang() <= 0) { + Map err = new HashMap<>(); + err.put("code", -1); + err.put("msg", "第" + (i + 1) + "个商品的数量不合法!"); + return err; + } + + OrderItem item = new OrderItem(); + item.setShangpinMingcheng(ir.getShangpinMingcheng()); + item.setDanjia(ir.getDanjia()); + item.setShuliang(ir.getShuliang()); + items.add(item); + } + + Order order = orderService.createOrder(req.getKehuMingzi(), items); + + if (order == null) { + Map err = new HashMap<>(); + err.put("code", -1); + err.put("msg", "创建订单失败,请检查参数!"); + return err; + } + + Map result = new HashMap<>(); + result.put("code", 0); + result.put("msg", "创建成功"); + result.put("data", order); + return result; + } + + @org.springframework.web.bind.annotation.GetMapping("/{id}") + public Object getOrder(@org.springframework.web.bind.annotation.PathVariable("id") long id) { + System.out.println("[Controller] 查询订单: " + id); + Order order = orderService.getOrder(id); + Map result = new HashMap<>(); + if (order == null) { + result.put("code", -1); + result.put("msg", "订单" + id + "不存在"); + } else { + result.put("code", 0); + result.put("msg", "查询成功"); + result.put("data", order); + } + return result; + } + + /** + * 查询所有订单 + */ + @org.springframework.web.bind.annotation.GetMapping("") + public Object getAllOrders() { + System.out.println("[Controller] 查询所有订单"); + List orders = orderService.getAllOrders(); + Map result = new HashMap<>(); + result.put("code", 0); + result.put("msg", "查询成功"); + result.put("data", orders); + result.put("total", orders.size()); + return result; + } + + /** + * 按商品名搜索订单 + */ + @org.springframework.web.bind.annotation.GetMapping("/search") + public Object searchOrders(@org.springframework.web.bind.annotation.RequestParam("name") String name) { + System.out.println("[Controller] 搜索商品名称: " + name); + List orders = orderService.searchByProductName(name); + Map result = new HashMap<>(); + result.put("code", 0); + result.put("msg", "搜索成功"); + result.put("data", orders); + result.put("total", orders.size()); + return result; + } + + /** + * 取消订单 + */ + @org.springframework.web.bind.annotation.PutMapping("/{id}/cancel") + public Object cancelOrder(@org.springframework.web.bind.annotation.PathVariable("id") long id) { + System.out.println("[Controller] 取消订单: " + id); + try { + boolean ok = orderService.cancelOrder(id); + Map result = new HashMap<>(); + result.put("code", ok ? 0 : -1); + result.put("msg", ok ? "取消成功" : "取消失败"); + return result; + } catch (Exception e) { + Map err = new HashMap<>(); + err.put("code", -1); + err.put("msg", "系统错误:" + e.getMessage()); + return err; + } + } + + /** + * 订单统计报表 + */ + @org.springframework.web.bind.annotation.GetMapping("/report") + public Object getReport() { + System.out.println("[Controller] 生成报表"); + String report = orderService.getOrderReport(); + Map result = new HashMap<>(); + result.put("code", 0); + result.put("report", report); + return result; + } + + // ======== 内部请求DTO(放在Controller里,没有独立文件) ======== + + /** + * 创建订单请求体 + */ + public static class OrderRequest { + private String kehuMingzi; + private List items; + + public String getKehuMingzi() { return kehuMingzi; } + public void setKehuMingzi(String kehuMingzi) { this.kehuMingzi = kehuMingzi; } + + public List getItems() { return items; } + public void setItems(List items) { this.items = items; } + } + + public static class ItemRequest { + private String shangpinMingcheng; + private Double danjia; + private Integer shuliang; + + public String getShangpinMingcheng() { return shangpinMingcheng; } + public void setShangpinMingcheng(String shangpinMingcheng) { this.shangpinMingcheng = shangpinMingcheng; } + + public Double getDanjia() { return danjia; } + public void setDanjia(Double danjia) { this.danjia = danjia; } + + public Integer getShuliang() { return shuliang; } + public void setShuliang(Integer shuliang) { this.shuliang = shuliang; } + } +} diff --git a/src/main/java/com/legacy/order/dao/OrderDao.java b/src/main/java/com/legacy/order/dao/OrderDao.java new file mode 100644 index 0000000..09e4d5a --- /dev/null +++ b/src/main/java/com/legacy/order/dao/OrderDao.java @@ -0,0 +1,237 @@ +package com.legacy.order.dao; + +import com.legacy.order.model.Order; +import com.legacy.order.model.OrderItem; +import com.legacy.order.util.DbUtil; + +import java.sql.*; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +public class OrderDao { + + // ==================== 订单CRUD ==================== + + public void save(Order order) { + Connection conn = null; + PreparedStatement ps = null; + ResultSet rs = null; + try { + conn = DbUtil.getConn(); + String sql = "INSERT INTO t_order (dingdan_bianhao, kehu_mingzi, zhuangtai, total_amount, create_time, update_time) " + + "VALUES (?, ?, ?, ?, ?, ?)"; + ps = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS); + ps.setString(1, order.getDingdanBianhao()); + ps.setString(2, order.getKehuMingzi()); + ps.setString(3, order.getZhuangtai()); + ps.setDouble(4, order.getTotalAmount()); + ps.setTimestamp(5, new Timestamp(order.getCreateTime().getTime())); + ps.setTimestamp(6, new Timestamp(order.getUpdateTime().getTime())); + ps.executeUpdate(); + + rs = ps.getGeneratedKeys(); + if (rs.next()) { + order.setId(rs.getLong(1)); + } + + // 保存订单项 + if (order.getItems() != null) { + for (int i = 0; i < order.getItems().size(); i++) { + OrderItem item = order.getItems().get(i); + saveItem(order.getId(), item); + } + } + + System.out.println("订单保存成功: " + order.getDingdanBianhao()); + } catch (Exception e) { + e.printStackTrace(); + } finally { + closeQuietly(rs); + closeQuietly(ps); + closeQuietly(conn); + } + } + + private void saveItem(long orderId, OrderItem item) { + Connection conn = null; + PreparedStatement ps = null; + try { + conn = DbUtil.getConn(); + String sql = "INSERT INTO t_order_item (order_id, shangpin_mingcheng, danjia, shuliang, jine) VALUES (?, ?, ?, ?, ?)"; + ps = conn.prepareStatement(sql); + ps.setLong(1, orderId); + ps.setString(2, item.getShangpinMingcheng()); + ps.setDouble(3, item.getDanjia()); + ps.setInt(4, item.getShuliang()); + ps.setDouble(5, item.getJine()); + ps.executeUpdate(); + } catch (Exception e) { + e.printStackTrace(); + } finally { + closeQuietly(ps); + closeQuietly(conn); + } + } + + public Order findById(long id) { + Connection conn = null; + PreparedStatement ps = null; + ResultSet rs = null; + try { + conn = DbUtil.getConn(); + ps = conn.prepareStatement("SELECT * FROM t_order WHERE id = ?"); + ps.setLong(1, id); + rs = ps.executeQuery(); + if (rs.next()) { + Order order = rowToOrder(rs); + order.setItems(findItemsByOrderId(order.getId())); + return order; + } + } catch (Exception e) { + e.printStackTrace(); + } finally { + closeQuietly(rs); + closeQuietly(ps); + closeQuietly(conn); + } + return null; + } + + public List findAll() { + List list = new ArrayList(); + Connection conn = null; + Statement stmt = null; + ResultSet rs = null; + try { + conn = DbUtil.getConn(); + stmt = conn.createStatement(); + rs = stmt.executeQuery("SELECT * FROM t_order ORDER BY create_time DESC"); + while (rs.next()) { + Order order = rowToOrder(rs); + order.setItems(findItemsByOrderId(order.getId())); + list.add(order); + } + } catch (Exception e) { + e.printStackTrace(); + } finally { + closeQuietly(rs); + closeQuietly(stmt); + closeQuietly(conn); + } + return list; + } + + /** + * 按商品名称搜索 + */ + public List findByProductName(String name) { + List list = new ArrayList(); + Connection conn = null; + Statement stmt = null; + ResultSet rs = null; + try { + conn = DbUtil.getConn(); + stmt = conn.createStatement(); + String sql = "SELECT DISTINCT o.* FROM t_order o " + + "INNER JOIN t_order_item i ON o.id = i.order_id " + + "WHERE i.shangpin_mingcheng LIKE '%" + name + "%'"; + System.out.println("执行SQL: " + sql); + rs = stmt.executeQuery(sql); + while (rs.next()) { + Order order = rowToOrder(rs); + order.setItems(findItemsByOrderId(order.getId())); + list.add(order); + } + } catch (Exception e) { + e.printStackTrace(); + } finally { + closeQuietly(rs); + closeQuietly(stmt); + closeQuietly(conn); + } + return list; + } + + public void cancelOrder(long id) { + Connection conn = null; + PreparedStatement ps = null; + try { + conn = DbUtil.getConn(); + ps = conn.prepareStatement("UPDATE t_order SET zhuangtai = 'CANCELLED', update_time = ? WHERE id = ?"); + ps.setTimestamp(1, new Timestamp(System.currentTimeMillis())); + ps.setLong(2, id); + ps.executeUpdate(); + System.out.println("订单已取消: " + id); + } catch (Exception e) { + e.printStackTrace(); + } finally { + closeQuietly(ps); + closeQuietly(conn); + } + } + + // ==================== 订单项查询 ==================== + + private List findItemsByOrderId(long orderId) { + List items = new ArrayList(); + Connection conn = null; + PreparedStatement ps = null; + ResultSet rs = null; + try { + conn = DbUtil.getConn(); + ps = conn.prepareStatement("SELECT * FROM t_order_item WHERE order_id = ?"); + ps.setLong(1, orderId); + rs = ps.executeQuery(); + while (rs.next()) { + OrderItem item = new OrderItem(); + item.setId(rs.getLong("id")); + item.setOrderId(rs.getLong("order_id")); + item.setShangpinMingcheng(rs.getString("shangpin_mingcheng")); + item.setDanjia(rs.getDouble("danjia")); + item.setShuliang(rs.getInt("shuliang")); + item.setJine(rs.getDouble("jine")); + items.add(item); + } + } catch (Exception e) { + e.printStackTrace(); + } finally { + closeQuietly(rs); + closeQuietly(ps); + closeQuietly(conn); + } + return items; + } + + // ==================== 工具方法 ==================== + + private Order rowToOrder(ResultSet rs) throws SQLException { + Order o = new Order(); + o.setId(rs.getLong("id")); + o.setDingdanBianhao(rs.getString("dingdan_bianhao")); + o.setKehuMingzi(rs.getString("kehu_mingzi")); + o.setZhuangtai(rs.getString("zhuangtai")); + o.setTotalAmount(rs.getDouble("total_amount")); + o.setCreateTime(new Date(rs.getTimestamp("create_time").getTime())); + o.setUpdateTime(new Date(rs.getTimestamp("update_time").getTime())); + return o; + } + + private void closeQuietly(AutoCloseable resource) { + if (resource != null) { + try { + resource.close(); + } catch (Exception e) { + } + } + } + + private void closeQuietly(Connection conn) { + if (conn != null) { + try { + conn.close(); + } catch (Exception e) { + } + } + } +} diff --git a/src/main/java/com/legacy/order/model/Order.java b/src/main/java/com/legacy/order/model/Order.java new file mode 100644 index 0000000..61c49ac --- /dev/null +++ b/src/main/java/com/legacy/order/model/Order.java @@ -0,0 +1,44 @@ +package com.legacy.order.model; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +public class Order { + + private long id; + private String dingdanBianhao; + private String kehuMingzi; + private String zhuangtai; // "NEW", "CANCELLED", "DONE" - 用String而非枚举 + private double totalAmount; + private Date createTime; + private Date updateTime; + private List items = new ArrayList(); + + public Order() { + } + + public long getId() { return id; } + public void setId(long id) { this.id = id; } + + public String getDingdanBianhao() { return dingdanBianhao; } + public void setDingdanBianhao(String dingdanBianhao) { this.dingdanBianhao = dingdanBianhao; } + + public String getKehuMingzi() { return kehuMingzi; } + public void setKehuMingzi(String kehuMingzi) { this.kehuMingzi = kehuMingzi; } + + public String getZhuangtai() { return zhuangtai; } + public void setZhuangtai(String zhuangtai) { this.zhuangtai = zhuangtai; } + + public double getTotalAmount() { return totalAmount; } + public void setTotalAmount(double totalAmount) { this.totalAmount = totalAmount; } + + public Date getCreateTime() { return createTime; } + public void setCreateTime(Date createTime) { this.createTime = createTime; } + + public Date getUpdateTime() { return updateTime; } + public void setUpdateTime(Date updateTime) { this.updateTime = updateTime; } + + public List getItems() { return items; } + public void setItems(List items) { this.items = items; } +} diff --git a/src/main/java/com/legacy/order/model/OrderItem.java b/src/main/java/com/legacy/order/model/OrderItem.java new file mode 100644 index 0000000..fecc218 --- /dev/null +++ b/src/main/java/com/legacy/order/model/OrderItem.java @@ -0,0 +1,32 @@ +package com.legacy.order.model; + +public class OrderItem { + + private long id; + private long orderId; + private String shangpinMingcheng; + private double danjia; + private int shuliang; + private double jine; + + public OrderItem() { + } + + public long getId() { return id; } + public void setId(long id) { this.id = id; } + + public long getOrderId() { return orderId; } + public void setOrderId(long orderId) { this.orderId = orderId; } + + public String getShangpinMingcheng() { return shangpinMingcheng; } + public void setShangpinMingcheng(String shangpinMingcheng) { this.shangpinMingcheng = shangpinMingcheng; } + + public double getDanjia() { return danjia; } + public void setDanjia(double danjia) { this.danjia = danjia; } + + public int getShuliang() { return shuliang; } + public void setShuliang(int shuliang) { this.shuliang = shuliang; } + + public double getJine() { return jine; } + public void setJine(double jine) { this.jine = jine; } +} diff --git a/src/main/java/com/legacy/order/model/OrderStatus.java b/src/main/java/com/legacy/order/model/OrderStatus.java new file mode 100644 index 0000000..1c2e743 --- /dev/null +++ b/src/main/java/com/legacy/order/model/OrderStatus.java @@ -0,0 +1,18 @@ +package com.legacy.order.model; + +public enum OrderStatus { + + NEW("新建"), + CANCELLED("已取消"), + DONE("已完成"); + + private String desc; + + OrderStatus(String desc) { + this.desc = desc; + } + + public String getDesc() { + return desc; + } +} diff --git a/src/main/java/com/legacy/order/service/OrderService.java b/src/main/java/com/legacy/order/service/OrderService.java new file mode 100644 index 0000000..ee44647 --- /dev/null +++ b/src/main/java/com/legacy/order/service/OrderService.java @@ -0,0 +1,247 @@ +package com.legacy.order.service; + +import com.legacy.order.dao.OrderDao; +import com.legacy.order.model.Order; +import com.legacy.order.model.OrderItem; +import com.legacy.order.model.OrderStatus; + +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +public class OrderService { + + private static int orderNumberSequence = 1000; + + private OrderDao orderDao = new OrderDao(); + + public String generateOrderNumber() { + orderNumberSequence = orderNumberSequence + 1; + SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd"); + String dateStr = sdf.format(new Date()); + String seq = String.valueOf(orderNumberSequence); + // 补齐6位 + while (seq.length() < 6) { + seq = "0" + seq; + } + return "ORD" + dateStr + seq; + } + + public Order createOrder(String kehuMingzi, List items) { + System.out.println("=== 开始创建订单 ==="); + System.out.println("客户名称: " + kehuMingzi); + + if (kehuMingzi == null || kehuMingzi.equals("")) { + System.out.println("客户名称不能为空"); + return null; + } + if (items == null || items.size() == 0) { + System.out.println("订单项不能为空"); + return null; + } + + for (int i = 0; i < items.size(); i++) { + OrderItem item = items.get(i); + if (item.getShangpinMingcheng() == null || item.getShangpinMingcheng().equals("")) { + System.out.println("第" + (i + 1) + "个订单项的商品名称为空"); + return null; + } + if (item.getDanjia() <= 0) { + System.out.println("第" + (i + 1) + "个订单项的单价必须大于0"); + return null; + } + if (item.getShuliang() <= 0) { + System.out.println("第" + (i + 1) + "个订单项的数量必须大于0"); + return null; + } + } + + Order order = new Order(); + order.setDingdanBianhao(generateOrderNumber()); + order.setKehuMingzi(kehuMingzi); + order.setZhuangtai(OrderStatus.NEW.name()); + order.setCreateTime(new Date()); + order.setUpdateTime(new Date()); + + for (int i = 0; i < items.size(); i++) { + OrderItem item = items.get(i); + double tmp = item.getDanjia() * item.getShuliang(); + item.setJine(tmp); + item.setOrderId(0); + } + + double subtotal = 0; + for (int i = 0; i < items.size(); i++) { + OrderItem item = items.get(i); + subtotal = subtotal + item.getDanjia() * item.getShuliang(); + } + System.out.println("小计: " + subtotal); + + double discount = 0; + if (subtotal > 10000) { + discount = subtotal * 0.2; + } else if (subtotal > 5000) { + discount = subtotal * 0.15; + } else if (subtotal > 1000) { + discount = subtotal * 0.1; + } else if (subtotal > 500) { + discount = subtotal * 0.05; + } else if (subtotal > 100) { + discount = subtotal * 0.02; + } + int intDiscount = (int) discount; + System.out.println("折扣金额: " + intDiscount); + + double afterDiscount = 0; + for (int i = 0; i < items.size(); i++) { + OrderItem item = items.get(i); + afterDiscount = afterDiscount + item.getDanjia() * item.getShuliang(); + } + afterDiscount = afterDiscount - intDiscount; + + double taxRate = 0.17; + double tax = afterDiscount * taxRate; + double finalAmount = afterDiscount + tax; + + System.out.println("折后金额: " + afterDiscount); + System.out.println("税额(" + (taxRate * 100) + "%): " + tax); + System.out.println("最终金额: " + finalAmount); + + order.setTotalAmount(finalAmount); + order.setItems(items); + + orderDao.save(order); + + for (int i = 0; i < items.size(); i++) { + items.get(i).setOrderId(order.getId()); + } + + System.out.println("订单创建完成: " + order.getDingdanBianhao()); + System.out.println("=== 创建订单结束 ==="); + + return order; + } + + public Order getOrder(long id) { + System.out.println("查询订单: " + id); + Order order = orderDao.findById(id); + if (order == null) { + System.out.println("订单不存在: " + id); + } + return order; + } + + public List getAllOrders() { + System.out.println("查询所有订单"); + return orderDao.findAll(); + } + + public List searchByProductName(String name) { + System.out.println("搜索商品名称: " + name); + if (name == null || name.equals("")) { + return new ArrayList(); + } + return orderDao.findByProductName(name); + } + + public boolean cancelOrder(long orderId) { + System.out.println("取消订单: " + orderId); + Order order = orderDao.findById(orderId); + + if (order.getZhuangtai().equals(OrderStatus.CANCELLED.name())) { + System.out.println("订单已取消,不能重复取消"); + return false; + } + if (order.getZhuangtai().equals(OrderStatus.DONE.name())) { + System.out.println("已完成订单不能取消"); + return false; + } + + orderDao.cancelOrder(orderId); + System.out.println("订单取消成功: " + orderId); + return true; + } + + public String getOrderReport() { + System.out.println("生成订单统计报表"); + List allOrders = orderDao.findAll(); + int total = allOrders.size(); + double totalAmount = 0; + int cancelledCount = 0; + + for (int i = 0; i < allOrders.size(); i++) { + Order o = allOrders.get(i); + totalAmount = totalAmount + o.getTotalAmount(); + if (o.getZhuangtai().equals(OrderStatus.CANCELLED.name())) { + cancelledCount = cancelledCount + 1; + } + } + + String report = "===== 订单统计报表 =====\n" + + "订单总数: " + total + "\n" + + "总金额: " + totalAmount + "\n" + + "已取消: " + cancelledCount + "\n" + + "========================"; + System.out.println(report); + return report; + } + + public int importOrdersFromExcel(String filePath) { + // TODO: 等老王把POI依赖加进来再实现 + System.out.println("批量导入暂未实现: " + filePath); + return 0; + } + + private void sendEmail(String to, String subject, String content) { + String smtpHost = "192.168.1.100"; + int smtpPort = 25; + String from = "noreply@company.com"; + + System.out.println("发送邮件..."); + System.out.println(" SMTP: " + smtpHost + ":" + smtpPort); + System.out.println(" From: " + from); + System.out.println(" To: " + to); + System.out.println(" Subject: " + subject); + System.out.println(" Content: " + content); + + // TODO: 实际发送逻辑,等运维给正式的SMTP账号 + System.out.println("邮件发送成功(模拟)"); + } + + public double calcItemJine(double danjia, int shuliang) { + return danjia * shuliang; + } + + public double calcWithTax(double amount) { + return amount * 1.17; + } + + public double calcDiscount(double amount) { + if (amount > 10000) { + return amount * 0.2; + } else if (amount > 5000) { + return amount * 0.15; + } else if (amount > 1000) { + return amount * 0.1; + } else if (amount > 500) { + return amount * 0.05; + } else if (amount > 100) { + return amount * 0.02; + } + return 0; + } + + public boolean validateItem(OrderItem item) { + if (item.getShangpinMingcheng() == null || item.getShangpinMingcheng().equals("")) { + return false; + } + if (item.getDanjia() <= 0) { + return false; + } + if (item.getShuliang() <= 0) { + return false; + } + return true; + } +} diff --git a/src/main/java/com/legacy/order/util/DbUtil.java b/src/main/java/com/legacy/order/util/DbUtil.java new file mode 100644 index 0000000..1b2753b --- /dev/null +++ b/src/main/java/com/legacy/order/util/DbUtil.java @@ -0,0 +1,50 @@ +package com.legacy.order.util; + +import java.sql.Connection; +import java.sql.DriverManager; + +public class DbUtil { + + private static final String URL = "jdbc:h2:mem:legacydb;DB_CLOSE_DELAY=-1"; + private static final String USER = "sa"; + private static final String PASSWORD = "password123"; + + private static DbUtil instance; + private static Connection conn; + + private DbUtil() { + System.out.println("DbUtil初始化了"); + } + + public static DbUtil getInstance() { + if (instance == null) { + instance = new DbUtil(); + } + return instance; + } + + public Connection getConnection() { + try { + Class.forName("org.h2.Driver"); + conn = DriverManager.getConnection(URL, USER, PASSWORD); + System.out.println("创建了新数据库连接"); + return conn; + } catch (Exception e) { + e.printStackTrace(); + return null; + } + } + + public void closeConnection() { + try { + if (conn != null && !conn.isClosed()) { + conn.close(); + } + } catch (Exception e) { + } + } + + public static Connection getConn() { + return DbUtil.getInstance().getConnection(); + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties new file mode 100644 index 0000000..7436067 --- /dev/null +++ b/src/main/resources/application.properties @@ -0,0 +1,12 @@ +server.port=8080 + +spring.datasource.url=jdbc:h2:mem:legacydb;DB_CLOSE_DELAY=-1 +spring.datasource.driver-class-name=org.h2.Driver +spring.datasource.username=sa +spring.datasource.password=password123 + +spring.h2.console.enabled=true +spring.h2.console.path=/h2-console + +spring.datasource.initialization-mode=always +spring.datasource.schema=classpath:schema.sql diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql new file mode 100644 index 0000000..920e1ab --- /dev/null +++ b/src/main/resources/schema.sql @@ -0,0 +1,22 @@ +DROP TABLE IF EXISTS t_order_item; +DROP TABLE IF EXISTS t_order; + +CREATE TABLE t_order ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + dingdan_bianhao VARCHAR(32) NOT NULL, + kehu_mingzi VARCHAR(100), + zhuangtai VARCHAR(20) DEFAULT 'NEW', + total_amount DECIMAL(10,2) DEFAULT 0.00, + create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE t_order_item ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + order_id BIGINT, + shangpin_mingcheng VARCHAR(200), + danjia DECIMAL(10,2), + shuliang INT, + jine DECIMAL(10,2), + FOREIGN KEY (order_id) REFERENCES t_order(id) +); diff --git a/teacher-docs/README.md b/teacher-docs/README.md new file mode 100644 index 0000000..3b7406f --- /dev/null +++ b/teacher-docs/README.md @@ -0,0 +1,268 @@ +# 遗留订单系统 - 项目说明与学习指南 + +> 本项目是一个刻意构造的“遗留系统”代码练习场,供团队内部进行代码审查、重构、安全测试等技能训练使用。 + +--- + +## 一、项目概述 + +这是一个模拟真实环境的电商订单系统,代码风格、架构设计、命名规范均刻意模仿了多年维护、多人经手的遗留项目。系统本身**功能可用**,但代码质量和安全性存在大量隐患。 + +### 技术栈 + +| 组件 | 版本 | +|------|------| +| 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,以及如何在设计层面避免。