写着写着就发现,现在做毕设的同学十个里有五个会选“xx商城”,而“二手书城”又是商城类题目里最经典的那一档,没有之一。原因很简单:它有完整的前后端交互闭环,又不像电商那样复杂到需要支付分账和物流状态机;它有用户、图书、订单这些核心实体,够写一版正式的系统设计,撑得起毕业答辩的提问。我手上刚好做过一套基于微信小程序的二手书城,前端原生小程序,后端SpringBoot + MySQL,跑通之后顺便整理成了源码+文档的完整交付包,今天把整个设计和实现过程拆开揉碎讲一遍,后面想做类似题目,或者想把这个项目二次开发、改造成自己毕设的,可以直接照着复盘。
这篇文章覆盖的东西比较多:项目立项时怎么选型、登录和订单这些核心模块怎么写、联调时最容易踩的坑,以及最后怎么把项目打包成一份能拿得出手的毕设材料。内容偏实战,如果你是奔着“别人总结好的最优解”来的,那正好,我从头到尾都是用能落地的方案写出来的。
1. 项目整体设计与技术选型
1.1 为什么是微信小程序 + 自建后端
先说选型。这些年做商城类毕设,无外乎三条路:纯H5网页、App、微信小程序。纯H5网页老气不说,答辩时演示效果也一般,老师看习惯了;App又绕不开安装包、签名、真机适配这些麻烦事,光环境问题就能耗掉一周;微信小程序是这几年最稳妥的选择,原因很实在:上手门槛低,前端就是WXML + WXSS + JS,会写网页就会写小程序页面;演示方便,手机扫码就能看,不用装任何东西;需求方和老师的接受度也高,毕竟现在大家都用小程序购物。
但小程序只是前端壳子,后台业务逻辑放哪儿才是核心决策。现在有两种主流做法:一种走微信云开发,数据库、云函数、存储全用平台能力,开发速度快,但对后端专业能力的展示就弱了;另一种自建后端,用SpringBoot提供RESTful接口,数据库用MySQL,这种传统前后端分离结构能完整展现业务逻辑、数据库设计、接口规范,答辩时讲点也更多。我最后选的是后者,因为毕设评审更看重你对完整系统设计的掌握程度,而不是你有没有用某个云平台的快捷功能。
这套项目的技术栈大概是这样:小程序端用原生框架,没有引入复杂的第三方UI组件库,原因是减少学习成本和依赖风险;后端用SpringBoot 2.x + MyBatis Plus + MySQL,文件存储用本地磁盘目录,管理后台用一套简单的Vue页面,目标是让整个项目在本地能干干净净跑起来,不依赖外部服务,这样交付和演示都不会出幺蛾子。
1.2 功能模块划分与数据库设计
二手书城这种题目,功能设计要做到“麻雀虽小,五脏俱全”,但也不能贪多。我最终落地的是用户端和管理端两个视角,模块划分如下:
用户端模块:登录、首页、图书分类、搜索、图书详情、发布图书、我的发布、购物车、订单管理、收货地址、个人中心。管理端模块:用户管理、图书审核、图书下架、订单管理、数据统计。这里尤其要解释一下图书审核机制,这是二手交易类项目和普通商城最大的区别。普通商城里的商品由运营统一维护,二手市场是C2C,用户自己发布,所以必须有一个审核环节,防止图片乱传、描述夸大、违禁物品上架。这个点也是答辩时老师容易追问的:你们怎么保证平台内容安全?这时候你就可以从“发布状态流转”的角度讲:用户发布后默认待审状态,管理员审核通过才变为上架,否则可以驳回并填写驳回理由。
数据库设计是整个系统的基础,核心表我列一下:用户表(user)、图书表(book)、分类表(category)、轮播图表(banner)、购物车表(cart)、订单表(orders)、订单详情表(order_item)、收货地址表(address)。图书表最关键,字段设计决定了后面功能的复杂度。我的book表重点字段包括:book_id、user_id、category_id、title、author、publisher、isbn、original_price、sell_price、degree(成色,比如95成新/9成新)、description、cover_image、images(多图JSON)、status(0待审核/1在售/2已售出/3下架/4审核驳回)、create_time、update_time。
这里想重点提醒一张表的设计:购物车。很多人把购物车只做一个user_id和book_id的关联表,但二手书场景下同一本书可能是唯一库存,加购物车时就必须做状态判断,否则用户把一件“已售出”的书加进购物车,下单时就会出冲突。所以我专门在购物车表里冗余了一个book_status字段,下单时再和后端对照一次,保证不会卖出重复的书。这种细节在答辩时讲出来,会给老师一种你“真正考虑过业务”的感觉。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 核心细节解析与实操要点
2.1 登录模块:自建账号体系 + 微信授权
登录是任何小程序项目的第一个难题,也是好多新手卡住的地方。先捋清楚流程:小程序端通过wx.login()拿到一个临时code,把code发给后端;后端拿着这个code,调微信的接口换区openid和session_key,然后拿openid去查用户表,查不到就自动创建一个新用户;最后后端生成一个自定义的token返回给小程序的storage,后续所有请求都在header里带这个token。
很多同学会有一个误区:以为微信登录叫“不用登录”,其实微信只负责帮你识别身份,你的系统该建的账号体系还是得建。我在用户表里除了openid之外,还设计了nickname、avatar、phone、role这几个字段。role是区分普通用户和管理员的,管理员账号可以在数据库里直接指定,也可以在管理端注册一个初始入口。
第二个容易翻车的地方是拿用户昵称头像。早年可以用wx.getUserInfo,但微信在基础库升级后改了策略,现在更稳的是用“头像昵称填写能力”:页面上放一个button组件,open-type="chooseAvatar"让用户选择头像;昵称则用input组件的type="nickname"。这个能力在真机上体验很好,在开发者工具里有时候展示会不正常,注意用真机预览验证。如果项目里还保留了旧版wx.getUserProfile的代码,基础库版本一升高,大概率就是报“获取登录后的微信用户失败”,这个后面第4章会专门讲排查思路。
token的生成方式不用太花哨,我用的是UUID + sessionId存储到Redis,过期时间7天。如果没有Redis环境,也可以直接用JWT,把userId签名进去,减少会话存储的复杂度。但要注意:不管是哪种方式,登录返回的数据里绝对不能出现openid,openid相当于微信侧的“身份证号”,一旦被前端拿到,接口就有了越权访问的入口,这在毕设里是不专业的体现。
2.2 图书发布与图片上传
图书发布是整个C2C模式的核心功能,也是代码量比较集中的一块。页面要收集的信息不少:书名、作者、出版社、ISBN、原价、售价、成色、描述、封面图,多张实拍图。表单里“成色”就是一个典型的微信小程序单选场景,用radio-group或者picker都行,我实际选择用picker组件,做成下拉选择,数据更规整。再比如ISBN的校验,格式基本固定,后端校验一下位数和校验位,能有效过滤乱填的数据。
图片上传的实现路径是:wx.chooseMedia先让用户选图,拿到临时文件路径;wx.uploadFile把临时文件POST到后端的/upload接口;后端把文件保存到服务器磁盘,返回可访问的URL;前端拿到URL后,连同其他表单字段一起通过POST提交给图书新增接口。
这里面有两个细节坑。第一,临时文件路径是真的会过期的,所以一定要在选完图之后立刻执行上传,不能先把图片路径存数据库,等提交时候再上传;第二,微信对上传文件的默认大小限制是10M,手机拍出来的图很大,很容易超。我建议在chooseMedia的回调里先调wx.compressImage压缩质量到80%,并限制最长边到1280px,这样既保住清晰度,又不会让服务器存储膨胀。这个细节,在演示时如果用的是老手机,会非常明显。
后端保存图片时,我建议单独建一个upload目录,按日期分文件夹存放,比如/upload/2025/06/。然后给SpringBoot配置一个静态资源映射,把/upload/**映射到磁盘路径,这样访问URL就是http://localhost:8080/upload/2025/06/xxx.jpg。如果不做这个映射,图片上传成功但前端打死显示不出来,也是一个经典问题,后面有专门一节。
2.3 检索与列表展示
首页是用户进入后看到的第一屏,也是演示时最能抓眼球的地方。我首页设计是:顶部搜索框 + 轮播图 + 分类入口 + 推荐图书列表。搜索走专门的/search接口,支持按书名、作者、ISBN模糊查询;分类入口跳转到list页面,通过categoryId过滤。这两块逻辑都不复杂,但分页是必须做的,后端分页参数pageNum和pageSize,用MyBatis Plus的Page对象直接查,返回总记录数和当前页列表。
分页这个问题,几乎每个答辩老师都会问到:如果图书量很大,一次性返回所有记录会怎样?你只要能答出“会造成性能瓶颈,客户端渲染压力大,用户流量浪费”,然后说明你用了limit和count查询,就足够。为了让首页不那么空,我在首次启动时候预置了大概30条图书数据,图片用本地静态图,保证演示效果稳定。
另外,列表排序上,我做了两个维度:默认按发布时间倒序,同时支持按价格升序/降序切换。在SQL层面就是order by create_time desc或者order by sell_price asc,再用一个小开关控制,是非常基础的实现,但观感提升很明显。
2.4 购物车与订单状态机
购物车虽然简单,但下单流程才是整个系统里最容易出bug的地方。我在设计订单时把流程拆成了四步:从购物车或详情页确认下单 -> 后端创建订单(状态为待付款) -> 前端模拟支付回调(也可预留真实微信支付入口) -> 支付成功,订单变为待发货,同时将图书标记为已售出。为了应付可能出现的超时未支付场景,我用了定时任务,每5分钟扫描超过30分钟未支付的订单,自动取消并把图书状态回滚为在售。这套逻辑在毕设里属于加分项,因为完整覆盖了“状态一致性”的考题。
订单号生成我自己想了套规则:yyyyMMddHHmmss + 4位随机数 + userId后4位,保证演示环境下不会重复。订单状态我用了整数枚举:0待付款、1待发货、2待收货、3已完成、4已取消。每个状态之间的流转要固定,前端按钮和接口都要做状态判断,比如“已取消”的订单不能再次支付,“已完成”的订单不能申请售后(当然售后这种扩展功能可以留着后面加)。
这里有一个细节我反复测过很多次:用户把书A加进购物车后,管理员可能在管理端把书A下架了,这时候用户再下单就会产生脏数据。处理方式是:创建订单时,遍历购物车选中的商品,逐个检查book.status是否等于1,有任何一本不满足,整个订单创建失败,并返回具体是哪本书出了问题。这种“交易前置校验”逻辑,在真实项目里是必须的,写在毕设文档里也是亮点。
3. 实操过程与核心环节实现
3.1 项目初始化与目录结构规划
拿到一套项目源码,第一件事不是急着点运行,而是把目录结构先看清。我这套项目分了两个大目录:miniapp(小程序端)和server(SpringBoot后端)。小程序端按照pages、components、utils、api、static分包组织,pages下每个模块一个文件夹,比如pages/index、pages/book、pages/order;api目录统一封装了所有wx.request请求,utils里放置日期格式化、金额计算这类公共方法。
后端的包结构是:com.oldbook.controller、service、mapper、entity、config、common。common里放了统一返回类Result、异常处理、工具类。统一返回结构是个很容易被忽略但非常重要的设计,我所有接口都返回Result对象,包含code、message、data三个字段,前端api.js里做拦截,code不等于200就统一toast错误信息。这样写的好处是,新增接口时不用反复写错误处理,而且答辩时你能说“我的系统有统一异常处理机制”,这是工程化意识的体现。
项目启动的大概步骤是:第一步,用Navicat执行db.sql,初始化数据库,库名oldbook;第二步,修改application.yml里的数据库用户名密码;第三步,启动SpringBoot,确认8080端口起来;第四步,微信开发者工具导入miniapp目录,把AppID改成自己的测试号,在详情设置里勾选“不校验合法域名”,基础库选到3.0.0以上;第五步,编译运行,登录后能进首页基本就算通了。
3.2 前后端接口联调的关键代码
联调时最容易出问题的就是baseUrl。微信开发者工具的运行环境分三层:开发者工具模拟器、真机预览、真机调试。模拟器里可以直接用http://localhost:8080访问本机后端;但真机预览时,手机访问不到你电脑的localhost,必须把后端服务地址改成电脑的局域网IP,比如http://192.168.1.101:8080,而且手机和电脑必须在同一个Wi-Fi下。所以我的api.js里写了一个全局常量BASE_URL,统一切换,而不是在几十个页面里到处硬编码。
登录接口的联调我贴一下关键代码。后端controller大致长这样:
java复制@PostMapping("/login")
public Result login(@RequestBody LoginRequest req) {
String code = req.getCode();
String openid = wxService.code2Session(code);
User user = userMapper.selectByOpenid(openid);
if (user == null) {
user = new User();
user.setOpenid(openid);
user.setNickname("微信用户");
user.setRole(1);
userMapper.insert(user);
}
String token = jwtUtil.generateToken(user.getUserId());
return Result.success(new LoginVO(token, user));
}
小程序端api.js里封装request方法时,要记得把token塞到header。我之前见过一个项目,登录成功后token拿到了,但后续所有请求没带,导致每次请求都查不到用户,页面数据全空,排查了很久才发现是header没处理。这个非常基础,但值得新人特别注意。
3.3 图书发布功能的完整流程
图书发布页面,我建议用表单 + 图片选择组件组合。字段校验放两处:前端做必填校验,后端再做一次完整校验。前端校验的目的是体验,后端校验的目的是安全,这两者不能互相替代。后端校验里特别要限制卖价的范围,比如0到99999之间,防止有人传负数或者天文数字。发布接口的代码逻辑大概是:先存储图片(如果图片还没传过),再把封面和图片URL数组拼到实体里,调用bookMapper.insert,初始status为0。
发布成功后跳转到“我的发布”页面,这里要做一个我发布过的列表,并且每本书能操作:下架、重新上架、编辑、删除。下架就是status改为3,重新上架要判断审核状态。这里有个经验:图书编辑时图片字段不能整段覆盖,否则用户没动图片,提交时拿不到图片路径,就会把已有图片清空。我的解决办法是把图片列表单独做成一个可以拖拽排序和删除的区域,提交时把图片URL数组原样POST过去,后端先查数据库旧值,再合并更新。
3.4 下单与模拟支付流程
为了不让系统依赖微信支付资质,我做了一套模拟支付流程,但在代码结构上预留了微信支付的接口位。流程是:用户从购物车点击结算,后端创建订单,返回订单号;前端跳转到确认支付页面,显示“模拟支付”按钮;点击后调后端/pay/mock接口,传入orderNo,后端把订单状态从0改为1,并同步把相关图书status改为2;前端轮询或直接刷新,订单列表显示“待发货”。
在真实项目中,支付回调是微信服务器发起请求通知后端结果,而不是前端直接调用。毕设里用模拟支付完全可以解释清楚,答辩时老师问“你这里支付怎么实现的”,你可以说“因没有商户资质备案,先用模拟支付跑通完整订单流转,数据库层面已经设计了微信支付回调接口payCallback,后续接入只需在接口内校验签名和幂等性”。这个回答既诚实,又体现了对业务闭环的理解。
下单时还有个小细节:订单创建后,要在事务里同时插入订单主表和订单详情表。主表存订单金额、状态、用户ID、收货地址快照;详情表存每本书的bookId和价格快照。为什么用“快照”这个词?因为商品价格是可以变的,订单保存时必须把这个瞬间的单价固定下来,否则后续订单金额跟着商品价格变,账就对不上了。这种设计在商品订单系统里是底层通用逻辑。
4. 常见问题与排查技巧实录
4.1 真机预览失败 net::ERR_CONNECTION_RESET
这个报错在我反复联调时出现了太多次。现象很统一:开发者工具模拟器一切正常,真机预览时页面白屏或接口超时,控制台报net::ERR_CONNECTION_RESET。先别慌,按这个顺序排查:先确认手机和电脑连的是不是同一个Wi-Fi;再ping一下电脑的局域网IP能不能通;然后确认后端启动端口没被防火墙拦截,Windows下可以在控制台临时关闭防火墙试试;最后检查baseUrl是不是用了电脑的局域网IP而不是localhost。绝大多数情况下,问题都出在前三个环节,真正需要改代码的情况极少。
还有一个更低级的坑:后端服务起来了,但IDE运行的是旧实例,你以为改了接口代码,实际请求还是打在旧进程上。排查这个可以直接看后端控制台有没有收到请求日志。我前面在logback里加了请求日志打印,就是为了联调时能看到前端每一次请求的到达情况。
4.2 获取登录后的微信用户失败
这个报错经常出现在老项目复制出来的场景里。最早的低版本基础库广泛使用wx.getUserInfo接口,新版本升级后权限收紧,接口无声无息地挂掉了,页面就一直显示“获取登录后的微信用户失败”。排查方法:打开调试器,找到App.js里的onLaunch或对应登录页的调用,看控制台具体报错类型。通常是因为基础库版本太旧或AppID没有注册“小程序用户信息相关接口”的权限。
解决办法是彻底改为新策略,在个人中心页面用button open-type="chooseAvatar"给用户选头像,用input type="nickname"输入昵称,登录时只需要wx.login拿到code,整体流程不需要授权弹窗,这也是目前微信侧推荐的方案。改完之后,新用户也能正常创建,老用户直接匹配到openid即可。顺带一提,开发者工具里的头像选图有时候不弹,这不是代码问题,换成真机调试就正常了。
4.3 图片上传成功但前端显示不出来
这个问题看上去是后端bug,实际大半是访问路径配置问题。如果上传接口返回了图片相对路径,而前端渲染时需要的是http开头的完整URL,情况就不对了。解决方式有两种:后端在返回时直接拼接好完整路径返回;或者前端渲染时统一用BASE_URL + 相对路径去拼接。我推荐后端直接返回完整URL,这样小程序端不需要知道静态资源的base地址,也避免路径拼接前后不一致。
另外一个容易被忽略的情况:如果你把图片存在了服务器磁盘的某个自定义目录,而SpringBoot没有配置静态资源映射,图片URL请求会直接404。必须添加如下配置:
java复制@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/upload/**")
.addResourceHandler("file:" + uploadPath + "/");
}
同时注意uploadPath路径最后要带/,否则会拼接出错,这个细节我因为没带斜杠排查了半小时。
4.4 MySQL连接失败与中文乱码
启动后端报连接失败,九成是application.yml配置问题。先检查数据库密码、端口、数据库名是否和你本地环境一致;如果连接正常但中文乱码,那就是characterEncoding没有设置为utf8mb4。现在微信生态里用户昵称emoji很常见,如果数据库字符集是latin1或者utf8,emoji会直接被截成乱码。统一方案:创建数据库时设置CHARSET=utf8mb4 COLLATE utf8mb4_general_ci,同时连接url加上useUnicode=true&characterEncoding=utf8mb4。
这里最好把MySQL的sql_mode也检查一下,有时候默认启用了only_full_group_by,分组查询会报语法错误。我在db.sql开头就设置了SET sql_mode,可以避免很多低版本MySQL的兼容问题。
4.5 端口被占用与项目启动异常
SpringBoot默认端口是8080,很多同学本地同时跑了好几个服务,8080很容易被占。启动异常日志会提示Port already in use。Windows下用netstat -ano | findstr 8080查PID,再在任务管理器结束对应进程,或者直接改application.yml里的server.port。为了省事,我整套项目统一用的8080,文档里写清了如果占用怎么排查。
还有一种情况是JDK版本不一致。SpringBoot 2.x要求JDK8以上,如果你本机装的是JDK17,部分依赖版本可能需要同步升级。我的建议是直接使用项目文档里标注的JDK版本,避免因为版本差异产生一些莫名其妙的问题。
5. 项目交付与二次开发方向
5.1 毕设资料怎么整理才像样
源码能跑只是第一步,真正拉开档次的是交付材料。这套项目的文档我按论文结构拆开:需求分析、总体设计、数据库设计、系统详细设计、系统测试、总结展望。每个章节都有对应正文,数据库设计里附了完整ER图和建表SQL。答辩时老师翻文档,最关注的通常不是长篇大论,而是数据库表设计是否合理、功能模块图是否清晰、核心流程是否说明白。
另外建议写一份README,放在源码根目录,写清楚环境要求、启动步骤、默认账号、目录说明。很多人拿到源码第一件事就是看README能不能带他跑起来,能一句话启动成功的项目,观感会好非常多。我也把初始管理员账号和测试用户账号放进了README,方便演示时直接切换到不同角色。
5.2 远程调试时怎么准备才不浪费时间
远程调试和现场演示是两个场景,细节侧重点不同。远程调试时,最常见的翻车点是两边环境不一致。我自己的习惯是提前把项目打成压缩包,把MySQL数据导出一份,确保对方安装的是5.7或8.0版本;后端用Maven的package打成可执行jar,让对方通过java -jar启动,省去IDE配置时间;小程序端则让对方自己导入微信开发者工具,并把AppID设为测试号。
讲解时不要照着代码一行行读,而是按业务故事串:用户登录 -> 发布图书 -> 管理员审核 -> 买家浏览下单 -> 支付模拟 -> 订单流转 -> 卖家发货。每个环节再对应到代码里的一个模块,老师或同学听起来会有画面感。远程Debug时,可以把idea和微信开发者工具都打开,需要打断点时现场展示,但这个对网络稳定要求比较高,建议提前录一段关键流程视频作为保底。
5.3 让项目变得更有辨识度的扩展方向
如果做完基础版还有时间,我建议按这个优先级做扩展。第一,收藏功能,给图书加一个收藏按钮,用户在个人中心能看收藏列表,属于比较轻量的功能增强;第二,书评功能,用户下单完成之后可以对图书进行评论,形成内容沉淀,也让系统多一个“评价模块”;第三,微信订阅消息,用户下单后通过小程序订阅消息通知卖家,这个功能在毕设题目里很加分,而且技术方案成熟,属于“推送消息方案”的热门考点;第四,首页按“最新上架”和“热门图书”做两套推荐的切换,热门可以按订单次数统计。
另外有一点容易被忽视:小程序的顶部导航栏高度在不同机型上不一样。如果做自定义导航,要记得用wx.getMenuButtonBoundingClientRect()来动态计算胶囊按钮位置,同时处理底部tabBar和全面屏安全区域,这些细节做好了,整个项目的完成度能提一个档次。二次开发时,模块之间尽量保持单向引用,后端按controller、service、mapper分层,前端每个页面的逻辑独立封装,万一定制阶段改动需求,不会波及到其他功能。
做完整套二手书城,我最大的体会是:这种“小而全”的项目,把登录、商品、订单、文件上传这四条主线跑通以后,你对一个业务系统的理解会从“会写代码”变成“能设计系统”。最后再分享一个交付和答辩通用的技巧:正式演示前,把所有核心数据准备好放到数据库里,首页轮播、图书列表、待发货订单、管理员账号,一步到位,千万不要临时创建数据,那样不仅慢,还容易暴露bug。如果现场实在没网,就打开开发者工具本地模式,把所有接口走本地联调。这些小经验,都是踩了几次坑换来的,做一个项目就顺手记下来,比看十个教程都管用。
