
1. 选题背景与需求拆解做了这么多年Java开发也带过不少毕业设计说实话校园二手交易这个方向每年都有人做但真正做得有含金量的不多。大部分人的思路就是做个列表页加个详情页能增删改查就完事了。但如果是基于Android的校园网上拍卖平台情况就不一样了——它的核心不在“卖东西”的CRUD而在“拍卖”这两个字上。拍卖和普通二手交易最大的区别是什么是出价逻辑和实时性。普通闲置交易是卖家挂一个固定价格买家觉得合适就拍下付款。而拍卖是卖家只定起拍价和加价幅度买家在规定时间内不断出价价高者得。这里边涉及到的技术点一下就多了倒计时怎么算、出价怎么防并发、超时后怎么自动成交、保证金怎么处理、流拍或者悔拍怎么办。这些才是这个项目真正值钱的地方。从用户需求来看校园场景有别于公共二手市场。学生群体的闲置物品以教材、数码配件、宿舍小家电、运动器材为主单价普遍不高交易半径小卖家买家大概率都在同一个校区甚至同一栋宿舍楼。这个背景决定了功能设计不能照搬闲鱼或者转转要给“当面交易”留出口要支持校内实名认证降低信任成本还要让安卓端操作足够轻量毕竟学生不太乐意装一个很大的App只是为了卖一本旧教材。适合谁来做这个题目如果你是大四计算机、软件工程相关专业的学生Java基础一般安卓刚上手没多久这个题复杂度适中。它既有用户登录、商品发布、分类浏览这种常规模块来保证工作量又有竞拍出价、自动成交、消息通知这些有深度的业务点来撑起答辩过程中的项目亮点。如果是在答辩时老师问一句“你这个系统安全性怎么考虑的”你能讲清楚并发出价怎么处理、超时订单怎么兜底这部分就能很出彩。整套系统做下来技术栈是前端Android原生加后端Spring Boot加MySQL属于标准的Java全栈方向。关键在于把拍卖时序和并发控制讲透把移动端体验优化做到位这两点直接决定项目的完成度和答辩分数。2. 整体方案与技术选型2.1 为什么选Android原生而不是H5或小程序现在做移动端毕设绕不开一个选择原生开发、H5打包App还是微信小程序。我给的建议是这个题老老实实用Android原生。原因第一条Java是你的主语言。毕设答辩的时候老师一定会问架构和底层实现。原生Android用Java写你要解释Activity生命周期、RecyclerView缓存机制、AsyncTask或者线程池这些都在你的知识范围内。如果用H5套壳核心界面全是WebView渲染技术上确实简单但一被追问性能问题就露怯了。原因第二条拍卖系统对实时交互有要求。出价竞拍需要倒计时驱动界面要在关键时间节点上刷新状态原生Android的Handler消息机制、Service后台轮询、通知栏推送都比H5方案更可靠。虽然也能用WebSocket做实时推送但要做移动端轮询兜底原生代码控制更顺手。原因第三条招聘市场上Android原生岗位仍以Java为主。做完这个项目简历里可以写“熟悉Android生命周期与常用组件了解RecyclerView复用机制”这句描述对找Java开发实习也有帮助。当然你完全可以引入Kotlin但如果你是拿这个题当毕业设计而不是产品上线Java可以帮你少踩很多版本和语法层面的坑。2.2 后端框架选型Spring Boot MyBatis 还是 SSH说实话十年前的教材还在教SSHStruts2 Spring Hibernate现在的毕业设计再用那套老框架从答辩角度就不占优势了。Spring Boot 2.x MyBatis是最主流的选择。Spring Boot解决的是配置地狱问题。SSH时代要写一大堆XML配置才能把三个框架整合起来光是把Struts的Action和Spring的Bean接起来就够折腾两周。Spring Boot用自动配置和Starter机制一个注解一个依赖就能跑起来把宝贵的时间留给业务逻辑。MyBatis和Hibernate比较我推荐MyBatis的原因很务实SQL可控性强。拍卖系统里有几个核心SQL会比较绕——查询当前最高出价、查询我参与的竞拍、超时未付款订单统计这些场景下MyBatis写原生SQL语义清晰、调优方便Hibernate那种一复杂就自动拼接查询的机制反而麻烦。登录鉴权这里如果不做第三方登录自己用Token就够了别上Spring Security或者Shiro毕设阶段那玩意配置起来成本远大于收益。Tomcat端口默认8080MySQL端口3306Redis如果你会就加上不会也不影响核心流程用数据库表存当前出价信息同样跑得通只是高并发场景要加锁。2.3 移动端架构MVP还是MVVMAndroid端我最终用的是MVP严格来说是一个轻量化的MVP变体。Activity当View层里面持有Presenter对象Presenter负责业务调度数据层用Repository模式包装。这套东西写起来比MVVM的LiveData和ViewModel要直白也更容易在答辩的时候讲清楚每一层在干什么。很多同学的代码问题出在Activity里塞了太多事。一个Activity里既调接口又解析JSON弹对话框还要刷新列表写到后面一个类一两千行自己都看不下去。MVP至少强制你把接口请求和UI更新拆开。比如首页展示正在进行中的拍卖列表这个过程的数据加载跑在线程里通过接口回调把结果抛回UI线程Presenter在中间做桥接Activity里只更新适配器。有一点要提醒不要为了架构而架构。如果你的项目所有页面加起来十个左右接口调用也没超过二十个MVP的体量刚好合适。不要去套Dagger2做依赖注入不要去上RXJava做响应式编程这些框架学习曲线陡峭出了Bug调试成本也高。架构的目的是让代码清晰而不是为了在答辩里多摆几个框架名字。3. 数据库设计与核心流程3.1 表结构与设计思路根据用户角色和业务流程我设计了以下核心数据表表名主要字段说明userid, student_no, password, nickname, avatar, phone, credit_score学生用户学号做唯一标识goodsid, seller_id, title, description, category, images, condition_level, old_price商品信息旧价用于参考auctionid, goods_id, seller_id, start_price, increment, current_price, start_time, end_time, status拍卖场次核心表bid_recordid, auction_id, user_id, bid_price, bid_time出价记录每次出价留痕orderid, auction_id, buyer_id, seller_id, final_price, status, create_time成交后生成订单messageid, from_user, to_user, content, type, is_read站内聊天与系统通知favoriteid, user_id, goods_id收藏关注addressid, user_id, detail, lat, lng校内见面地点简化可不用地图为了赶一个上半年的交稿节点我把五张必备表user、goods、auction、bid_record、order设计放在前面message和favorite作为扩展。这样即使开发时间紧张核心链路依然是完整的。程序里各项数值类型需要注意价格字段用DECIMAL(10,2)而不是FLOAT避免浮点精度误差。拍卖结束时间用DATETIME且带索引因为“找到所有正在进行的拍卖”这个查询是首页的核心SQL会频繁按end_time过滤。bid_record表要建联合索引(auction_id, bid_price)方便查询某场拍卖的当前最高价和历史出价竞争情况。3.2 拍卖状态机与时间策略拍卖系统的关键在于状态流转。我定义了一个状态机0-未开始 - 1-竞拍中 - 2-已成交 - 3-已流拍 - 4-已关闭发布商品后进入0状态到起拍时间自动切到1。有人出价并正常结束时切到2没人出价或未达保留价切到3。卖家主动下架或违规关闭时切到4。这些状态切换最初肯定有人说用定时任务每分钟扫一次数据库就行。但这种方法有两个问题第一定时任务有延迟最坏情况下某场拍卖已经结束59秒了界面还在允许出价第二如果数据库量大全表扫描压力不小。我采用的方案是双保险接口层做强校验定时任务做兜底清理。出价接口执行前先判断当前时间是否超过end_time超过了直接返回“本场拍卖已结束”。同时系统每分钟跑一次Spring的Scheduled任务把已经过期且未处理的auction批量标记状态生成对应的订单或流拍记录。这样既保证了API层面不会有人卡着最后几秒提交成功又确保数据库状态不会长期挂起。这里有一个细节容易忽略前端倒计时和后端时间必须统一用服务器时间。如果学生手机本地时间不准调快两分钟拍卖还没结束就看不到出价按钮了。我的做法是登录时从后端拉取服务器时间戳倒计时全部基于这个时间戳计算前端只管做差值显示。4. 后端核心模块与接口设计4.1 出价接口与并发控制出价是拍卖系统最核心也最容易被老师追问的接口。先描述需求用户对某场进行中的拍卖出价出价必须高于当前价加上最小加价幅度同一用户不能连续领先自己拍卖结束后不能出价。这个接口真正的难度在于并发。如果同时有十个人出价数据库读一个当前价9.8元然后各自算出10元、11元、12元去更新最终可能出现低级价格覆盖高级价格的情况——典型的“更新丢失”。处理这个问题的标准方案是乐观锁具体嘴上说的是用数据库行锁手头做的是在更新语句里加条件。UPDATE auction SET current_price #{newPrice}, current_bidder_id #{userId} WHERE id #{auctionId} AND current_price #{oldPrice} AND status 1 AND end_time NOW()这条SQL的巧妙之处在于更新条件里带上了旧价格、状态、时间三个校验条件。当两个请求同时读到旧价格9.8元第一个请求执行成功current_price变成12元第二个请求再用current_price9.8作为条件去更新发现匹配不到任何行影响行数为0就知道自己出价失败了代码里据此返回“价格已变化请刷新后重试”。这个方案不用加悲观锁不用Redis分布式锁性能损失小而且逻辑简单好讲。接口里别忘了一个细节出价成功后要立刻查询最新价格返回给客户端同时插入一条bid_record记录保证审计可追踪。4.2 倒计时与自动成交逻辑后端倒计时的实现我看到很多同学的第一反应是“每个拍卖起一个Timer线程”。这是典型的坑如果同时有200场拍卖就要起200个线程数据库连接、内存占用全都会被拖垮。我的做法是不在后端维护每个拍卖的实时倒计时倒计时只在前端计算。前端拿到服务端返回的end_time本地不断刷新剩余时间显示“还剩02:15:33”。到点了前端停止出价按钮后台用定时任务扫描数据库统一处理超时拍卖。自动成交的定时任务核心代码如下Scheduled(fixedDelay 60000) public void handleExpiredAuctions() { ListAuction expiredList auctionMapper.findExpiredAuctions(); for (Auction auction : expiredList) { if (auction.getCurrentPrice() ! null auction.getCurrentPrice().compareTo(auction.getReservePrice()) 0) { auction.setStatus(2); // 成交 orderService.createOrder(auction); } else { auction.setStatus(3); // 流拍 } auctionMapper.updateStatus(auction); } }需要注意的点是定时任务是分布式部署时的重复执行问题毕设单机部署不存在这个问题。但如果你上了两台服务器记得给任务加分布式锁否则同一场拍卖会生成两张订单。此外订单生成时要初始化状态为“待付款”并给买家推送一条消息通知提醒其尽快完成线上支付或联系卖家当面交易。4.3 接口列表与RESTful设计接口规划这块没什么玄学就是足够清晰。我整理一下核心接口列表可以直接抄作业接口方法说明/api/user/registerPOST注册校验学号唯一/api/user/loginPOST登录返回token/api/auction/listGET分页查询拍卖列表支持分类筛选/api/auction/detail/{id}GET拍卖详情含出价记录/api/auction/publishPOST发布拍卖商品/api/auction/bidPOST参与出价/api/auction/myGET我发起的拍卖/api/auction/joinGET我参与的竞拍/api/order/listGET我的订单列表/api/order/pay/{id}POST模拟支付标记已付款每个接口返回统一的消息封装体格式是{code, message, data}。code用纯数字200为成功400一般是参数问题500是服务器异常。这个封装体虽然简单但能避免团队协作时各写一套返回格式的混乱。登录这块用JWT生成Token是主流做法把用户ID和角色放进载荷有效期设置为7天。拦截器在请求头里取Token并校验校验失败直接返回401。这里要强调的是拦截器里不要每请求都查一次用户表——用户状态信息放到Token里只有走敏感操作出价、下单时才去校验用户信用分等动态属性。5. Android端架构与UI实现5.1 页面结构与导航设计移动端页面规划必须围绕“三步走”的原则设计——浏览、出价、管理不能让用户绕路。我用单Activity加多Fragment的结构底部四个Tab分别是首页、分类、消息、我的。首页展示推荐拍卖流分类页按品类展示教材、数码、生活用品等消息页做站内聊天和系统通知的聚合我的页面放个人发布的拍卖、参与的竞拍、订单管理、个人资料。技术选型上列表用RecyclerView加CardView卡片布局每个卡片显示商品封面图、标题、当前价、出价人数、倒计时。适配器要复用自定义的ViewHolder避免每次创建视图带来的性能损耗。图片加载用Glide并在列表页设置缩略图加载而不是原图不然WiFi环境下都能卡半天。发布拍卖的流程要控制步骤数量第一步填商品基本信息标题、分类、新旧程度、描述第二步上传图片最多9张第三步设置起拍价、加价幅度、拍卖时长。三个步骤用Fragment切换最后统一提交数据先存本地草稿防止填写中途App被系统回收导致内容丢失。这里的草稿功能虽小但在答辩演示的时候可以顺便秀一秀数据持久化的处理能力。5.2 倒计时与刷新策略的移动端实现拍卖页面的倒计时控件是UI层的重头戏。我直接用RecyclerView的Adapter配合一个全局的Handler实现。简单说Adapter里每个Item持有一个剩余时间的TextViewHandler每隔一秒发一个Message适配器收到后重新计算所有Item的剩余时间并更新对应TextView。实际编码的时候有个坑必须提醒你千万不要在Adapter的onBindViewHolder里直接创建Handler或者CountDownTimerRecyclerView的Item复用机制会让这些实例疯狂创建和泄漏。正确做法是在Fragment级初始化一个单一的Handler把时间更新任务丢给它统一调度。当剩余时间小于60秒时Item的背景色从白色变成淡黄色出价按钮的文案从“出价”变成“即将结束”。最后10秒内客户端直接锁定出价按钮同时后端接口也做了过期校验双重保险防止卡时间出价。如果某场拍卖的价格被更新了怎么让所有在看的用户同步我用了最朴素也够用的方案——轮询。当前正在查看拍卖详情的用户每5秒拉一次最新出价记录和当前价。为什么不加WebSocket因为毕设项目不是大型游戏5秒的延迟对拍卖场景完全可接受而WebSocket连接管理、心跳保活这些机制做起来反而会占大量时间。如果技术有富余可以用OkHttp加WebSocket优化一下但这不是必需项。5.3 移动端性能优化的小心得项目快收尾的时候我花了两天做性能优化虽然改动的代码量不多但体验提升明显。总结下来核心就四点。第一RecyclerView图片加载必须用占位图和渐进式加载。Glide的placeholder()方法传入一个默认灰色底图图片加载完成后淡入替换视觉效果会舒服很多。如果是大图先加载低分辨率版本再加载高清版滑动时候不会掉帧。第二减少无谓的接口请求。翻页的时候用户没有离开当前页面就不要刷新数据进入详情页先加载缓存数据再请求最新数据保证界面秒开。本地缓存用SharedPreferences就够了别杀鸡用牛刀上数据库。第三列表数据的分页逻辑统一封装。上拉加载更多时用一个isLoading标志位防止重复请求下拉刷新时清除旧数据并重新加载第一页。这个逻辑每个用到列表的页面都要写最好抽成一个通用的PageListFragment基类子类只要传入接口和适配器就能复用。第四善用Android Profiler。我在优化后发现首页滑动掉帧用Profiler一查是图片加载线程占满了CPU。解决方式是给Glide设置diskCacheStrategy(DiskCacheStrategy.ALL)让图片在磁盘和内存都做缓存滑动流畅度立刻好了很多。6. 实战中的常见问题与避坑指南6.1 并发出价导致的“价格覆盖”问题排查这个Bug是我自己实际踩过的。最初版本出价接口的逻辑是先查询当前价在Java里比较然后执行更新更新结果就是两个用户同时出价后提交的那个用较旧的价格基准去更新覆盖了更高出价。排查的过程倒是思路清晰看后端日志发现同秒内有两条更新语句影响行数都是1说明两条都更新成功了但后一条把前一条的价格压下去了。解决办法就是上面说的把校验逻辑挪到SQL更新语句的条件里用影响行数判断是否成功。这个方案改完再压测100个并发请求同时出价最终数据库里只有一条成功其余全部返回“价格已被更新”。这个案例在答辩时特别值得讲从发现问题、分析原因到解决问题的全链路展示比在PPT里背概念生动得多。如果你有时间还可以顺手把这个测试过程写成记录附上并发前后price字段的变化对比图答辩老师基本都会觉得这项目是真的自己动手做过。6.2 MySQL连接超时与连接池配置开发过程中遇到一个诡异现象App运行一会儿、隔段时间再点第一次刷新总是报“Communications link failure”。后来定位到是MySQL服务器的wait_timeout参数默认为8小时长时间无操作后服务端主动断开空闲连接而连接池里的连接没有及时剔除程序拿到的已经是失效连接。解决方案有两种第一种是改MySQL配置把wait_timeout调大第二种是给连接池配置验证。我用的是Druid连接池加了一个testWhileIdletrue和validationQuerySELECT 1配置让连接池定期检测空闲连接的有效性无效连接自动丢弃再用testOnBorrowtrue确保取出的连接一定是可用的。当然毕设里只去掉testOnBorrow变成false性能会好一点但稳定性优先吧。6.3 图片上传与Android存储权限的适配发布商品的图片上传最初使用Android 6时代的外置存储写入权限结果在Android 10以上真机上直接崩溃。Android 10开始强制分区存储App不能随意访问外部存储器任意目录。这个问题几乎是每届毕设的经典坑。我的处理方式是按现代规范来动态申请权限把用户选择的图片通过Uri读取到应用私有目录再上传到服务器。服务器的图片存储路径用一个配置文件统一管理前端访问时拼接http://服务器IP:8080/images/xxx.jpg来加载避免硬编码IP导致换环境就得改代码。注意Cleartext HTTP traffic not permitted的问题Android 9开始默认禁止明文HTTP请求需要在AndroidManifest里配置android:usesCleartextTraffictrue否则开发环境下图片全加载不出来。6.4 数据一致性与超时支付的状态处理最后讲一个关于订单状态的细节。拍卖成交后生成订单状态是“待付款”买家需要在24小时内完成支付。如果超时未支付怎么办我给了一个折中方案超时后订单状态变为“已取消”系统自动扣除买家信用分同时给卖家发送提示消息卖家可以选择重新上架商品或者联系买家线下交易。这个设计不是真实电商那种严格自动流拍——校园场景下确实存在“拍卖成功后线下当面交易”的情况一刀切取消订单会伤害用户体验。所以在代码里超时取消的逻辑做了二次校验如果买家和卖家在消息模块里有过聊天记录就默认允许延迟一天付款。这个细节虽然增加了一点代码量但让整个系统的业务逻辑显得非常完整和有人情味答辩时可以作为“业务特殊性考量”来展示你对场景的理解。7. 实际体验与个人总结整个项目从需求分析到Android端跑通前后花了一个多月其中大部分时间其实花在业务逻辑的梳理和联调上。真正动手写代码前建议先把数据库表结构和接口列表定下来找人按照你的接口文档做一个简易前端页面来模拟请求确认接口返回数据结构没问题再开始Android的UI开发能省掉大量返工时间。在开发过程中我遇到最闹心的问题是Android模拟器跑起来太慢后来直接改用真机调试速度提升明显。如果你也没有Android真机可以用安卓模拟器但尽量分配2GB以上内存否则界面切换卡到怀疑人生。另外联调时把后端日志级别调整为DEBUG模式出问题时能快速定位到具体接口和数据内容排查效率提升几何级。做完全部模块我最大的体会是毕业设计的核心不是“功能多”而是“链路通”。一个能完整跑通发布、出价、成交、支付、评价全流程的项目比十个只做了列表没有深层逻辑的半成品要有价值得多。把这个拍卖平台的链路打通再把并发控制思路讲清楚你答辩的时候自然有底气——因为整条链路里每一个技术决策背后都有真实的问题场景撑腰老师问一句你能答十句这才是好项目的标准。最后再分享一个提升体验的小技巧给项目加上一个“新手教程”弹层首次登录时用半透明的浮层引导用户点击关键按钮。这个小设计几乎不花什么开发时间但在答辩演示时会给老师留下“这个同学考虑到了用户体验”的印象亲测有效。