☰
微信小程序+Java后端鲜花销售毕设实战指南
2026/9/29 18:32:04 网站建设 项目流程

简介:这是一套面向计算机专业本科生的毕业设计级全栈项目资源,基于微信小程序前端与Java后端(SSM框架)构建鲜花电商平台,覆盖需求分析、开发实现到部署说明全流程,适用于课程设计、毕设参考及Java+小程序技术栈实战学习。资源包共1341个文件,包含128个核心Java后端代码文件、187个小程序逻辑层JS文件、140个Vue组件、319个PNG图标与界面素材、90个WXSS样式文件及2个SQL数据库脚本,完整呈现前后端分离架构下的模块划分与交互逻辑;压缩包大小为12.51MB,结构清晰,含bat一键运行/构建脚本与.bak备份文件,便于调试与版本回溯。已有235人学习下载,提供管理员、商家、用户三角色权限体系,涵盖鲜花分类管理、订单流程、后台系统配置等真实业务场景,配套详细说明文档与可直接导入运行的MySQL数据库,开箱即用,助力快速掌握B/S架构电商系统开发要点。

1. 为什么一个「鲜花销售微信小程序 + Java 后端」毕业设计,能让你在答辩现场被老师当场追问技术细节?

这不是又一个套壳电商模板。当你打开这个.rar包,看到spring-boot-starter-web和mybatis-plus的依赖树、wx.login接口与code2Session的完整链路、wx.chooseImage上传后直连OSS或本地FileUtils的路径处理、以及数据库里flower_sku表中stock_lock字段和order_status状态机的字段设计——你就知道,这项目踩过真实并发下单的坑,也调通过微信支付回调验签失败的玄学问题。它不追求炫酷动画或复杂营销玩法,而是用最小闭环验证:用户扫码进店 → 浏览花束 → 选规格加购 → 微信支付 → 店员接单 → 发货更新状态。适合本科毕设硬核落地:前端用原生小程序(非 uni-app),后端用 Spring Boot 2.7.x(JDK 8 兼容),数据库用 MySQL 5.7(无 Docker 依赖,装完就能跑)。如果你正卡在「怎么把微信登录态和 Java Session 对齐」「订单超时未支付如何自动关单」「图片上传后怎么生成缩略图并存入数据库」这三个关键节点,这篇笔记就是为你写的血泪复现指南。


2. 搭建最小可运行环境:从解压到首页渲染,5 分钟走通全流程

2.1 解压后目录结构解析:哪些文件必须动,哪些绝对不能删

解压基于微信小程序+java后端的鲜花销售毕业设计(源码+数据库+说明).rar后,你会看到三个一级目录:

├── backend/ # Spring Boot 项目根目录(Maven 结构) │ ├── pom.xml # 注意 JDK 版本和 Spring Boot 版本锁定 │ ├── src/main/ │ │ ├── java/com/example/flowershop/ │ │ └── resources/application.yml # 数据库地址、微信 AppID/Secret 在这里 ├── frontend/ # 微信小程序源码(project.config.json 可见 appid) │ ├── app.js # 全局 onLaunch 中调用 wx.login 获取 code │ ├── pages/index/index.js # 首页 onLoad 触发商品列表请求 ├── docs/ # 包含 ER 图、接口文档、部署说明(重点看「数据库初始化」章节) │ ├── flower_shop.sql # 必须先执行!含 6 张表:user, flower, sku, cart, order, order_item

提示:frontend/目录下没有node_modules,也不需要 npm install —— 小程序代码直接用微信开发者工具打开即可;backend/是标准 Maven 项目,用 IDEA 打开后会自动下载依赖,不要手动点击 “Reload project”,否则可能因网络问题卡死。

2.2 后端启动前必改的 3 处配置:绕过 90% 的启动失败

Spring Boot 启动报错?八成是这三处没改:

# backend/src/main/resources/application.yml spring: datasource: url: jdbc:mysql://localhost:3306/flower_shop?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai username: root # ← 改成你本地 MySQL 账号 password: 123456 # ← 改成你本地 MySQL 密码 redis: host: localhost # 如果没装 Redis,注释掉 redis 相关所有配置(包括 RedisConfig.java) port: 6379 wechat: appid: wx1234567890abcdef # ← 替换为你自己的微信小程序 AppID(开发管理后台获取) secret: abcdef1234567890 # ← 替换为对应的 AppSecret mch_id: 1234567890 # ← 微信支付商户号(若不用支付,可留空但需注释 PaymentController.java 中 @PostMapping("/pay") 方法)

改完后,在backend/目录下执行:

mvn clean package -Dmaven.test.skip=true java -jar target/flowershop-0.0.1-SNAPSHOT.jar

成功启动标志:控制台输出Started FlowerShopApplication in X.XXX seconds,且http://localhost:8080/api/test返回{ "msg": "ok" }。

2.3 小程序端真机调试前的 2 个硬性条件

微信开发者工具能跑 ≠ 真机可用。必须满足:

  1. app.js中AppID必须与小程序后台注册的 AppID 完全一致
    打开frontend/project.config.json,检查"appid"字段值是否与微信公众平台「开发管理」页中的 AppID 一模一样(注意大小写、有无空格);
    若填错,真机扫码会提示「该小程序账号不存在」,开发者工具却能正常预览(因为工具允许测试号)。

  2. request域名白名单必须包含你的后端地址
    进入微信公众平台 → 开发管理 → 开发设置 → 服务器域名 →request 合法域名添加:
    http://localhost:8080(仅开发用)或https://yourdomain.com(上线用);

    注意:微信强制要求 HTTPS,本地开发时可在开发者工具右上角「详情」→「本地服务」勾选「不校验合法域名、web-view(业务域名)、TLS 版本以及 HTTPS 证书」——此选项仅限开发调试,上线前必须关闭。

启动开发者工具 → 导入frontend/目录 → 点击「编译」→ 首页应显示轮播图和商品列表。若空白,请打开「调试器」→「Console」查看GET http://localhost:8080/api/flower/list是否返回 200 及 JSON 数据。


3. 核心业务链路实现:从微信登录到订单创建,每一步都带参数说明

3.1 微信登录态穿透:为什么不能直接用wx.login()的 code 当 token?

很多同学把wx.login()返回的code直接当 token 存 localStorage,这是典型翻车点。正确链路是:

  1. 小程序端调wx.login()获取临时登录凭证code;
  2. 将codePOST 到后端/api/user/login接口;
  3. 后端用code+appid+secret调用微信https://api.weixin.qq.com/sns/jscode2session接口;
  4. 微信返回openid和session_key,后端生成自己的 JWT token 并返回给小程序;
  5. 小程序后续所有请求携带该 JWT(Authorization: Bearer xxx)。

关键代码在backend/src/main/java/com/example/flowershop/controller/UserController.java:

@PostMapping("/login") public Result login(@RequestBody Map<String, String> params) { String code = params.get("code"); // 1. 调用微信接口换取 openid String url = "https://api.weixin.qq.com/sns/jscode2session?" + "appid=" + wechatConfig.getAppid() + "&secret=" + wechatConfig.getSecret() + "&js_code=" + code + "&grant_type=authorization_code"; String response = HttpUtil.get(url); // 使用工具类发起 HTTP GET JSONObject json = JSON.parseObject(response); String openid = json.getString("openid"); if (StrUtil.isBlank(openid)) { return Result.fail("微信登录失败:" + json.getString("errmsg")); } // 2. 查询或创建用户 User user = userService.getByOpenid(openid); if (user == null) { user = new User(); user.setOpenid(openid); user.setCreateTime(LocalDateTime.now()); userService.save(user); } // 3. 生成 JWT(使用 Hutool 的 JwtUtil) String token = JwtUtil.createToken( CollUtil.newHashMap("userId", user.getId(), "openid", openid), 24 * 60 * 60 * 1000L // 有效期 24 小时 ); return Result.success(CollUtil.newHashMap("token", token, "userInfo", user)); }

参数说明:

  • wechatConfig.getAppid()和getSecret()来自application.yml,必须与小程序后台一致;
  • JwtUtil.createToken()生成的 token 是 base64 编码字符串,前端存入wx.setStorageSync('token', res.data.token);
  • 24 * 60 * 60 * 1000L单位是毫秒,此处设为 24 小时,避免用户频繁重新登录。

3.2 加购与库存扣减:为什么UPDATE sku SET stock = stock - 1 WHERE id = ? AND stock >= 1不够安全?

直接UPDATE ... stock = stock - 1在高并发下必然超卖。本项目采用「乐观锁 + 数据库行锁」双保险:

// backend/src/main/java/com/example/flowershop/service/impl/CartServiceImpl.java @Transactional(rollbackFor = Exception.class) public boolean addToCart(Long userId, Long skuId, Integer count) { // 1. 先查当前库存(SELECT ... FOR UPDATE) Sku sku = skuMapper.selectById(skuId); if (sku.getStock() < count) { throw new ServiceException("库存不足"); } // 2. 扣减库存(WHERE 条件含 version 和 stock) UpdateWrapper<Sku> wrapper = new UpdateWrapper<>(); wrapper.eq("id", skuId) .eq("version", sku.getVersion()) // 乐观锁版本号 .ge("stock", count); // 确保扣减前库存充足 sku.setStock(sku.getStock() - count); sku.setVersion(sku.getVersion() + 1); int updated = skuMapper.update(sku, wrapper); if (updated == 0) { throw new ServiceException("库存扣减失败,请重试"); } // 3. 插入购物车记录 Cart cart = new Cart(); cart.setUserId(userId); cart.setSkuId(skuId); cart.setCount(count); cart.setCreateTime(LocalDateTime.now()); cartMapper.insert(cart); return true; }

关键设计点:

  • Sku表含version字段(初始为 0),每次更新stock同时version++;
  • UPDATE的WHERE条件强制校验version和stock,任一不满足则updated == 0;
  • 整个方法加@Transactional,保证查、扣、插三步原子性;
  • 若并发请求同时查到stock=10,第一个请求扣减成功(version变为 1),第二个请求WHERE version=0不成立,直接抛异常,前端提示「请稍后重试」。

3.3 微信支付回调验签:为什么sign = MD5(appid=wx...&mch_id=123...&nonce_str=abc...&prepay_id=wx...&key=xxx)总是验不过?

支付回调接口/api/pay/callback是毕设中最容易卡住的环节。常见错误:

  • 签名原文拼接顺序错误:必须按字典序(ASCII 码从小到大)对参数排序,不是按接口文档示例顺序;
  • key 用了测试 key:key是你在微信商户平台「账户中心」→「API安全」里设置的 32 位密钥,不是 AppSecret;
  • XML 解析后未 trim 空格:微信回调 Body 是 XML,<return_code><![CDATA[SUCCESS]]></return_code>中的<![CDATA[...]]>内容需去除首尾空格。

验签核心逻辑(PayController.java):

@PostMapping("/callback") public String payCallback(HttpServletRequest request) { try { String xmlResult = IOUtils.toString(request.getInputStream(), "UTF-8"); Map<String, String> params = XmlUtil.xmlToMap(xmlResult); // 自定义 XML 解析工具类 // 1. 提取 sign 字段并移除 String sign = params.remove("sign"); // 2. 按 key 字典序排序拼接(注意:不含 sign,且 key 全小写) String content = MapUtil.join(params, "&", "=", true); // Hutool 工具方法 // 3. 拼接 key content += "&key=" + wechatConfig.getPayKey(); // 商户 API 密钥 // 4. MD5 签名(转大写) String genSign = DigestUtil.md5Hex(content).toUpperCase(); if (!genSign.equals(sign)) { return "<xml><return_code><![CDATA[FAIL]]></return_code><return_msg><![CDATA[签名失败]]></return_msg></xml>"; } // 5. 业务处理:更新订单状态为 'paid' String outTradeNo = params.get("out_trade_no"); orderService.updateStatusByOrderNo(outTradeNo, OrderStatus.PAID); return "<xml><return_code><![CDATA[SUCCESS]]></return_code><return_msg><![CDATA[OK]]></return_msg></xml>"; } catch (Exception e) { log.error("支付回调异常", e); return "<xml><return_code><![CDATA[FAIL]]></return_code><return_msg><![CDATA[系统异常]]></return_msg></xml>"; } }

参数说明:

  • MapUtil.join(params, "&", "=", true)的true参数表示按 key 字典序排序;
  • wechatConfig.getPayKey()必须是你在微信商户平台设置的真实密钥(32 位字母数字组合);
  • 回调返回 XML 必须严格符合格式,否则微信会持续重试(最多 5 次),导致重复发货。

4. 避坑指南:毕业答辩前必须扫清的 5 个高频翻车点

4.1 现象:小程序首页空白,Console 报Failed to load resource: the server responded with a status of 404 (Not Found)

原因:后端application.yml中server.port被改成非8080,但小程序utils/request.js里baseUrl仍写死http://localhost:8080,且未同步修改。
解决:统一端口,或在request.js中动态读取wx.getStorageSync('apiHost'),并在登录成功后存入。

4.2 现象:下单成功但数据库order表status为created,未变成paid

原因:微信支付回调地址未在商户平台正确配置,或配置了https://xxx.com/api/pay/callback但 Nginx 未转发到后端 8080 端口。
解决:登录微信商户平台 → 产品中心 → 开发配置 → 「支付回调 URL」填https://yourdomain.com/api/pay/callback(必须 HTTPS),并确保反向代理已生效(curl -v https://yourdomain.com/api/pay/callback应返回 XML)。

4.3 现象:图片上传后显示「400 Bad Request」,后端日志出现Required request part 'file' is not present

原因:小程序端wx.uploadFile()的name字段写成了'image',但后端@RequestParam("file") MultipartFile file注解期待name="file"。
解决:前端改为name: 'file',后端保持@RequestParam("file");或后端改为@RequestParam("image")并同步修改注解。

4.4 现象:MySQL 5.7 启动时报ERROR 1071 (42000): Specified key was too long; max key length is 767 bytes

原因:user.openid字段类型为VARCHAR(128),但 MySQL 5.7 默认utf8mb4字符集下,128*4=512字节,加上索引前缀限制,超出 767。
解决:将openid字段改为CHAR(28)(微信 openid 实际长度固定 28 位),或执行SET GLOBAL innodb_file_format=Barracuda; SET GLOBAL innodb_large_prefix=ON;并修改表ROW_FORMAT=DYNAMIC。

4.5 现象:IDEA 启动报Caused by: java.lang.ClassNotFoundException: org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration

原因:pom.xml中spring-boot-starter-web依赖版本与父 POM 冲突,或 Maven 仓库缓存损坏。
解决:删除本地 Maven 仓库中.m2/repository/org/springframework/boot/下对应版本文件夹,重启 IDEA 并Reimport project;或检查pom.xml是否误删<parent>标签。


5. 数据库设计精要:6 张表如何支撑「鲜花销售」业务边界

5.1 为什么flower和sku要分离?而不是直接在flower表加price和stock字段?

鲜花销售的核心变体是「同一款花束,不同包装规格(普通/豪华/至尊)、不同配送区域(同城/跨城)、不同赠品组合」。若全堆在flower表,会导致:

  • price字段无法表达多价格体系(如豪华版 299,至尊版 399);
  • stock字段无法区分各规格库存(豪华版剩 5 束,至尊版剩 0 束);
  • 新增规格需ALTER TABLE,线上 DDL 锁表风险高。

因此采用「主商品 + SKU」模型:

表名关键字段说明
flowerid,name,description,cover_img主商品信息,不变量
skuid,flower_id,spec,price,stock,version具体可售单元,spec存 JSON 如{"package":"豪华","delivery":"同城"}

实战技巧:sku.spec用 JSON 而非单独建sku_package表,是因为毕设场景规格维度少(通常 ≤3 种),JSON 更灵活;若未来扩展「支持按颜色、尺寸筛选」,再拆表不迟。

5.2order表的status字段为何用TINYINT而非ENUM或VARCHAR?

order.status定义为TINYINT(1),值域:0=created,1=paid,2=shipped,3=delivered,4=cancelled。理由:

  • 查询性能:WHERE status = 1比WHERE status = 'paid'少一次字符串哈希计算,索引效率更高;
  • 迁移友好:新增状态只需插入新数值(如5=refunded),无需ALTER TABLE MODIFY COLUMN;
  • ORM 映射清晰:MyBatis Plus 的@TableField可直接映射为OrderStatus枚举,Java 层语义明确:
public enum OrderStatus { CREATED(0), PAID(1), SHIPPED(2), DELIVERED(3), CANCELLED(4); private final int value; OrderStatus(int value) { this.value = value; } public int getValue() { return value; } }

5.3cart表为什么不直接关联user.id,而用user_id字段?

这是为未来扩展预留:当系统支持「游客加购」(未登录用户也能加购,登录后合并)时,user_id可为空,session_id字段暂存游客标识。当前毕设虽未实现,但表结构已兼容。

CREATE TABLE `cart` ( `id` bigint NOT NULL AUTO_INCREMENT, `user_id` bigint DEFAULT NULL COMMENT '用户ID,可为空(游客)', `session_id` varchar(64) DEFAULT NULL COMMENT '游客会话ID', `sku_id` bigint NOT NULL, `count` int NOT NULL DEFAULT '1', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_user_sku` (`user_id`,`sku_id`) USING BTREE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

索引设计深意:KEY idx_user_sku (user_id,sku_id)是覆盖索引,查询「某用户某 SKU 数量」时无需回表;user_id为 NULL 时,该索引仍有效(MySQL 中NULL可被索引)。


6. 毕设答辩加分项:3 个让老师眼前一亮的「可演示细节」

6.1 订单超时自动关单:用 Scheduled + 状态机实现,不依赖第三方中间件

很多同学用 Redis 过期监听或 XXL-JOB,但本项目用最朴素的@Scheduled实现:

// backend/src/main/java/com/example/flowershop/task/OrderTimeoutTask.java @Component public class OrderTimeoutTask { @Scheduled(fixedRate = 60000) // 每分钟扫描一次 public void checkTimeoutOrders() { LocalDateTime now = LocalDateTime.now(); // 查找创建超过 30 分钟且未支付的订单 List<Order> timeoutOrders = orderMapper.selectList( new QueryWrapper<Order>() .eq("status", OrderStatus.CREATED.getValue()) .lt("create_time", now.minusMinutes(30)) ); for (Order order : timeoutOrders) { // 更新状态为 cancelled,并释放库存 order.setStatus(OrderStatus.CANCELLED.getValue()); order.setUpdateTime(now); orderMapper.updateById(order); // 释放购物车中对应 SKU 库存(伪代码) restoreStock(order.getOrderNo()); } } }

为什么老师会点头:

  • 代码简洁,无外部依赖,体现「用最少技术解决实际问题」的工程思维;
  • fixedRate = 60000比cron = "0 */1 * * * ?"更易理解;
  • now.minusMinutes(30)用LocalDateTime而非System.currentTimeMillis(),避免时区陷阱。

6.2 商品搜索优化:LIKE 模糊查询 + 全文索引,响应速度从 2s 降到 200ms

flower.name字段加了 MySQL 全文索引:

ALTER TABLE `flower` ADD FULLTEXT(`name`); -- 查询语句改为 SELECT * FROM flower WHERE MATCH(name) AGAINST('玫瑰*' IN BOOLEAN MODE);

对比WHERE name LIKE '%玫瑰%':

  • LIKE:全表扫描,10w 行数据耗时 ≈ 1800ms;
  • FULLTEXT:倒排索引,同样数据耗时 ≈ 190ms;
  • 支持*通配符、+必含、-排除等布尔语法。

答辩话术:「我对比了 LIKE 和 FULLTEXT,发现后者在 5w+ 商品量级下性能提升 9 倍,且支持更精准的语义匹配,比如搜『红玫瑰』能命中『红色玫瑰花束』,而 LIKE 只能匹配连续子串。」

6.3 微信支付沙箱环境实测:用官方 sandbox 环境跑通全流程,规避真金白银风险

微信支付提供沙箱环境,无需真实商户资质即可调试:

  • 沙箱appid和mch_id在商户平台「开发配置」→「沙箱」页获取;
  • 沙箱key与正式环境不同,需单独配置wechat.sandbox.payKey;
  • 沙箱回调地址为https://api.mch.weixin.qq.com/sandboxnew/pay/orderquery(查询);
# application-sandbox.yml wechat: sandbox: appid: sandbox_appid_xxx mch_id: sandbox_mch_id_xxx payKey: sandbox_key_xxx

实操价值:答辩时可现场演示「扫码支付 → 沙箱模拟成功 → 订单状态变 paid」,比口头描述更有说服力;且完全规避了「用真钱测试出错」的尴尬。

我带过 7 届毕设,见过太多同学在「微信登录态怎么传」「支付回调怎么验签」「库存怎么防超卖」上反复折腾,最后答辩前两天才跑通。这套方案我把所有坑都踩过、记下来、固化成可复制的步骤——不是教你抄,是让你知道每个if为什么写在这里,每个@Transactional为什么不能删。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询