1. 项目全貌与面向人群
1.1 这套毕业设计到底在做什么
第一次拿到“springboot二次元商品销售”这个题目时,大多数人想到的是“又一个商城系统”。确实,它的核心跑不出商品展示、购物车、下单、支付、后台管理这套电商老链路,但真正让我觉得适合拿来当毕业设计的原因,恰恰是它不贪大、不浮夸,却把Spring Boot开发中最常用的知识点都串了起来:实体建模、数据持久化、接口设计、权限控制、前后端联调、部署上线,整个流程走完一遍,你对Spring Boot的理解会从“会用”变成“会做”。
我理解的“二次元商品销售”并不是单纯卖虚拟周边,更常见的是手办、盲盒、立牌、徽章这类实体商品。它的业务量级不需要设计成淘宝那种分布式架构,一个单体Spring Boot应用加MySQL加Redis完全够用。对于计算机专业毕业生来说,这套系统的价值在于:它有真实业务背景,有明确角色划分,有订单状态流转,有库存和金额计算,几乎覆盖了企业级开发中的基础通用场景。你在论文里能画出完整业务流程图、功能模块图、ER图,在答辩时也能讲清楚每个接口为什么这么设计。
这套系统通常包含两个端口:用户端和小型管理后台。用户端负责注册登录、浏览商品、加购、下单、模拟支付、查看订单;管理后台负责商品上下架、分类维护、订单处理、发货和统计。平台角色分为普通用户和管理员,权限通过Spring Boot拦截器加JWT Token来控制。如果你是第一次做这种项目,建议先把这个主链路跑通,再叠加上传、搜索、热销榜这类扩展功能。
1.2 技术选型为什么这么定
技术选型是毕业设计里我最看重的一步,因为它直接影响你后面开发的顺利程度和答辩时老师提问的深度。后台主框架我选了Spring Boot 2.7.x,不是最新的3.x,原因有两个:一是2.7.x对JDK 8的支持最稳定,很多学校机房或老旧电脑默认装的就是JDK 8,避免环境问题;二是网上资料最多,遇到问题一搜一大把,对毕设党非常友好。如果你对Java 17和Spring Boot 3.x已经很熟,用新版本也没问题,但要是求稳,2.7.x是更省心的选择。
数据持久层我用MyBatis Plus。为什么不选原生MyBatis?因为写复杂动态SQL太耗时,MyBatis Plus自带通用CRUD、分页插件和逻辑删除,能让开发效率拉满。它并没有剥夺你写自定义SQL的能力,复杂查询仍可以写在Mapper XML里,既省事又灵活,面试时也说得上话。数据库用MySQL 8.0,免费、轻量、学校普遍要求。缓存我加了一个Redis,主要用来做首页商品缓存和Token黑名单,量级不大,但对“高性能”这个点是一个很好的答辩话术。
前端部分,我推荐Vue 3加Element Plus。很多毕设源码会给一套自由模板,质量参差不齐。我的建议是只要你会简单的Vue语法,就自己配一套Vite加Vue 3的项目,也不复杂,重点是接口对接你能完全掌控。UI组件库用Element Plus直接管理后台,用户端可以手写一点点样式配合Tailwind或普通CSS,再不行就用开源商城模板改主题。前后端分离的好处不仅是开发方便,还是你在论文里展示“前后端分离架构”的底气。
JWT鉴权我用的jjwt库,密码加密用的BCrypt。文件上传可以放在本地磁盘,再用一个虚拟映射目录提供给前端访问,没必要为了毕设硬上OSS。支付环节一般接一个模拟支付接口,点击支付按钮后直接修改订单状态和流水,不要真的接支付宝或微信,审核周期和资质要求都不是毕设阶段该碰的。
2. 工程骨架搭建与配置细节
2.1 初始化Spring Boot项目结构
新建工程时我会直接用IntelliJ IDEA的Spring Initializr,先选Maven项目,语言Java,Spring Boot版本按刚刚说的2.7.x。项目结构这块,很多人一上来就按controller/service/mapper三层建包,结果越写越乱。我习惯先按模块分,再按技术分层,比如:
com.example.animemarket ├── common # 通用类:结果封装、异常、常量、工具 │ ├── result │ ├── exception │ └── utils ├── config # 配置类:WebMvc、Cors、MybatisPlus分页、Redis ├── controller # 控制层 │ ├── admin │ └── api ├── service │ ├── impl ├── mapper ├── entity ├── dto └── vo这样分的好处是,管理后台接口和用户端接口可以从controller包上就分离开,权限拦截器也更容易按路径前缀做区分,比如/admin/和/api/。很多新手把全部controller扔在一个包里,靠注解区分角色,后面加个拦截器就头疼,因为拦截规则不好写。
pom.xml里依赖没必要全加,几样必需的:spring-boot-starter-web、spring-boot-starter-validation、mybatis-plus-boot-starter、mysql-connector-j、lombok、jjwt、spring-boot-starter-data-redis。注意MyBatis Plus和Spring Boot版本要匹配,我用的是mybatis-plus-boot-starter 3.5.3.1,配Boot 2.7没问题。Redis如果暂时不想配置,可以先把Lettuce连接池加进去,本地没装Redis就先把缓存逻辑做成开关,否则启动会报连接失败。
2.2 配置文件里的关键参数
application.yml是每次启动报错的高发区,贴一份我常用的配置基线,照着调就行:
server: port: 8080 servlet: context-path: / spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/animemarket?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true username: root password: 123456 redis: host: localhost port: 6379 database: 0 jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: Asia/Shanghai mybatis-plus: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.animemarket.entity 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 jwt: secret: your-secret-key-please-change expire-hours: 24这里有两个坑要提醒你。第一个是serverTimezone必须写明Asia/Shanghai,否则MySQL连接会报时区错误;第二个是allowPublicKeyRetrieval=true,MySQL 8.0以上用caching_sha2_password认证时,IDE和连接池经常因为拿不到公钥报错,加上这个参数能少很多莫名其妙的问题。MyBatis Plus的map-underscore-to-camel-case一定要开,数据库字段下划线自动映射驼峰,省掉一堆@TableField。
我还习惯把不同环境拆分,application-dev.yml和application-prod.yml,本地开发用dev,部署时用prod。Spring Boot会自动根据spring.profiles.active=dev读取对应配置。毕设没必要搞配置中心,这已经够用,而且还能在论文里写一小节“多环境配置方案”。
2.3 统一返回结构和全局异常
前后端联调最烦的就是每个接口返回结构不一样,一会儿返回Map,一会儿直接返回List,前端拿数据全靠猜。我从一开始就定义统一返回体,接口的所有返回值都包一层。
@Data public class R<T> { private Integer code; private String message; private T data; public static <T> R<T> ok(T data) { R<T> result = new R<>(); result.setCode(200); result.setMessage("success"); result.setData(data); return result; } public static <T> R<T> fail(Integer code, String message) { R<T> result = new R<>(); result.setCode(code); result.setMessage(message); return result; } }配合这个,我还写了一个GlobalExceptionHandler,用@RestControllerAdvice捕获所有业务异常和校验异常。比如参数校验失败统一返回400,业务异常返回500或自定义code,系统异常统一记录日志并返回“系统繁忙”。这样做的价值在答辩现场最明显,老师如果问“你系统里异常是怎么处理的”,你就能直接讲出一整套异常体系,而不是支支吾吾说try catch。
业务异常我单独定义了一个BizException,内部存一个错误码和错误信息。Service层遇到库存不足、订单状态非法、商品已下架这些情况,直接throw new BizException(ErrorCode.STOCK_NOT_ENOUGH),由全局异常处理器包装成R返回给前端。好处是Controller层不会满屏try catch,代码干净,逻辑清晰。
3. 数据库建模:从商品到订单的完整链路
3.1 核心表结构
数据库是整套系统的地基,表设计得不好,后面写代码全是泪。我整理了一套“用户-分类-商品-购物车-订单-订单明细-地址”的标准链路,这是商城类项目最普适的模型。
用户表user是最基础的表,字段包括id、username、password、nickname、avatar、phone、role、status、create_time。其中role区分管理员和普通用户,普通值为1,管理值为0。status用来做禁用账户的开关,删除用户不是真删,而是用逻辑删除字段deleted。
商品表product字段相对多:id、category_id、name、subtitle、main_image、detail、price、stock、sales、status、create_time。price用DECIMAL(10,2),千万别用double或float,金额精度问题在电商系统里属于基础常识,用错了一样被老师问倒。status控制上下架,我直接用0下架1上架,不走逻辑删除,因为下架商品数据要保留在订单里。
商品图片我这里做主图标字段main_image,详情图可以拆一个product_image表,或者直接存在detail富文本里。毕设阶段用一张主图加详情轮播图就够了,避免过度设计。
订单相关是核心中的核心。order_info表保存一次订单的总体信息:order_no、user_id、total_amount、pay_amount、freight_amount、status、receiver_name、receiver_phone、receiver_address、create_time、pay_time、ship_time、finish_time。order_item表保存订单里的每一件商品快照:order_id、product_id、product_name、product_image、price、quantity、total_price。这里必须强调的是快照概念,下单时的商品名称、价格、图片都要原样复制到订单明细里,不能通过product_id去关联查询,否则商品改名或改价后,用户历史订单就全乱了。这是很多初学者从来没想过的问题,但恰恰是电商系统的专业点。
购物车表cart_item相对简单:id、user_id、product_id、quantity、checked。地址表shipping_address:id、user_id、receiver_name、receiver_phone、province、city、district、detail、is_default。
3.2 关系约束与状态机
表关系不一定要在数据库里大量使用物理外键,我更推荐在应用层维护关系,数据库里只保留普通索引。物理外键在并发写入和分表场景下会有性能问题,而且删数据时会很痛苦。但是逻辑上,你要能画清楚:用户1对多购物车、1对多订单;商品1对多订单明细;分类1对多商品;订单1对多订单明细。ER图画清楚,答辩时老师一眼就能看懂你的业务建模能力。
订单状态是整套业务里最容易讲深度的地方。我定义了如下状态码:
| status | 含义 | 触发动作 |
|---|---|---|
| 0 | 待付款 | 下单成功 |
| 1 | 待发货 | 支付成功 |
| 2 | 待收货 | 管理员发货 |
| 3 | 已完成 | 用户确认收货 |
| 4 | 已取消 | 用户取消或超时取消 |
| 5 | 已退款 | 售后审批通过 |
这个状态机是有方向约束的,比如待付款可以取消或支付,待发货只能发货不能直接跳到已完成。这个流转逻辑写在Service层里,每个方法先判断当前状态是否允许目标操作,不允许就抛业务异常。我在代码里还加了一个枚举OrderStatusEnum,把状态码和描述绑定,避免魔法数字到处飞。
3.3 初始化数据怎么写
数据库设计好后,不要急着写代码,先把初始化SQL准备好。除了建表语句,我建议把管理员账号、分类数据、测试商品数据都写进去。管理员账号密码用BCrypt加密后写入,固定一个admin/123456。分类数据比如手办、模型、周边、扭蛋、盲盒这些,根据自己的主题设定。商品数据至少准备10到20条,确保首页和列表有东西可看。
我还会在初始化SQL里插入几个不同状态的测试订单,方便开发时直接测试订单流转。比如一条待付款、一条待发货、一条待收货、一条已完成,这样后端联调时不用一边跑下单一边切状态。这个小技巧虽然不起眼,但能省不少调试时间,也让项目交付给老师演示时显得业务数据完整。
4. 核心功能开发实战
4.1 用户登录与JWT鉴权
用户模块是入口,登录接口的逻辑看起来简单,但做好它需要几步配合。用户提交username和password后,Service层先根据username查询用户,再用BCryptPasswordEncoder的matches方法比对密码。这里不要自己写MD5加盐,标准做法是BCrypt,安全性高,网上资料也多。登录成功后我生成一个JWT Token,把用户id和角色放进token的claims里,然后把token返回给前端。
String token = JwtUtil.createToken(user.getId(), user.getRole());生成token的密钥配置在application.yml里,expire-hours是过期时间。毕设场景设24小时足够了,演示期间不会出现频繁重新登录。JWT的解析放在拦截器里,我写了一个AuthInterceptor implements HandlerInterceptor,在preHandle里从请求头Authorization拿到token,解析成功就把userId和role放到ThreadLocal或Request attribute里,方便Controller直接获取当前登录用户。
拦截器注册比较讲究,不能把所有接口都拦截,登录注册和首页商品公开接口要放行。我是这样定义的:
addInterceptor(authInterceptor) .addPathPatterns("/api/**") .excludePathPatterns("/api/user/login", "/api/user/register", "/api/product/**");管理后台接口单独用/admin/**前缀,我再加一个AdminInterceptor,先解析token,再校验role是否为管理员。两个拦截器组合起来,普通用户进不了后台接口,权限边界很清楚。
4.2 商品列表与搜索
商城最核心的浏览入口就是商品列表。这里的实现点不是简单查所有,而是“分类筛选+关键字搜索+分页+排序”。我用MyBatis Plus的分页插件,先配置PaginationInnerInterceptor,然后写一个自定义Mapper查询方法。
ProductQuery对象里包含categoryId、keyword、pageNum、pageSize、sort字段。排序支持综合、销量、价格升序、价格降序,这会拼到Order By里。注意psort字段要做白名单校验,不能直接接收前端传来的字符串拼到SQL里,否则会有SQL注入风险。一个简单做法是后端先定义好排序方式枚举,再映射到具体的数据库字段。
我在商品列表里还会关联当前用户购物车里的选中状态,这个需求很简单,查购物车时用userId+productId批量查,放到一个Map里。首页我加了缓存,把热门商品列表放到Redis里,key例如hot_product_list,缓存10分钟。如果商品数据变更,就手动删除缓存。缓存可以加但不要复杂化,Caffeine也可以,Redis更能展示技能。
4.3 购物车到下单流程
购物车功能是一个成熟的增删改查:加入购物车、修改数量、批量删除、勾选商品、计算总价。加入购物车时要注意,如果同一个商品已经在购物车里,应该增加数量而不是新增一条。这个逻辑很多人会忽略,但实际使用中却很常见。我在CartItemServiceImpl里先按userId和productId查询,存在就quantity加1,不存在才插入新记录。
下单流程是整套系统最值得慢慢讲的部分。用户从前端拿到选中的购物车条目,点击结算,提交收货地址id和备注。后端要做四件事:
第一,校验购物车条目都属于当前用户,且商品都是上架状态。第二,遍历购物车条目,锁定每件商品的库存。第三,计算总金额,用BigDecimal累加每个商品的价格乘数量,加上运费(这项目我设满99免运费,不满运费收8元)。第四,创建主订单和订单明细,扣减库存,增加销量,清空已购买的购物车条目。
库存扣减的代码看起来简单,但并发下会有超卖问题。毕业设计阶段用乐观锁足够,执行Update语句时加上stock >= #{quantity}条件:
UPDATE product SET stock = stock - #{quantity} WHERE id = #{productId} AND stock >= #{quantity}如果返回影响行数为0,就说明库存不足,抛异常回滚事务。我在Service方法上加@Transactional(rollbackFor = Exception.class),保证订单生成失败时库存也不会被扣掉。这块实现很加分,因为就算老师不懂并发细节,也能看出你考虑了数据一致性。
下单成功后就进入模拟支付。用户点击支付,后端把订单状态从待付款改成待发货,记录pay_time,然后生成一条支付流水记录。如果你还想做得更完整,可以在订单创建时加入超时未支付自动取消的机制,用Redis的过期key加定时补偿。毕设里做成一个简单的定时任务扫表取消超时订单也可以,代码量不大,实现起来够讲三分钟。
5. 管理后台与接口安全设计
5.1 前后端分离下的权限划分
管理后台单独做一个前端页面,包路径是/admin/。账号必须能区分用户角色,我的做法是用户表里加role字段,0是管理员,1是普通用户。前端根据登录接口返回的role字段决定跳转到商城首页还是后台首页,这只是体验层面,真正的防线在后端接口。
我设计了一个AdminInterceptor,匹配路径:
registry.addInterceptor(adminInterceptor) .addPathPatterns("/admin/**");这个拦截器先解析token,再从Redis或数据库查用户的角色,如果角色不是管理员,直接返回403。为什么非要查数据库或Redis,而不是依赖token里的role字段?因为用户角色可能被管理员修改,token里的信息是登录时的快照,存在时效性。毕设系统里你当然可以说“为了实时权限控制”,这也是一个不错的答辩点。
后台接口统一使用/admin前缀,RESTful风格设计。比如:
POST /admin/product 新增商品 PUT /admin/product/{id} 编辑商品 PUT /admin/product/{id}/status 上下架 GET /admin/order/list 订单列表 PUT /admin/order/{id}/ship 发货这样接口语义清晰,文档都好写很多。
5.2 商品与订单管理接口设计
商品管理是后台最基本的功能,新增商品时除了基础字段,还要处理图片。上传接口我单独设计一个POST /admin/upload,接收MultipartFile,保存到服务器本地目录,返回可访问的URL。保存路径放配置里,比如upload.path=/data/upload,WebMvc配置里用addResourceHandlers把该目录映射为/upload/**静态资源。
订单管理的关键操作是发货和售后。发货接口需要校验订单状态必须是待发货,然后更新状态为待收货,设置ship_time和物流单号。物流单号这里我简化为可在后台手动填写,不接物流查询接口。订单列表一定要支持多条件筛选:订单号、用户、状态、时间范围,因为管理后台的真实使用场景就是快速找到目标订单。
我写订单分页查询时用了一个OrderQueryVO,包含用户昵称、订单状态数组、起始时间、结束时间。Mapper XML里写动态SQL,where条件用if标签拼装,分页交给MyBatis Plus插件。后台列表返回的数据不要直接返回实体,要转成管理端VO,把userId替换成username等可读信息,前端展示更友好。
5.3 参数校验与SQL注入预防
安全这块我不建议做得太重,但基本的要求要有。Controller入口的参数对象上加javax.validation注解:@NotNull、@NotBlank、@Size,比如注册接口的username必须非空且长度在4到20之间。数据库字段长度也要和校验对应,防止超长内容导致SQL执行失败。
SQL注入预防主要是两点:第一,所有查询尽量使用MyBatis的#{}预编译,不要用${}直接拼接字符串。排序字段这种动态列名的场景,必须用白名单映射,我在前面提到过。第二,LIKE模糊查询如果手动拼接百分号,也要注意格式:
LambdaQueryWrapper<Product> wrapper = new LambdaQueryWrapper<>(); wrapper.like(StringUtils.isNotBlank(query.getKeyword()), Product::getName, query.getKeyword());不要把关键字直接拼进Order By或表名。另外,文件上传接口要限制文件类型和大小,比如只允许jpg、png、webp,最大5MB。这是一个很容易被忽略的漏洞点,写进论文里比写那些花哨的功能实在得多。
6. 前端对接与部署上线
6.1 Vue端接口对接与跨域
前端我建议用Vite创建Vue3项目,安装axios和element-plus。接口请求封装成一个request.js,统一通过axios实例发送,请求头加token,响应体拦截器统一处理R结构里的code,不是200就toast错误信息。这样一个写法能保证全站接口调用风格一致,也方便后续维护。
联调时最常踩的坑是跨域。前后端分离后,前端跑在5173端口,后端跑在8080端口,浏览器会拦截跨域请求。解决方式有两种:后端配Cors或前端用Vite代理。我推荐开发环境用Vite代理,因为生产环境nginx也要做反向代理,前后端用同源方式最省事:
server: { port: 5173, proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true }, '/admin': { target: 'http://localhost:8080', changeOrigin: true } } }但在实际开发中,我还会在后端配一个CorsConfig,这样别人直接用前端地址访问也能通。两种方式并存不冲突,生产环境则通过nginx把/api和/admin转发到后端服务。前端路由我分了用户端和后台模块,用户端是商城首页、商品列表、商品详情、购物车、结算页、订单列表;后台是商品管理、订单管理、分类管理、用户管理。页面组件不多,但足够演示完整流程。
6.2 打包部署与nginx配置
毕业设计最终要能跑起来给老师看,部署环节不能掉链子。后端打包前先确认profile是prod,数据源和Redis地址都改成服务器可访问的地址。然后执行:
mvn clean package -DskipTests生成jar包后,放到服务器上,用nohup启动:
nohup java -jar animemarket-0.0.1.jar --spring.profiles.active=prod > app.log 2>&1 &前端打包是npm run build,生成dist目录。把dist目录放到nginx的html目录,前端页面直接访问服务器IP或域名。nginx配置里需要做两个location转发,一个是页面静态资源,一个是接口代理。因为采用的是history路由,还需要配置try_files重写到index.html,否则刷新页面会404。
location / { root /home/www/animemarket; index index.html; try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; }部署完成后,访问前端地址,登录账号,走一遍从浏览商品到下单支付的完整流程。如果有地方报错,去app.log里看异常日志,根据日志标题定位问题。建议在服务器上用uswgi或者定时脚本监控端口状态,但毕设阶段一般没必要,手动重启就够。
7. 典型问题排查与避坑实录
7.1 金额字段用BigDecimal还是float
这个坑几乎是每个商城毕设都会遇到的。如果你用double或float做订单金额,计算99.9加8.1这类小数时,结果往往是107.99999999999999,存入数据库后取出来人看着就难受,累计金额还可能差一分钱。正确做法是所有涉及金额的字段,Java类型用BigDecimal,数据库字段用DECIMAL(10,2)。
MyBatis Plus映射时,BigDecimal到DECIMAL是天然支持的,不需要额外处理。计算总价时:
BigDecimal itemTotal = price.multiply(BigDecimal.valueOf(quantity));绝不能先转double再乘。还有一个小坑:BigDecimal的divide方法在除不尽时会抛ArithmeticException,如果遇到需要算单价或折扣,记得指定小数位数和舍入模式:
price.divide(BigDecimal.valueOf(3), 2, RoundingMode.HALF_UP)这个知识点在答辩时多提一嘴,老师会觉得你懂业务。
7.2 时间格式和JSON序列化
Spring Boot默认的Jackson序列化LocalDateTime时,返回给前端的是类似“2025-01-01T10:20:30”的ISO字符串,很多前端组件展示不了。我通常在application.yml里配置:
spring: jackson: date-format: yyyy-MM-dd HH:mm:ss但说实话,date-format对LocalDateTime并不完全奏效,最好还是用@JsonFormat注解统一处理:
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") private LocalDateTime createTime;如果你的字段比较多,也可以自定义一个全局Jackson配置类,注册LocalDateTimeSerializer,一劳永逸。数据库里存DATETIME类型,实体用LocalDateTime,代码里不要再用java.util.Date,新项目用LocalDateTime更规范。
7.3 联调时接口报404和跨域问题
前后端联调最常见的报错是404,尤其post请求。先检查前端请求路径是否和后端Controller的@Request MAPPING路径完全一致,包括大小写和斜杠。再检查后端context-path是否设置,如果设置了/,请求路径前面有没有多写一层。我习惯接口路径全部小写,杜绝大小写不匹配的低级问题。
跨域报错一般是CORS配置没生效。如果你用了自定义拦截器,注意拦截器可能拦截了OPTIONS预检请求,导致跨域失败。处理方法是在拦截器里放行OPTIONS请求:
if ("OPTIONS".equalsIgnoreCase(request.getMethod())) { return true; }另一个容易忽略的地方是Spring Security和拦截器使用顺序,但该项目没引入Security,只靠拦截器,所以这个坑会更少。如果还是跨域,用前端的Vite代理绕开,开发环境下最省事。
7.4 并发场景下库存扣减
单纯用先查询库存再Update会导致超卖。比如两个用户同时查到库存为1,都去扣,最后一次Update可能把库存改成-1。我前面给的SQL方案是把判断条件写在Update里,用数据库行锁来保证原子性:
UPDATE product SET stock = stock - #{quantity} WHERE id = #{productId} AND stock >= #{quantity}执行后判断AffectedRows是否为1,如果为0则说明库存不足。这样的写法对商城类业务已经足够。可如果你在论文里想升华一下,可以提到Redis预减库存加MQ异步扣减,但那会引入RabbitMQ,复杂度直线上升。毕设阶段做到乐观锁扣减,已经能体现你的工程意识。
8. 个人经验与后续扩展建议
8.1 毕设答辩前的自测清单
项目做完了,答辩前我习惯按一条用户主链路从头到尾再走一遍,同时整理出最容易翻车的几个点。第一,用新注册的普通账号登录,确认没有管理员权限,访问/admin接口必须返回403。第二,从商品列表加入购物车,修改数量,确认购物车合计金额和结算页金额一致。第三,下单后把库存扣减的SQL日志拿出来看,确认扣减的是对的。第四,用管理员账号发货,用户端订单状态要能实时变化。第五,刷新前端页面,确认JWT过期后能正确跳转登录。
还有一点很多人会忘:把数据库里的测试数据清理一下,不要给老师演示一个全是“测试商品123”的管理后台。多放点正常的二次元周边商品数据,价格和分类也尽量合理,第一印象真不一样。如果时间允许,写一份README,把项目启动方式、测试账号、核心功能模块都列清楚,老师拿到手能直接跑起来,印象分直接拉满。
8.2 还能往哪些方向扩展
这套系统的扩展空间很大。想突出“高性能”,可以把首页商品缓存、订单超时取消、热点商品榜单都做成Redis方案写成论文小章节。想突出“工程化”,可以加Docker部署,用docker-compose一键启动MySQL、Redis和后端应用,这也非常符合现在的开发习惯。想突出“算法”,二次元商品场景加一个简单的推荐功能,比如“浏览过该商品的用户还看过”,本质上就是基于商品分类或标签的协同过滤,实现并不难。
如果导师要求功能比较丰富,还可以加用户收藏、评论、优惠券、秒杀活动。但我的个人经验是,不要为了堆功能而做功能,每个扩展都最好能和你的技术选型联系起来,否则答辩时只是罗列功能,讲不清设计动机。像优惠券就涉及金额分摊,评论就需要内容审核,每个都能讲出设计难度。
最后再分享一个实际操作中的体会:做毕设项目,最忌讳的就是拿着一套源码闷头跑起来,却不知道每一层代码在干什么。你能把一个商城从建表开始,一步步写到部署,这个过程学到的东西比毕业设计本身更重要。这套Spring Boot二次元商品销售系统,规模和难度都很适合拿来练手,做完之后,Spring Boot的控制器、服务层、持久层、拦截器、配置体系,基本就成你自己的了。真到了面试聊项目时,你能把这些细节讲明白,比简历上写“精通Spring Boot”管用得多。