最近是毕设开题的高峰期,几乎每天都有人来问小程序相关的题目。我接触过太多做了两三个月还卡在登录环节、或者答辩前一周后台接口才勉强跑通的案例,这次专门把"基于手机端的陕西地区特色农产品团购平台"这个小程序毕设项目的完整脉络拆一遍,从需求梳理、技术选型、数据库设计、核心代码实现到调试运行、答辩准备,一条线捋到底,不管是正在选题、卡在开发还是准备交文档的同学,都能直接照着调整自己的进度。这个项目最大的优势在于业务场景真实、模块边界清晰、前后端分离架构完整,既能体现微信生态的能力(登录、手机号授权、支付),又不需要多高级的算法支撑,属于典型的"工作量适中、过稿率高、答辩好讲"的经典题。
1. 项目定位与需求拆分
1.1 为什么选"陕西农产品团购"这个方向
微信小程序诞生的这几年,电商类小程序一直是最成熟的流量变现形态,而"地区特色农产品+团购"这种组合在毕设里有两个天然优势。第一,农产品地域特色明显,比如洛川苹果、周至猕猴桃、汉中仙毫、清涧红枣、富平柿饼,这些名字一拿出来就是一张陕西名片,需求文档里写"助力乡村振兴""推广地标特产"完全是顺理成章的事,导师看了也觉得选题有社会价值。第二,团购模式天然具备社交裂变的业务逻辑,小程序本身就是依托微信生态运转的,"开团-邀请好友参团-成团发货"这个链路跟小程序的分享能力配合得严丝合缝,做出来的演示效果比普通商城小程序生动得多。
有些同学担心农产品项目会不会显得技术含量低,这里要澄清一下,毕设考察的是完整的软件工程能力,不是算法难度。这个项目牵扯到用户体系、商品体系、订单体系、团购状态机、后台管理五个大模块,前端要处理微信登录、手机号授权、支付回调,后端要处理并发开团、库存扣减、状态流转,这一套东西能把前后端分离、RESTful接口设计、数据库事务、定时任务全部覆盖,已经超过大部分毕设题目的复杂度了。
1.2 功能需求全景拆解
我按角色拆分需求。普通用户端(消费者)的功能是最核心的,也是演示时重点展示的:
- 用户注册登录:微信授权登录、手机号绑定、微信用户信息同步
- 首页模块:轮播图、农产品分类导航、团购活动专区、热销榜单
- 商品模块:商品列表(按分类筛选)、商品搜索、商品详情(图文详情+参数+团购信息)
- 团购模块:发起团购(开团)、参与团购(参团)、团购进度展示、拼团结果
- 购物车模块:加入购物车、修改数量、删除、批量结算
- 订单模块:确认订单、收货地址选择与新增、提交订单、模拟支付、订单状态跟踪
- 个人中心:头像昵称展示、我的订单(全部/待支付/待发货/待收货/已完成)、团购记录、收货地址管理
运营管理端这里有一个取舍问题。如果做前台的独立管理端入口,工作量会呈指数上升,所以毕设里最常见的方案是做一个后台管理网页或者直接写在Service层用测试接口维护。我建议后台用一套极简的管理页面,放在同一个工程里,做成"商品管理、订单管理、团购活动管理、用户管理"四个tab,一方面文档里能写清楚"本系统包含前端展示与后台管理两部分",另一方面演示的时候进入后台改个商品价格、看订单列表,整体完整度会高一个档次。
1.3 工作量与时间规划
这个项目的开发周期正常是6到8周,我拆一下工时给后面做计划的同学参考。第一周肯定是需求分析、数据表设计、接口文档定义,这个阶段一定不要省,数据库字段后面再改真的想哭。第二周到第三周做前端静态页面和公共组件,WXML和WXSS本身不复杂,难一点的是把一个选品列表写得好看。第四周实现后端接口,用户登录、商品CRUD、购物车增删改查这些标准接口,一天三四个接口是完全现实的。第五周前后端联调,处理跨域、字段不一致、请求参数错误。第六周完善团购逻辑和模拟支付,这两块是最容易返工的。后面预留一到两周写文档、录演示视频、准备答辩PPT,时间紧的话连熬三个晚上的情况也不是没见过,但显然不建议。
2. 技术选型与架构设计
2.1 前端技术选型:原生小程序还是Uniapp
我接触过的毕设里,前端方向争论最多的是"原生微信小程序"和"Uniapp+Vue语法"。如果你单纯为了省事,Uniapp能一套代码跑小程序、H5、App,看着很美,但对毕设来说我强烈不建议,原因并不复杂。答辩现场你演示的载体是微信开发者工具,Uniapp编译出来的小程序,遇到报错排查时错误堆栈经过了一层跨端转换,你自己都难快速定位问题;而原生小程序,报错了直接定位到对应的WXML或者JS文件,调试路径最短。
原生小程序也没有那么难。核心就是三件套:WXML负责页面结构,类似于HTML;WXSS负责样式,接近CSS;JS负责逻辑,这个就是普通的前端逻辑,不需要Vue的概念也能写。如果你是React或者Vue用得很熟的人,小程序页面里的data养状态、setData改状态、bindtap绑事件,本质上跟MVVM是一致的,两天就能上手。
另外原生小程序有一个特别适合毕设的细节,就是微信开发者工具自带页面模板创建功能,右键点击pages目录新建页面,框架会把四个文件自动生成好,省了一大堆初始化工程的时间。
2.2 后端技术选型:Spring Boot / PHP / Node怎么选
后端是毕设里面最需要认真定的技术栈,这个选择直接决定你是6周轻松收工还是12周痛不欲生。
对于计算机科班的同学,后端默认选Java Spring Boot,理由不是Java有多香,而是面试和答辩场景中这是覆盖面最广的技术栈。Spring Boot的优势在于生态完善,Spring MVC处理请求分发、MyBatis操作数据库(也有用MyBatis-Plus的)、Spring Security处理权限拦截(毕设里可以简化成拦截器),这一套是市面上Java后端的主流配置,答辩时任何一个理工科老师问了都能接得住。依赖管理也方便,一个pom.xml引入spring-boot-starter-web、mybatis-plus-boot-starter、mysql-connector-java,配置一把梭。
以前端的同学为例,后端选Node.js Express算是最顺手的选择,Express路由、中间件的那套心智模型跟小程序前端非常匹配,一两周就能写完接口。但要注意课堂考核中老师是否认可Node.js作为毕设后端,有些学校明确规定后端必须Java或C#。
PHP在毕设里其实被低估了,上手极快、没有编译打包,改完刷新就能看到效果。如果文档里的需求分析、数据库设计是你自己写的,后端用什么其实没那么关键,关键是后台管理页面的代码量会小很多,因为PHP天然适合服务器端渲染。不过现在用PHP做毕设的比例确实在下降,优先级放到Java后面吧。
2.3 数据库核心表设计
我直接把我常用的表结构给出来,直接用这一版,字段命名规范,外键关系清晰,评审老师看完不会有意见。
第一个是用户表,user,核心字段包openid,注意这个字段必须建唯一索引,因为登录判断全靠它;nickname头像这种字段存的是微信返回的原始数据;phone字段在用户授权手机号后回填。第二个是分类表category,就是农产品的一级分类,字段是id和name,最多再加一个排序字段。第三个是商品表product,这个表设计上有一个细节,一个规格一个库存,价格用decimal(10,2),如果是团购商品则带一个group_price字段存团购价,还要存一个group_num字段存成团人数,比如3人成团。商品主图存一个URL,详情用富文本HTML字符串,小程序里的rich-text组件直接能渲染。
第四张是购物车表cart,字段包括user_id、product_id、quantity、checked(是否选中)。第五张是订单主表orders,订单号order_no做成唯一,user_id、group_id(非团购订单为空)、total_amount、pay_amount、status,状态这里务必要统一枚举。第六张是订单明细表order_item,记录下单时的商品快照,字段有product_id、product_name、product_image、price、quantity,为什么要快照呢?因为商品价格会变,后台改了价钱,你订单里的价格不能跟着变,这个在答辩的时候是加分项。第七张是团购活动表group_activity,字段是product_id、group_price、group_num、start_time、end_time。第八张是团购记录表group_record,字段是activity_id、leader_user_id、current_num、need_num、status(0待成团/1已成团/2已取消),这里要注意保留团长ID,方便做免单逻辑。
2.4 团购业务状态机设计
团购是这个项目区别于普通商城小程序的灵魂,一定要把状态流转画清楚,写清楚,答辩必问。团购记录从开团到结束一共有四个状态。第一个是待成团,第一个用户支付成功后创建记录,自己是团长,此时状态0;第二个用户在同一个活动里看到这个团,点参团并支付成功,团人数加1;第三个是已成团,当团人数达到活动设置的成团人数,状态变成1,所有参与者的订单自动进入已支付阶段,等待后台发货;第四个是已取消,当活动有效期内没有凑齐人数,状态变成2,参与者的订单也要同步变成已取消,并触发退款流程。
这里有一个容易写错的地方,团购状态与订单状态一定要分开管理。团购记录表存整体状态,订单表存各自的订单状态。一个用户参团成功,他的订单状态可能是已支付,但团购整体还是待成团状态,这两个不能混在一起。
前端围绕这些状态做不同的UI展示:待成团时展示"还差几个人"、已成团展示"拼团成功"、已取消展示"退款处理中",用户点击邀请好友时用的是小程序的分享能力,把团购记录的groupId和活动id带在分享路径上,新用户从分享卡片点进来就能直接看到这个团。
3. 核心功能实现与代码拆解
3.1 微信登录与手机号获取的完整链路
微信登录这个模块是整个项目的地基,因为几乎所有业务接口都需要先判断当前用户是谁,做不好其他功能都是空中楼阁。微信小程序的登录逻辑并不是用户在界面上输入用户名密码,而是前端调用微信的wx.login接口换取一个临时code,再用这个code请求你自己的后端,后端拿code去微信服务器换openid,整个过程是一个静静的三方握手。
小程序端的核心代码其实很少,但每一步都要踩坑。先看前端部分:
// pages/login/login.js wx.login({ success: (res) => { // res.code 是临时登录凭证,有效期5分钟 wx.request({ url: 'https://你的服务器地址/api/user/login', method: 'POST', data: { code: res.code }, success: (resp) => { const { token, userInfo } = resp.data.data; wx.setStorageSync('token', token); wx.setStorageSync('userInfo', userInfo); wx.switchTab({ url: '/pages/index/index' }); } }); } });后端拿到code之后,调用微信的接口换开放在"code2Session"接口上,拿到openid和session_key。我直接给一个Spring Boot版本的示例,基于RestTemplate实现:
// 后端 UserService 关键代码 public String code2Session(String code) { String url = "https://api.weixin.qq.com/sns/jscode2session?appid=" + appId + "&secret=" + appSecret + "&js_code=" + code + "&grant_type=authorization_code"; RestTemplate restTemplate = new RestTemplate(); String response = restTemplate.getForObject(url, String.class); // response 里包含 openid 和 session_key JsonObject json = JsonParser.parseString(response).getAsJsonObject(); String openid = json.get("openid").getAsString(); // 根据 openid 查数据库,找到则返回已有用户,找不到则自动注册 return openid; }登录成功后,我自己习惯的做法不是直接把openid返回给前端,而是后端生成一个自定义token存入缓存或数据库,后续所有接口前端在请求头里携带这个token,后端用一个拦截器统一解析,解析出用户后存到ThreadLocal里,这一步设计在答辩时表明你理解了"无状态服务"的核心。
获取手机号在毕设里是另一道坎。现在的小程序规范里,获取手机号必须是用户点击带有open-type="getPhoneNumber"的button按钮,触发 bindgetphonenumber 事件,回调事件里拿到一个code,再拿这个code请求你自己的后端,后端再拿code去微信的获取手机号接口交换手机号。这里注意,目前只有企业主体的小程序有这个权限,个人主体的小程序是不能直接调这个接口的。毕设阶段没有企业主体,最稳妥的方案是让用户手动输入手机号存库,前端做一个弹窗验证加个防刷就可以了,演示的时候完全不掉价。
3.2 团购拼单核心逻辑实现
团购逻辑是这个项目里唯一真正有并发考量的模块,也是写代码时最要小心的地方。我以"3人成团"为例子,讲清楚开团和参团的完整流程。
开团场景。用户A在商品详情页点击"立即开团",后端接收请求后先校验这个商品是否还在团购活动有效期内,再校验当前商品库存足够,然后生成一条团购记录,团长就是A自己,当前人数count为1,目标人数为3,状态为待成团。与此同时给A生成一笔订单,订单金额是团购价,状态为待支付。前端拿到返回的groupId之后,跳转到支付环节。
// 开团 @Transactional public Long createGroup(GroupCreateDTO dto) { // 1. 校验商品和活动有效性 Product product = productMapper.selectById(dto.getProductId()); if (product == null || product.getStatus() != 1) { throw new BusinessException("商品已下架"); } // 2. 查当前有效团购活动 GroupActivity activity = groupActivityMapper.selectActiveActivity(dto.getProductId()); if (activity == null) { throw new BusinessException("该商品暂未开启团购"); } // 3. 创建团购记录 GroupRecord group = new GroupRecord(); group.setActivityId(activity.getId()); group.setLeaderUserId(dto.getUserId()); group.setCurrentNum(1); group.setNeedNum(activity.getGroupNum()); group.setStatus(0); groupMapper.insert(group); // 4. 创建团长订单(状态待支付) Orders order = createOrder(dto.getUserId(), product, activity, group.getId()); return group.getId(); }参团的逻辑多了并发控制的考虑。用户B看到A发起的团,点击参团。后端先通过groupId查到这条团购记录,然后要判断两件事:团购记录的状态必须是待成团,当前人数还小于目标人数。这两个判断不能分开做,否则两个人同时参团,都查到自己是第3个,都往里加,最终人数会超过目标。我采用的是更新时再加条件判断,用一个条件更新SQL代替先查后改:
// 参团 @Transactional public Long joinGroup(GroupJoinDTO dto) { // 核心SQL:UPDATE group_record // SET current_num = current_num + 1 // WHERE id = #{groupId} AND status = 0 AND current_num < need_num int updateCount = groupMapper.joinGroupByCondition(dto.getGroupId()); if (updateCount == 0) { throw new BusinessException("手慢了,该团已满或已结束"); } // 之后检查是否成团 GroupRecord group = groupMapper.selectById(dto.getGroupId()); if (group.getCurrentNum() >= group.getNeedNum()) { groupMapper.updateStatus(group.getId(), 1); } return group.getId(); }这种写法本质上是把判断和更新合并成一个原子操作,数据库行锁天然保证了并发安全。答辩的时候你把这个点讲出来,老师基本会点头。
还有一个细节是团购超时未成团。毕设阶段不需要上延迟队列,最简单的方案是在后端加一个定时任务,每分钟扫描一次group_record表,把所有超过活动有效期还没成团的记录置为已取消状态,同时把相关订单改成已取消。这样实现简单,文档里解释为"定时任务驱动",非常有说服力。
3.3 商品展示、购物车与下单流程
商品展示页面的核心是一个列表组件加一个详情组件。分类切换由tab切换触发,前端请求接口通过categoryId进行过滤。详情页采用小程序的rich-text渲染富文本详情,这里要注意一点,直接从富文本编辑器中拿到的HTML可能存在兼容性问题,存储前可以清洗一遍,只保留标准标签。
购物车算是一个标准模块,但有一个坑值得写出来。购物车列表通常要展示商品最新的价格,但这样做容易导致用户加入购物车的时候看到的价格跟最后结算页面不一致,因为后台商品可能调价了。毕设项目可以不处理这么较真,但至少要做到前端每次进入购物车页面时都重新请求一次商品最新信息,而不是使用本地缓存的商品数据。在实践中小程序app.js的全局存储是购物车状态的常见归宿,页面onShow重新拉取,数据实时性就有了。
下单环节的数据结构设计是关键。订单确认页面展示商品信息、收货地址、配送方式和支付金额。从购物车选中的商品提交订单,后端生成订单主表和订单明细表,两个表要在同一个SQL事务中完成,否则会出现订单总金额跟明细对不上的情况。订单号生成建议用时间戳加随机数,精度再高一点可以用日期加自增序列加两位随机,前端直接用订单号展示给用户查询。
3.4 订单管理与模拟支付
支付环节是很多学生挂掉的重灾区,原因只有一个:微信支付需要认证的小程序商户号,个人开发者根本没有。毕设阶段直接接模拟支付是绝大多数人的选法,但也有人想在文档里写明"本系统的支付模块采用模拟支付方式,用户点击支付后将订单状态直接置为已支付",这个描述要干掉,因为看起来像是偷懒,要解释成"对接真实支付需要企业资质,本系统已完成支付逻辑闭环,只需要替换为微信支付统一下单接口即可实现真实支付,这里模拟逻辑的接口结构完全参照官方文档设计"。
模拟支付的效果是演示时用户点击"立即支付",按钮会跳到一个模拟支付页面,显示"模拟支付中,3秒后返回",然后订单状态从待支付变成已支付。前端定时刷新订单状态即可。
订单的状态流转要做一个前后端状态机。前端通过订单状态展示不同的操作按钮:待支付显示"继续支付"和"取消订单",已支付显示"等待发货",已发货显示"确认收货",已完成显示"申请售后"(这个功能毕设可以做成售后申请的表单,后台能看到申请记录就行)。每个状态转换时后端都做一次校验,比如只有待支付状态的订单才能变更为已取消,否则抛异常。这一点看起来是小事,实际是订单模块的稳定器。
4. 调试运行与高频问题排查
4.1 环境搭建与工程导入
这个项目的运行环境无非是四个部分:MySQL数据库(8.0或者5.7都可以)、Java环境(JDK 1.8及以上)、微信开发者工具、后端IDE(IntelliJ IDEA)。数据库方面直接用一个Navicat导SQL脚本,创建leet数据库,导入预置的SQL文件,里面已经带了几十条陕西农产品的测试数据和测试用的轮播图URL。如果导入SQL时报错,大概率是SQL文件里的字段名跟当前数据库版本有冲突,重点检查 date、desc 这类保留字是否加了反引号。
后端工程导入IDEA后,要改的关键配置都在application.yml里,数据库连接地址、端口、小程序appid和secret。如果本地启动的时候控制台报"Access denied for user",检查MySQL账号密码是否一致,登录权限是否为localhost。启动成功后访问http://localhost:8080/api/product/list能返回JSON数组,就说明后端环境没问题了。
前端工程在微信开发者工具中打开,注意首次打开的时候,工具栏"详情"里的"本地设置",务必勾选"不校验合法域名",否则请求会因为微信对域名的限制而直接拦截。本地调试阶段都用这个选项,上线才需要配置request域名白名单。
4.2 真机调试与接口联调
微信开发者工具里直接运行,打开模拟器,能正常看到页面、点进商品、加购下单,这就是通过了前端自测。之后要真机预览,点击工具栏的"预览",手机上打开二维码即可。
真机联调跟工具联调最大的差异在于网络环境。模拟器上小程序与后端是localhost直连,真机上必须把后端地址改成电脑的局域网IP,比如http://192.168.1.100:8080,否则手机上所有请求都会失败。这里有一个很常见的坑是IP地址动态变化,建议后端配置文件里用常量统一管理baseUrl,方便切换。同时Windows防火墙默认会拦截8080端口的外部入站请求,排查时先去关闭专用的防火墙规则。
调试运行中经常要用到抓包工具,我习惯用Charles。手机和电脑连同一个WiFi,设置手机的HTTP代理指向电脑IP,端口8888,这样手机上的请求都会经过Charles,可以很清晰看到每个请求的请求体、响应体和HTTP状态码。对比前端预期和后端实际返回,接口联调的问题基本都能定位。这在小程序开发调试里面是基本功,强烈建议每个做毕设的人都熟悉这套流程。
4.3 毕设选题的一票否决项
最后写一段关于选题的硬话。有不少同学会想,不做农产品,做宠物、做跑腿、做面馆点餐,是不是也差不多。对,差不多的架构,但踩坑点决定了你的工作量。凡是选题是"跑腿""众包""社区团购"的,时间宽度天然比普通商城多两到三周,因为派单逻辑、定位导航、多角色抢单这套流程比单纯下订单复杂太多。而"陕西农产品团购"在业务边界上天然收敛:购买路径清晰,直接下单、成团、发货,不需要额外的骑手调度。我见过很多选题因为临时加了"地图选点""多人实时在线""消息推送"最终导致延期,毕设的聪明做法是把边界切小,把每个模块做得完整、可用、有余地,而不是在功能堆砌上给自己挖坑。
4.4 高频踩坑问题速查表
我把带学生过程中出现频率最高的坑汇总成一张表,每个都是真实发生过的,几乎每届都有中招的。
- token失效未处理,用户登录后过了一段时间再操作,后端直接返回未登录错误,前端没有任何跳转处理。解决办法:统一封装请求函数,收到401时自动跳转登录页。
- 购物车商品删除后,列表重新加载没有刷新,导致UI还显示已删除商品。解决办法:删除接口成功后,重新调用查询列表接口,或者使用this.setData局部替换数组。
- 订单提交后未跳转支付页,点击返回上一页又生成了一条新订单。解决办法:确认订单页提交成功后在防重复提交状态,支付失败不重生成订单。
- 定时任务扫描团购时,日期判断用本地时间还是服务器时间混乱。解决办法:全项目统一使用服务器时间,不要在SQL里混用NOW()和Java时间的日期参数。
- 商品详情页富文本图片不显示。常见原因是富文本编辑器上传到的内容存储了带有编辑器的私有样式,或图片是相对路径,小程序无法访问。解决办法:存入前做一次图片URL绝对化处理。
- 后台管理修改商品信息后,小程序端商品列表还是旧数据。解决办法:接口返回的每个商品数据都带update_time,前端根据时间差做增量刷新,或者直接后端加缓存失效逻辑。
这些坑没有多深的技术含量,但会吃掉你大量时间。我的经验是每次遇到问题先定位自己是不是踩了已知的坑,再考虑是不是原理层面的问题,几轮下来调试效率会高非常多。
4.5 答辩前文档与演示准备
代码跑通了,最后一个关卡是答辩。这个阶段我建议做好三件事。第一,把开发文档里的功能需求、数据库设计、核心流程描述这三章写透,数据库设计要包含E-R图、表结构说明、字段含义,流程图至少把业务主流程和团购状态流转图画出来,做到逻辑自洽。第二,录制一段完整的演示视频,视频里从头到尾走一遍:用户授权登录、浏览商品、加入购物车、提交订单、开团参团、成团、后台发货,全程操作稳定,不出现调试工具黄色错误日志。视频文件可以放在提交文件的demo目录下,这是给导师和评审老师最直接的成果展示。第三,准备五个左右高频问题的回答,集中在为什么选微信小程序、如何保证并发开团不超员、如果对接真实微信支付需要哪些配置、超时未成团怎么处理、后台管理权限怎么控制,这些都在前面模块里讲到了,理清楚自己的实现细节,就能答得好。
这个项目做完,放在作品集里也是拿得出手的,它把真实电商业务的闭环走了一遍,比课程作业里那些"我有一个表单"的项目强太多。只要踏踏实实把每个模块按文档走完,答辩台上一定是自信的。