init: 遗留订单系统初始版本
This commit is contained in:
14
.gitignore
vendored
Normal file
14
.gitignore
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
# === Maven ===
|
||||
target/
|
||||
|
||||
# === IDE ===
|
||||
.idea/
|
||||
*.iml
|
||||
.vscode/
|
||||
.settings/
|
||||
.project
|
||||
.classpath
|
||||
|
||||
# === OS ===
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
64
README.md
Normal file
64
README.md
Normal file
@@ -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)。
|
||||
|
||||
65
mytest.txt
Normal file
65
mytest.txt
Normal file
@@ -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
|
||||
53
pom.xml
Normal file
53
pom.xml
Normal file
@@ -0,0 +1,53 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<groupId>com.legacy</groupId>
|
||||
<artifactId>legacy-order-system</artifactId>
|
||||
<version>1.0.0</version>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<name>order-system</name>
|
||||
<description>订单管理系统</description>
|
||||
|
||||
<parent>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
<version>2.1.18.RELEASE</version>
|
||||
<relativePath/>
|
||||
</parent>
|
||||
|
||||
<properties>
|
||||
<java.version>1.8</java.version>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-jdbc</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.h2database</groupId>
|
||||
<artifactId>h2</artifactId>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
15
src/main/java/com/legacy/order/LegacyOrderApplication.java
Normal file
15
src/main/java/com/legacy/order/LegacyOrderApplication.java
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
190
src/main/java/com/legacy/order/controller/OrderController.java
Normal file
190
src/main/java/com/legacy/order/controller/OrderController.java
Normal file
@@ -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<String, Object> err = new HashMap<>();
|
||||
err.put("code", -1);
|
||||
err.put("msg", "客户名称不能为空!");
|
||||
return err;
|
||||
}
|
||||
|
||||
if (req.getItems() == null || req.getItems().isEmpty()) {
|
||||
Map<String, Object> err = new HashMap<>();
|
||||
err.put("code", -1);
|
||||
err.put("msg", "订单项不能为空!");
|
||||
return err;
|
||||
}
|
||||
|
||||
List<OrderItem> 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<String, Object> err = new HashMap<>();
|
||||
err.put("code", -1);
|
||||
err.put("msg", "第" + (i + 1) + "个商品的名称为空!");
|
||||
return err;
|
||||
}
|
||||
if (ir.getDanjia() == null || ir.getDanjia() <= 0) {
|
||||
Map<String, Object> err = new HashMap<>();
|
||||
err.put("code", -1);
|
||||
err.put("msg", "第" + (i + 1) + "个商品的单价不合法!");
|
||||
return err;
|
||||
}
|
||||
if (ir.getShuliang() == null || ir.getShuliang() <= 0) {
|
||||
Map<String, Object> 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<String, Object> err = new HashMap<>();
|
||||
err.put("code", -1);
|
||||
err.put("msg", "创建订单失败,请检查参数!");
|
||||
return err;
|
||||
}
|
||||
|
||||
Map<String, Object> 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<String, Object> 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<Order> orders = orderService.getAllOrders();
|
||||
Map<String, Object> 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<Order> orders = orderService.searchByProductName(name);
|
||||
Map<String, Object> 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<String, Object> result = new HashMap<>();
|
||||
result.put("code", ok ? 0 : -1);
|
||||
result.put("msg", ok ? "取消成功" : "取消失败");
|
||||
return result;
|
||||
} catch (Exception e) {
|
||||
Map<String, Object> 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<String, Object> result = new HashMap<>();
|
||||
result.put("code", 0);
|
||||
result.put("report", report);
|
||||
return result;
|
||||
}
|
||||
|
||||
// ======== 内部请求DTO(放在Controller里,没有独立文件) ========
|
||||
|
||||
/**
|
||||
* 创建订单请求体
|
||||
*/
|
||||
public static class OrderRequest {
|
||||
private String kehuMingzi;
|
||||
private List<ItemRequest> items;
|
||||
|
||||
public String getKehuMingzi() { return kehuMingzi; }
|
||||
public void setKehuMingzi(String kehuMingzi) { this.kehuMingzi = kehuMingzi; }
|
||||
|
||||
public List<ItemRequest> getItems() { return items; }
|
||||
public void setItems(List<ItemRequest> 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; }
|
||||
}
|
||||
}
|
||||
237
src/main/java/com/legacy/order/dao/OrderDao.java
Normal file
237
src/main/java/com/legacy/order/dao/OrderDao.java
Normal file
@@ -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<Order> findAll() {
|
||||
List<Order> list = new ArrayList<Order>();
|
||||
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<Order> findByProductName(String name) {
|
||||
List<Order> list = new ArrayList<Order>();
|
||||
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<OrderItem> findItemsByOrderId(long orderId) {
|
||||
List<OrderItem> items = new ArrayList<OrderItem>();
|
||||
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) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
44
src/main/java/com/legacy/order/model/Order.java
Normal file
44
src/main/java/com/legacy/order/model/Order.java
Normal file
@@ -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<OrderItem> items = new ArrayList<OrderItem>();
|
||||
|
||||
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<OrderItem> getItems() { return items; }
|
||||
public void setItems(List<OrderItem> items) { this.items = items; }
|
||||
}
|
||||
32
src/main/java/com/legacy/order/model/OrderItem.java
Normal file
32
src/main/java/com/legacy/order/model/OrderItem.java
Normal file
@@ -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; }
|
||||
}
|
||||
18
src/main/java/com/legacy/order/model/OrderStatus.java
Normal file
18
src/main/java/com/legacy/order/model/OrderStatus.java
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
247
src/main/java/com/legacy/order/service/OrderService.java
Normal file
247
src/main/java/com/legacy/order/service/OrderService.java
Normal file
@@ -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<OrderItem> 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<Order> getAllOrders() {
|
||||
System.out.println("查询所有订单");
|
||||
return orderDao.findAll();
|
||||
}
|
||||
|
||||
public List<Order> searchByProductName(String name) {
|
||||
System.out.println("搜索商品名称: " + name);
|
||||
if (name == null || name.equals("")) {
|
||||
return new ArrayList<Order>();
|
||||
}
|
||||
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<Order> 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;
|
||||
}
|
||||
}
|
||||
50
src/main/java/com/legacy/order/util/DbUtil.java
Normal file
50
src/main/java/com/legacy/order/util/DbUtil.java
Normal file
@@ -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();
|
||||
}
|
||||
}
|
||||
12
src/main/resources/application.properties
Normal file
12
src/main/resources/application.properties
Normal file
@@ -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
|
||||
22
src/main/resources/schema.sql
Normal file
22
src/main/resources/schema.sql
Normal file
@@ -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)
|
||||
);
|
||||
268
teacher-docs/README.md
Normal file
268
teacher-docs/README.md
Normal file
@@ -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,以及如何在设计层面避免。
|
||||
Reference in New Issue
Block a user