
1. 网上超市系统的需求本质先从业务角度拆清楚做了这么多年Java后端我发现很多人一拿到“网上超市系统”这类题目第一反应就是赶紧建项目、写Controller、连数据库结果做到一半才发现购物车逻辑乱了、库存扣减对不上、订单状态捋不顺最后整个项目推倒重来。其实这类系统的核心难点从来不是CRUD而是业务状态的流转设计和数据一致性的把控。网上超市系统本质上是传统电商的简化版核心用户是两类人C端消费者和B端运营管理员。消费者关心的是能不能快速找到商品、加购物车、下单支付、查看订单管理员关心的是商品上下架、库存管理、订单处理、用户管理。如果做毕业设计或面试项目这四块业务就足够撑起整个系统的骨架了。但这里有一个很关键的设计决定不要一上来就做分销、秒杀、优惠券那一套复杂营销玩法。我见过太多人为了“显得项目大”硬塞一堆模块结果自己都讲不清楚答辩时一问三不知。网上超市系统的核心价值在于把“商品—购物车—订单—库存”这条主链路打磨顺再扩展会员、分类、公告这些辅助功能就完全够用且有说服力了。立项之初我给自己定的功能边界是这样的前台用户注册登录、商品分类浏览与搜索、商品详情、购物车、订单结算、个人中心订单查看/信息维护后台管理员登录、商品管理增删改查/上下架、分类管理、订单管理发货/状态更新、用户管理、基础统计这套边界既覆盖了电商领域的经典业务闭环又不至于因为范围过大而失控。实测下来从零开发到跑通全流程四个功能模块全部落地大概是三周左右的业余时间节奏非常舒服。技术选型上我用的是Spring Boot 2.x MyBatis Plus MySQL 8.0 Redis可选前端是ThymeleafBootstrap没有做前后端分离。原因后面详说先记住结论这是一个用最少成本把业务闭环讲清楚的最佳组合。2. 为什么这样搭Spring Boot 电商项目的技术选型心得2.1 Spring Boot 版本怎么选2.x是当下最稳妥的答案我看到现在还偶尔有人直接用Spring Boot 3.x做这类项目其实不是不行但对大多数同学来说Spring Boot 2.7.x是更推荐的选择。原因在于3.x是基于Jakarta EE的很多老教程、老依赖的包名从javax改成jakarta网上查资料时会发现大量报错解法对不上白白浪费调试时间。另外就是MyBatis Plus的兼容性问题。MP虽然已经支持3.x但如果你参考的文档、视频课是2.x时代的你会发现导入依赖后一堆API报红分页插件配置方式也变过。为了把精力花在业务逻辑而不是环境适配上面我个人建议用2.7.18这个版本它是个非常成熟的稳定版资料量也最大。核心依赖清单大致如下parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version /parent dependencies !-- Web 层 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency !-- Thymeleaf 模板引擎 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-thymeleaf/artifactId /dependency !-- MyBatis Plus 持久层框架 -- dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3.1/version /dependency !-- MySQL 驱动 -- dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency !-- 密码加密工具 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-security/artifactId /dependency !-- Lombok 简化实体类 -- dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency /dependencies2.2 为什么用Thymeleaf而不是前后端分离这是个很容易被忽视的问题。现在前端框架满天飞Vue、React哪个不比服务端渲染“时髦”但如果你的目标是做课程设计、毕业设计或者面试演示项目Thymeleaf Bootstrap的服务端渲染方案实际上是最优解。原因有三点。第一这类项目PPT答辩时面试官/老师最在意的是你能否讲清楚业务逻辑前后端分离意味着你要维护两套工程联调成本、跨域问题、Token鉴权这些都会把主线搅浑。第二Thymeleaf天然和Spring Boot集成后端传Model数据直接在HTML里渲染代码量少一大截演示的时候刷新一个页面就能看到效果变化。第三部署简单一个jar包丢服务器上就能跑省去Nginx配置、前端构建那些环节。我当时也犹豫过要不要用Vue重写一遍前端后来实测下来用Thymeleaf五天的页面工作量换成Vue至少要两周而这个项目本身核心是后端业务所以果断放弃。2.3 MyBatis Plus带来的效率提升非常明显早期项目我用的是原生MyBatis每个实体类都要写一堆XML映射文件找bug的时候还得来回切换Java和XML效率非常低。后来切换到MyBatis Plus单表CRUD几乎不用手写SQL内置的Wrapper条件构造器让动态查询变得极其简洁。举一个典型例子后台商品列表的筛选查询public PageProduct queryProductPage(int page, int size, String keyword, Integer categoryId) { PageProduct pageParam new Page(page, size); LambdaQueryWrapperProduct wrapper new LambdaQueryWrapper(); // 关键字模糊搜索商品名称 wrapper.like(StringUtils.hasText(keyword), Product::getName, keyword); // 分类筛选精确匹配 wrapper.eq(categoryId ! null categoryId 0, Product::getCategoryId, categoryId); // 按上架时间倒序排列 wrapper.orderByDesc(Product::getCreateTime); return productMapper.selectPage(pageParam, wrapper); }看到没整个查询逻辑就这几行没有XML、没有SQL拼接、没有烦人的if标签。关键点在于LambdaQueryWrapper里的那个布尔条件参数StringUtils.hasText(keyword)为false时这个条件会被自动忽略。这意味着你只需要写一套查询逻辑就同时兼顾了无条件查询和带条件查询这在MyBatis里至少要写5个动态SQL标签。2.4 密码安全是这类项目最容易忽视的点很多学生的项目里密码都是明文存数据库的这要是拿到面试官面前基本等于直接暴露了自己没有基本的安全意识。我用的是Spring Security自带的BCryptPasswordEncoder做加密处理。注册时加密PasswordEncoder encoder new BCryptPasswordEncoder(); String encodedPassword encoder.encode(user.getPassword()); user.setPassword(encodedPassword);登录时校验if (encoder.matches(rawPassword, user.getPassword())) { // 密码匹配成功跳转登录 }BCrypt的特别之处在于它是带盐的哈希算法同一个明文密码每次生成的哈希值都不同但校验时依然能匹配。这意味着即使数据库被拖库攻击者也无法通过彩虹表反查密码安全性有基本保障。3. 数据库表设计电商系统最重要的一张图3.1 六张核心表搞定整个业务闭环网上超市的表结构设计我最终定的是六张核心业务表加一张日志表。很多人喜欢设计十几张表但仔细一看很多表之间根本没有业务关联纯粹是凑数量。表不在多能够形成闭环才是关键。我最终的建表清单如下t_user用户表存账号、密码BCrypt加密后的、昵称、手机号、创建时间t_category商品分类表父级ID用于支持二级分类t_product商品表存分类ID、名称、主图、价格以分为单位存储、库存、上下架状态、销量t_cart购物车表用户ID 商品ID 数量 勾选状态t_order订单表订单号、用户ID、总金额、收货信息、订单状态t_order_item订单明细表订单ID 商品ID 单价 数量 小计t_address收货地址表这个可以并入用户表但独立出来更清晰这里有一个非常容易被新手忽略的设计金额字段一律用整数以分为单位存储禁止用double或float。为什么因为浮点数在计算机底层是二进制近似表示的0.1 0.2在Java里可能等于0.30000000000000004。把199.9元的商品计算9折浮点运算就会出现精度问题。我用分存储所有计算都是整数运算展示时再除以100转成元一劳永逸。3.2 订单表的设计是重中之重订单表是整个系统的核心也是面试官最爱深挖的地方。我的t_order表核心字段如下CREATE TABLE t_order ( id BIGINT NOT NULL AUTO_INCREMENT COMMENT 主键, order_no VARCHAR(32) NOT NULL COMMENT 订单编号, user_id BIGINT NOT NULL COMMENT 用户ID, total_amount BIGINT NOT NULL COMMENT 订单总金额(单位:分), receiver_name VARCHAR(32) NOT NULL COMMENT 收货人姓名, receiver_phone VARCHAR(20) NOT NULL COMMENT 收货人电话, receiver_address VARCHAR(255) NOT NULL COMMENT 收货地址, status TINYINT NOT NULL DEFAULT 0 COMMENT 订单状态:0待付款 1待发货 2待收货 3已完成 4已取消, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, pay_time DATETIME DEFAULT NULL, deliver_time DATETIME DEFAULT NULL, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT订单表;订单状态字段用TINYINT数字表示代码里用枚举类去语义化这是很标准的做法。状态流转严格按照电商逻辑走待付款→待发货→待收货→已完成同时允许待付款→已取消。这里的核心是状态的变化一定要由后端方法控制绝不能让前端直接传一个状态值来改。3.3 购物车和库存的关系这是电商事务的核心战场购物车表本身很简单就是记录了用户往购物车加了多少商品。真正的难点在下单那一刻涉及三个表的并发操作扣减商品库存、创建订单、清空购物车。这三个操作要么全成功要么全失败这就需要事务控制。我在写订单提交逻辑时用的是Spring的Transactional注解Transactional(rollbackFor Exception.class) public Order createOrder(Long userId, ListCartItem items, Address address) { // 1. 生成订单主记录状态为待付款 Order order buildOrder(userId, items, address); orderMapper.insert(order); // 2. 扣减库存SELECT ... FOR UPDATE 锁行 for (CartItem item : items) { Product product productMapper.selectByIdForUpdate(item.getProductId()); if (product.getStock() item.getQuantity()) { throw new BusinessException(商品【 product.getName() 】库存不足); } product.setStock(product.getStock() - item.getQuantity()); productMapper.updateById(product); } // 3. 插入订单明细 for (CartItem item : items) { OrderItem orderItem buildOrderItem(item); orderItemMapper.insert(orderItem); } // 4. 清空购物车 cartMapper.deleteByUserId(userId); return order; }这里我需要特意强调selectByIdForUpdate这个方法它是通过SELECT ... FOR UPDATE语句对商品行加了悲观锁。为什么要加锁想象一个场景商品库存只剩1件A和B两个用户同时下单。如果不加锁两个事务都读到库存为1都认为库存充足然后各自扣减为0实际上卖出了2件这就是超卖。加了FOR UPDATE之后A事务先锁住这行商品进行扣减B事务必须等A提交或回滚后才能读这行数据此时此刻B读到的库存已经是0了直接提示库存不足。库存一致性由此得到保证。3.4 订单号生成一个容易翻车的小细节订单号的生成方式看起来是个小问题但实际上非常容易踩坑。有人用System.currentTimeMillis()拼接随机数并发情况下容易出现重复。我用的方案是时间戳 用户ID后四位 随机数的组合public static String generateOrderNo(Long userId) { String timePart new SimpleDateFormat(yyyyMMddHHmmss).format(new Date()); String userPart String.format(%04d, userId % 10000); int randomPart ThreadLocalRandom.current().nextInt(1000, 9999); return timePart userPart randomPart; }这个方案在没有分布式锁的场景下已经足够使用但如果要做到严格不重复更稳妥的办法是在数据库层面加唯一索引兜底真要是撞上了就重新生成一次。数据库的唯一约束才是最后一道防线生成算法只是降低冲突概率的手段。4. 核心功能实现细节从代码层面逐一拆解4.1 用户登录与拦截器不写拦截器等于白做用户登录这块我用的是最简单的Session方案没有引入JWT原因还是那个——项目体量决定技术复杂度。Session方案对单体应用完全够用代码也容易理解。登录成功后把用户对象丢进Sessionsession.setAttribute(loginUser, user);然后写一个拦截器拦截需要登录才能访问的路径public class LoginInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HttpSession session request.getSession(); Object loginUser session.getAttribute(loginUser); if (loginUser null) { response.sendRedirect(/user/login); return false; } return true; } }注册到Web配置里的时候要注意放行哪些路径。如果不放行静态资源CSS、JS全被拦下来页面会变得惨不忍睹Configuration public class WebConfig implements WebMvcConfigurer { Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new LoginInterceptor()) .addPathPatterns(/**) .excludePathPatterns( /user/login, /user/register, /product/**, // 商品浏览不需要登录 /category/**, /css/**, /js/**, /images/** ); } }拦截器的作用相当于给系统装了一道门禁游客可以逛超市看商品但结账和查订单必须登录。这道逻辑如果漏掉任何用户都能通过直接修改URL访问后台功能那就出大事了。4.2 购物车的三种实现方案对比购物车是网上超市系统里实现方式最多样化的模块我在调研阶段对比过三种常见方案方案一纯Session存储。购物车数据放Session里代码量最少但用户一关浏览器购物车就消失体验很差。方案二Cookie存储。购物车数据放在浏览器Cookie里不占服务端存储但Cookie大小限制4KB存不了多少东西而且用户可以随意篡改。方案三数据库存储。购物车表记录用户与商品的关系数据持久化换设备也能看到缺点是每次增删改查都要和数据库交互。我最终选了方案三因为这是一个完整项目数据库存储购物车会让业务闭环更完整而且面试时能顺带聊到“如何用联合唯一索引防止重复添加购物车记录”。购物车表设计了UNIQUE KEY uk_user_product (user_id, product_id)这样同一个用户添加同一个商品如果记录已存在就直接更新数量而不是插入新记录public void addToCart(Long userId, Long productId, Integer quantity) { CartItem existing cartMapper.selectByUserIdAndProductId(userId, productId); if (existing ! null) { existing.setQuantity(existing.getQuantity() quantity); cartMapper.updateById(existing); } else { CartItem cartItem new CartItem(); cartItem.setUserId(userId); cartItem.setProductId(productId); cartItem.setQuantity(quantity); cartItem.setChecked(true); // 默认勾选 cartMapper.insert(cartItem); } }4.3 商品搜索用MyBatis Plus的like实现模糊查询网上超市的商品搜索需求很简单就是根据关键词匹配商品名称和描述。用LambdaQueryWrapper写非常清晰public ListProduct searchProducts(String keyword) { LambdaQueryWrapperProduct wrapper new LambdaQueryWrapper(); wrapper.like(Product::getName, keyword) .or() .like(Product::getDescription, keyword) .eq(Product::getStatus, 1); // 只查询上架状态 return productMapper.selectList(wrapper); }但这里有一个坑like和or组合时如果不加括号SQL的优先级可能不符合预期。MyBatis Plus的or()方法默认是全局的通常会配合and(wrapper - wrapper.like(...).or().like(...))来把逻辑分组。这样生成的SQL语句才是WHERE (name LIKE ? OR description LIKE ?) AND status 1。public ListProduct searchProducts(String keyword) { LambdaQueryWrapperProduct wrapper new LambdaQueryWrapper(); wrapper.and(w - w.like(Product::getName, keyword) .or() .like(Product::getDescription, keyword)) .eq(Product::getStatus, 1); return productMapper.selectList(wrapper); }4.4 秒杀式并发场景的简化处理乐观锁版本号之前说扣库存用悲观锁SELECT ... FOR UPDATE逻辑简单且安全。但如果你想让项目多一点亮点可以考虑把库存扣减换成乐观锁方案。思路是在商品表加一个version字段更新时带上版本号条件UPDATE t_product SET stock stock - #{quantity}, version version 1 WHERE id #{id} AND version #{oldVersion} AND stock #{quantity}如果更新影响行数为0说明版本已变化或库存不足则重试或抛出异常。两者对比一下悲观锁实现简单但在高并发下锁等待会导致吞吐量下降。乐观锁无锁竞争并发效率高但需要处理更新失败后的重试逻辑。对于网上超市这种级别的系统用悲观锁就可以了因为并发量并不高。但面试如果被问到“如何防止超卖”你能同时说出悲观锁和乐观锁两种方案会加不少分。5. 后台管理模块运营人员的工作台怎么设计5.1 商品管理图片上传与表单校验后台商品管理是运营人员最常用的功能核心是表单提交和图片上传。图片上传用Spring Boot的MultipartFile接收文件保存到服务器本地目录再把访问路径存到数据库。PostMapping(/admin/product/save) public String saveProduct(Product product, RequestParam(file) MultipartFile file) throws IOException { if (!file.isEmpty()) { // 生成不重复的文件名时间戳 原始文件名后缀 String originalFilename file.getOriginalFilename(); String suffix originalFilename.substring(originalFilename.lastIndexOf(.)); String fileName System.currentTimeMillis() suffix; // 保存到指定上传目录 String uploadDir D:/upload/; File dest new File(uploadDir fileName); file.transferTo(dest); product.setImage(/upload/ fileName); } productMapper.insert(product); return redirect:/admin/product/list; }需要额外配置一个静态资源映射把/upload/**路径映射到本地磁盘目录Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(/upload/**) .addResourceHandler(file:D:/upload/); }这个配置漏掉的话商品图片全部404页面展示全挂这是我自己踩过的坑印象极其深刻。5.2 订单管理发货操作驱动的状态流转后台订单管理最核心的操作是发货。管理员点击发货订单状态从“待发货”变为“待收货”同时记录发货时间PostMapping(/admin/order/deliver) ResponseBody public Result deliverOrder(RequestParam Long orderId) { Order order orderMapper.selectById(orderId); if (order null) { return Result.error(订单不存在); } if (order.getStatus() ! OrderStatus.WAIT_DELIVER.getCode()) { return Result.error(当前状态不可发货); } order.setStatus(OrderStatus.WAIT_RECEIVE.getCode()); order.setDeliverTime(new Date()); orderMapper.updateById(order); return Result.success(); }这段代码最大的亮点在于状态校验。发货前先检查当前状态是否为“待发货”如果不是直接拒绝操作。这个逻辑称为“状态机模式”可以保证订单状态永远按照预定义的路径流转不会出现从“已取消”直接跳到“待收货”这种非法操作。5.3 简单的数据统计分析订单和用户的整体情况后台首页我放了一个简易的面板展示四个核心指标总用户数、上架商品数、总订单数、当日订单数。这些统计用SQL聚合函数就能搞定public DashboardVO getDashboardData() { DashboardVO vo new DashboardVO(); vo.setTotalUsers(userMapper.selectCount(null)); vo.setTotalProducts(productMapper.selectCount( new LambdaQueryWrapperProduct().eq(Product::getStatus, 1) )); vo.setTotalOrders(orderMapper.selectCount(null)); // 当日订单数 LocalDateTime startOfDay LocalDateTime.now().with(LocalTime.MIN); vo.setTodayOrders(orderMapper.selectCount( new LambdaQueryWrapperOrder().ge(Order::getCreateTime, startOfDay) )); return vo; }这里要记住统计查询如果数据量变大一定要在create_time字段上建索引否则全表扫描会让查询越来越慢。我一般在建表SQL里就直接加上索引。6. 遇到过的坑和解决方案这份避坑指南价值不低6.1 坑一Thymeleaf页面加载不出CSS这个问题初学时几乎人人都碰过。现象是页面打开只有纯文本样式全部丢失。按我的排查经验按以下顺序排查90%的问题能解决浏览器控制台查看报错确认CSS文件的HTTP状态码是404还是403。如果404检查Controller里是否有RequestMapping(/css)之类的路径冲突。如果403检查拦截器是否把静态资源拦截了。检查Thymeleaf页面里引用CSS的th:href{/css/style.css}写法注意{}是必须的。配置了Spring Security的项目还要确认SecurityConfig里放行了静态资源。6.2 坑二金额精度不对1.12.2成了3.3000000000000003这是我当初用double类型存金额时踩过的经典坑。199.9元的订单打个95折算出来的金额居然是189.90500000000001。虽然位数很小但展示给用户就是很奇怪。解决方案就是我前面说的金额统一以“分”为单位的整数存储。数据库层用BIGINTJava层用Long展示层用工具方法除以100转成字符串public static String formatPrice(Long priceInCents) { if (priceInCents null) return 0.00; return String.format(%.2f, priceInCents / 100.0); }这样整个系统里永远不会出现浮点运算精度问题从根源上被消除。6.3 坑三MyBatis Plus分页不生效我早期用MP分页时查出来的数据总是全量翻页根本没效果。排查后发现原因很简单MP的分页需要通过MybatisPlusInterceptor配置一个分页插件不配置的话selectPage只是返回全部数据。Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }这是一个配置类的事但漏掉的人非常多。分页不生效先看这个别去纠结SQL。6.4 坑四事务不生效Transactional注解看起来很简单但有很多细节会导致它悄悄失效。最常见的三种情况注解加在了private方法上Spring的事务代理是基于接口或类的代理实现的private方法不会走代理。同类内部方法调用比如A类方法调用A类的另一个带Transactional的方法此时事务不生效。因为代理对象被绕过了。异常被吞掉了事务方法内try-catch捕了异常没抛出Spring感知不到事务自然不回滚。解决方法是被事务调用的方法放在不同类里或者把异常抛出到事务方法外Transactional加在public方法上配置rollbackFor Exception.class确保任何异常都触发回滚。7. 文档与演示源码之外的加分项7.1 这份项目的文档应该怎么组织很多人拿到源码不知道文档怎么总结。我的经验是文档的价值在于让人快速理解项目而不是把人绕晕。我一般按以下结构组织项目介绍一句话说清楚系统干什么技术栈说明框架版本、核心依赖数据库设计说明每张表的用途核心字段的解释表间关系模块功能介绍前台模块和后台模块的划分每个模块的页面路径部署运行指南从导入数据库到启动项目的每一步包括数据库密码在哪里改核心逻辑说明订单状态流转、库存扣减逻辑、购物车实现方式测试账号管理员账号、测试用户账号文档不求长但求准确。每一步都必须实测虚假的运行步骤会直接消耗读者信任。7.2 演示演示什么最加分项目演示时不要上来就展示CRUD。我的习惯是围绕三条链路演示用户主链路注册→登录→浏览商品→加购物车→下单→查看订单。管理员主链路登录后台→新增商品→处理订单→发货。亮点链路演示库存不足时的提示效果演示未登录访问购物车时的跳转拦截。这三条链路走完整个系统的业务闭环已经清楚了。我再额外手动改一下数据库里的订单状态演示管理员的发货操作对前台订单列表的实时影响这会让“状态流转”这个概念瞬间变得具体可感。7.3 部署上线一台服务器项目做完之后我觉得只在自己电脑上跑没啥意思就买了一台一年的轻量云服务器把系统部署了上去顺便练习了Linux环境下的Java项目部署流程。部署的核心步骤很简单# 1. 安装JDK和MySQL yum install -y java-1.8.0-openjdk mysql-server # 2. 上传项目jar包 scp target/supermarket.jar root服务器IP:/opt/ # 3. 初始化数据库将sql文件导入 mysql -uroot -p supermarket.sql # 4. 启动项目 nohup java -jar supermarket.jar /opt/logs/supermarket.log 21 # 5. 配置安全组规则开放8080端口半个小时的活儿但收获极大。你能亲眼看到自己的系统在真实的公网环境下被访问那种感觉跟本地localhost完全是两码事。部署过程中你还会遇到数据库字符集配置、云服务器安全组规则、进程后台运行管理等实际问题这些都是面试时非常加分的谈资。8. 写在最后关于这类项目的一些真实体会做完这套网上超市我自己最大的感慨是做项目的价值不在于用了多新的技术而在于把一件事情的完整流程跑通并且能清晰地说出每一步为什么这么做。从一开始的模糊想法到需求梳理、数据库设计、编码实现、测试、部署每个环节都会遇到问题而解决问题的过程恰恰是能力提升的过程。比如我直到做这个项目才真正理解了悲观锁和乐观锁的适用场景理解了为什么订单状态要用数字存储而不是字符串理解了Transactional为什么经常“不生效”。如果让我给你一个建议拿到一个项目题目先花两天时间画清楚数据库的表关系图再动代码。数据库设计好了后面的编码就像填空题一样简单。表关系没想清楚就急于编码后期必然会经历大量的改表和返工。这套系统的完整代码、SQL建表脚本和开发文档我都整理成了一个压缩包包含源码目录、数据库脚本、部署说明和演示截图四部分。拿到之后按文档里的步骤操作就能跑起来。如果你在部署或运行过程中卡壳了欢迎随时来交流我可以帮你排查环境或者引导你定位问题。代码这种东西多跑几次就熟了。