Java与微信小程序自助购药系统:源码解析、部署排错与答辩要点
简介这是一份基于 Java SSM 框架 微信小程序实现的自助购药系统完整源码适合 Java 方向毕业设计、课程设计以及小程序开发入门者参考。项目按 B/S 架构设计覆盖首页、用户管理、商家管理、药品信息与分类、发票信息、系统管理等模块并配套 MySQL 5.7 数据库文件和前后端完整工程可直接导入 IDE 运行调试。压缩包共 1319 个文件约 22.55MB包含 java、vue、js、xml、sql、wxml、wxss 等类型前端页面、后端逻辑、数据库脚本与配置文件一应俱全目录结构清晰便于对照学习。目前已有 57 人学习下载。通过这份源码读者可掌握 SSM 框架整合、小程序端与后端接口联调、数据库表设计等实践技能同时也能借助完整业务逻辑快速完成同类电商或医药类项目的功能搭建是一份兼顾教学与演示价值的参考资料。1. 自助购药小程序源代码一份能跑通、能答辩的毕设骨架「基于小程序的自助购药小程序源代码java小程序mysqlLW」这类压缩包在毕设圈里几乎年年有人问。它对应的是一个完整的微信小程序前端 Java 后端 MySQL 存储的三层工程核心流程就三条线浏览药品、加购下单、后台管理。对这个包感兴趣的人多半不是想造轮子而是想快速拿到一套能演示、能改、能写进论文的骨架——学生用来过答辩药店经营者想验证小程序卖药的原型。下文先把这套代码的功能边界和数据设计拆开再给一条从解压到真机预览的完整通路最后把最容易翻车的几个坑列清楚。2. 先看懂功能与数据自助购药的业务边界与表设计拿到源代码后我建议先别急着启动花二十分钟看看工程里有哪些页面、哪些表。自助购药和普通商城有区别它不追求营销玩法核心诉求是药品信息准、下单路径短、管理操作轻。搞清楚业务边界后面改代码和写论文都有依据。2.1 用户端与管理端的功能边界从功能上拆这套系统一般分两端。用户端面向普通消费者常见模块是注册登录、药品分类浏览、关键词搜索、药品详情、加入购物车、提交订单、历史订单列表。管理端面向运营者核心是药品上下架、库存调整、价格修改、订单状态处理。在微信小程序生态里这两端通常做成同一个前端项目登录时根据用户角色字段切换入口。普通用户看到的是购药首页管理员登录后能看到管理菜单这是一种省事的做法也方便答辩演示一个账号切换角色。购药流程里最容易被忽略的是购物车的数据结构。常见的做法是本地缓存与服务端同时写用户加购时先更新本地缓存让界面立刻有反馈点击结算时把购物车数据整体提交给后端。这样做的好处是体验流畅切后台重进不会丢数据但它要求后端购物车接口能接收商品 id 列表和数量列表而不是只接收一个「购物车 id」这一点在改代码时容易踩坑。管理端不需要做得太重常见实现是「后端接口 小程序内嵌管理页面」。管理员登录后看到药品列表能改库存、改价格、上下架。答辩时管理端演示的价值比复杂的权限体系更高所以要把压轴戏放在「管理端改库存后用户端详情页实时可见」这个联动效果上。2.2 数据表设计从药品到订单这类项目的数据库一般不会太复杂核心表在五张左右。我整理了一张常见的表结构清单你解压后打开 sql 脚本对照一下基本能对号入座。表名核心字段关键设计点userid, openid, nickname, phone, roleopenid 唯一索引role 区分用户/管理员drugid, name, category, price, stock, image, descriptionprice 用 decimalstock 不能为负数cartid, user_id, drug_id, quantityuser_id drug_id 建唯一索引ordersid, order_no, user_id, total_amount, status, create_timeorder_no 唯一索引status 用数字枚举order_itemid, order_id, drug_id, drug_name, price, quantity保存商品快照不反查 drug 表订单明细保存 drug_name 和 price 快照是这类系统中最重要的设计之一。原因是药品价格和名称可能后续被修改如果历史订单去关联药品表实时取名称和价格后台一改价半年前的订单显示也跟着变这在逻辑上是说不通的。快照字段就是为了让每笔订单永远显示「下单当时的信息」。库存字段值得单独说。stock 用 int 类型扣减必须走「原子更新」而不是先查再改。所谓原子更新就是一条 UPDATE 语句里既判断库存充足又完成扣减类似UPDATE drug SET stock stock - 1 WHERE id ? AND stock 1。如果影响行数为 0说明库存不足此时再抛异常回滚事务。这个问题在第五章会展开是并发场景下最典型的扣库存翻车点。金额字段统一用 decimal(10,2)不要用 float 或 double。药品单价、订单总额、明细金额都要一致避免浮点误差导致对不上账。主键建议用自增 idorder_no 单独建唯一索引不要把订单号当主键——订单号通常带有日期和随机串作为主键会让索引变大查询效率也不如自增 id。2.3 LW 文档的定位与先读哪一节压缩包名称里的 LW一般指配套的论文或设计文档不参与编译运行却是答辩时的主要依据。拿到包后先读文档里的「需求分析」和「数据库设计」两章用它们对照源码里的页面和表结构能快速判断这份代码的功能覆盖度。再看「系统测试」一节看作者用什么账号、什么路径验证过系统这些路径往往可以直接复用到你的演示脚本里。不建议一上来通读「技术栈介绍」或「开发环境」章节那些内容大多是背景铺垫信息密度低。更实用的做法是把论文中描述的功能点逐条列出来然后在代码里找到对应的 Controller 和页面文件做成一张「论文功能 ↔ 代码位置」的对照表。答辩时老师问「这个功能在哪儿实现的」你能直接翻到具体文件和行号印象分会明显不同。如果发现文档里的表结构和 sql 脚本不一致以代码和 sql 脚本为准并把文档统一过去。答辩现场的演示是跑真实代码的文档写得再漂亮和代码对不上反而会被追问。3. 通信链路小程序端到后端接口token 鉴权与业务落点这一章解决「数据怎么在小程序和 Java 后端之间流动」的问题。理解了这条链路你改接口、改字段、排查问题都会顺手很多。花钱买源码最怕的是拿到手像黑匣子跑起来是一回事想改需求却不知道从哪儿下手。3.1 请求封装baseURL 切换、wx.request 封装与登录态处理小程序端每个页面都要发请求把所有请求收敛到一个公共模块里是基本功。常见的做法是在 utils 下建一个 request.js统一处理 baseURL、请求头、错误码和登录态过期。下面是一段可用的封装示例// utils/request.js const BASE_URL getApp().globalData.baseUrl function request(path, method GET, data {}) { return new Promise((resolve, reject) { wx.request({ url: BASE_URL path, method: method, data: data, timeout: 10000, header: { Content-Type: application/json, token: wx.getStorageSync(token) || }, success: (res) { if (res.statusCode 200 res.data.code 200) { resolve(res.data.data) } else if (res.statusCode 401) { wx.removeStorageSync(token) wx.navigateTo({ url: /pages/login/login }) } else { reject(res.data) } }, fail: (err) reject(err) }) }) } module.exports { request }BASE_URL 放在 globalData 里是为了切换环境时只改一处。开发时用http://localhost:8080/api真机预览时换成电脑的局域网 IP比如http://192.168.1.100:8080/api。如果每个页面各自写死域名光替换就得改几十个文件而且容易漏。header 里挂 token 是关键。后端拦截器从请求头取出名为 token 的字段做校验这里必须和后端约定的 key 保持一致。有些项目后端取的是 Authorization前端传的却是 token导致登录后所有接口 401这一类联调问题常见且隐蔽。timeout 建议设 10 秒以上因为小程序首次编译、手机网络波动都可能导致请求变慢默认超时时间不够就会误报失败。3.2 后端分层Controller、Service、Mapper 的职责与事务边界Java 后端常见的分层是 Controller 接请求、Service 写业务、Mapper 操作数据库。买来的源码基本都遵循这个结构区别在于业务逻辑写在哪一层。一个判断代码质量的办法是看 Service 里有没有大量 SQL 拼串或直接操作 Map如果有说明业务逻辑没有沉淀成对象后期维护会吃力。下单是购药小程序最核心的业务也最能看出源码的质量。下面是一段常见的下单 Service 核心逻辑// OrderServiceImpl.java 下单核心方法节选 Override Transactional(rollbackFor Exception.class) public Long createOrder(OrderCreateDTO dto, Integer userId) { // 1. 生成订单号并创建订单主记录 Order order new Order(); order.setOrderNo(generateOrderNo()); order.setUserId(userId); order.setStatus(0); // 待支付 ListOrderItem items new ArrayList(); BigDecimal total BigDecimal.ZERO; for (CartItem cartItem : dto.getItems()) { // 2. 扣库存先更新再判断受影响行数 int rows drugMapper.deductStock(cartItem.getDrugId(), cartItem.getQuantity()); if (rows 0) { throw new BizException(药品库存不足); } Drug drug drugMapper.selectById(cartItem.getDrugId()); // 3. 保存商品快照避免后续改价影响历史订单 OrderItem item new OrderItem(); item.setOrderId(order.getId()); item.setDrugId(drug.getId()); item.setDrugName(drug.getName()); item.setPrice(drug.getPrice()); item.setQuantity(cartItem.getQuantity()); items.add(item); total total.add(drug.getPrice().multiply(BigDecimal.valueOf(cartItem.getQuantity()))); } order.setTotalAmount(total); orderMapper.insert(order); orderItemMapper.insertBatch(items); return order.getId(); }Transactional(rollbackFor Exception.class) 保证订单创建、明细写入、库存扣减三件事要么全部成功要么全部回滚。rollbackFor 显式指定为 Exception.class是因为 Spring 默认只对 RuntimeException 回滚检查异常默认不回滚。如果这里不写 rollbackFor下单过程中抛出一个受检异常订单可能半途落库库存却已经扣了这种数据不一致在答辩时会被追问。deductStock 的 SQL 设计是关键。它应该写成带库存判断的原子更新而不是先select stock再判断再update。原子更新的写法是UPDATE drug SET stock stock - #{quantity} WHERE id #{id} AND stock #{quantity}数据库层面保证扣减和判断是同一瞬间完成的不会出现两个请求同时读到库存为 1结果都通过判断的问题。3.3 登录与鉴权wx.login 换 openid、token 生成与拦截器小程序的登录机制和网页登录不同它依赖微信的 code 换 openid 流程。前端调用 wx.login 拿到临时 code传给后端后端拿着 code 请求微信接口换取 openid用 openid 作为用户唯一标识。下面是一段典型的登录接口// WxAuthController.java 登录接口节选 PostMapping(/login) public Result login(RequestBody WxLoginDTO dto) { // 1. 用 wx.login 返回的 code 调用微信接口换取 openid String openid wxService.code2Session(dto.getCode()); // 2. 根据 openid 查用户不存在则自动注册 User user userMapper.selectByOpenid(openid); if (user null) { user new User(); user.setOpenid(openid); user.setRole(1); // 1 普通用户2 管理员 userMapper.insert(user); } // 3. 生成 token 返回给前端后续请求都带这个 token String token JwtUtil.createToken(user.getId(), user.getRole()); return Result.ok(new LoginVO(token, user)); }生成 token 后后续所有需要登录的接口都由拦截器统一校验。拦截器的逻辑很简单从请求头取 token解析成功就放行解析失败就返回 401。// AuthInterceptor.java 拦截器节选 Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { String token request.getHeader(token); if (token null || token.isEmpty()) { response.setStatus(401); return false; } try { Claims claims JwtUtil.parseToken(token); request.setAttribute(userId, claims.get(userId)); request.setAttribute(role, claims.get(role)); return true; } catch (Exception e) { response.setStatus(401); return false; } }拦截器里只校验 token 合法性不查数据库是为了减少一次磁盘 IO。在毕设这种并发量级下这种设计完全够用。如果你要扩展权限控制可以在拦截器里加角色判断Integer role (Integer) claims.get(role)这里需要把角色信息在生成 token 时就写入 Claims。有一点要注意注册拦截器时登录接口、药品列表接口这些不需要鉴权的路径要放进白名单否则用户还没登录就无法浏览药品业务逻辑就卡死了。常见做法是在 WebMvcConfigurer 里用addPathPatterns(/api/**).excludePathPatterns(/api/auth/login, /api/drug/list)这样的方式配置具体路径看项目接口命名。4. 从解压到跑通导入 MySQL、启动 Java 后端、小程序真机预览的完整步骤这一章是照着做就能跑通的实操路径。解压后先别急着双击任何 exe按下面的顺序把环境确认好每一步验证通过再进下一步能少走不少弯路。4.1 环境确认JDK、Maven、MySQL 8 与微信开发者工具先打开命令行确认三个基础环境是否就绪java -version mvn -v mysql --version这个项目后端是 Java常见搭配是 JDK 8 或 JDK 11。如果你机器上装了多个 JDK 版本先确认java -version指向的版本和项目 pom.xml 里java.version配置一致版本不一致会导致编译报错。Maven 用 3.6 以上就行版本太老可能拉取依赖时出问题。MySQL 5.7 和 8.0 在这类项目里都有可能出现但连接配置上略有不同。8.0 对时区处理更严格连接串里需要加时区参数这一点到 4.2 节会说。微信开发者工具直接到官网下稳定版安装后先扫码登录能打开一个空项目即可不需要急着导入。真机预览时还需要一个已注册的小程序账号 AppID如果没有可以用测试号代替但测试号不支持某些能力尽量用自己的 AppID 演示效果更完整。4.2 导入 SQL 脚本并调整后端连接配置在项目里找到 sql 目录或 db 目录下的 .sql 文件用命令行导入这是最常见的做法比用 Navicat 可视化导入更直接、更不容易出错mysql -uroot -p buy_medicine.sql导入完成后打开后端的配置文件。Spring Boot 项目通常是 src/main/resources/application.yml 或 application.properties找到数据库连接部分重点关注下面这几个参数# application.yml 数据库连接片段 spring: datasource: url: jdbc:mysql://localhost:3306/buy_medicine?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driverurl 里的 characterEncodingutf8 保证药品名称、订单备注这类中文数据不乱码serverTimezoneAsia/Shanghai 解决 MySQL 8.0 启动时常见的时区报错allowPublicKeyRetrievaltrue 解决 MySQL 8.0 的 caching_sha2_password 认证方式下 JDBC 报 Public Key Retrieval 错误。driver-class-name 用 com.mysql.cj.jdbc.Driver 对应 MySQL 8.0用旧驱动会提示找不到类。密码这里要说一句血泪经验如果你的 MySQL 账号密码里有特殊字符比如或#yaml 里必须做对应处理或者用引号包起来否则 Spring Boot 启动时会把密码截断报 Access denied。调试阶段最省事的办法是临时把密码改成纯数字跑通后再改回正常密码减少变量。4.3 启动后端再联调小程序端后端启动命令在项目根目录执行cd backend mvn spring-boot:run第一次启动会下载大量依赖耐心等几分钟看到 Spring Boot 启动成功的日志后再做下一步。不要盯着控制台一直看可以在另一个终端窗口执行curl http://localhost:8080/api/drug/list验证接口是否已就绪。能返回 JSON 数据说明后端这一环已经通了剩下就是小程序端的事。打开微信开发者工具导入小程序前端目录然后在 app.js 的 globalData 里找到 baseUrl// app.js 节选 globalData: { baseUrl: http://localhost:8080/api }开发者工具里先用 localhost 跑通确认整条链路没问题后再看真机预览。真机预览时把 localhost 换成电脑的局域网 IP形如http://192.168.1.100:8080/api。这里有两个前置条件手机和电脑连同一个 WiFi电脑防火墙放行 8080 端口。Windows 系统在「允许应用通过防火墙」里加上 8080 端口即可Mac 一般不用额外配置。这一步不做手机上所有请求都会失败而开发者工具却一切正常是典型的联调玄学问题。5. 跑项目的排错清单5 个最容易翻车的地方与对策这一章把跑这类项目最常见的故障集中列出来每一条都按「现象 → 原因 → 解决」来写。你可以把这一章当作排错索引遇到问题先翻对应的小节。5.1 请求返回 request:fail或 Network 面板一片红现象小程序端所有接口都请求失败控制台报 request:fail后端控制台没有任何请求日志。原因最常见的是 baseURL 配错了比如少了 /api 前缀或者后端没启动成功但前端先发起了请求。其次是开发者工具默认拦截了非法的 http 域名本地调试时后端是 http://localhost不符合微信要求的安全域名规则需要手动放行。解决先用浏览器直接访问 baseURL 下的一个接口路径比如http://localhost:8080/api/drug/list能返回数据就说明后端正常。然后回到小程序端检查 baseURL 是否和接口路径拼接正确。最后在开发者工具右上角「详情」→「本地设置」里勾选「不校验合法域名、web-view业务域名、TLS 版本以及 HTTPS 证书」这一项不勾选本地 http 请求一定失败。5.2 登录成功后其他接口全部返回 401现象登录接口正常返回 token但紧接着的药品列表、购物车、订单接口全部 401弹回登录页陷入死循环。原因前端请求 header 里 token 的字段名和后端拦截器里取的名字不一致。比如前端写的是token后端取的是Authorization两边对不上拦截器自然认为没有凭证。也有一种情况是登录返回的 token 没存入wx.setStorageSyncrequest.js 里读出来是空字符串。解决打开开发者工具的 Network 面板看登录成功后任意一个请求的请求头里有没有 token 字段。没有就检查登录回调是否存储了 token有但后端仍报 401去后端拦截器打日志打印request.getHeader(token)的值。两边的 key 名统一即可建议全部用token简单直接。5.3 后端启动时报 Public Key Retrieval 或时区错误现象Spring Boot 启动日志里出现Public Key Retrieval is not allowed或The server time zone value is unrecognized数据库连不上。原因这两个问题都是 MySQL 8.0 引入的。8.0 默认用户认证插件是 caching_sha2_passwordJDBC 驱动默认不允许客户端直接向服务器获取公钥所以报 Public Key Retrieval时区报错则是 8.0 对连接串里的时区参数有强制要求。解决在 jdbc url 末尾追加两个参数allowPublicKeyRetrievaltrueserverTimezoneAsia/Shanghai。如果项目里的驱动类写的是com.mysql.jdbc.Driver改成com.mysql.cj.jdbc.Driver前者是 MySQL 5.x 时代的驱动在 8.0 下会有兼容问题。改完配置重新启动即可。5.4 库存变负数或超卖后订单依然创建成功现象反复下单同一药品库存可以扣成负数或者两个人同时买最后一盒药两个订单都提示成功。原因这是典型的并发问题。代码如果先select stock判断大于 0 后再update stock stock - 1两个请求同时通过判断后到达 update库存就被透支了。在毕设这种低并发场景下不容易触发但一旦用两个微信账号同时操作问题立刻暴露。解决把扣库存改成原子更新语句让数据库在一条 SQL 里同时完成「判断库存足够」和「扣减库存」-- DrugMapper.xml 中的扣库存 SQL UPDATE drug SET stock stock - #{quantity} WHERE id #{drugId} AND stock #{quantity}MyBatis 里受影响行数为 0就说明库存不足此时 Service 层抛异常触发事务回滚。这个改法不需要引入 Redis 锁也足够应对当前场景而且面试或答辩时能讲清楚原理是加分项。5.5 手机预览连不上后端开发者工具却一切正常现象开发者工具里跑得好好的手机一扫码预览所有接口全挂页面白屏或转圈。原因手机访问的是电脑的局域网 IP而不是 localhost。一种可能是后端启动时绑定了 127.0.0.1只允许本机访问另一种可能是电脑防火墙拦截了 8080 端口的入站请求还有可能是手机和电脑不在同一个 WiFi 下。解决先在手机浏览器里访问http://电脑IP:8080/api/drug/list能返回数据就说明网络通、端口通问题出在小程序端 baseURL 没改打不开就检查两件事——后端启动配置里 server.address 是否写死成 127.0.0.1把它注释掉或改成 0.0.0.0Windows 防火墙是否放行 8080 端口入站。排查顺序建议是「手机浏览器访问接口 → 检查 baseURL → 检查防火墙 → 检查后端监听地址」。6. 进阶与验证给订单防超卖答辩前做一轮演示演练把代码跑通只是起步真正拉开差距的是你对这套系统的理解深度和演示完成度。我拿到这类项目后通常做两件事一是把库存扣减改成原子更新并验证并发效果二是把「管理端改价 → 用户端实时变化」做成一条完整的演示链路。库存防超卖的具体改法在 5.4 节已经给出这里说验证方法开发者工具里开两个窗口登录两个账号同时购买同一件只剩 1 件库存的药品观察结果——一个成功、一个提示「库存不足」且事务回滚这个实测过程在答辩时最有说服力。答辩演示不要临时发挥用下面这张清单过一遍主流程每个场景对应一个技术讲解点演示场景操作路径要讲清楚的点注册登录小程序点登录授权微信信息openid 获取流程、token 生成与存储浏览药品分类筛选、搜索关键词前端请求封装、后端参数校验加购下单加入购物车、结算购物车数据结构、订单事务边界库存扣减两个账号同时买同一药品原子更新与受影响行数判断后台管理管理员改库存、上下架角色鉴权、数据实时同步答辩追问最常落在「两个用户同时买到最后一盒药怎么办」这个问题上。把 5.4 节的 SQL 和事务逻辑讲清楚就足够了不需要硬扯 Redis 锁。能够说明「当前量级用数据库原子操作够了如果吞吐量上来我会引入 Redis 预扣库存做缓冲」这比什么都堆上去更能体现工程思维。演示前还有一个习惯值得养成把后端停掉在小程序端操作一遍购物流程看前端会不会给出友好的错误提示而不是一直转圈。这一步能暴露很多黑匣子问题也能体现你对自己系统的掌控力。我当年第一次演示就是临场才发现手机连不上后端从那以后所有演示都先走一遍主链路再改需求。希望这个套路对你有帮助把这套自助购药小程序从解压到真机跑通比看十篇经验帖都实在。本文还有配套的精品资源点击获取