超市收银系统是Java初学者练手的经典项目,它涵盖了实体设计、集合操作、流程控制、数据统计等多个知识点,同时又不需要复杂的界面技术,用控制台就能完整跑通整个业务流程。本文将从系统模块划分讲起,逐步实现商品管理、购物车逻辑、结算找零和销售报表输出,给出一份可以直接运行的完整方案。

一、系统模块划分与实体类设计
动手写代码之前,先把业务拆成几个清晰的模块。一个最小可用的收银系统通常包括四块:商品管理(维护商品档案与库存)、购物车(记录本次待结算的明细)、结算模块(计算金额、处理优惠与找零)以及报表模块(保存销售记录并输出统计)。模块之间通过实体类传递数据,职责清晰才方便后续扩展。
实体类是整个系统的骨架。先定义Product表示商品,包含编号、名称、单价、库存等字段;再定义CartItem表示购物车中的一条明细,持有商品引用并记录购买数量;最后定义Order表示一笔完成的交易,保存交易时间、明细列表、应收实收金额等信息。建议所有实体类的字段都用private修饰,通过getter和setter访问,这是面向对象封装的基本要求。
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
public class Product {
private String id; // 商品编号
private String name; // 商品名称
private double price; // 单价
private int stock; // 库存数量
public Product(String id, String name, double price, int stock) {
this.id = id;
this.name = name;
this.price = price;
this.stock = stock;
}
public String getId() { return id; }
public String getName() { return name; }
public double getPrice() { return price; }
public int getStock() { return stock; }
public void setStock(int stock) { this.stock = stock; }
}
public class CartItem {
private Product product;
private int quantity;
public CartItem(Product product, int quantity) {
this.product = product;
this.quantity = quantity;
}
public Product getProduct() { return product; }
public int getQuantity() { return quantity; }
public void setQuantity(int quantity) { this.quantity = quantity; }
// 该明细的小计金额
public double subtotal() {
return product.getPrice() * quantity;
}
}
public class Order {
private LocalDateTime time;
private List<CartItem> items = new ArrayList<>();
private double total;
private double discount;
private double paid;
public Order(LocalDateTime time, List<CartItem> items,
double total, double discount, double paid) {
this.time = time;
this.items = items;
this.total = total;
this.discount = discount;
this.paid = paid;
}
public double getActual() { return total - discount; }
public LocalDateTime getTime() { return time; }
public List<CartItem> getItems() { return items; }
public double getTotal() { return total; }
public double getDiscount() { return discount; }
public double getPaid() { return paid; }
}这里有一个容易踩的坑:CartItem持有的是Product对象的引用,如果结算后扣减库存,购物车明细里的库存字段会同步变化。这本身没有问题,但要注意顺序,必须先计算小计再扣库存,否则可能出现负数库存。另外金额计算不要直接用double相乘累加后直接输出,最好在展示前用String.format格式化保留两位小数,避免出现0.30000000000000004这类浮点误差影响观感。
二、商品管理与购物车的核心逻辑
商品管理用Map来存储,以商品编号为key,查找效率是O(1)。初始化时预置几条测试数据,真实的场景可以从文件或数据库加载,但逻辑层完全一样,这体现了面向接口编程的好处。库存校验是收银场景的关键:收银员输入商品编号和数量时,必须先判断商品是否存在,再判断库存是否充足,任何一步不满足都要给出明确提示并要求重新输入,而不是直接抛异常让程序崩溃。
购物车逻辑的核心是"同一件商品只保留一条明细"。向购物车添加商品时,先遍历判断该商品是否已存在,存在则把数量累加到原有明细上,不存在才新建一条CartItem。这样输出的购物清单更符合收银小票的习惯。下面的示例用Map以商品编号为key存储明细,查找和更新都很方便。
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
public class Store {
private Map<String, Product> products = new HashMap<>();
public Store() {
// 预置商品数据
products.put("P001", new Product("P001", "可口可乐 500ml", 3.5, 100));
products.put("P002", new Product("P002", "农夫山泉 550ml", 2.0, 150));
products.put("P003", new Product("P003", "康师傅方便面", 4.5, 80));
products.put("P004", new Product("P004", "抽纸一提", 12.8, 50));
}
public Product findProduct(String id) {
return products.get(id);
}
}
public class ShoppingCart {
// LinkedHashMap保证打印顺序与加入顺序一致
private Map<String, CartItem> items = new LinkedHashMap<>();
public void addItem(Product product, int qty) {
CartItem existing = items.get(product.getId());
if (existing != null) {
existing.setQuantity(existing.getQuantity() + qty);
} else {
items.put(product.getId(), new CartItem(product, qty));
}
}
public boolean removeItem(String productId) {
return items.remove(productId) != null;
}
public void clear() { items.clear(); }
public boolean isEmpty() { return items.isEmpty(); }
public Map<String, CartItem> getItems() { return items; }
public double calculateTotal() {
double total = 0;
for (CartItem item : items.values()) {
total += item.subtotal();
}
return total;
}
}除了添加和删除,实际收银中还经常需要修改数量。思路与添加类似:找到对应明细后直接setQuantity,但要再次校验新数量不能超过当前库存,且不能小于1。所有校验逻辑集中在一个方法里处理,主流程就会非常干净,这也是控制台程序避免嵌套过深的一个实用技巧。
三、结算流程与优惠规则处理
结算是最能体现业务逻辑复杂度的环节。基础流程是:计算商品总额,套用优惠规则得到优惠金额,得出应收金额,接收顾客付款,校验付款是否充足,最后计算找零。优惠规则建议单独封装成方法,例如会员九折和满100减10可以叠加,计算顺序是先算满减再算折扣,规则之间互不干扰,将来增加新活动只需要加一个方法调用。
金额计算建议统一用分做单位的int类型处理,最后再除以100转回元,这样可以彻底规避浮点误差。初学阶段用double配合格式化输出也能接受,但至少要保证比较金额时不用等号判断,而是用差值小于某个极小值的方式。下面的代码演示完整的结算逻辑。
import java.util.Scanner;
public class CashierService {
private Store store;
private ShoppingCart cart;
private Scanner scanner = new Scanner(System.in);
public CashierService(Store store, ShoppingCart cart) {
this.store = store;
this.cart = cart;
}
// 会员折扣与满减规则
public double calcDiscount(double total, boolean isVip) {
double discount = 0;
if (total >= 100) {
discount += 10; // 满100减10
}
if (isVip) {
discount += (total - discount) * 0.1; // 会员再打九折
}
return discount;
}
public void checkout(boolean isVip, SalesLog salesLog) {
if (cart.isEmpty()) {
System.out.println("购物车为空,无法结算!");
return;
}
double total = cart.calculateTotal();
double discount = calcDiscount(total, isVip);
double actual = total - discount;
System.out.printf("商品总额:%.2f 元,优惠:%.2f 元,应收:%.2f 元%n",
total, discount, actual);
System.out.print("请输入收款金额:");
double paid = scanner.nextDouble();
if (paid < actual) {
System.out.println("收款不足,结算取消!");
return;
}
System.out.printf("找零:%.2f 元%n", paid - actual);
// 扣减库存
for (CartItem item : cart.getItems().values()) {
Product p = item.getProduct();
p.setStock(p.getStock() - item.getQuantity());
}
// 生成订单并记录
Order order = new Order(java.time.LocalDateTime.now(),
new ArrayList<>(cart.getItems().values()),
total, discount, paid);
salesLog.add(order);
cart.clear();
System.out.println("结算完成,感谢惠顾!");
}
}注意结算失败的处理:收款不足时不能扣库存也不能清空购物车,代码中直接return返回主菜单,让收银员可以重新操作。这种"先校验后修改"的写法是事务思维在控制台程序中的简化体现,保证了数据的一致性。
四、销售记录与日报表输出
每完成一笔交易,就把Order对象存入一个列表,用Collections或Stream做统计就非常轻松。日报表至少要包含:交易笔数、销售总额、优惠总额、实收总额,再按商品维度统计销量排行,方便店主了解哪些商品卖得好。控制台输出用String.format对齐列宽,即使没有图形界面也能呈现出接近小票的排版效果。
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
public class SalesLog {
private List<Order> orders = new java.util.ArrayList<>();
public void add(Order order) { orders.add(order); }
public void printDailyReport() {
System.out.println("============ 日报表 ============");
System.out.printf("交易笔数:%d 笔%n", orders.size());
double totalSum = 0, discountSum = 0, actualSum = 0;
for (Order o : orders) {
totalSum += o.getTotal();
discountSum += o.getDiscount();
actualSum += o.getTotal() - o.getDiscount();
}
System.out.printf("销售总额:%.2f 元%n", totalSum);
System.out.printf("优惠总额:%.2f 元%n", discountSum);
System.out.printf("实收总额:%.2f 元%n", actualSum);
// 按商品统计销量排行
Map<String, Integer> rank = new java.util.LinkedHashMap<>();
for (Order o : orders) {
for (CartItem item : o.getItems()) {
rank.merge(item.getProduct().getName(),
item.getQuantity(), Integer::sum);
}
}
rank.entrySet().stream()
.sorted((a, b) -> b.getValue() - a.getValue())
.forEach(e -> System.out.printf("%-16s 销量:%d%n",
e.getKey(), e.getValue()));
System.out.println("================================");
}
}如果想把报表落到文件里,可以在printDailyReport的基础上追加一段BufferedWriter的写入逻辑,把同样的内容写入sales.txt,营业结束后留档备查。再进一步,可以把Order序列化后保存,程序重启时加载历史数据,实现跨天的累计统计,这已经接近一个简化版POS系统的持久层设计了。
五、主程序入口与运行效果
最后用main方法把所有模块串起来,主菜单循环展示功能选项:1添加商品、2移除商品、3查看购物车、4结算、5打印日报表、0退出。Scanner读取选项后用switch分发到对应模块,循环直到选择退出。控制台程序的交互骨架基本都是这个模式,掌握了它再去做图书管理、点餐系统都是同一套思路。
public class Main {
public static void main(String[] args) {
Store store = new Store();
ShoppingCart cart = new ShoppingCart();
SalesLog salesLog = new SalesLog();
CashierService cashier = new CashierService(store, cart);
java.util.Scanner sc = new java.util.Scanner(System.in);
while (true) {
System.out.println("\n===== 超市收银系统 =====");
System.out.println("1.添加商品到购物车");
System.out.println("2.从购物车移除商品");
System.out.println("3.查看购物车");
System.out.println("4.结算");
System.out.println("5.打印日报表");
System.out.println("0.退出系统");
System.out.print("请选择操作:");
String choice = sc.next();
switch (choice) {
case "1":
System.out.print("请输入商品编号:");
String id = sc.next();
Product p = store.findProduct(id);
if (p == null) {
System.out.println("商品不存在!");
break;
}
System.out.printf("商品:%s 单价:%.2f 库存:%d%n",
p.getName(), p.getPrice(), p.getStock());
System.out.print("请输入数量:");
int qty = sc.nextInt();
if (qty <= 0 || qty > p.getStock()) {
System.out.println("数量不合法或库存不足!");
break;
}
cart.addItem(p, qty);
System.out.println("已添加到购物车。");
break;
case "2":
System.out.print("请输入要移除的商品编号:");
cart.removeItem(sc.next());
break;
case "3":
for (CashierServiceignored : java.util.Collections.emptyList()) {}
cart.getItems().values().forEach(i ->
System.out.printf("%-16s x%-3d 小计:%.2f 元%n",
i.getProduct().getName(),
i.getQuantity(), i.subtotal()));
break;
case "4":
System.out.print("是否会员(y/n):");
boolean vip = "y".equalsIgnoreCase(sc.next());
cashier.checkout(vip, salesLog);
break;
case "5":
salesLog.printDailyReport();
break;
case "0":
System.out.println("系统已退出。");
return;
default:
System.out.println("无效选项,请重新输入。");
}
}
}
}运行后依次添加商品、结算,再打印报表,就能看到完整的交易闭环。这个项目虽然小,但把实体封装、Map与List的操作、Stream统计、格式化输出、流程控制全部串联了一遍。后续如果想升级,可以考虑加入文件持久化、登录验证、退货流程等功能,每加一个功能都是对设计能力的一次锻炼,代码结构也完全可以在这个骨架上平滑扩展。