ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

基于SSM框架的校园汉服租赁系统开发实践

基于SSM框架的校园汉服租赁系统开发实践 简介面向计算机相关专业毕业设计与课程设计的SSM框架结合微信小程序的校园汉服租赁系统完整项目资料围绕Spring、SpringMVC、MyBatis后端与微信小程序前端实现从汉服展示、在线租借到订单归还的全流程管理。资源共1133个文件压缩包16.86MB包含103个Java源文件、125个Vue组件、160个JS脚本、76个WXML与78个WXSS页面文件同时提供SQL初始化脚本、XML/Properties配置、PNG/SVG图标资源及Markdown说明文档覆盖前后台完整链路。核心模块包括用户注册登录、汉服商品管理、租赁订单流转、支付接口对接以及后台数据维护目录结构按功能拆分便于系统学习整套业务逻辑。已有93人学习下载适合作为毕业设计参考、课程项目实战或SSM与小程序整合练手可直接导入开发工具运行调试也可作为快速二次开发的基础模板。1. 校园汉服租赁系统的真实需求和SSM技术选型每年校园文化节、汉服社纳新、毕业照拍摄都会涌出一波汉服租赁需求这也是SSM这个技术组合在校园项目里最常见的落地场景之一。社团传统做法是在微信群接龙登记尺码档期再人工核对Excel同一件衣服被两个时段重复预订是常事归还日期记错、押金靠转账记录对账更是让人头疼。拆过几个同类学生项目后我发现难点从来不是功能多少而是预约-取衣-归还-结算这条链路一旦脱节后端对账就要全部人工兜底。选择SSM加微信小程序对校园场景有现实考量小程序免安装微信内直接打开学生零学习成本SSM框架在课程设计和毕业设计中资料最齐全遇到问题容易搜到现成解法MySQL配合MyBatis能精确控制每一条SQL订单状态和租赁库存的边界比JPA更可控。这个项目技术栈不新但把状态流转和并发扣减做对才是它真正的价值。2. SSM框架的职责边界与数据库表结构设计2.1 Spring容器、SpringMVC控制器、MyBatis映射的分层逻辑很多人做SSM项目时把Controller写得很厚Service形同虚设MyBatis只用自动生成的单表CRUD。租赁订单涉及商品、库存、用户、订单明细多张表联动分层不清楚后续改需求会非常痛苦。Spring在这个项目里承担容器和事务管理职责。Service实现类交给Spring托管通过Autowired注入Mapper和其他Service事务边界默认放在Service层一个createOrder方法里同时插入订单、扣减库存、记录明细任何一个失败都要回滚。SpringMVC只做HTTP协议层的事接收参数、校验格式、调用Service、把结果包装成JSON返回。我的习惯是Controller里不允许出现业务判断比如判断库存够不够、计算租赁天数这些必须下沉到Service。MyBatis的职责是SQL和Java对象之间的映射。有一个容易被忽略的点MyBatis默认的一级缓存是SqlSession级别的与Spring整合后每次Mapper调用通常是独立SqlSession一级缓存意义不大不要试图靠它提升性能。真正影响性能的往往是N1查询商品列表页查一次商品表又循环查一次库存表这类问题可以靠SQL联表或批量查询解决。2.2 核心表结构用户表、汉服商品表、租赁订单表先看订单表。租赁和购买不同订单不仅要记录租了什么还要记录租了多久、什么时候归还。所以订单表必须有lease_start_date、lease_end_date、deposit押金、rental_fee租金、status这几个核心字段。CREATE TABLE rental_order ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL COMMENT 订单编号RENTyyyyMMddHHmmss4位随机数, user_id BIGINT NOT NULL COMMENT 用户表主键, goods_id BIGINT NOT NULL COMMENT 汉服商品主键, goods_spec_id BIGINT NOT NULL COMMENT 商品尺码库存主键, lease_start_date DATE NOT NULL COMMENT 租赁开始日期, lease_end_date DATE NOT NULL COMMENT 租赁结束日期, deposit DECIMAL(10,2) NOT NULL DEFAULT 0 COMMENT 押金金额, rental_fee DECIMAL(10,2) NOT NULL DEFAULT 0 COMMENT 租金金额, status TINYINT NOT NULL DEFAULT 0 COMMENT 0待支付 1已支付待取货 2租赁中 3已归还 4已完成 5已取消 6退款中, pay_time DATETIME COMMENT 支付成功时间, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, KEY idx_user_id (user_id), KEY idx_goods_spec_id (goods_spec_id), KEY idx_status (status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT租赁订单表;字段上有两个细节。status使用TINYINT而不是字符串避免拼写不一致产生的脏数据order_no由Java代码在Service层生成不使用数据库自增ID直接对外展示防止订单号被遍历猜测。核心字段类型设计理由order_noVARCHAR(32)全局唯一业务单号用于支付回调、用户查询、对账goods_spec_idBIGINT关联到具体尺码避免M码无货时误扣L码库存deposit / rental_feeDECIMAL(10,2)金额统一用DECIMAL禁止FLOAT避免精度丢失statusTINYINT紧凑存储状态配合状态机流转汉服商品要区分款式和库存。同一款汉服可能有S、M、L三个尺码每个尺码的库存量不同。常见误区是把汉服尺码拼成一条商品记录每新增一个尺码插入一条新商品商品详情页会非常杂乱。CREATE TABLE goods ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100) NOT NULL COMMENT 汉服款式名称, description TEXT COMMENT 详情描述形制、适用场合, cover_url VARCHAR(255) COMMENT 封面图URL, daily_price DECIMAL(10,2) NOT NULL COMMENT 单日租金, deposit_amount DECIMAL(10,2) NOT NULL COMMENT 押金标准, status TINYINT NOT NULL DEFAULT 1 COMMENT 1上架 0下架, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT汉服商品表; CREATE TABLE goods_spec ( id BIGINT PRIMARY KEY AUTO_INCREMENT, goods_id BIGINT NOT NULL, spec_name VARCHAR(20) NOT NULL COMMENT 尺码S/M/L/XL, stock INT NOT NULL DEFAULT 0 COMMENT 当前可租库存, UNIQUE KEY uk_goods_spec (goods_id, spec_name) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT商品尺码库存表;订单和库存的关联必须通过goods_spec_id而不是goods_idM码没货但L码有货的场景下才能正确扣减对应尺码的库存这条在实际项目里踩坑概率非常高。2.3 用户表的openid与学号绑定用户表比较特殊。微信小程序登录拿到的openid标识微信用户但校园租赁需要知道租借人是哪个学生所以要额外存学号和姓名。openid设为唯一索引学号单独存储但不能作为登录凭证。CREATE TABLE user ( id BIGINT PRIMARY KEY AUTO_INCREMENT, openid VARCHAR(64) NOT NULL COMMENT 微信用户唯一标识, nickname VARCHAR(50) COMMENT 微信昵称, avatar_url VARCHAR(255) COMMENT 头像地址, student_no VARCHAR(20) COMMENT 学号下单前补充, real_name VARCHAR(20) COMMENT 真实姓名, phone VARCHAR(20) COMMENT 联系电话, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_openid (openid) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表;注意openid只在单一微信小程序内唯一如果后续要接入公众号或App需要换成unionid作为跨平台统一标识。这个项目只有一个小程序入口用openid足够。3. 微信小程序登录态与后端接口的联调实现3.1 用code换openid的完整链路小程序的登录不是直接拿用户手机号来标识身份。完整流程是小程序端调用wx.login()拿到一个临时code传给后端后端带着code、AppID和AppSecret去微信接口服务换session_key和openid。这个code有效期只有5分钟而且只能使用一次。请求参数说明是否必填appid小程序唯一标识是secret小程序密钥只放后端是js_codewx.login获取的临时凭证是grant_type固定值authorization_code是SpringMVC接口的核心是发起一次HTTP请求到微信登录凭证校验地址并处理返回值。RestController RequestMapping(/api/auth) public class AuthController { Autowired private UserService userService; PostMapping(/login) public Result login(RequestBody LoginRequest request) { // 1. 用code换openid String url https://api.weixin.qq.com/sns/jscode2session?appid appid secret secret js_code request.getCode() grant_typeauthorization_code; String response HttpClientUtil.get(url); JSONObject obj JSON.parseObject(response); String openid obj.getString(openid); if (openid null) { return Result.error(登录失败 obj.getString(errmsg)); } // 2. 查用户不存在则自动注册 User user userService.findByOpenid(openid); if (user null) { user userService.register(openid); } // 3. 生成自定义token返回给小程序 String token UUID.randomUUID().toString().replace(-, ); userService.cacheToken(token, user.getId()); return Result.success(token); } }这段代码做了三件事用code换openid拿openid查用户表查不到就自动创建用户记录用户第一次打开小程序不需要先填注册表单生成一个自定义token作为后续请求的登录凭证。不要把openid直接作为登录态返回给小程序任何人拿到openid都能伪造身份。token建议放Redis并设置过期时间课程设计里用内存Map模拟也可以但服务重启后所有用户都会掉线。小程序端在登录页面先取code再调后端// pages/login/login.js const { request } require(../../utils/request); Page({ onLoad() { wx.login({ success: (res) { request(/auth/login, POST, { code: res.code }).then(token { wx.setStorageSync(token, token); wx.switchTab({ url: /pages/goods/list }); }); } }); } })session_key是jscode2session的另一个返回值用于解密用户手机号等敏感信息。session_key绝对不能返回到前端也不能写进操作日志。这个项目里手机号让用户手动填写即可不需要走getPhoneNumber解密链路。3.2 前端request封装与商品列表渲染小程序端所有请求需要一个统一封装不能每个页面各写一遍wx.request。我在utils/request.js里封装方法自动携带token统一处理HTTP 401和业务错误码。// utils/request.js const BASE_URL https://yourdomain.com/api; function request(path, method, data) { return new Promise((resolve, reject) { wx.request({ url: BASE_URL path, method: method || GET, data: data || {}, header: { Content-Type: application/json, token: wx.getStorageSync(token) }, success: (res) { if (res.statusCode 401) { wx.navigateTo({ url: /pages/login/login }); reject(res); return; } if (res.data.code ! 0) { wx.showToast({ title: res.data.msg, icon: none }); reject(res); return; } resolve(res.data.data); }, fail: (err) reject(err) }); }); } module.exports { request };header里的token从本地缓存读取后端接口通过拦截器校验。后端返回401时前端直接跳登录页并清除缓存这是处理token过期最简单的方式。业务错误码与HTTP状态码分开设计HTTP只表示网络层成功失败业务层的库存不足状态不允许统一用code字段传递这样前端能针对业务错误给出友好提示而不是笼统报网络异常。商品列表页在onLoad里拉取数据。顺序很关键先保证登录态存在再请求列表否则第一次进页面时token还没生成商品接口直接返回401用户看到的是空白页而不是商品列表。// pages/goods/list.js const { request } require(../../utils/request); Page({ data: { goodsList: [], loading: true }, onLoad() { this.loadGoods(); }, loadGoods() { request(/goods/list, GET).then(list { this.setData({ goodsList: list, loading: false }); }).catch(() this.setData({ loading: false })); } })对应WXML用wx:for循环渲染卡片view classgoods-grid view classgoods-card wx:for{{goodsList}} wx:keyid bindtapgoDetail>public class OrderStateMachine { private static final MapInteger, SetInteger TRANSITIONS new HashMap(); static { TRANSITIONS.put(0, new HashSet(Arrays.asList(1, 5))); TRANSITIONS.put(1, new HashSet(Arrays.asList(2, 5))); TRANSITIONS.put(2, new HashSet(Arrays.asList(3))); TRANSITIONS.put(3, new HashSet(Arrays.asList(4))); TRANSITIONS.put(6, new HashSet(Arrays.asList(4))); } public static boolean canTransition(int from, int to) { SetInteger allowed TRANSITIONS.get(from); return allowed ! null allowed.contains(to); } }流转规则集中在一处加新状态或调整规则时不用去翻散落在各个Service里的if判断。数据库更新时再加一层WHERE status? 条件能防止并发请求把订单置入非法状态。4.2 微信支付回调处理与幂等保护微信支付成功后异步通知商户后端一个回调地址。这个回调有两条特性可能重复通知、延迟不可控。所以回调处理必须幂等同一个订单收到多次回调不能重复改状态。RestController RequestMapping(/api/pay) public class PayNotifyController { PostMapping(/notify) public String notify(RequestBody String xmlData) { // 1. 验签防止伪造回调 boolean valid WxPayUtil.verifySign(xmlData); if (!valid) { return xmlreturn_codeFAIL/return_codereturn_msg验签失败/return_msg/xml; } // 2. 解析订单号和支付金额 MapString, String data WxPayUtil.xmlToMap(xmlData); String orderNo data.get(out_trade_no); String totalFee data.get(total_fee); // 3. 幂等更新订单状态 boolean success orderService.handlePaid(orderNo, new BigDecimal(totalFee)); if (success) { return xmlreturn_codeSUCCESS/return_codereturn_msgOK/return_msg/xml; } return xmlreturn_codeFAIL/return_codereturn_msg处理失败/return_msg/xml; } }回调接口返回必须是指定XML格式而且只能返回SUCCESS或FAIL。返回SUCCESS后微信不再重发返回FAIL或超时微信按间隔策略重试多次。handlePaid里的幂等可以用一条条件更新实现UPDATE rental_order SET status 1, pay_time NOW() WHERE order_no #{orderNo} AND status 0;影响行数为0说明订单已经是已支付状态或不存在直接返回成功即可不会因为重复回调产生副作用。注意total_fee字段单位是分数据库里租金单位是元比较金额前要除以100。更稳妥的做法是数据库金额统一以分存储展示层再转换为元。4.3 扣减库存的条件更新写法下单并发时最怕两件同尺码汉服被两个用户同时选中都通过库存充足检查各自扣减库存导致超卖。SSM项目里常用条件更新保证原子性。Mapper public interface GoodsSpecMapper { Update(UPDATE goods_spec SET stock stock - 1 WHERE id #{specId} AND stock 0) int deductStock(Param(specId) Long specId); }调用方根据返回值判断是否继续Transactional public void createOrder(Long specId) { int rows goodsSpecMapper.deductStock(specId); if (rows 0) { throw new BusinessException(该尺码库存不足请重新选择); } orderMapper.insertOrder(...); orderMapper.insertOrderItem(...); }关键在WHERE条件里的stock 0。MySQL执行UPDATE时会对匹配行加行级锁两个并发事务同时执行这条SQL时第二个事务会阻塞等待第一个提交然后在最新快照下判断stock是否仍大于0不满足则影响行数为0。这和先SELECT再UPDATE的两步操作有本质区别后者并发下无法避免超卖。方法上必须加Transactional让扣库存和插入订单在同一事务里。库存扣减成功但订单插入失败时事务回滚会同时撤销扣减如果不开启事务两个操作之间任何失败都会留下库存对不上的脏数据。5. 上线前的三类验证SQL日志、域名白名单与500排查5.1 用MyBatis日志核对SQL参数排错时先确认SQL执行情况。在application.yml中把Mapper包日志级别调到debuglogging: level: com.example.mapper: debug控制台会打印每个Mapper方法的完整SQL和参数绑定值。重点看订单查询的where条件绑定值是否为null。我遇到过用户查不到订单日志显示user_id参数为null原因是前端入参名与后端方法参数名不一致MyBatis按名匹配失败后直接传了null这类问题看代码很难定位日志一眼就能暴露。5.2 小程序域名白名单开发者工具里勾选不校验合法域名后本地一切正常真机预览却一直request:fail。原因是正式环境域名必须在微信公众平台配置且要完成ICP备案并启用HTTPS。request、uploadFile、downloadFile三类域名要分别配置后端接口和图片CDN是不同域名时要全部加入白名单。验证方式是打开真机调试看network面板的具体报错域名不合法会给出明确提示按提示补配置即可。5.3 500错误的三类常见诱因最常见的500是MyBatis的resultType映射问题。数据库下划线字段和Java驼峰字段没有开启map-underscore-to-camel-case时查询结果映射到实体全为null后续逻辑空指针。其次是事务缺失库存扣减成功但订单插入失败没有Transactional时扣减不会回滚界面报错但库存已少。更隐蔽的是微信支付回调的请求体只能读一次。验签逻辑如果先消费了request.getInputStream()Controller再用RequestBody接收直接抛I/O异常。解决方法是自定义Filter把body缓存到包装类中让验签和参数解析共享同一份数据。项目包里自带的1-install.bat、2-run.bat、3-build.bat把环境安装、启动、打包三个动作脚本化直接运行自带脚本就能复现本地环境。真机点一遍从支付到归还的完整流程再翻一遍SQL日志比写一百行测试用例都管用。本文还有配套的精品资源点击获取
返回列表