简介:这套基于Spring Boot、SSM与uniapp开发的药店管理系统源码,面向Java开发者、小程序学习者以及需要完成课程设计或毕业设计的学生。系统覆盖用户注册登录、退出、密码重置、药品分类与信息管理、收货地址、购物车、留言板、文件上传下载等业务模块,并内置条件查询、记录统计、求和汇总等通用接口,便于快速掌握前后端联调。压缩包共1271个文件,大小约14.63MB,主要包含112个Java后端源码、132个Vue页面、169个JS逻辑、78个WXSS样式、76个WXML模板,以及JSON、XML、SQL、图片等辅助资源,目录结构完整,便于导入编译或二次开发。目前已有105人学习下载。资源附带1-install.bat、2-run.bat、3-build.bat等运行脚本及配置文件、开发环境说明,可帮助读者快速启动项目并理解前后端交互流程,整体目录结构清晰,便于按模块检索与二次开发,适合课程设计、毕业设计或小程序开发参考。
1. 药店管理系统,为什么偏偏是 Spring Boot 加微信小程序
药店管理系统的业务本身很朴素:店员打开手机查药有没有货、什么价格,顾客买药时能下单结账,下班前对一遍库存和营业额。它轻,但琐碎。用 Excel 记会漏,上大型 ERP 又太重。常见的落地方案是 Spring Boot 做后端,管药品数据、用户和订单;微信小程序做店员端,扫一扫就能用,不用装 App。这套组合的源码项目很多,新手能照着跑通,小药店也能真拿去用,所以一直是 Java 全栈里被问得最多的方向之一。
这篇文章会按我平时落地的顺序讲:先拆模块和数据表,再把 Spring Boot 后端跑通,接着把微信小程序端接上,最后单独列出最容易让项目翻车的几个坑。读完你可以照着复现这套系统,也能避掉我在本地联调时踩过的那些莫名其妙的问题。
2. 系统模块与数据模型:先把药店业务拆成能落地的四张表
2.1 为什么不是微服务:Spring Boot + MyBatis 在这个场景的选型逻辑
很多人一拿到“药店管理系统”就想着上微服务,注册中心、网关、分布式事务全来一套。但真实药店的规模吃不下这套东西。一家普通药房,一天几百笔订单,同时在线的店员也就一二十人,单体 Spring Boot 应用打成一个 jar 就能跑,部署、备份、排查都简单。微服务拆开以后,服务间调用链路、分布式事务一致性、日志追踪全成了额外负担,对这个项目毫无必要。
Spring Boot + MyBatis 才是这个场景最现实的组合,网上大量 spring boot + mybatis 的 java 开源商城源码、管理系统源码,用的几乎都是同一套。选它不是因为新潮,是因为 MyBatis 对 SQL 的掌控力比全自动 ORM 更直接。药店系统里条件查询和统计特别多,比如“按编码查某个药还剩多少”“按时间段统计门店销售额”“查近一周处方药销量”,这些业务直接写 SQL 最好排查,也最好调索引。
小程序端为什么不换成 App?药店店员不一定会装陌生 App,而且上架审核、版本更新对个体药店来说都是麻烦。微信小程序扫码即用,后端发版后店员端下次打开就自动更新,不需要任何安装动作。桌面后台用网页,店员端用小程序的组合,是这个项目里成本最低、见效最快的形态。
2.2 核心数据表:药品、库存、订单、用户四张表怎么建
把药店业务抽到最简,核心流转链是“店员下单、药品出库、金额入账”。围绕这条链最少需要五张表:用户(店员)、药品、库存、订单、订单明细。这是一套最小可用的建表方案,直接用 MySQL 执行即可。
CREATE TABLE tb_user ( id BIGINT AUTO_INCREMENT PRIMARY KEY, openid VARCHAR(64) NOT NULL, nickname VARCHAR(64) DEFAULT '', phone VARCHAR(20) DEFAULT '', role TINYINT NOT NULL DEFAULT 1, -- 1店员 2管理员 create_time DATETIME NOT NULL, UNIQUE KEY uk_openid (openid) ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4; CREATE TABLE tb_drug ( id BIGINT AUTO_INCREMENT PRIMARY KEY, drug_code VARCHAR(32) NOT NULL, drug_name VARCHAR(128) NOT NULL, specification VARCHAR(64) DEFAULT '', -- 规格,比如 10mg*14片 unit VARCHAR(16) DEFAULT '', -- 单位:盒/瓶/袋 price DECIMAL(10,2) NOT NULL, prescription_flag TINYINT NOT NULL DEFAULT 0, -- 0非处方 1处方 create_time DATETIME NOT NULL, UNIQUE KEY uk_drug_code (drug_code) ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4; CREATE TABLE tb_stock ( id BIGINT AUTO_INCREMENT PRIMARY KEY, drug_id BIGINT NOT NULL, quantity INT NOT NULL DEFAULT 0, updated_at DATETIME NOT NULL, UNIQUE KEY uk_drug (drug_id) ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4; CREATE TABLE tb_order ( id BIGINT AUTO_INCREMENT PRIMARY KEY, order_no VARCHAR(32) NOT NULL, user_id BIGINT NOT NULL, total_amount DECIMAL(10,2) NOT NULL, status TINYINT NOT NULL DEFAULT 0, -- 0已下单 1已支付 2已取消 create_time DATETIME NOT NULL, UNIQUE KEY uk_order_no (order_no) ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4; CREATE TABLE tb_order_item ( id BIGINT AUTO_INCREMENT PRIMARY KEY, order_id BIGINT NOT NULL, drug_id BIGINT NOT NULL, quantity INT NOT NULL, price DECIMAL(10,2) NOT NULL -- 下单时价格,冗余保存 ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;tb_drug 里的 drug_code 是店员能直接报出来的药品编码,不要拿自增 id 去当编码给店员看,实际业务里店员更习惯对着药盒上的编码来核对。tb_order_item 里的 price 是冗余字段,这点很关键:药品以后一定会调价,但订单成交价不能被调价影响,所以下单那一刻把价格复制一份到明细表。金额用 DECIMAL(10,2) 而不是 float,避免浮点误差。
外键我一般不建物理约束,订单和明细之间用逻辑外键就够。原因是扣库存、写明细时 InnoDB 的行锁和索引行为更可控,物理外键在复杂 SQL 下容易给优化器添乱。业务一致性靠 Service 层事务保证,这正是 3.3 节要处理的事。
2.3 接口约定:给小程序用的一套 Result 返回格式
前后端分离后,第一个要统一的就是返回体。如果每个接口各自返回不同的 JSON 结构,小程序端每个请求都要单独判断,后面根本维护不动。我习惯先把 Result 类定下来,所有接口统一走它。
public class Result<T> { private Integer code; // 0成功 401未登录 500业务异常 private String message; private T data; public static <T> Result<T> ok(T data) { Result<T> r = new Result<>(); r.code = 0; r.message = "ok"; r.data = data; return r; } public static <T> Result<T> fail(int code, String message) { Result<T> r = new Result<>(); r.code = code; r.message = message; return r; } }接口层的约定只有三条:成功把数据放进 data,失败返回 message,未登录返回 401。小程序端在 request.js 里统一判断 code,不跟 HTTP 状态码强绑定。这样后端即使把某些业务错误包装成 HTTP 200,前端也能按业务 code 区分。
接口清单初版就四个:POST /api/auth/login 用微信 code 换 token,GET /api/drugs 分页查药品,POST /api/order 下单扣库存,GET /api/order/list 查历史订单。权限上用 JWT 拦截器统一校验,管理员维护药品和库存的接口单独加 role 校验。核心原则是小程序端任何页面都不要直连数据库表,一切走 REST API,以后换前端、接第三方都不用动表结构。
3. 后端从零跑通:Spring Boot 工程启动、登录接口与库存扣减
3.1 最小可启动工程:pom.xml、application.yml 与端口配置
先解决一个问题:intellij idea 社区版怎么用 Spring Boot?社区版没有 Spring Initializr 向导,但不影响开发,新建 Maven 工程后手动补 pom.xml 和启动类,Maven 一样能编译。常见的做法是先建一个空 Maven 工程,坐标随便填,然后把下面的 pom 依赖粘进去。
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.7.18</version> <relativePath/> </parent> <dependencies> <!-- Web 启动器,包含内嵌 Tomcat --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- MyBatis 与 Spring Boot 集成 --> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.3.2</version> </dependency> <!-- MySQL 8 驱动,运行时使用 --> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency> <!-- JWT 令牌生成与校验 --> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-api</artifactId> <version>0.11.5</version> </dependency> </dependencies>版本选择上,Spring Boot 2.7.18 是 2.x 的收尾版本,网上资料最多;MyBatis starter 用 2.3.2 配套最合适。如果你非要上 Spring Boot 3.x,注意 mybatis-spring-boot-starter 要换 3.0+,同时 javax 包全部改名 jakarta,这个切换坑过不少人。
接着写 application.yml,这里最容易影响启动:
server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/pharmacy?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false username: root password: your_password jackson: time-zone: Asia/Shanghai mybatis: mapper-locations: classpath:mapper/*.xml configuration: map-underscore-to-camel-case: trueurl 里的 serverTimezone=Asia/Shanghai 在 MySQL 8 驱动下必填,否则启动直接报时区异常;useSSL=false 是本地开发通用配置。mybatis.map-underscore-to-camel-case 打开后,数据库里的 drug_name 自动映射成 drugName,省掉手写 resultMap。server.port 就是喝多人问的 spring boot 修改端口号的位置,改完重启立即生效。
启动类三行就够:
@SpringBootApplication @MapperScan("com.example.pharmacy.mapper") public class PharmacyApplication { public static void main(String[] args) { SpringApplication.run(PharmacyApplication.class, args); } }@MapperScan 指定 Mapper 接口所在包,容易漏,漏了会报找不到 mapper bean。启动成功后在控制台看到 Tomcat started on port 8080 就算基础工程通了。
3.2 微信登录接口:用 code 换 openid,再换 JWT
微信小程序登录的标准流程是:小程序 wx.login 拿到临时 code,传给后端;后端拿 code 加上小程序 appid、secret,去调微信的 jscode2session 接口换 openid;openid 是这个用户在药店系统里的唯一身份。后端给小程序返回一个 JWT,后续请求带上 token,不用再反复调微信接口。
@RestController @RequestMapping("/api/auth") public class AuthController { // 登录:小程序传 code,后端返回 JWT @PostMapping("/login") public Result<String> login(@RequestBody LoginRequest req) { // 1. 用 code 请求微信接口换 openid String openid = wechatAuthService.code2Session(req.getCode()); // 2. 按 openid 查用户,不存在则自动注册 User user = userService.findOrCreate(openid); // 3. 生成 JWT,有效期 7 天 String token = JwtUtil.createToken(user.getId(), user.getRole()); return Result.ok(token); } }调用微信接口这一步,很多源码直接 new 一个 RestTemplate 就用,我一般会把 RestTemplate 定义成单例 Bean,避免每次登录都创建连接池。微信接口有频率限制,连接复用在高峰期差别很大。
public String code2Session(String code) { RestTemplate rt = new RestTemplate(); String url = "https://api.weixin.qq.com/sns/jscode2session" + "?appid=" + appid + "&secret=" + secret + "&js_code=" + code + "&grant_type=authorization_code"; Map<String, Object> resp = rt.getForObject(url, Map.class); // resp 里会有 openid、session_key,失败时带 errcode if (resp == null || resp.get("openid") == null) { throw new BizException("微信登录失败"); } return resp.get("openid").toString(); }appid 和 secret 不要硬编码在业务类里,用 @Value 从 application.yml 读取,方便不同环境切换。JWT 的签名密钥也放配置文件,要求不低于 32 位随机字符串。token 校验用拦截器统一处理,从 Header 里取 Authorization 的 Bearer token,解析失败就返回 401 让前端跳登录页。
这里最容易踩的坑是 token 过期时间和小程序端本地缓存不同步。小程序端如果一直拿着旧 token 请求,后端一直返回 401,用户会反复被踢回登录页,所以 4.2 节的 request.js 里必须对 401 做统一处理,不要在每个页面写自己的判断。
3.3 库存扣减与下单:事务边界写在 Service 层
药店下单的核心动作是“插订单 + 扣库存”,这两步必须在一个事务里。错误做法是先 SELECT 查库存判断够不够,再 UPDATE 扣减,因为并发场景下两个请求可能同时读到同一个库存数,都判断通过,都去扣,最后库存变成负数。正确做法是写一条原子 UPDATE,让数据库决定扣不扣成功。
@Transactional public Long createOrder(OrderCreateRequest req) { // 1. 生成订单号,格式 yyyyMMddHHmmss + 随机数 String orderNo = buildOrderNo(); // 2. 按明细计算总价,插入订单主表 BigDecimal totalAmount = calculateTotal(req.getItems()); Order order = Order.create(req.getUserId(), orderNo, totalAmount); orderMapper.insert(order); // 3. 逐行插入明细,同时扣库存 for (OrderItem item : req.getItems()) { orderItemMapper.insert(OrderItem.create(order.getId(), item)); int rows = stockMapper.reduceStock(item.getDrugId(), item.getQuantity()); if (rows == 0) { throw new BizException("库存不足: " + item.getDrugId()); } } return order.getId(); }对应的 Mapper 语句写在 XML 里:
<update id="reduceStock"> UPDATE tb_stock SET quantity = quantity - #{quantity}, updated_at = NOW() WHERE drug_id = #{drugId} AND quantity >= #{quantity} </update>这条 UPDATE 的 WHERE 条件带上了 quantity >= #{quantity},数据库行锁会保证同一时刻只有一个事务能成功更新同一行;影响行数为 0 就说明库存不够,直接抛异常,整个事务回滚。@Transactional 必须加在最外层 Service 方法上,写在 Controller 或 Mapper 接口上都不会生效,这是事务边界最容易搞错的地方。
另一个细节:下单前不要为了“判断库存”而先查一次库存。查询和 update 之间有窗口期,查到的结果在并发下根本不可信。查询库存只用于列表展示,真正判断成败的是 UPDATE 的影响行数。
4. 小程序端实操:登录、药品列表渲染与下单链路
4.1 原生小程序目录结构:页面、app.json 与顶部导航栏高度
小程序端最省事的方案是原生开发,页面结构直接,编译后体积可控,不用额外引入 uniapp 那套依赖。顶层目录我一般这样组织:
pharmacy-miniapp/ ├── app.js ├── app.json ├── app.wxss ├── utils/ │ └── request.js ├── pages/ │ ├── login/ │ ├── drugs/ │ ├── order/ │ └── mine/app.json 里配置页面路径和全局导航栏:
{ "pages": [ "pages/login/login", "pages/drugs/drugs", "pages/order/order", "pages/mine/mine" ], "window": { "navigationBarTitleText": "药店管理", "navigationBarBackgroundColor": "#1aad19", "navigationBarTextStyle": "white" } }导航栏高度在小程序里由系统控制,不同机型不一样,这就是很多人反复问“微信小程序顶部导航栏高度怎么设”的原因。默认导航栏直接用 navigationBarTitleText 就行,不用管高度;只有做自定义导航栏,才需要 wx.getWindowInfo 读 statusBarHeight,再叠加胶囊按钮高度去计算。药店系统建议直接用默认导航栏,省掉一整块机型适配工作。
页面之间跳转用 wx.navigateTo,登录、药品、订单、我的四个页面足够覆盖最小闭环。订单详情这种低频页面用一层内跳完成,不需要额外申请分包。
4.2 登录页:wx.login 拿 code,用 button 拿手机号
微信登录获取手机号现在是热门话题,因为政策收得很紧。小程序里拿手机号必须用户主动点 button 授权,open-type 设为 getPhoneNumber,而且小程序需要完成认证、满足平台条件后才拿得到手机号明文。药店店员登录场景不建议死磕手机号,先用 wx.login 完成身份登录,手机号作为选填资料后续再补,这样既不影响下单,也避免被微信认证费用和审核规则卡住。
登录页核心代码只有这么一段:
// pages/login/login.js const request = require('../../utils/request') Page({ async onLoginTap() { // wx.login 拿到的是临时 code,5分钟内有效,只能使用一次 const { code } = await wx.login() const token = await request.post('/api/auth/login', { code }) wx.setStorageSync('token', token) wx.switchTab({ url: '/pages/drugs/drugs' }) } })wx.login 返回的 code 有效期短,拿到后必须立刻传给后端,中间不要插任何异步操作。如果后端返回 401 或业务失败,错误提示要交给统一封装的 request 层去 toast,不要在页面里再写一遍错误处理。
utils/request.js 是全局命脉,token 注入和统一鉴权都在这层:
// utils/request.js const request = { baseUrl: 'https://your-domain.com/api', post(url, data) { return new Promise((resolve, reject) => { wx.request({ url: this.baseUrl + url, method: 'POST', data, header: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + wx.getStorageSync('token') }, success(res) { if (res.data.code === 0) { resolve(res.data.data) } else if (res.data.code === 401) { // 登录失效:清 token,统一跳登录页 wx.removeStorageSync('token') wx.navigateTo({ url: '/pages/login/login' }) } else { wx.showToast({ title: res.data.message, icon: 'none' }) reject(res.data) } }, fail: reject }) }) } }这里最重要的不是 request 本身,而是和 2.3 节 Result 约定的对应关系。凡是 code 为 401 就清 token 跳登录,凡是业务失败就统一 toast。后续新增页面时错误分支不用重写。
4.3 药品列表页:请求封装、token 注入与本地购物车
药品列表给店员看,重点不是花哨 UI,而是信息密度。药名、规格、价格、库存、是否处方药要一眼看到。列表页在 onLoad 时拉一次,前端按关键字过滤,减少无效请求;如果加了分页,就要配合 onReachBottom 触底追加。
<!-- pages/drugs/drugs.wxml --> <view class="drug-list"> <view class="drug-item" wx:for="{{drugs}}" wx:key="id"> <view class="drug-name">{{item.drugName}}</view> <view class="drug-spec">{{item.specification}} / {{item.unit}}</view> <view class="drug-price">¥{{item.price}}</view> <button size="mini" bindtap="addToCart">Page({ data: { drugs: [], cart: {} }, async onLoad() { const drugs = await request.get('/drugs?page=1&size=20') this.setData({ drugs }) }, addToCart(e) { const id = e.currentTarget.dataset.id // 合并同类项:同一药品只增加数量 const cart = { ...this.data.cart, [id]: { id, name: findDrug(id).drugName, price: findDrug(id).price, count: (this.data.cart[id]?.count || 0) + 1 } } this.setData({ cart }) wx.setStorageSync('cart', cart) } })这里有个容易翻车的点:不要把本地 price 当作最终价格传给后端。订单最终金额以后端查表为准,前端只传药品 id 和数量。如果前端把价格也传过去,后端又不校验,就可能出现有人改请求参数把价格改低的情况。
购物车本地状态的好处是响应快,店员点加购没有网络往返。但要注意清空时机,订单提交成功后必须清掉本地购物车,别把上一次的脏数据留到下一单。
5. 避坑与排查:从微信小程序 10002 报错到库存超卖
5.1 端口被占:Spring Boot 起不来时的第一眼排查
现象:控制台报 Port 8080 already in use,或者提示 Failed to bind to 0.0.0.0:8080。最常见原因是 IDEA 或终端里残留了上一次的 Java 进程没有退出。
排查和解决先看当前端口被谁占用:
# Linux / macOS lsof -i :8080 kill -9 <pid> # Windows netstat -ano | findstr :8080 taskkill /PID <pid> /F找到残留 PID 后杀掉,再重新启动。把 server.port 改成 8081 也能解决,但那是治标不治本,项目里如果还有其他依赖 8080 的配置,端口一改反而引入新问题。Spring Boot 修改端口号本身没有玄学,改配置重启即可。
5.2 小程序请求报错 10002:合法域名校验不过
现象:真机预览时 wx.request 直接失败,控制台提示 request:fail url not in domain list。在开发者工具里勾选“不校验合法域名”后能通,一上真机又挂。
原因:微信小程序正式环境要求请求域名必须加入小程序后台的 request 合法域名列表,同时要求 HTTPS,且证书链要被微信校验通过。错误码 10002 在登录这类接口上很常见,多半是域名证书不完整,微信判定为非法请求被拒。
解决:开发调试期可以在详情里勾选不校验合法域名,上线前必须到微信公众平台配置 request 合法域名,换成正式签发的 HTTPS 证书后重新预览。这也能解释为什么很多源码项目本地能用、换个手机就废——请求域名根本没有通过微信的合法域名校验。用抓包工具确认一次实际请求的 URL 和证书状态,比反复猜原因快得多。
5.3 MySQL 时区报错:驱动 8.x 的 serverTimezone 必填
现象:应用启动时数据源初始化直接抛异常,报错里出现 The server time zone value 或常见的 InvalidConnectionAttributeException。
原因:MySQL Connector/J 8.x 出于安全策略,强制要求明确指定客户端与服务器之间的时区换算标准,不填它就拒绝连接。这个报错在本地 MySQL 8 环境首次启动时必现。
解决:在 datasource 的 url 里加 serverTimezone=Asia/Shanghai,同时 JVM 时区、MySQL 时区保持一致性。改完还差 8 小时,说明连接字符串没生效,或者项目里引了旧版本驱动;把驱动统一换成 mysql-connector-j 8.0.33,不要再用老的 mysql-connector-java。
5.4 库存变负数:并发扣减的第一个血泪教训
现象:看起来完全没问题的下单代码,并发压测时库存出现 -2、-5。
原因:“先查库存,再 UPDATE 扣一”这两个操作之间存在窗口期。两个请求同时查到库存够用,于是都执行扣减,库存就穿过了零变成负数。
解决:改成单条原子 UPDATE,WHERE 条件带上 quantity >= #{quantity},用受影响行数判断成败,这就是 3.3 节的方案。不要用先查再改加 synchronized 的方式,分布式环境下 synchronized 只在单机有效。药店场景下并发量有限,单条 UPDATE 是成本最低、最可靠的做法;等以后量大了再考虑 Redis 预扣减或乐观锁,不必提前过度设计。
5.5 真机白屏、编译失败:小程序包超过 2MB 总限制
现象:我在一次交付时遇到 uniapp 导出的小程序包报 source size 2612kb exceed max limit 2mb,工程编译不过,真机直接白屏。
原因:图片、字体、大 JSON 数据、非必要库都塞在主包里,超出了小程序主包 2MB 上限。
解决:图片全部换到在线存储或 CDN,不要本地打包;继续超限,把商品详情、图表这类低频页面拆成分包。小程序 2MB 只是主包上限,整个项目还可以申请总共不超过 20MB 的分包额度。如果项目是 uniapp 打包出来的,还要在构建配置里去掉未引用的组件和工具库。压缩体积这件事放到功能跑通之后再做,别一开始就为了体积牺牲代码结构。
6. 交付前再补一脚:抓包验证、HTTPS 部署与下一步扩容
6.1 用抓包工具验证接口链路
本地联调看开发者工具的 Network 面板就够,真机环境就得用抓包工具。Charles 这类 PC 端抓包工具可以看到小程序发出的完整请求,重点确认三件事:baseUrl 是不是生产域名、token 有没有正确带在 Authorization 头、微信 jscode2session 那几次请求的返回值是不是正常。登录失败时抓包看一次返回的 errcode,能快速区分是 appid 配置错、secret 过期,还是域名证书问题,不用瞎猜。
6.2 部署:打 jar 包,配好 HTTPS 域名再发布
后端部署最省心的是 Maven 打成 jar,扔到云服务器上用 java -jar 直接运行。小程序发布之前,URL 必须是已备案、已配置的 HTTPS 域名,并且要加进微信后台合法域名列表。我在生产环境里会把 Nginx 绑好证书,把外部 HTTPS 流量转到本机 Spring Boot 端口,前端 baseUrl 写域名不带端口,这样以后切换服务器或者加多实例,小程序端不用改代码。证书到期是个容易忽略的定时雷,记得提前加日历提醒。
6.3 如果还要继续做:批次、过期预警与报表
现在这套系统的库存只有数量,而药店实际业务还有批次、生产日期、有效期这些维度。往上扩展的方向是给 tb_stock 加批次表,在 tb_order_item 里记录批次号,再做一个临期预警定时任务。报表模块按天汇总营业额、毛利、处方药占比,这些在现有四张表上增量扩展就能做,不用推翻重来。
我自己的习惯是交付前把 token 有效期、包体积、HTTPS 证书三个检查项放进验收清单,吃过演示现场翻车的亏之后,这三点我一次都不敢漏。小程序打包前先看一次主包体积,超出 2MB 就拆分包;上线前在微信群内测一周,让店员真实操作一个完整的买药流程,把所有报错截图统一处理。这套 Spring Boot 加微信小程序的药店系统,难点不在单个技术点,而在把登录、下单、扣库存这条链路完整闭环,过程中每一步能跑通、每个报错能定位,就足够支撑一家小药店的日常运营了。希望帮到你。
本文还有配套的精品资源,点击获取