每年到了开题季,总有人私聊我“Java校园二手市场交易系统这题到底能不能选”“会不会太烂大街”。我的回答一直很直接:能选,而且它是很稳的选题,但前提是你要真把系统跑起来,并讲清楚每张表、每条核心逻辑是干什么的。单纯从网上扒一个源码压缩包,解压后连启动类都找不到,那才是大型翻车现场。
这篇内容围绕Java校园二手市场交易系统的完整生命周期展开,从选题理由、需求边界、技术选型、数据库建模,到四条核心业务链路的代码拆解、部署演示、答辩追问,一次聊透。适合三类人读:还没开题正在纠结的学生,代码写了一半或者已经拿到源码但跑不通的同学,以及想拿同一个业务题改成Python、PHP、小程序APP端的折腾型选手。
1. 这个题为什么年年有人选——先把需求边界想清楚
1.1 经典选题背后的三重价值
第一是场景真实。大学里谁没在表白墙、跳蚤群、宿舍楼下卖过旧书?教材、小电瓶、自行车、键盘、音箱、羽毛球拍,这些都是真实存在的闲置物品。需求看得见摸得着,写开题报告不用编,评委一看就懂。
第二是难度适中。一个完整的校园二手市场系统,包含用户、商品、订单、分类、收藏、后台管理,基本覆盖了业务系统该有的登录会话、增删改查、文件上传、分页搜索、事务处理,但又不至于像电商秒杀那样牵扯高并发、消息队列、分布式一致性问题。对本科生来说,这个范围刚好能独立完成、答辩也能讲清楚。
第三是可扩展性极强。同一个业务模型,往左可以做微信小程序端,往右可以加数据可视化报表,往下可以把后端从Java换到Python Flask、PHP ThinkPHP,往上可以加权限框架Shiro或Spring Security。有些题目天生只有一条路走到底,而这个题天然是多路径的,后面我用专门一节讲怎么延伸。
1.2 需求分析:先砍掉那些不切实际的功能
毕设最忌讳的不是功能少,而是需求边界模糊。我带项目时,习惯让学生先画一张功能地图,把能想到的所有功能列出来,然后按“必须做、可以加、直接删”三档分类。
校园二手市场的核心角色有两个:普通用户和管理员。核心流程可以概括为一句话:用户注册登录后发布闲置商品,通过分类浏览或关键字搜索找到商品,看中后下单购买(校内线下自提),交易完成后进入订单管理;管理员在后台管理用户、审核商品、管理分类。
围绕这条主线,必须做的功能大概是这些:
| 模块 | 功能点 | 说明 |
|---|---|---|
| 用户 | 注册、登录、退出 | 学生用户,学号/手机号/邮箱均可 |
| 商品 | 发布、编辑、上下架 | 标题、描述、图片、原价、转让价、成色 |
| 商品 | 浏览、分类筛选、搜索 | 按分类/关键字/价格排序 |
| 交易 | 下单、确认交易 | 校内自提场景,不做物流 |
| 订单 | 我卖出的、我买到的 | 订单状态可流转 |
| 收藏 | 我的收藏 | 可加,答辩讲起来有话题 |
| 后台 | 用户管理、商品管理、分类管理 | 管理员独立角色,做权限隔离 |
加分项可以加:举报功能、访问量统计、站内私信、ECharts统计报表、Redis缓存商品浏览数。直接删掉的是:真实在线支付,除非你对接沙箱,否则答辩必被问“钱去哪了”;物流跟踪,校内自提根本不需要;推荐算法,数据量撑不起来,容易引火烧身。
1.3 MVP思维:先把主流程跑通,再谈加分
很多人在写系统前特别喜欢纠结UI,比如“用哪套后台模板”“要不要换皮肤”。我的建议一律是:先跑通“注册-登录-发布-搜索-下单-后台管理”这条完整链路,再去碰其他。这个项目拆成MVP之后,最终形态其实就是六个页面:首页商品列表、商品详情、个人中心、发布页、下单/订单页、后台管理页。只要这六页通了,整个系统的骨架就立住了,剩下都是往里填肉。
2. 技术选型的平衡——Java为主,Python/PHP/小程序怎么延伸
2.1 Java方向的标准组合与版本搭配
如果是Java毕设,我推荐的最稳组合永远是:Spring Boot 2.7.x + MyBatis Plus 3.5.x + MySQL 5.7/8.0 + Thymeleaf(或者JSP)+ Bootstrap/Layui。这套组合几乎是毕设圈的“标准答案”,因为它最大程度减少了环境配置成本:Spring Boot内嵌Tomcat,一个jar包就能跑;MyBatis Plus把单表CRUD封装好,代码量直接砍一半;Thymeleaf在服务端渲染,前端调试没那么复杂。
版本上我列一个实际验证过的搭配:
| 组件 | 推荐版本 | 备注 |
|---|---|---|
| JDK | 1.8 或 11 | 如果机器只有JDK17,Spring Boot要选2.7+,注意兼容 |
| Spring Boot | 2.7.18 | 稳定,资料多,兼容JDK8 |
| MyBatis Plus | 3.5.3 | 分页插件用起来方便 |
| MySQL | 5.7 或 8.0 | 8.0需要配时区参数 |
| 模板 | Thymeleaf | 语法比JSP友好,和Boot集成更顺 |
| 前端 | Bootstrap 4 / Layui | 后台管理推荐Layui,表格是现成的 |
这里我想强调一个很多人忽略的点:能不上Vue就别上前后端分离。不是Vue不好,而是对毕设来说,前后端分离意味着本地演示时要同时启动后端服务和前端Node服务,现场万一前端端口被占、node_modules装不上、跨域配置忘改,任何一个问题都能让你当场表演“重启大法”。服务端模板渲染虽然老派,但演示时只会有一个8080端口,稳定性优先。
2.2 答辩时你需要的“选型理由线”
老师大概率会问“你为什么选Spring Boot而不是SSM”。回答的核心逻辑不是“因为省事”,而是“因为减少重复配置、聚焦业务实现”。可以准备这样一条回答线:Spring Boot的自动装配机制把Spring MVC、内嵌Tomcat、数据源配置都整合好了,让项目重心回到业务逻辑;MyBatis Plus提供通用Mapper和分页插件,避免为每个实体的CRUD写重复SQL;Thymeleaf在服务端渲染,让页面和数据处在同一事务上下文里,简单场景下比前后端分离更直接。这条线讲出来,既是技术选型,也顺便证明你理解了这个框架的定位。
如果老师往后追问“Spring Boot自动配置是怎么实现的”,你能答上“通过META-INF/spring.factories或AutoConfiguration.imports加载自动配置类,配合@ConditionalOnMissingBean等条件注解按需装配”,这个问题的印象分就拿下了。
2.3 同一个题改成Python、PHP、小程序APP其实很容易
标题里提到“可做计算机毕设Java、Python、PHP、小程序APP”,这不是虚的,校园二手市场的业务模型足够通用,可以平移。
- Python方向:用Flask或Django重写后端,ORM换成SQLAlchemy或Django ORM,数据库表结构基本不动。Python版本演示时有天然优势,代码量少、能现场改逻辑、渲染模板也快。
- PHP方向:ThinkPHP 6 + MySQL,部署用phpstudy即可,演示时打开Apache/Nginx直接访问,非常轻。
- 小程序APP方向:后端用Java写REST接口,前端用uniapp做微信小程序。核心是:把Thymeleaf渲染的页面改成小程序page,把Controller改成纯JSON返回,登录由Session换Token,文件上传路径改成云存储或服务器静态目录。
这三个方向我都实际帮人理过,结论一致:表设计不变,变的只有接入层。所以开头我说这题“可扩展性强”不是客套话,它真的适合多语言多端复刻。
3. 数据库表设计——核心不是商品字段,而是状态与关联
3.1 六张核心表的结构
数据库设计是答辩时最容易暴露水平的部分。很多学生的表结构有明显硬伤:密码明文存储、商品图片存base64、状态字段用varchar存中文、没有任何逻辑删除标记。这些细节别小看,老师扫一眼建表SQL就能看出你到底有没有实操过。
我建议的六张核心表如下:
| 表名 | 用途 | 关键字段 |
|---|---|---|
| t_user | 用户表 | id, student_no, username, password, phone, avatar, role, status, create_time |
| t_category | 商品分类表 | id, name, sort, status |
| t_goods | 商品表 | id, user_id, category_id, title, description, price, original_price, degree, images, status, view_count, create_time |
| t_order | 订单表 | id, order_no, goods_id, seller_id, buyer_id, price, status, create_time, update_time |
| t_favorite | 收藏表 | id, user_id, goods_id, create_time |
| t_comment | 评论表(可选) | id, goods_id, user_id, content, create_time |
核心表之间的关系其实很清晰:用户一对多商品,商品多对一分类,用户和商品通过订单关联起来(一个订单包含买家用户和卖家用户两个外键),收藏是用户和商品的多对多关系表。
3.2 建表DDL中值得抄作业的细节
下面这段是根据我平时给学生的模板改出来的精简版,重点不是全,而是每个约束都有存在理由:
CREATE TABLE t_user ( id BIGINT AUTO_INCREMENT PRIMARY KEY, student_no VARCHAR(20) NOT NULL UNIQUE COMMENT '学号,登录账号', password VARCHAR(64) NOT NULL COMMENT '保存BCrypt加密后的密码', username VARCHAR(30) NOT NULL, phone VARCHAR(20), avatar VARCHAR(255) COMMENT '头像图片相对路径', role TINYINT DEFAULT 0 COMMENT '0普通用户 1管理员', status TINYINT DEFAULT 1 COMMENT '1正常 0封禁', create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表'; CREATE TABLE t_goods ( id BIGINT AUTO_INCREMENT PRIMARY KEY, user_id BIGINT NOT NULL, category_id BIGINT NOT NULL, title VARCHAR(100) NOT NULL, description TEXT, price DECIMAL(10,2) NOT NULL COMMENT '转让价', original_price DECIMAL(10,2) COMMENT '入手价,用于显示折扣', degree TINYINT DEFAULT 1 COMMENT '成色:1全新 2九成 3七成 4五成以下', images VARCHAR(1000) COMMENT '多张图片用逗号分隔的路径', status TINYINT DEFAULT 0 COMMENT '0在售 1已下架 2已售出', view_count INT DEFAULT 0, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_user (user_id), KEY idx_category (category_id), KEY idx_status (status), CONSTRAINT fk_goods_user FOREIGN KEY (user_id) REFERENCES t_user(id), CONSTRAINT fk_goods_category FOREIGN KEY (category_id) REFERENCES t_category(id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='二手商品表';几个设计意图说一下:
第一,密码字段长度设为64而不是32,因为用BCrypt加密时结果通常是60位字符串,32不够存。哪怕你只有时间做MD5加盐,也建议留大一点。
第二,商品图片用images字段存逗号分隔的多个相对路径,而不是存JSON字符串或重复建一张图片表。毕设场景下这样最省事,前端回显时按逗号split就行。真正的大型系统会单独建图片表,但对毕设属于过度设计。
第三,商品状态字段我们设计成int枚举,配合索引。为什么用int不用varchar?因为状态是一个有限集合,int存储空间小、比较快、对索引友好,语义通过Java枚举统一管理,不会出现“上架”“上架中”“已上架”这种脏数据。
3.3 订单表里的“双用户”陷阱
订单表是很多人容易搞错的地方。一个订单里同时有seller_id和buyer_id,两个都指向t_user表。网上很多源码直接写成user_id,导致“我买到的”和“我卖出的”分不清。正确做法是:
CREATE TABLE t_order ( id BIGINT AUTO_INCREMENT PRIMARY KEY, order_no VARCHAR(32) NOT NULL UNIQUE COMMENT '订单号,可用时间戳+随机数生成', goods_id BIGINT NOT NULL, seller_id BIGINT NOT NULL COMMENT '卖家=商品发布者', buyer_id BIGINT NOT NULL COMMENT '买家=下单用户', price DECIMAL(10,2) NOT NULL COMMENT '成交价=下单时商品价格', status TINYINT DEFAULT 0 COMMENT '0待确认 1已确认 2已完成 3已取消', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, CONSTRAINT fk_order_goods FOREIGN KEY (goods_id) REFERENCES t_goods(id), CONSTRAINT fk_order_seller FOREIGN KEY (seller_id) REFERENCES t_user(id), CONSTRAINT fk_order_buyer FOREIGN KEY (buyer_id) REFERENCES t_user(id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单表';这里我专门用order_no字段,而不是直接用id做订单号。原因是演示和答辩时你可以拿着这个订单号讲“订单编号生成规则”,比如年月日时分秒加随机数。虽然不是核心技术,但它是老师眼里“像真实项目”的信号。
4. 四条主链路代码拆解——登录、发布、搜索、下单
4.1 登录拦截器:不要在每个Controller里写if判断
第一个容易翻车的地方是登录状态校验。新手常见写法是在每个Controller方法开头写if (user == null) return "redirect:/login";,页面一多漏写一个,就会出现“未登录也能访问个人中心”的漏洞。正确做法是写一个拦截器统一处理。
public class AuthInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HttpSession session = request.getSession(); User user = (User) session.getAttribute("loginUser"); String uri = request.getRequestURI(); // 公开接口直接放行:首页、登录页、商品列表、商品详情、静态资源 if (uri.startsWith("/goods/list") || uri.startsWith("/goods/detail") || uri.startsWith("/login") || uri.startsWith("/register") || uri.startsWith("/static")) { return true; } if (user == null) { response.sendRedirect("/login"); return false; } return true; } }然后在配置类里注册拦截,并设置排除路径:
@Configuration public class WebConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new AuthInterceptor()) .addPathPatterns("/**") .excludePathPatterns("/login", "/register", "/static/**", "/error"); } }这里有个细节值得在答辩时讲:拦截器本质上是Spring MVC的HandlerInterceptor机制,preHandle返回false时请求就被拦下,不会继续走Controller。拦截器负责“认证”,如果要控制“授权”,比如管理员后台接口只允许role=1访问,可以在拦截器里再写一层角色判断,更优雅的做法是在后台Controller上加自定义注解配合拦截器解析。讲到这一层,说明你理解认证和授权的区别。
4.2 图片上传:绝对路径与静态资源映射
发布商品必然涉及图片上传。很多学生第一天跑源码就遇到“图片显示不出来”,问题基本都出在路径上。正确方案是:图片保存到服务器本地的一个upload目录,数据库里只存相对路径,比如/upload/20250601/xxx.jpg,再通过Spring Boot的静态资源映射把路径暴露出来。
@PostMapping("/goods/publish") public String publish(@RequestParam("title") String title, @RequestParam("price") BigDecimal price, @RequestPart("images") MultipartFile[] files, HttpSession session) { User user = (User) session.getAttribute("loginUser"); Goods goods = new Goods(); goods.setUserId(user.getId()); goods.setTitle(title); goods.setPrice(price); // 其他字段省略... if (files != null && files.length > 0) { StringBuilder sb = new StringBuilder(); for (MultipartFile file : files) { // 生成唯一文件名,避免中文乱码与重名覆盖 String suffix = StringUtils.getFilenameExtension(file.getOriginalFilename()); String fileName = System.currentTimeMillis() + "_" + new Random().nextInt(1000) + "." + suffix; String subDir = new SimpleDateFormat("yyyyMMdd").format(new Date()); String dirPath = uploadDir + "/" + subDir; File dir = new File(dirPath); if (!dir.exists()) { dir.mkdirs(); } file.transferTo(new File(dir, fileName)); sb.append("/upload/").append(subDir).append("/").append(fileName).append(","); } goods.setImages(sb.toString()); } goodsService.save(goods); return "redirect:/goods/my"; }同时在配置类里加静态资源映射:
@Override public void addResourceHandlers(ResourceHandlerRegistry registry) { // uploadDir为本地绝对路径,比如 D:/project/upload/ registry.addResourceHandler("/upload/**") .addResourceLocations("file:" + uploadDir); }这个方案有三个坑需要提前排掉:
一是文件名必须重命名,否则两个人上传同名图片,后上传的会覆盖先上传的;二是数据库存相对路径而不是绝对路径,否则换电脑、换部署路径后所有图片都404;三是上传目录要记录清楚,别把文件传到项目的target目录里,因为重新打包后target会被清掉。最好的实践是把upload目录配置在application.yml里,通过@Value注入,换环境只改配置。
4.3 商品列表搜索与分页:MyBatis Plus的QueryWrapper组合
商品列表页通常需要支持分类筛选、关键字搜索、价格排序、分页。用MyBatis Plus写起来很快,但问题也集中:Service层写成了“花式拼接SQL”,条件一多就乱。我的建议是用QueryWrapper把条件从Controller传下来,在Service里统一处理。
@Override public IPage<Goods> queryGoods(Integer pageNum, Integer pageSize, Long categoryId, String keyword, String orderBy) { Page<Goods> page = new Page<>(pageNum, pageSize); QueryWrapper<Goods> wrapper = new QueryWrapper<>(); // 默认只查在售商品,下架和已售出的不能在列表展示 wrapper.eq("status", 0); if (categoryId != null) { wrapper.eq("category_id", categoryId); } if (StringUtils.hasText(keyword)) { // 标题和描述双字段模糊搜索 wrapper.and(w -> w.like("title", keyword).or().like("description", keyword)); } if ("price_asc".equals(orderBy)) { wrapper.orderByAsc("price"); } else if ("price_desc".equals(orderBy)) { wrapper.orderByDesc("price"); } else { wrapper.orderByDesc("create_time"); } return goodsMapper.selectPage(page, wrapper); }分页插件别忘记配置:
@Configuration public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }这里有一个很多毕设源码都没做好的点:列表里默认过滤掉非在售商品。如果你不加这个条件,用户下架的商品还在列表里展示,点击详情又提示已失效,演示时被老师一刷就现形。这种业务条件不是SQL炫技,而是真实的领域规则,写的时候要多想一层。
4.4 下单与并发:一个@Transactional怎样避免“同一件商品被两个人买走”
二手市场最核心的业务逻辑是下单。它涉及多个操作:校验商品是否在售、把商品状态改成已售出、创建订单、可能还要生成通知。如果这些步骤不放在一个事务里,中途报错就会出现“订单创建了但商品状态没改成已售出”之类的脏数据。
下面是一个简化版的下单Service:
@Service public class OrderService { @Transactional(rollbackFor = Exception.class) public Long createOrder(Long goodsId, Long buyerId) { // 1. 查询商品并加锁,防止并发下单 Goods goods = goodsMapper.selectByIdForUpdate(goodsId); if (goods == null || goods.getStatus() != 0) { throw new BizException("商品不存在或已下架"); } // 2. 修改商品状态为已售出 goods.setStatus(2); goodsMapper.updateById(goods); // 3. 创建订单 Order order = new Order(); order.setOrderNo(generateOrderNo()); order.setGoodsId(goodsId); order.setSellerId(goods.getUserId()); order.setBuyerId(buyerId); order.setPrice(goods.getPrice()); order.setStatus(0); orderMapper.insert(order); return order.getId(); } }为什么在第一步用selectByIdForUpdate?这其实是行级锁。MySQL InnoDB引擎下,SELECT ... FOR UPDATE会对命中行加锁,第二个事务执行到同一行时会被阻塞,直到第一个事务提交。再加上@Transactional保证整个流程要么全成功要么全失败,就避免了“同一本书被两个人抢单成功”的经典事故。
这里必须提醒一个@Transactional的经典坑:它只对public方法生效,且通过本类内部的this调用无效。因为Spring的声明式事务本质是AOP代理,同类调用不走代理,注解就不生效。很多人把业务方法写在一个Service里调内部private方法,结果事务失效,排查了一下午。遇到这种场景,把业务方法拆到不同Service里互相调用,或者注入自身代理调用,就能解决。
订单状态流转也建议整理成一张清晰的状态表,答辩时很好用:
| 状态值 | 状态名 | 触发操作 | 后续操作 |
|---|---|---|---|
| 0 | 待确认 | 买家下单成功 | 买卖双方线下自提确认 |
| 1 | 已确认 | 双方确认交易 | 等待评价/直接完成 |
| 2 | 已完成 | 标记完成 | 卖家商品彻底完结 |
| 3 | 已取消 | 任意一方取消 | 商品状态回滚为在售 |
商品状态、订单状态的联动逻辑,是最值得在说明文档里写清楚的部分,因为它让这个系统区别于普通“增删改查生成器”。
5. 从本地跑到演示录像——这些坑我替你先踩了
5.1 MySQL连接与时区:最容易卡住的第一个报错
很多人在这一步浪费半天:项目明明导入成功,一启动就报连接数据库失败或时区错误。如果你用的是MySQL 8.0,JDBC连接串里必须加时区和关闭SSL:
spring: datasource: url: jdbc:mysql://localhost:3306/campus_market?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true username: root password: 你的密码 driver-class-name: com.mysql.cj.jdbc.Driver其中allowPublicKeyRetrieval=true这个参数经常被忽略,但MySQL 8.0使用caching_sha2_password认证时,缺了它会在连接阶段报“Public Key Retrieval is not allowed”。另外建库时一定要指定utf8mb4,否则中文插入后可能出现乱码。演示前先登录MySQL执行一条查询,确认表里数据正常再开始录屏。
5.2 JDK与打包:jar跑还是war跑
Spring Boot项目建议直接打成jar包运行,命令就是:
mvn clean package -DskipTests java -jar target/campus-market-0.0.1-SNAPSHOT.jar如果你用的是老版本Tomcat外置部署,或者老师要求必须能扔进Tomcat,那就改打包方式为war,并继承SpringBootServletInitializer。但站在毕设演示的角度,我强烈推荐jar包方式,原因很简单:内嵌Tomcat端口可控、启动日志直观、不会出现“Tomcat部署路径里放错目录”这种莫名其妙的问题。
有个细节:application.yml里建议配置多环境支持:
spring: profiles: active: dev再拆一个application-prod.yml,把上传目录、数据库密码等按环境区分。演示时用dev跑,现场设备出新问题时,你改的也只是配置而不是代码,心理压力会小很多。
5.3 演示录像怎么录才不丢分
源码和演示录像一起交付时,很多人录着录着就暴露短板。我给的标准脚本是:先黑屏停两秒,亮出项目结构和启动方式,证明代码是你自己的;接着启动MySQL和项目,注册一个账号,登录后发布一件带图片的商品,在首页搜索自己发布的商品,用另一个账号下单,观察订单状态变化,再登录管理员账号,在后台看到新增的商品和订单,最后封禁或下架一个违规商品,结束。
整个录像不要超过八分钟,也不要在里面剪环境安装过程。数据要预置得漂亮:商品图片不要用默认占位图,找几张真实的二手书、耳机、自行车照片;建议预先创建两三件商品,让首页看起来丰富,而不是空荡荡的。
关于录屏工具,Windows上能用OBS或Bandicam,关键是把分辨率设置到1920x1080以上,代码字体调大一点,老师大概率会逐帧看你的代码。
6. 答辩现场最常被追问的六个问题——提前准备好答案
6.1 每个问题背后都在考察什么
我把带过的学生被问到的高频问题整理成一个对照表,答案思路也一并给出:
| 追问 | 考察点 | 推荐回答思路 |
|---|---|---|
| 为什么选这个课题? | 选题动机与真实性 | 从校园闲置浪费切入,联系自己使用表白墙/跳蚤群的体验,强调系统解决信息分散、二手交易难管理的问题 |
| 用户和管理员的权限怎么控制的? | 认证与授权理解 | 拦截器+Session保存登录用户,后台接口通过角色字段判断;可以提一句认证与授权分离 |
| 表之间的关联关系? | 数据库设计基本功 | 说明一对多、多对多关系;重点讲订单表双外键的设计意图 |
| 如果多人同时买一个商品怎么办? | 并发处理意识 | 行级锁FOR UPDATE+事务;也可以讲乐观锁版本号,说明自己为什么选悲观锁,毕设场景冲突概率低、实现直观 |
| 密码为什么不用明文存? | 安全意识 | BCrypt加盐哈希;说明加盐能防止彩虹表攻击;如果项目里只用MD5,诚恳说这是后续改进点 |
| 遇到过什么难点,怎么解决? | 工程实践能力 | 讲图片上传路径问题或事务失效问题即可,真实踩坑永远比背概念有说服力 |
这里有个答法技巧:回答问题时不要背名词,而是讲“我遇到了什么现象→怎么排查→怎么解决”。只要按这个结构答,哪怕方案朴素,老师也觉得你在干活,而不是在读文档。
6.2 怎么让这套通用代码变成“你的项目”
拿到一套能跑的源码之后,最忌讳原封不动交上去。我一般建议做三件低成本高识别度的事:
第一,改名字。包名、项目名、数据库名全部改成有辨识度的命名。这个改动不难,但能直接打消“是不是从网上抄的”的怀疑。
第二,加一个自己擅长的功能点。哪怕只是一个数据统计报表、一个公告管理、一个验证码登录,都要亲自动手写出来。答辩时老师问“项目里最有亮点的地方”,你就能指着自己加的那块讲十分钟。
第三,整理一份README和演示文档。内容包括运行环境、启动步骤、默认管理员账号密码、核心功能截图、数据库初始化脚本说明。这份文档是老师判断你工程习惯的直接证据。
6.3 扩展方向:往小程序或报表方向延伸
如果时间充裕,可以在Java版基础上再做一个uniapp版小程序,后端不需要重写,只需要把Controller改成返回JSON即可。这样项目就变成了“Java后端+微信小程序”,难度和含金量同步上一个台阶。我的建议是后期优先做这块,既有时效性,又符合现在移动端的实际使用场景。
最后再说几句实在话
按照我带项目的习惯,这种题目最后都会叮嘱几句:写代码的时候,心里始终想着“我是在给一个真实场景设计系统”,而不是完成一个交作业任务。把每个字段为什么存在、每个状态为什么这样流转想清楚之后,答辩根本不用背稿子,因为你做过的每一件事都能讲出理由。
真到交付那天,记得先把项目从零配置环境跑一遍,别到老师面前才开始第一次启动。这个系统不复杂,但它值得你用对待真实产品的心态去做一遍。等你自己亲手把“发布-发现-下单-确认”这条流程完整跑通,那种感觉还是挺爽的。