小区蔬菜水果商城系统实战:Spring Boot从设计到部署全解析
做了几年 Spring Boot 项目其实我有个很深的体会“小区蔬菜水果商城系统”这种题目看起来就是一套普通电商的增删改查但真动手做的时候你会发现“小区”和“蔬菜水果”这两个词才是决定整个系统设计走向的关键。先说结论这是一个非常适合 Spring Boot 入门到进阶的实战项目。它覆盖了用户端、管理端、订单交易、库存、支付、文件存储、缓存、部署这些完整链路技术栈又不会难到劝退。但同时它和那些卖标品、数码产品的电商系统有很多细节差异——生鲜有损耗、有配送时效、有库存实时性要求小区场景又有典型的“小范围、高频次、强时效”特征。如果只是照搬一套通用商城做完会发现很多地方是拧巴的。这篇博客我会从实际开发角度把这个系统的功能边界、表结构、核心交易链路、文件存储、部署兼容性一步步拆开讲顺便把我在这个项目里踩过的坑、最后怎么解决的也一起交代了。适合正在做毕设、想拿一个完整项目练手、或者准备面试时需要一个电商类项目经验的开发者参考。1. 这个小区的商城系统和普通电商到底差在哪先把业务想清楚再写代码这是我做项目一贯的顺序。不然代码写了一半需求一变返工成本很高。1.1 两个关键词带来的设计约束“小区”意味着什么意味着配送范围极小、用户群体集中、复购率高。用户可能今天买几个西红柿明天买一把青菜下单频率比淘宝那种“想起来了买一次”高得多。所以这个系统对操作便捷度的要求很高——商品浏览要快、下单要快、购物车要好用你总不能让人家每次买个菜还要像逛淘宝一样翻半天。“蔬菜水果”意味着什么意味着SKU多但库存浅、保质期短、价格波动频繁。白菜不会因为你在系统里设置了1000件库存就真的放1000件在货架上它今天进多少货、卖完就下架剩下的晚上还要打折清掉。所以库存字段的设计、上下架的逻辑比起标品电商要更灵活。这两个约束落到系统设计上就是几件事用户的默认收货地址基本就是小区内部甚至可以是自提点配送逻辑不需要复杂的物流系统。商品的库存数量要小但上下架状态要优先级很高库存为0自动下架是刚需。需要支持配送时段比如“17:00-18:30送达”因为社区团购和生鲜配送有这个典型交互。订单状态不能照搬淘宝那套“待发货/已发货/已签收”应该更贴合“待支付/备货中/配送中/已完成”。1.2 功能边界先做核心闭环别一上来就造大车很多初学者拿到题目后第一反应是我要做会员积分、要做拼团砍价、要做秒杀、要做多商家入驻。我劝你冷静。一个毕设或者练手项目最忌讳的就是功能清单拉得很长结果每个模块都是半成品。我当时给自己定的范围是这样的用户端注册登录、商品分类浏览、商品搜索、购物车、下单、支付支付宝沙箱、订单列表与详情、收货地址管理。管理端商品管理含上下架、库存调整、分类管理、订单管理发货/配送状态流转、会员列表、基础数据统计销量、销售额、热销商品。这个范围跑通之后核心交易链路是完整的上架商品 → 用户浏览 → 加购物车 → 提交订单 → 支付 → 管理员备货/配送 → 用户确认收货。至于拼团、秒杀、优惠券这些营销玩法属于第二期的事先把地基打好。提示对毕设来说完整跑通的核心闭环远比一堆“有界面但没逻辑”的功能值钱。面试官或者答辩老师问起来你能把一个闭环讲清楚每个表字段、每个状态都说得明明白白这是加分项。2. 表结构设计库存、订单和商品分类怎么建模表结构是这个系统的地基设计得好后面写业务代码就是顺畅的流水线设计得不好后面每写一个查询都在跟表结构较劲。2.1 核心四大表用户、分类、商品、购物车先看不那么复杂的三张表。用户表没太多花样字段基本就是用户名、密码BCrypt加密、手机号、角色USER/ADMIN、状态、创建时间。注意角色字段因为管理端和用户端共用一张用户表后面做权限拦截时就是靠这个字段区分的。分类表要注意的是层级问题。蔬菜水果这个场景分类往往是两级的一级是“蔬菜”“水果”“肉禽蛋”“粮油调味”二级是“叶菜类”“茄果类”“根茎类”这种。我用的是经典的 parent_id 方案父分类的 parent_id 为0查询时先用一级分类加载二级分类列表。商品表是整个系统里字段最多的表之一。我当时的字段大致是商品名称、分类ID、主图、轮播图多张用逗号分隔或JSON数组、详情描述、单位斤/份/个、原价、现价、库存、销量、上架状态、创建时间、更新时间。这里有几个细节值得敲黑板价格用 DECIMAL(10,2)绝对不要用 DOUBLE。浮点数做加法减法会出现 0.10.20.30000000000000004 这种问题钱这种东西一分钱都不能差。主图和轮播图分开存。主图用于列表页小图轮播图用于详情页大图。我见过不少项目把所有图塞进一个字段最后前端展示很难受。库存和销量分开。有些人图省事把库存做成“总库存-销量”算出来然后直接用销量去减。这样一旦有退货、上下架操作记录数据就乱了。购物车表的设计有不同方案有人说用 Redis有人说用数据库表。我最终选了数据库表理由很实在购物车是强数据用户加了几件商品不能因为 Redis 缓存过期就没了。而且这个系统的购物车查询量不大数据库完全扛得住还能直接联表查商品名称、图片、价格。2.2 订单与订单明细一主多从的经典结构订单相关的设计“订单主表 订单明细表”是标配一主多从。订单主表关注的是这笔交易的整体信息订单号、用户ID、总金额、实付金额、支付方式、支付时间、订单状态、收货人、收货地址、备注、创建时间。订单明细表关注的是商品维度的信息订单ID、商品ID、商品名称下单时快照、商品图片下单时快照、单价、数量、小计金额。这里最重要的一个设计理念订单明细里的商品名称、价格、图片必须是在用户下单那一刻存下来的快照下单之后不要再SQL去商品表联查。为什么因为商品表是时刻在变的。今天白菜卖3块你下单时是3块明天涨到5块了你的订单明细如果去联查商品表显示出来的单价就变5块了这订单还怎么对账这种问题开发阶段不会暴露上线运营了必然出事。所以下单时把商品关键信息冗余到订单明细表这是电商系统里人尽皆知的规则但也恰恰是第一次做商城最容易忽略的地方。订单号我用了时间戳随机数的方案比如20250112103045加几位随机数。别直接用数据库自增主键当订单号会暴露你的订单量而且生成规则太容易被猜。雪花算法当然更好但毕设项目没必要引入额外依赖时间戳加随机数够用。2.3 库存扣减谁减库存什么时候减这是一个在系统设计阶段就要定下来的关键决策它的影响贯穿整个订单流程。我把规则定成了支付成功才扣减库存而不是下单时预占库存。原因也简单下单但不支付如果预占库存那么这批货就被“冻住”了用户不支付、定时任务又没及时释放真实库存是够的但系统里显示没货。生鲜类商品库存浅预占机制带来的一致性处理成本偏高对毕设项目不友好。支付时扣库存配合“超时未支付订单自动关闭”的定时任务逻辑上更直观。当然这个方案有自己的代价——存在超卖窗口。两个用户同时为一个商品提交订单并支付理论上可能出现库存只剩1件但两单都支付成功的情况。解决这个问题的经典方案后面第四章我会详细讲这里先说结论用数据库的乐观锁做库存扣减判断是最适合这个项目量级的方案。3. 工程搭建与基础配置从一个干净的Spring Boot工程说起业务设计完了开始搭工程。这一章讲的是我从零到能跑起来的完整过程包括目录怎么分、依赖怎么选、配置怎么写。3.1 分层结构与包命名项目用的是经典的三层架构 plus 扩展controller 层接收请求、service 层业务逻辑、mapper 层数据库操作、entity 包实体类、dto 包数据传输对象、vo 包视图对象、config 包配置类、common 包统一返回结果、异常处理、工具类。com.example.freshshop ├── controller // 接口层 ├── service // 业务逻辑层 ├── mapper // 数据库访问层 ├── entity // 实体类 ├── dto // 入参对象 ├── vo // 出参对象 ├── config // 配置类 └── common // 统一返回、异常、工具类实体类和 DTO/VO 一定要区分开这是我见过太多项目做烂的地方。实体类里的字段对应数据库字段一个萝卜一个坑DTO 是接收前端传参用的VO 是返回给前端用的。有些人不区分直接拿实体类去接收前端参数一旦前端传了一个你数据库里没有的字段MyBatis-Plus 做插入时可能直接报错或者产生脏数据。3.2 依赖选型Maven 管理别什么都塞进来Maven 项目的构建就是维护好 pom.xml这块值得花点心思。我的核心依赖不算多Spring Boot 基础包、Spring Web、Spring Validation参数校验、MyBatis-Plus数据库操作、MySQL 驱动、Redis 客户端Spring Data Redis、Lombok、JWT 相关库jjwt、Hutool工具类、MinIO SDK文件存储。提示用 MyBatis-Plus 而不是原版 MyBatis不是因为原版不好而是它的内置 CRUD、分页插件、条件构造器能让开发效率提升很多。商城系统的数据库操作大部分是单表操作MyBatis-Plus 写起来非常顺手而且它对新手极其友好几乎不需要手写 SQL。另外建议加上 spring-boot-starter-validation参数校验这个事千万别小看。比如下单接口前端传的商品 ID 可能是不存在的、购买数量可能是负数、收货地址可能是空字符串。不校验脏数据就进了数据库后期排查成本非常高。3.3 application.yml 核心配置与常见坑配置文件是整个服务能不能跑起来的关键。我当时的配置要点大致如下server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/fresh_shop?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 123456 redis: host: localhost port: 6379 database: 0 servlet: multipart: max-file-size: 10MB max-request-size: 50MB mybatis-plus: configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0几个容易踩的坑逐一说明MySQL 连接串必须有 serverTimezone 参数。不然后面连接数据库时会报时区错误那个报错信息对新手特别不友好字面意思完全看不出来是这个参数的问题。map-underscore-to-camel-case 一定打开。数据库字段比如create_timeJava 属性是createTime这个配置打开后 MyBatis-Plus 才能自动做映射。不然你查出来一个字段是一个字段赋值全为 null。multipart 上传限制要主动设置。Spring Boot 默认文件上传大小是 1MB商品图片随便一张手机拍的图就超了一开始不设置后面传图必报MaxUploadSizeExceededException。3.4 统一返回结果与全局异常处理前后端联调最怕的就是每个接口返回格式都不一样前端解析要写一堆 if else。所以我在 common 包里做了一个统一的返回结构public class ResultT { private Integer code; private String message; private T data; }正常返回时 code 是 200业务异常时 code 按错误类型定义比如 401 未登录、403 无权限、500 系统错误。前端拿到 code 非 200 的情况直接弹 message 就行非常简单。全局异常处理用RestControllerAdvice实现。业务异常、参数校验异常、兜底异常分别写处理方法避免异常信息直接暴露给前端——对毕设项目来说给用户看 “NullPointerException: null” 这样的堆栈信息既不专业也更危险。4. 核心交易链路认证、缓存、下单与库存扣减前面的都是底座真正有含金量的业务逻辑在这个部分。4.1 登录认证JWT 拦截器登录方案我用了 JWT这也是现在 Spring Boot 项目的主流选择。流程是这样用户登录成功后后端生成一个 JWT 令牌返回给前端前端后续请求在 Header 里带上Authorization: Bearer token后端注册一个拦截器每次请求先解析 token解析失败直接返回 401 未登录解析成功把用户信息放进 ThreadLocal 里供后续业务取用。拦截器里必须区分管理员和普通用户。比如/api/admin/**开头的接口先验证 token再验证角色是否是 ADMIN不是就返回 403。这个思路对保护管理端接口非常关键——商品上下架、订单状态流转这些操作如果被普通用户调用整个系统就失控了。我在 token 里放的是用户 ID 和角色有效期设为 24 小时管理端的 token 有效期短一些这是安全上的小讲究。4.2 商品列表与 Redis 缓存商品列表是这个系统访问量最大的接口首页、分类页、搜索页都要查。数据库本身扛得住但把热数据缓存到 Redis 里响应速度能从几十毫秒降到个位数毫秒这种优化做起来不难性价比很高。我的缓存策略是分类维度缓存。比如访问“蔬菜”分类下的商品列表缓存 key 是goods:list:category:1第一次请求从数据库查然后写入 Redis 并设置 10 分钟的过期时间后续请求直接读缓存直到过期或者管理员主动更新商品信息时删除对应缓存。这里有一个核心问题缓存和数据库的一致性怎么做我的处理方式是删缓存而不是更新缓存。管理员把白菜的价格从3块改成5块时我先更新数据库然后删掉goods:list:category:1这个缓存 key用户下一次请求发现缓存 miss就会从数据库拉最新数据再把缓存建好。这是业界经典的 Cache Aside 模式简单可靠适合这个项目。复杂的一致性方案像延迟双删、读写锁、消息队列串行化在这个项目里属于杀鸡用了牛刀理解更深的同学可以用但毕设阶段没有太大必要。4.3 下单流程多个步骤如何保证数据安全创建订单的核心逻辑我拆成了下面几步校验用户登录状态从购物车或“立即购买”传入的商品列表里查出商品最新数据校验商品是否存在、是否上架、库存是否充足按实时价格计算总金额生成订单号和订单明细快照扣减库存清空购物车中对应的商品这个链路里最要小心的就是库存扣减。第一种写法是经典的超卖写法Goods goods goodsMapper.selectById(goodsId); if (goods.getStock() quantity) { goods.setStock(goods.getStock() - quantity); goodsMapper.updateById(goods); }两个用户同时查到库存是1都判断能买然后都去更新最后数据库里库存变成0但产生了两个订单。这就是超卖。这个场景我没用悲观锁SELECT FOR UPDATE而是用了数据库乐观锁给商品表加一个 version 字段更新时带着旧的 version 去更新更新的 SQL 里带上 version 条件int count goodsMapper.updateStock(goodsId, quantity, version); if (count 0) { throw new BusinessException(库存不足或商品已变更请重新下单); }对应的 SQL 思路是UPDATE goods SET stock stock - #{quantity}, version version 1 WHERE id #{goodsId} AND version #{version} AND stock #{quantity}affected rows 为0说明要么版本号变了要么库存不够此时直接让用户重新下单。这个方案在并发量不大的小区生鲜场景里非常稳而且代码量极少。4.4 超时未支付订单的关闭机制用户下单后如果一直不支付订单会占着系统资源。我当时的方案是 Spring 定时任务 状态判断每 30 秒扫描一次“已创建但超过 15 分钟未支付”的订单把它们状态改成已关闭同时把当时扣减的库存加回来。Scheduled(fixedDelay 30000) public void closeExpiredOrders() { ... }这里要提醒一下**这个方案靠轮询扫描数据库在大规模场景下效率不高但在这个项目量级下完全够用而且实现简单、可解释性强。**如果在毕设答辩中老师问“为什么不用延迟消息队列”你可以说这个项目的订单量级使轮询不会带来性能瓶颈同时轮询方案更稳定不引入额外中间件也降低了系统复杂度。如果你学有余力引入 RabbitMQ 的延迟队列或者 Redis 过期监听来做确实是更优雅的方案但绝不是必选项。5. 商品图片与文件上传MinIO接入Spring Boot的完整方案商品图片的管理看着不起眼做不好很影响体验。图片不能存数据库BLOB 很蠢也不能就直接丢到项目本地目录因为项目一旦打包部署新上传的图片只要服务重启或者容器重建就丢了。所以我用了 MinIO——一个开源的对象存储服务兼容 S3 接口非常轻量。5.1 为什么是 MinIO 而不是 FastDFS很多教程喜欢用 FastDFS但我个人更推荐 MinIO。原因有三点第一MinIO 部署极其简单就是一个 Docker 命令而 FastDFS 要启动 tracker 和 storage 两个服务配置相对麻烦。第二MinIO 自带了一个 Web 管理界面上传的文件可以直接在浏览器里看排查问题很方便。第三MinIO 官方提供了 Java SDK接入 Spring Boot 非常顺滑代码写起来很直观。5.2 MinIO 的部署与集成部署用 Docker 一条命令就能完成docker run -d -p 9000:9000 -p 9001:9001 \ --name minio \ -e MINIO_ROOT_USERminioadmin \ -e MINIO_ROOT_PASSWORDminioadmin \ -v /opt/minio/data:/data \ minio/minio server /data --console-address :90019000 是 API 端口9001 是管理控制台端口。启动后先到控制台创建一个 bucket比如叫fresh-shop并且把访问策略设置为 public。因为商品图是要被用户直接访问的如果没有 public 权限图片 URL 即使生成出来也无法在浏览器里加载。Spring Boot 集成时我建了一个配置类Configuration public class MinioConfig { Bean public MinioClient minioClient() { return MinioClient.builder() .endpoint(http://localhost:9000) .credentials(minioadmin, minioadmin) .build(); } }上传接口的核心逻辑如下public String upload(MultipartFile file) { String extension FilenameUtils.getExtension(file.getOriginalFilename()); String objectName UUID.randomUUID() . extension; minioClient.putObject(PutObjectArgs.builder() .bucket(fresh-shop) .object(objectName) .stream(file.getInputStream(), file.getSize(), -1) .contentType(file.getContentType()) .build()); return objectName; }文件名用 UUID 生成不要用用户上传的原始文件名。两个原因一个是防乱码中文文件名在 URL 里会出现编码问题另一个是防止覆盖不同用户上传两张都叫“葡萄.jpg”的图片不能互相覆盖。5.3 图片访问的坑这里有两个坑是我实际踩到的第一对象名和URL的拼接。如果桶策略是 public那么图片URL就是http://localhost:9000/fresh-shop/图片对象名。但对象名里如果包含目录前缀比如2025/01/12/uuid.jpgURL需要完整拼接目录前缀。我最终选择不使用目录前缀直接拉平存以当前量级完全够用省去一堆URL拼接的麻烦。第二内网地址与外网地址的问题。本机测试时上传URL是http://localhost:9000但部署到服务器后这个地址必须改成服务器的公网地址不然后端上传接口返回的图片URL用户无法访问。我先把这个配置做成了常量也建议改成配置项部署时改一处就行。6. 打包部署与版本兼容从本机到Docker本地跑通不是终点部署到服务器上稳定运行才是。这个环节看似枯燥实际坑比写业务代码还多。6.1 版本选择Spring Boot 2.7.x 还是 3.x这是个现实问题。如果你用的是 JDK 8老老实实选 Spring Boot 2.7.x如果你用 JDK 17可以上 3.x。热搜里经常看到“springboot版本太高”的抱怨本质上是这样Spring Boot 3.x 是建立在 JDK 17 和 Jakarta EE 9 基础上的很多老项目的第三方库和写法在 3.x 下跑不了。比如 javax.servlet 变成 jakarta.servlet这个改动导致大量基于传统 Servlet 的第三方组件要大改才能兼容。我当时选的是 Spring Boot 2.7.x JDK 8。原因很实在MyBatis-Plus、MinIO SDK、各种工具类对 2.7.x 的兼容性经过大量验证资料最多踩坑后能搜到答案的概率最高。这个项目是毕设或练手项目没必要为了新版而新版。6.2 经典流程Vue前端打包放进Spring Boot这个系统的前端我用的是 Vue开发阶段前后端分离前端跑 8081 端口后端跑 8080 端口通过代理解决跨域。但部署阶段不可能搞两个服务最省事的方案是让 Spring Boot 同时提供静态页面和 API。具体做法非常简单前端执行npm run build产出 dist 目录把 dist 目录下的所有文件复制到 Spring Boot 的src/main/resources/static目录下重新打包 Spring Boot一个 jar 搞定所有内容这种做法部署简单一个进程托管整个应用特别适合个人项目。但要确认前端里的 API 请求地址必须是相对路径比如/api/goods/list而不是http://localhost:8080/api/goods/list。不然前端页面虽然打包进了后端但请求还是发到 localhost在服务器上必然跑不通。6.3 Docker Compose 编排整个环境我最终用 Docker Compose 把 MySQL、Redis、MinIO、应用服务四个组件编排在一起。这一步对毕设来说属于加分项但对理解现代部署方式帮助很大。version: 3.8 services: mysql: image: mysql:8.0 environment: MYSQL_ROOT_PASSWORD: 123456 MYSQL_DATABASE: fresh_shop ports: - 3306:3306 volumes: - mysql-data:/var/lib/mysql redis: image: redis:7-alpine ports: - 6379:6379 minio: image: minio/minio command: server /data --console-address :9001 ports: - 9000:9000 - 9001:9001 app: build: . ports: - 8080:8080 depends_on: - mysql - redis - minio应用服务的 Dockerfile 也很常规FROM openjdk:8-jre COPY target/fresh-shop.jar /app.jar ENTRYPOINT [java, -jar, /app.jar]这里有一个细节我踩过坑启动顺序问题。Spring Boot 应用启动时就要连数据库和 Redis如果 MySQL 容器还没完全就绪应用会反复报连接失败然后启动失败。我最后的处理方式是给应用加一个重启策略配合 Docker 默认的延时机制基本能解决并发启动的问题。你甚至可以写一个简单的 wait-for-it 脚本但多数情况下重启策略已经够用。6.4 springdoc 的坑不需要的接口文档插件直接关掉搜索热词里提到的 “springboot怎么关闭springdoc” 我正好遇到过。有时候你引入了一个全链路追踪的框架或者网关它会自动带进去 springdoc然后每次请求都多出一些文档相关的处理逻辑还会绑定一些意外端口。如果项目中确实用不到这些接口文档能力关闭方式很简单springdoc: api-docs: enabled: false或者直接用依赖排除。这个坑提醒我们引入依赖的时候一定要想清楚它到底带了多少附加组件建议每次引入新依赖后启动一次服务看控制台里多出的自动配置心里有数。7. 复盘如果重新做一遍我会在哪些地方重点投入最后一部分聊点项目做完后的回头思考。这些也是我在答辩和工作面试里最常被问到的问题。7.1 肯定要好好准备的三个细节第一事务要加对地方。创建订单操作涉及写订单表、写订单明细表、扣库存、清购物车这四个操作必须放在同一个数据库事务里。我用的是Transactional(rollbackFor Exception.class)注意带上 rollbackFor 参数这样遇到 RuntimeException 之外的异常时也会回滚。第二参数校验要全面。下单接口前端传过来的商品数量如果是一个负数或者一个天文数字后端必须拦下来。我见过很多项目在入参时完全不做校验导致库存变成负数或者金额对不上排障排到怀疑人生。第三订单状态的流转要有完整记录。至少要在管理端能看出“这个订单当前在哪个环节、什么时候创建的、什么时候支付的、什么时候配送的”。如果后面加上状态变更历史表就是一个加分项。7.2 一定要避开的三个设计坑一是不要把图片存数据库。BLOB字段存图片数据库越来越庞大备份慢、查询慢毫无好处。MinIO方案只需要存一个对象名。二是不要忽视逻辑删除与唯一索引的冲突。用了 MyBatis-Plus 的逻辑删除后同一个商品如果删了又新增数据库里会存在两条记录一个 deleted1 一个 deleted0。这种情况下商品名称无法建唯一索引因为你会存两条同名记录。这个问题的处理方案是自己维护一张“已删除记录归档表”或者允许重名二选一根据业务需求取舍。三是不要为了功能数量牺牲完成度。一个能跑通全流程的、所有按钮真实可用的、数据能对上的系统在答辩或者面试时的说服力远大于十个只做了页面没做逻辑的模块。7.3 下一步还能怎么扩展如果做完上面这些还有余力我有几个扩展方向建议按性价比排序第一个是引入 RabbitMQ 或者 ActiveMQ 做异步通知。比如用户支付成功后发短信通知管理员备货或者订单状态变更事件驱动缓存更新。搜索热词里“springboot整合activemq”“springboot远程调用”指的就是这类能力。第二个是加一个 JVM 本地缓存比如 Caffeine配合 Redis 做二级缓存再往上引一个 Spring Cache 抽象对读多写少的首页能有更明显的性能提升。第三个是数据统计模块做得更丰富一点比如近7日销量走势、分类销售占比这种用 ECharts 画图表展示对答辩很加分。我个人做过几个类似的商城项目之后最大的体会是写一个能跑的项目不难难的是每一个细节都经得起追问。库存为什么这么扣、订单为什么要存快照、缓存为什么删而不更新、事务加在哪里这些问题的答案决定了你做完这个项目之后是“会写代码”还是“理解系统设计”。这套小区蔬菜水果商城系统恰好是能把这一整条链路都串联起来的好项目。做一遍收获会很大。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →