init: 遗留订单系统初始版本

This commit is contained in:
micrograils
2026-06-13 16:53:08 +08:00
commit ec177c6a97
15 changed files with 1331 additions and 0 deletions

14
.gitignore vendored Normal file
View File

@@ -0,0 +1,14 @@
# === Maven ===
target/
# === IDE ===
.idea/
*.iml
.vscode/
.settings/
.project
.classpath
# === OS ===
.DS_Store
Thumbs.db

64
README.md Normal file
View 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
View 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
View 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>

View 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);
}
}

View 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; }
}
}

View 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) {
}
}
}
}

View 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; }
}

View 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; }
}

View 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;
}
}

View 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;
}
}

View 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();
}
}

View 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

View 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
View 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以及如何在设计层面避免。