微信小程序药店管理系统:SSM+MySQL后端设计与开发全解析
2026/9/19 14:59:44 网站建设 项目流程

简介:这是一份面向毕业设计撰写的《基于微信小程序的药店管理系统》论文文档,适合计算机、软件工程及相关专业学生作为课题参考。论文围绕药店管理小程序的开发全过程,从选题背景、研究现状与研究内容切入,详细介绍了微信开发者工具、JAVA技术、MySQL数据库、SSM框架等关键开发技术,并系统展开需求分析、可行性分析、界面设计与功能实现等核心章节。资源包共1个docx文档,大小3.16MB,包含中英文摘要、目录、绪论、开发工具介绍、系统分析等完整结构,能够帮助读者快速搭建论文框架;系统功能覆盖药品信息管理、库存监控、销售记录、用户反馈与数据分析等模块,对完成类似毕业设计或课程论文有直接借鉴价值。目前已有83人学习浏览,适合正在筹备药店管理或小程序方向毕业设计的同学参考学习。

1. 一个毕业设计题目的真实技术构成:药店小程序与 SSM 后端的边界

拿到“基于微信小程序的药店管理系统”这个题目,很多人的第一反应是“这不就是个 CRUD 小程序嘛”。但真把题目拆开看,你会发现它至少包含三条技术线:微信小程序端的页面与本地存储、Java 后端的 SSM 框架整合、MySQL 里那些带约束的订单和库存表。这三条线不是割裂的,而是靠一套接口协议串起来。这个资源的价值在于,它给的不是“能跑就行”的玩具代码,而是一套完整的管理员后台加用户端购物流程,覆盖了用户登录、药品分类展示、购物车、订单生成、用户充值、在线客服这些真实药店业务里的高频模块。适合两类人:一是需要快速完成毕业设计并讲清楚“为什么这么设计”的学生,二是想了解小程序+SSM+MySQL 三端如何分工协作的初级开发者。看懂这张图,你就知道论文目录里的“需求分析”和“数据库设计”到底对应代码里的什么东西。

2. 从需求到表结构:ER 模型与 MySQL 库表设计

2.1 为什么先定表结构再写接口

药店管理系统的后端基于 SSM 框架,MyBatis 负责 SQL 映射,数据库用的是 MySQL。开发这类系统时,我一般会先画 ER 图,再把实体关系转成数据表。论文里给出了用户信息、药品信息等实体的属性图,实际落地时,表结构比图更重要。因为后端的 Service 层所有业务逻辑,比如“用户下单后扣减库存”“管理员修改药品价格”,最终都要落到 SQL 上。表设计不合理,后面写接口时会反复改表,尤其是订单和购物车这类关联表,字段少一个都会导致联调失败。

2.2 核心业务表的字段与约束

参考文档里的表结构,yaopinxinxi(药品信息)是整个系统的核心表,它承接了药品的展示、搜索、加购和下单。以下字段在开发中必须关注:

字段名类型长度说明
yaopinbianhaovarchar255药品编号,业务上通常唯一
yaopinmingchengvarchar50药品名称
leibie / yaopinfenleivarchar50分类名称,冗余分类信息便于查询
tupianvarchar50图片路径,小程序端拼接域名访问
guigevarchar50规格,如“10片/盒”
baozhiqivarchar50保质期,注意这里不是 date 类型,是字符型
pricevarchar50价格,同样存成字符型,计算时需转换
onelimittimesvarchar50单次限购数量
alllimittimesvarchar50总限购数量

这里有个典型的毕业设计风格:很多字段都用varchar,连价格和限购数量也是。好处是前期插入数据方便,不用处理类型转换;坏处是写 SQL 做比较运算时容易出错。实际开发中,我会把price改成decimal(10,2),把onelimittimes改成int。但如果你要严格复现论文,保持原样也能跑通,只要在 Java 代码里做转换。

订单表orders和购物车表cart是关联最紧密的两张表。cart表记录了用户添加的药品、数量、单价和折扣价,字段tablename用来标识来源表,这是很多课程设计里常见的“万能购物车”写法。orders表则多出了orderidtotaldiscounttotalstatusaddress这几个字段,其中orderid是订单号,status控制订单状态流转,比如“待付款”“已发货”“已完成”。

2.3 用户与权限相关的表

系统分管理员和用户两个角色。理论上是基于 RBAC 设计角色权限,但实际表结构里做得比较简单:

  • users表:只有usernamepasswordrole三个字段,登录时按role区分跳转。
  • allusers表:这是 SSM 项目里常见的“管理员表”,字段包括usernamepwdcx(权限级别)。
  • yonghu表:用户表,存放用户姓名、性别等扩展信息。

这种设计在中小型毕业设计里很常见:管理员和用户分开两张表,各自维护登录凭据,没有复杂的权限表。如果你要优化,可以把usersyonghu合并,用role字段区分。但要注意,合并后users表需要增加yonghu表中的扩展字段,否则用户信息没法完整展示。

2.4 建表语句与 MyBatis 映射的对应关系

拿到.docx里的表结构,第一步不是直接写代码,而是把表定义转成实际的 DDL。这里给出yaopinxinxi表的建表语句,注意字符集和引擎:

CREATE TABLE `yaopinxinxi` ( `id` int(11) NOT NULL AUTO_INCREMENT, `yaopinbianhao` varchar(255) DEFAULT NULL, `yaopinmingcheng` varchar(50) DEFAULT NULL, `leibie` varchar(50) DEFAULT NULL, `yaopinfenlei` varchar(50) DEFAULT NULL, `tupian` varchar(50) DEFAULT NULL, `guige` varchar(50) DEFAULT NULL, `baozhiqi` varchar(50) DEFAULT NULL, `changjia` varchar(50) DEFAULT NULL, `yaopinxiangqing` varchar(50) DEFAULT NULL, `price` varchar(50) DEFAULT NULL, `onelimittimes` varchar(50) DEFAULT NULL, `alllimittimes` varchar(50) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

为什么用InnoDB?因为订单、购物车、余额充值这些操作涉及事务,MyBatis 的@Transactional注解依赖 InnoDB 的行级锁。如果是MyISAM,事务会静默失效,下单时并发扣库存就会出现超卖。utf8mb4也是必须的,药品名称里如果有特殊符号(比如 ℃),utf8会报错。

allusers表和users表的区别需要特别说明。allusers是管理员登录用的,通常在后台系统里存在,users是前台用户登录用的。论文里的“管理员”对应allusers,而“用户”对应usersyonghu。写登录接口时,要分别查询这两张表,根据用户输入的账号类型决定走哪条查询。

3. 小程序端与后端接口协作:从登录流程到购物车实现

3.1 微信小程序的目录结构与请求封装

微信开发者工具创建的小程序项目,目录结构必须包含pagesutilsapp.jsapp.jsonapp.wxss。在药店管理系统里,前端页面主要是pages/index(首页)、pages/yaopinxinxi(药品列表)、pages/cart(购物车)、pages/orders(订单)、pages/mine(个人中心)。这些页面通过wx.request与后端交互。

每次写wx.request都带urlmethodheader很麻烦,我习惯在utils/request.js里封装一个公共请求方法:

const BASE_URL = 'http://localhost:8080/pharmacy'; // 后端接口根路径 function request(path, method = 'GET', data = {}) { return new Promise((resolve, reject) => { wx.request({ url: BASE_URL + path, method: method, data: data, header: { 'Content-Type': 'application/json' }, success: (res) => { if (res.statusCode === 200) { resolve(res.data); } else { wx.showToast({ title: '请求失败', icon: 'none' }); reject(res); } }, fail: (err) => { wx.showToast({ title: '网络异常', icon: 'none' }); reject(err); } }); }); } module.exports = { request, BASE_URL };

这段代码做的事情很简单:统一管理接口根路径,把wx.request的回调风格改造成 Promise 风格。后面页面里调用request('/yaopinxinxi/list', 'GET', { page: 1 })就能拿到药品列表。注意BASE_URL在真机调试时要改成局域网 IP,不能写localhost,因为手机访问的localhost是手机自己。

3.2 登录流程:账号 password 与角色判断

论文里画了用户登录流程图,实际实现时,小程序端先通过wx.login获取临时code,但毕业设计里通常不会用微信官方登录,而是用账号密码登录。用户输入用户名和密码,前端调用登录接口:

// pages/login/login.js Page({ data: { username: '', password: '', role: 'users' // 默认用户角色 }, async handleLogin() { const { username, password, role } = this.data; if (!username || !password) { wx.showToast({ title: '请输入账号和密码', icon: 'none' }); return; } const res = await request('/login', 'POST', { username, password, role }); if (res.code === 0) { wx.setStorageSync('token', res.data.token); wx.setStorageSync('userInfo', res.data.userInfo); wx.switchTab({ url: '/pages/index/index' }); } else { wx.showToast({ title: res.msg || '登录失败', icon: 'none' }); } } });

后端对应的 Controller 逻辑是:根据role参数决定查询users表还是allusers表,若密码匹配,生成一个简单的会话标识。项目里没有引入 Redis 做会话共享,常见的做法是返回用户 ID 和用户名,前端存到本地。这里有个容易忽视的坑:password字段在数据库里如果明文存储,登录接口查询时直接比对即可;如果做了 MD5,前端提交时也要先加密。论文里没有提加密,实际答辩时如果老师问安全性,要能说出“后续可以引入 MD5 加盐”。

3.3 药品列表与分类筛选:union 查询还是冗余字段

首页要展示药品信息和分类。前端从yaopinfenlei表获取分类列表,再从yaopinxinxi表按分类字段筛选。后端接口可以这样写:

@RestController @RequestMapping("/yaopinxinxi") public class YaopinxinxiController { @Autowired private YaopinxinxiService yaopinxinxiService; @GetMapping("/list") public Map<String, Object> list( @RequestParam(value = "fenlei", required = false) String fenlei, @RequestParam(value = "page", defaultValue = "1") Integer page, @RequestParam(value = "limit", defaultValue = "10") Integer limit) { Map<String, Object> result = new HashMap<>(); List<Yaopinxinxi> list = yaopinxinxiService.queryList(fenlei, page, limit); int total = yaopinxinxiService.count(fenlei); result.put("code", 0); result.put("data", list); result.put("total", total); return result; } }

对应的 MyBatis Mapper XML 里,如果fenlei为空,就查全部;不为空就加WHERE yaopinfenlei = #{fenlei}。因为yaopinfenlei字段在设计时冗余了分类名称,所以不需要 JOIN 分类表,这在大数据量下会有冗余问题,但毕业设计的药品数据量通常只有几十条,查询效率完全可以接受。如果你想要规范化,可以把yaopinfenlei改成分类 ID,再关联yaopinfenlei表查分类名,但这会多一次关联查询,前端也要多处理一层数据转换。

3.4 购物车:前端本地存储还是后端数据库

购物车数据存在哪里,是这类系统的一个分水岭。论文里设计了cart表,说明购物车是存后端的。用户点击“加入购物车”时,小程序端向/cart/add发送 POST 请求,参数包含useridgoodidgoodnamepicturebuynumberpricediscountprice。后端先判断该用户购物车里是否已有同一药品,有则更新数量,没有则插入新记录。

这里有个细节:goodid是药品在yaopinxinxi表中的id,但前端拿到的goodid可能来自列表页的id字段。如果列表页的id是字符串,后端插入时注意类型转换。购物车列表接口返回的数据要按id倒序,保证用户后加的商品排在前面。

<select id="queryCartByUserId" resultType="map"> SELECT id, goodid, goodname, picture, buynumber, price, discountprice FROM cart WHERE userid = #{userid} ORDER BY id DESC </select>

为什么不直接存小程序本地wx.setStorageSync?因为用户如果更换设备,本地购物车就丢了。存后端的好处是管理员和用户都能看到购物车数据,后续订单管理也能直接关联。坏处是每次加购都要请求后端,网络差的时候体验不好。实际生产系统会做“本地缓存+后端同步”,毕业设计论文里不需要写这么复杂。

4. 订单与库存的关键约束:并发扣减与事务处理

4.1 从购物车生成订单的事务边界

用户提交订单时,后端要做三件事:读取购物车中的商品、生成订单记录、清空购物车。这三步必须在一个事务里完成,否则会出现“订单生成了但购物车没清空”或“购物车清了但订单没写成”的脏数据。SSM 框架下,使用@Transactional注解:

@Service public class OrderServiceImpl implements OrderService { @Autowired private CartMapper cartMapper; @Autowired private OrdersMapper ordersMapper; @Transactional(rollbackFor = Exception.class) public void createOrder(Long userId, String address) { // 1. 查询购物车列表 List<Cart> cartList = cartMapper.selectByUserId(userId); if (cartList.isEmpty()) { throw new RuntimeException("购物车为空"); } // 2. 计算总金额 BigDecimal total = BigDecimal.ZERO; for (Cart cart : cartList) { total = total.add(new BigDecimal(cart.getPrice()).multiply(new BigDecimal(cart.getBuynumber()))); } // 3. 插入订单主表 Orders order = new Orders(); order.setOrderid("PH" + System.currentTimeMillis()); order.setUserid(String.valueOf(userId)); order.setTotal(total.toString()); order.setStatus("待付款"); order.setAddress(address); ordersMapper.insert(order); // 4. 清空购物车 cartMapper.deleteByUserId(userId); } }

rollbackFor = Exception.class很关键。默认情况下,Spring 只对 RuntimeException 回滚,如果步骤 4 抛出受检异常,事务不会回滚,就会留下一个没有订单明细的订单记录。另外,new BigDecimal(cart.getPrice())之前最好做空值校验,因为表里pricevarchar,可能为null

4.2 库存扣减:先查再扣还是原子更新

药店管理系统的库存体现在onelimittimes(单次限购)和alllimittimes(总限购)上,但实际下单时不一定会严格校验这两个字段。如果你想把这个项目做成有亮点的版本,可以在下订单时增加库存扣减逻辑。常见的错误写法是:

// 错误示例:先查询库存,判断够不够,再扣减 int stock = yaopinxinxiMapper.selectStock(goodId); if (stock >= buyNumber) { yaopinxinxiMapper.reduceStock(goodId, buyNumber); }

这个写法在单机低并发下没问题,但如果有两个用户同时提交订单,两个请求都读到库存是 10,都判断可以扣,最后库存变成 -2。正确的做法是用一行 SQL 原子扣减:

<update id="reduceStock"> UPDATE yaopinxinxi SET alllimittimes = alllimittimes - #{buyNumber} WHERE yaopinbianhao = #{goodId} AND alllimittimes >= #{buyNumber} </update>

执行这个 update 后,通过返回的受影响行数判断是否扣减成功。如果返回 0,说明库存不足,事务直接回滚。这种写法依赖 MySQL 的行锁保证同一时间的多个更新请求串行执行,避免了超卖问题。

4.3 订单状态机与支付回调模拟

论文里没有接入真实微信支付,订单状态一般由管理员手动修改。前端“我的订单”页面展示订单列表,用户点击“确认收货”时,前端调用/orders/updateStatus,后端把status从“已发货”改成“已完成”。这里需要注意状态流转的合法性,比如“待付款”的订单不能直接改成“已完成”。后端可以写一个简单的校验:

switch (targetStatus) { case "已发货": if (!"待付款".equals(currentStatus)) throw new RuntimeException("当前状态不可发货"); break; case "已完成": if (!"已发货".equals(currentStatus)) throw new RuntimeException("当前状态不可确认收货"); break; default: throw new RuntimeException("非法状态"); }

如果是模拟支付,可以添加一个“模拟支付”按钮,把订单状态从“待付款”改为“待发货”。这与真实微信支付的差别在于没有回调验签,但毕业设计答辩时,说清楚“这里是模拟支付,生产环境需要接入微信支付 API 并验证回调签名”就足够了。

4.4 高并发场景下的乐观锁兜底

如果你的论文需要体现“性能分析”,可以提到并发控制。除了上面说的原子更新,还可以在yaopinxinxi表加一个version字段,每次更新时带上版本号:

UPDATE yaopinxinxi SET alllimittimes = alllimittimes - #{buyNumber}, version = version + 1 WHERE yaopinbianhao = #{goodId} AND version = #{version}

如果更新影响行数为 0,说明版本号已被别的请求修改,重新读取后再尝试。乐观锁在商品表上通常够用,因为单个药品的并发写不会太高。如果要支撑秒杀类场景,就需要 Redis 预扣库存了,那超出药店管理系统的范围。

5. 把整个流程跑通:微信开发者工具联调、真机预览与常见坑

5.1 本地联调的域名白名单问题

用微信开发者工具调试时,默认会校验合法域名。如果你的后端是本地http://localhost:8080,需要点击开发者工具右上角“详情”->“本地设置”,勾选“不校验合法域名、web-view(业务域名)、TLS 版本以及 HTTPS 证书”。否则wx.request会报url not in domain list。这个选项只对当前项目生效,换一台电脑重新导入项目时要再勾一次。

5.2 真机预览时的 IP 替换

开发者工具里能跑通,换到真机就请求失败,90% 是BASE_URL的问题。手机和电脑必须处于同一局域网,然后把BASE_URL改成电脑的局域网 IP。在微信开发者工具的控制台输入wx.getSystemInfoSync(),看不到电脑 IP,你需要在命令行执行ipconfig(Windows)或ifconfig(macOS)查询。还有一点,后端接口不能被防火墙拦截,Windows 开发机最好临时关闭防火墙,或者给 8080 端口加放行规则。

5.3 后端返回时间字段的坑

orders表如果通过OrderServiceImpl直接插入当前时间,一般用NOW(),没问题。但如果用 Java 的Date类型传给前端,小程序端看到的可能是2025-06-01T12:00:00.000+00:00这种格式。处理方式是利用 SpringMVC 的@JsonFormat

@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") private Date createtime;

timezone必须写成GMT+8,否则会少了 8 个小时。如果你不想改实体类,也可以在前端用正则裁剪字符串,但格式问题最好在后端一次性解决。

5.4 模拟数据与真实数据的切换

项目里通常有一套sql脚本,里面有管理员账号admin、用户账号user和若干测试药品。建议在application.yml里配置两个环境,开发时用application-dev.yml连本地库,部署时用application-prod.yml换远端库。切换环境时,只需要改spring.profiles.active=dev这一行。最怕的是把测试数据当成真实数据,价格带一堆小数点,或者过期日期是 2020 年。清理数据可以用下面的 SQL:

DELETE FROM cart; DELETE FROM orders; DELETE FROM storeup; UPDATE yaopinxinxi SET alllimittimes = 100, onelimittimes = 10;

这样每次测试下单,库存都是从 100 开始扣,订单和购物车不会越积越多。

5.5 验证整个系统是否达标的检查清单

联调完成后,按照下面的顺序手动检查一遍,可以帮你发现八成的问题:

  • 新用户注册后,用yonghu表里的账号登录,跳转到用户首页,而不是管理员首页。
  • 管理员在后台新增药品,小程序首页下拉刷新后能看到新药品,图片能正常显示。
  • 用户将药品加入购物车,修改数量,提交订单,订单状态变为“待付款”。
  • 管理员后台看到新订单,修改状态为“已发货”,用户端“我的订单”页面同步变为“已发货”。
  • 重复提交同一个药品到购物车,数量不会变成两条记录。
  • 清空购物车后,cart表里该用户的记录数为 0。
  • 断网时点击加购,微信客户端有“网络异常”提示,不会白屏。

把以上步骤在论文的测试章节里写成测试用例,再补一张测试结果表,比空写“系统运行正常”要有说服力得多。最后提醒一句,storeup表是收藏功能用的,功能模块图里的“我的收藏管理”对应它,别在测试时把它忘了。

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

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

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

立即咨询