1. 这个题目要解决的痛点:为什么我选了“助农产品销售”
打开毕业设计的选题列表时,那种感觉很多人应该懂:要么题目太虚,打开一看就是换个皮的CRUD系统;要么题目太大,一个电商平台加物流加社交,一个人做半年也做不完。我最后定下“基于Android的助农产品销售系统”,原因很简单——它处在一个很合适的难度平衡点:有完整的电商交易闭环,有农产品区别于普通零售的业务特殊性,而且不需要你接入真实支付渠道,一个人从需求到答辩完全能走通。
这个系统本质上是做一件什么事?一句话,让农产品从地头到餐桌的流通链路变短。农户或合作社在后台发布商品,买家在Android端浏览、下单、支付,卖家处理订单并发货,管理员负责审核和监管。听起来像普通电商,但农产品有几个不能忽略的差异点:按批次售卖(同一款脐橙,不同的采收批次,价格和品质都不同)、计量单位不统一(按斤、按箱、按份)、保鲜期敏感(需要用产地直发、保质期这类信息来建立信任)。这些差异点如果只当一个商城来做,就丢了灵魂;把它们设计进需求里,整个项目的业务故事就立住了。
我特别想提醒正在纠结选题的人一点:毕设的评判逻辑不是“功能越多越好”,而是“核心链路完整、每个选择都能讲出理由”。所以从一开始我就定了三个约束条件:可演示、可答辩、可解释。凡是不能在这三个维度上形成闭环的功能,统统先砍掉。事实证明这个思路救我大命——很多同学在中期检查之前还在纠结要不要做一个直播带货模块,而我已经把买家下单链路跑通了。后面的事情就好办得多,卖家端和管理员后台都是在核心链路上长出来的枝叶,不存在方向性返工。
2. 架构与技术选型:不是追求最新,而是追求最稳
整个系统的方案是这样的:
| 端 | 技术 | 说明 |
|---|---|---|
| 移动端 | Android原生 + Kotlin | 采用MVVM架构,Gradle构建 |
| 网络层 | Retrofit2 + OkHttp | RESTful API通信 |
| 后端 | Spring Boot + MyBatis-Plus | Java生态,事务控制成熟 |
| 数据库 | MySQL 8.x | utf8mb4字符集 |
| 认证 | JWT | 无状态登录方案 |
| 图片存储 | 后端本地磁盘静态映射 | 避免额外搭建文件服务 |
| 图片加载 | Glide | 列表图片异步加载 |
先说为什么是Android原生而不是微信小程序。我不否认小程序开发效率高、上线路径短,但放在毕业设计的场景里有两个问题:第一,小程序需要通过平台审核,你没法保证答辩那天线上版本能用最稳定的状态演示;第二,评委老师最期望看到的是一个能直接点开、交互流畅的App,Android APK提前装好在手机上,打开就是首页,即使现场网络出问题也能展示已经缓存好的页面。再加上Kotlin协程对异步逻辑的处理比Java回调清爽得多,代码可读性对论文里的核心代码展示也更友好。
后端选择Spring Boot,纯粹是冲着问题和方案的匹配度去的。Node.js和Python的Flask/Django也能写,但Spring Boot在校园项目语境下有不可替代的优势:MyBatis-Plus对简单CRUD的封装几乎就是为毕设量身定做的,事务注解和乐观锁的支持成熟到可以无脑使用,更关键的是中文资料极多,你随便卡住一个报错,搜索引擎五分钟内必然能找到同类问题。毕业设计不是比谁的技术栈新,而是比谁在有限时间里交付得更完整、更能讲清楚。MySQL作为数据库没有悬念,个人电脑跑得动、客户端工具丰富、SQL语法资料遍地都是,建表时统一utf8mb4,避免中文乱码和排序问题。
整体数据流是这样的:Android端发起HTTP请求,请求经过Retrofit到达后端Controller,Controller调Service层执行业务逻辑,Service通过Mapper操作MySQL数据库;后端返回统一结构体给前端解析。图片走的是后端的静态资源映射路径,也就是把上传的图片落到服务器磁盘,再通过配置好的访问前缀让客户端能直接加载。这套结构很常规,但常规意味着稳定,也意味着答辩时评委挑不出架构层面的硬伤。
3. 功能模块拆解:三个身份,一条闭环
系统的角色和功能必须从一开始就分清楚,不然后期一定会乱。我在需求文档里把用户划分为四类:游客、买家、卖家(农户)、管理员。游客只能看,买家能买,卖家能卖,管理员管全局。
核心功能链路拆出来是这样:
- 游客:浏览商品列表、查看商品详情、搜索农产品、查看平台公告
- 买家:注册登录、商品搜索分类筛选、加入购物车、下单、模拟支付、订单管理、确认收货、发表评价、收货地址管理
- 卖家:商品发布、商品上下架、库存管理、订单发货、查看销售额统计
- 管理员:用户管理(禁用/启用)、商品审核、订单监管、公告发布、数据概览
如果还停留在功能清单阶段,可能觉得上面已经不少了。但我额外加了一个动作:给所有功能标了P0、P1、P2优先级。P0是核心闭环——注册登录、商品浏览、商品详情、购物车、下单、支付、订单状态流转、发货收货、评价,这些必须最早全部打通。P1是支撑功能——搜索筛选、库存管理、用户管理、商品审核、订单监管、公告管理。P2才是加分项——三级地址联动、收藏、订单超时取消、数据统计图表。
为什么非得分优先级?因为我见过太多人一上来就做App首页的轮播图和底部导航,结果到答辩前一个星期核心的下单流程还是个半成品。正确的顺序是先打通整条交易链路,再回头包装门面。我实际开发的时候就是先把买家端做到能买货,再去做卖家发货功能,最后做管理后台。这样做还有一个隐藏好处:中期检查的时候,即使有些模块还没做,你演示的核心链路已经能讲清楚这个系统到底在干什么,评审老师的心理预期立刻就不一样了。
如果你也想做类似的系统,建议你一定做一个功能编号表,比如“F-B-001 买家注册登录”这样的格式,并让每个功能对应到代码实现里的Controller方法和数据库表级联关系。答辩的时候评委会带着你的需求文档看演示,随时可能问“你需求文档里的第几条功能在哪里”,你如果能立刻翻到代码说明,效果会非常加分。这也是我做完整个项目以后最庆幸的一件事。
4. 数据库设计:农产品属性的落地方案
数据库设计是整个项目里最不能急着写代码的部分,因为表结构一旦定型,改起来牵一发动全身。我最终设计了十张核心表:用户表、类目表、商品表、商品批次表、购物车表、收货地址表、订单主表、订单明细表、评价表和公告表。下面挑关键的说。
用户表(user)我合并了买家、卖家、管理员三种身份,用role字段区分,这样登录逻辑和权限校验都能统一处理。字段上除了基础的id、用户名、密码、昵称、手机号、头像外,特别注意加了status字段做禁用/启用,create_time和update_time也一个都不少。答辩时如果评委问“数据库表为什么都要有create_time”,你可以直接回答:这既是数据审计的需要,也是业务排序的基础。
商品表(product)是这个系统的灵魂。字段除了常规的id、类目、名称、封面图、多张详情图、描述、价格、库存、上下架状态外,我专门加了产地、计量单位、保质期这些农产品专属属性。这里做一个很重要的设计区分:价格字段我用的是decimal而非float/double,原因很简单,浮点数在金额计算时会出现精度误差,比如用户看到6.9元,结算时可能变成6.899999。这个细节在我做的单元测试里真的抓到过,修复一次之后整个对账流程都干净了,答辩时你主动提一句,评委就知道你认真做过账务逻辑。
商品批次表(product_batch)是我跟普通电商项目拉开差距的关键设计。农产品与工业品的核心区别就是同一款商品的货源是不连续、有批次的。我设计这张表用来记录同一商品下不同采收批次的信息,包括批次号、采收日期、产地,以及该批次的独立价格和库存。前端页面展示当前批次的价格,订单明细则记录下单时属于哪个批次,这样后续如果要做溯源,直接通过订单关联批次表就能查清楚。如果觉得这个设计太重,也可以在商品表里直接加harvest_date和batch_no两个字段做简化版,但我的建议是保留批次表,因为这是论文里业务创新点的体现。
订单主表(order)和订单明细表(order_detail)是标准的电商拆单设计。主表记录订单号、用户ID、总金额、收货人、收货电话、收货地址和订单状态,明细表记录每个商品的ID、名称、快照图片、单价、数量、小计。明细表里一定要存商品快照字段,因为商品价格或名称可能被卖家修改,订单一旦生成就必须保留下单那一刻的信息,这个细节我在做对账的时候深有体会。订单状态我用的是状态机思路:待支付、已支付、待发货、已发货、已收货、已完成、已取消、已退款,每一次状态变更都在代码里做了流转条件校验。
收货地址表(address)单独拆出来是因为一个用户可以维护多个地址,而且下单时要支持选择默认地址。公告表(notice)很简单,存标题和内容,App首页上展示最新的一条。评价表(comment)关联订单明细和商品,用户确认收货后填写评分和内容,商品详情页展示评价列表,形成交易闭环。对于外键,我没有在数据库层面强制创建,而是在应用层保证一致性,这样方便测试数据和后续调整。
索引方面,商品表在category_id和status上建了组合索引,订单表在user_id和status上建了索引,评价表在product_id上建了索引。这些都是根据实际查询路径反推出来的,不是为了建索引而建索引。
5. Android端实现:真正的细节都在这里
Android端的代码结构我采用包名分层的方式:api包放Retrofit接口定义,model包放数据实体,viewmodel包放状态与业务逻辑,ui包放Activity和Fragment,adapter包放列表适配器,utils包放工具类。这样做的核心收益是答辩时能很快说清楚每个类的作用,同时自己也维护得舒服。
网络层是重中之重。我用Retrofit加协程的方式,定义了一个统一的接口返回结构,大致是这样的:
data class ApiResponse<T>( val code: Int, val message: String, val data: T )所有接口都返回这个结构体,前端只需要解析一次通用字段,业务数据再单独转成model。这个约定的好处在联调阶段就体现出来了,不需要每个页面各自处理异常结构。登录接口大概是这样的写法:
interface ApiService { @POST("api/login") suspend fun login(@Body request: LoginRequest): ApiResponse<LoginResponse> }协程让异步逻辑看起来像同步代码,网络请求就写成了很自然的一行调用。网络层我配了一个拦截器,统一在请求头里携带JWT token,这样业务代码里就不需要每个接口手动传token了。token的存储用的是加密过的SharedPreferences,虽然毕业设计不至于对抗专业逆向,但至少不做明文保存,答辩的时候提一句“敏感信息是加密存储的”,评委在安全意识这个维度就不会多追问。
商品列表页和分页加载是我的一个重头戏。列表用的是RecyclerView加SwipeRefreshLayout,分页采用手动分页方案。逻辑上我设置了几个关键变量:pageNum、pageSize、isLoading、isLastPage,在滚动监听的onScrolled方法里判断当前滚动位置是不是到了底部附近,如果是并且isLoading为false,就请求下一页。下拉刷新的时候把pageNum重置为1,清空列表重新加载。整个分页逻辑虽然简单,但有一点特别容易踩坑——加载更多和下拉刷新是两个并发动作,一定要用isLoading标志位做互斥,否则列表会出现同样的数据前后两页都有,这在演示的时候非常难看。
图片加载全部用Glide,这是一个几乎不用思考的选择。列表的缩略图和商品详情的轮播图都走同一个图片URL,Glide自带内存和磁盘缓存,滑动手感流畅很多。如果你在实现时发现列表卡顿,十有八九是图片没走缓存库直接加载了原图,优先检查这一步。
购物车和下单流程上,我使用的是后端购物车表方案,这样换设备登录购物车还在,答辩时也能讲出一个优点:数据不在本地,后端可统一维护。购物车和订单之间最关键的是库存校验。我在客户端的下单请求里只传商品ID和购买数量,由后端在一个事务里完成“校验库存、锁定库存、创建订单”三个动作。事务里用的是乐观锁更新方式,更新SQL大概长这样:
UPDATE product SET stock = stock - #{count} WHERE id = #{id} AND stock >= #{count}这行语句在并发情况下不会超卖,因为MySQL的行锁机制保证同一商品ID的并发更新只能是串行,而stock >= #{count}的条件又保证了不会把库存扣成负数。如果影响行数为0,说明库存不够,直接抛出业务异常给前端提示。这个设计我在两个模拟器同时下单的时候实测过,一次一个订单成功,另一个提示库存不足,效果非常直观。
地址选择器我放在assets目录里存了一份省市区JSON数据,自己写了一个三级联动弹窗,这是我做的P2功能里性价比最高的一个——实现难度不大,但在演示时因为动效直观,收到的反馈往往比我预想的好得多。如果时间紧张,也可以用简单的文本框让用户手动填写地址,但有一个联动地址选择器在答辩时的观感确实不一样。
6. 后端接口与并发处理:从能用到能用得稳
后端部分的代码量其实比Android端还大,毕竟所有逻辑都在这边。我按照Controller、Service、Mapper三层去组织,Controller只负责参数接收和结果返回,Service层处理业务规则,Mapper层负责数据库CRUD。三个层级的依赖方向很清晰,答辩时讲架构图不需要费口舌。
接口规范上,我统一用了一个Result类包装返回值:
public class Result<T> { private Integer code; private String message; private T data; // 成功和失败两个静态方法 }Controller的每个接口都返回Result类型,前端根据code判断业务成功或失败,message用于直接弹出Toast或Snackbar提示。这样不仅Android端解析数据方便,后续如果有人要用网页后台对接同一套API,也不需要改任何东西。
库存并发处理是我花了最多时间地方。最早的代码是先查库存,判断>=购买数量,再执行update,这种“先查后改”的写法在两个并发请求同时到来时一定会出问题。我当时是在联调时用两个模拟器同时下单同一个商品才发现这个bug的,库里库存是1,但两条订单都创建成功了。排查后改成上面提到的条件更新SQL,配合@Transactional事务注解,这个问题彻底解决。这里分享一个排查思路的细节:遇到这类并发问题,不要凭感觉猜,先在日志里打印执行顺序,确认两个请求是在同一个时间窗口内进入的,再判断是更新条件的问题还是事务隔离的问题,整个过程要能复述给答辩评委听,这是体现工程能力的高光时刻。
订单超时未支付的处理,我没有引入消息队列和分布式定时任务,而是用一个Spring的@Scheduled定时任务,每隔一分钟扫描一次待支付时间超过30分钟的订单,把它们自动改成已取消状态,同时回补库存。这个功能在移动端也做了配套:订单倒计时显示剩余支付时间,超时后页面自动刷新变成已取消。毕设的粒度做到这个程度足够了,还能在论文里写一个“订单状态机设计”小节。
图片上传这里我必须单独提醒一个坑:上传接口用multipart/form-data接收文件,后端把文件保存到磁盘上的upload目录,然后返回一个相对路径给前端,前端拼上服务器的BaseUrl去加载图片。这里最容易出问题的是地址的写法:Android模拟器访问电脑本机要用10.0.2.2,而不是localhost;真机调试时手机和电脑必须在同一个局域网,而且要使用电脑的局域网IP。我见过身边有人在这个问题上卡了一整天,手忙脚乱改了一堆环境配置,最后发现只是两个地址写法的差异。我的建议是单独写一个BuildConfig字段配置BaseUrl,开发调试时使用模拟器地址,演示用真机时切换成局域网地址,不要把所有URL散落在代码里。
7. 测试与验收:我做了哪些准备让答辩不翻车
毕设的测试不必像企业那样追求自动化覆盖率,但至少要做到“每个功能都能被复现验证”。我自己做了三部分测试工作。
第一是功能测试清单。我把需求文档里每一个功能编号与测试用例对齐,比如登录功能对应的测试用例是:正确用户名密码登录成功、错误密码提示失败、密码为空提示必填、登录成功后跳转首页、token过期后自动退出。每个用例都手工执行一遍,结果记录在表格里。这张功能测试表打印出来放在桌上,答辩前自己过一遍,比临时去点App可靠得多。
第二是兼容性测试。我准备了一个Android 11模拟器、一个Android 13模拟器,外加一台真机,分别在三个环境上把核心链路走了一遍。重点检查两个点:一个是权限申请,因为targetSdkVersion在30以上,存储和相机权限必须在运行时动态申请,我在代码里做了权限回调处理;另一个是屏幕适配,我用了常见的dp布局加百分比适配,保证不同分辨率的模拟器不至于布局错乱。
第三是异常场景的演示准备。网络超时的时候,页面要有统一的错误提示而不是白屏或无限转圈,这需要在Retrofit的配置里设置合理的connectTimeout和readTimeout,并在catch异常分支里做提示。我在答辩前还特意把手机设为飞行模式,验证了几个页面缓存是否正常,这样万一现场网络环境差,至少商品列表和已加载的详情页还能展示。
答辩演示的素材要靠平时积累。我每完成一个功能,就立刻截屏保存,按模块存档。尤其是数据库内容变化的对比截图——下单前商品表的库存是多少、下单后变成多少,这种前后对不的截图在答辩PPT里放一张,比说一百句“系统支持库存管理”都有说服力。现场演示的时候,如果网络不稳定,直接切到截图讲述,整个预答过程依然可以完整走完。
8. 答辩常见提问:提前准备好这些答案
答辩是毕业设计里最容易被低估的一环。代码写得再好,讲不出来等于零。我根据实际经验和同组同学被问到的问题,总结了下面几个高频问题以及对应的回答思路。
“为什么选择Android原生而不是小程序?”——回答要点:演示便利性,APK可提前安装;代码可控性,全流程都是自己写的原生页面;“未来可扩展性”,原生架构可以接蓝牙设备、扫码枪等硬件。
“系统如何保证数据安全?”——回答要点:登录采用JWT,密码用加盐哈希存储,数据库操作全部用参数化SQL避免注入,核心接口在拦截器里验权,敏感信息不在日志中明文输出。每个点都能在自己的代码里指出来,比如打开Interceptor类给评委看那几行不带token就返回401的逻辑。
“商品并发下单如何防止超卖?”——这是所有电商类项目必然会问的问题。回答要点:先查库存再更新是错的,要用条件更新SQL加事务,并当场演示两个模拟器同时下单的效果。这一套组合拳下来,评委基本不会再追问。
“订单状态是怎么流转的?”——回答要点:定义待支付、已支付、待发货、已发货、已收货、已完成、已取消、已退款,用状态机约束合法流转路径,支付成功才能发货,发货后才能确认收货,已取消订单不允许支付。能现场打开代码里状态流转的方法给评委看最好。
“如果用户量大了,系统哪里会成为瓶颈?”——回答要点:数据库读压力大,可以加Redis缓存热点商品;接口层可以做限流;静态资源可以上对象存储和CDN;后端可以做水平扩容。这个问题的重点不是你真的做过,而是能给出一个合理的扩展思路,说明你理解系统的性能边界在哪里。
“你的数据库为什么没有用外键?”——回答要点:外键影响写入性能和耦合度,应用层保证一致性,订单细节里存了商品快照,即使商品被删,历史订单数据依然完整。这个回答反而会成为一个加分项。
我建议你提前把这些答案组织成一段两分钟的陈述,对着电脑屏幕说几遍。说顺了以后,答辩现场的压力会小很多,因为你会发现在真正的演示环节,评委提问的方向和这些问题几乎完全一致。
9. 关于时间和踩坑,最后再叮嘱几句
整个项目的周期,我一个人用了大约十周:需求与数据库设计用了两周,后端接口写了三周,Android端写了两周半,剩下两周半全部用来联调、测试和写文档。真实节奏比想象中紧凑,所以我特别想提醒几个我踩过或见过的坑。
第一,不要中途更换数据库或框架。有个同学因为觉得MySQL“不够高级”,做到一半想把数据库换成别的,结果所有Mapper的SQL都要重写,后面直到答辩前都没有恢复到正常进度。技术选型一旦定下来,只要没有硬伤,就不要因为“可能更好”而换。
第二,写代码时把所有App的接口地址集中管理。我见过有人的代码里写死了十几个不同的IP地址,调试的时候改了一处漏了另一处,光是排查接口为什么404就花了半天。用BuildConfig管理BaseUrl,换环境只改一处,是一个在项目早期就该建立的习惯。
第三,答辩前一定要做一次从零开始的完整部署。把数据库SQL脚本拿到一台环境干净的电脑上执行,克隆代码、导依赖、编APK、装到手机上,整条流程跑通。这样才能保证论文里的部署步骤是真实可复现的,也避免出现“我本地能跑但换了电脑就跑不了”的尴尬场面。
第四,数据库的SQL脚本要提前提交到项目仓库里。有个同学答辩前电脑出问题,本地数据库里的数据没有导出,最后只能对着PPT干讲,效果非常受影响。养成随时把数据结构变化同步到SQL脚本里的习惯,其实也是软件工程里版本管理的基本素养。
这个项目做下来,我最深的体会是:毕业设计的意义不在于你做出了一个多惊艳的产品,而在于你能够以一名开发者的身份,把一个问题从业务理解、方案设计、编码实现到测试交付完整走一遍。如果你也正在做类似方向的系统,希望这篇分享能帮你少走一点弯路,把时间真正花在让核心链路更扎实的地方。