拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

基于微信小程序的药店管理系统开发实战

基于微信小程序的药店管理系统开发实战

简介:本资源是一份面向计算机专业本科生的毕业设计论文,聚焦微信小程序在传统药店管理场景中的落地实践,解决药店日常运营中药品信息管理、库存监控、销售记录与用户反馈等核心需求。全文采用Java语言开发,后端基于SSM框架与MySQL数据库,前端依托微信小程序原生开发规范,完整呈现了从需求分析、界面设计、功能实现到测试优化的全流程技术方案。资源为单个3.16MB的Word文档(.docx),内容涵盖摘要、英文摘要、目录、绪论、关键技术介绍(含微信开发者工具、小程序框架、JAVA及Mysql)、系统分析与设计等章节,结构规范,适合作为毕设参考范本或课程设计拓展学习。目前已有83人下载学习,读者可直接获取完整论文框架、技术选型依据、模块功能说明及典型实现思路,助力快速掌握小程序+Java全栈开发在垂直行业应用中的设计逻辑与写作规范。

1. 为什么毕业设计选「基于微信小程序的药店管理系统」不是凑数,而是稳扎稳打的实战练兵?

很多同学看到“毕业设计”四个字就本能想绕开——怕需求模糊、怕技术堆砌、怕上线不了、怕答辩被问住。但如果你手头真有一个能跑通、能演示、能查数据库、能扫码入库、能按会员折扣结算的「基于微信小程序的药店管理系统」,它就不是Word文档里空转的流程图,而是你简历上可截图、可录屏、可现场调接口的硬通货。这个系统天然卡在三个高价值交汇点:微信生态的真实用户触达能力(无需下载App)、药店业务的强规则性(进销存+处方+会员+效期)和Java后端工程化落地的完整闭环(SSM + MySQL + 小程序云开发/自建API)。它不追求AI大模型或高并发秒杀,却把CRUD、权限隔离、库存扣减一致性、药品效期预警、微信登录态透传这些企业级开发中天天踩的坑,全打包塞进一个2000行代码+3张核心表+5个小程序页面的最小可行系统里。适合Java初学者用SSM搭骨架,也适合想补全“前后端联调+真机调试+数据校验”链条的应届生——因为药店场景足够具体:退药要审核、处方药要拍照、近效期要标红、会员积分要实时折算,每一条都是面试官爱问的“你怎么保证这个逻辑不翻车”。


2. 从零搭起:SSM后端服务如何支撑药店核心业务流

药店管理系统的业务骨架,本质是围绕“药”和“人”两条线展开:药有品类、规格、厂家、批号、效期、库存;人有顾客(普通/会员/医生)、员工(店员/药师/管理员)。SSM(Spring + SpringMVC + MyBatis)作为Java Web经典组合,不是为了复古,而是因为它把事务控制、SQL映射、请求路由这些关键能力都暴露在明处,方便你亲手拧紧每一颗螺丝。下面以最常被忽略但最易翻车的「药品入库+库存同步」为例,拆解SSM层怎么写才不埋雷。

2.1 数据库设计:三张表撑起药店主干,别急着加字段

药店系统不是ERP,初期聚焦「进→存→销→退」主链路。我建议先建这三张表,字段宁少勿滥,后期再按需扩展:

表名核心字段(带业务含义)关键约束说明
drug_info(药品主档)drug_id(PK),name,spec,manufacturer,approval_no(批准文号),valid_days(有效期天数)approval_no加唯一索引,避免同药不同批号重复录入
drug_stock(库存表)stock_id(PK),drug_id(FK),batch_no,produce_date,valid_date,quantity,lock_quantity(锁定库存)valid_date必须为DATE类型,禁止存字符串;lock_quantity用于防止超卖,后续扣减逻辑依赖它
user_account(用户账户)user_id(PK),openid(微信唯一标识),phone,level(0=普通,1=会员,2=药师),points(积分)openid加唯一索引,微信登录直接查;level用tinyint而非varchar,避免字符串比对

提示:MySQL建表时务必显式指定字符集CHARSET=utf8mb4和排序规则COLLATE=utf8mb4_unicode_ci,否则微信昵称里的emoji(如💊)会存成乱码或报错。

2.2 SSM层关键代码:库存扣减必须带事务+版本号校验

小程序端点击“销售”按钮,后端要完成:查库存 → 判效期 → 扣库存 → 记销售流水 → 更新会员积分。这5步必须原子执行,否则出现“扣了库存但没记流水”的黑匣子。MyBatis XML写法比注解更可控,尤其涉及多表更新时:

<!-- DrugStockMapper.xml --> <update id="deductStockWithVersion" parameterType="map"> UPDATE drug_stock SET quantity = quantity - #{quantity}, lock_quantity = lock_quantity - #{quantity} WHERE stock_id = #{stockId} AND quantity >= #{quantity} <!-- 库存充足校验 --> AND valid_date >= CURDATE() <!-- 效期未过期 --> AND version = #{version} <!-- 乐观锁版本号 --> </update>

对应Java Service方法:

@Transactional(rollbackFor = Exception.class) public boolean sellDrug(Long stockId, Integer quantity, String openid) { // 1. 先查当前库存和版本号 DrugStock stock = drugStockMapper.selectById(stockId); if (stock == null || stock.getQuantity() < quantity || stock.getValidDate().before(new Date())) { throw new BusinessException("库存不足或药品已过期"); } // 2. 构造参数,带版本号更新(version字段需在表中存在) Map<String, Object> params = new HashMap<>(); params.put("stockId", stockId); params.put("quantity", quantity); params.put("version", stock.getVersion()); // 关键!防止ABA问题 int updated = drugStockMapper.deductStockWithVersion(params); if (updated != 1) { throw new BusinessException("库存已被其他操作修改,请刷新重试"); } // 3. 记销售流水(省略) // 4. 更新会员积分(省略) return true; }

为什么用乐观锁不用悲观锁?
药店场景并发不高(单店日销百单),但库存扣减必须强一致。SELECT ... FOR UPDATE在高并发下易锁表,而乐观锁把冲突检测交给DB层面,失败时前端友好提示“请重试”,体验更轻量。血泪经验:version字段必须初始化为0,且每次成功更新后DB自动version = version + 1(MyBatis-Plus可配,原生MyBatis需手动在UPDATE语句里写version = version + 1)。

2.3 SpringMVC控制器:微信登录态如何安全透传到后端

小程序端调用wx.login()获取code,传给后端/api/login接口。绝对不要在小程序端直接调用微信登录接口(暴露appid和secret),必须走自己后端中转:

@RestController @RequestMapping("/api") public class AuthController { @PostMapping("/login") public Result<Map<String, Object>> wxLogin(@RequestBody WxLoginRequest request) { // 1. 向微信服务器换取session_key和openid String url = "https://api.weixin.qq.com/sns/jscode2session?" + "appid=" + WX_APPID + "&secret=" + WX_SECRET + "&js_code=" + request.getCode() + "&grant_type=authorization_code"; String response = HttpUtil.get(url); // 使用HttpClient或OkHttp JSONObject json = JSON.parseObject(response); if (json.containsKey("errcode")) { return Result.fail("微信登录失败: " + json.getString("errmsg")); } String openid = json.getString("openid"); String sessionKey = json.getString("session_key"); // 2. 根据openid查或创建用户,生成自己的token(JWT) UserAccount user = userAccountMapper.selectByOpenid(openid); if (user == null) { user = new UserAccount(); user.setOpenid(openid); user.setLevel(0); user.setPoints(0); userAccountMapper.insert(user); } String token = JwtUtil.generateToken(user.getUserId(), openid); Map<String, Object> data = new HashMap<>(); data.put("token", token); data.put("userInfo", user); return Result.success(data); } }

关键参数说明:

  • WX_APPID和WX_SECRET必须配置在application.yml中,严禁硬编码;
  • JwtUtil.generateToken()生成的token需设7天过期,小程序端存入wx.setStorageSync('token', token);
  • 后续所有API请求头必须带Authorization: Bearer {token},Spring Security或自定义拦截器校验。

3. 小程序端:如何让药店业务逻辑在微信环境里不掉链子

微信小程序不是网页,它的运行机制(双线程、WXML编译、setData异步)决定了很多Web开发习惯在这里会翻车。药店系统里最典型的三个“玄学”问题:扫码识别药品批号后页面不刷新、会员折扣计算结果错位、库存变化后列表滚动位置丢失——根源都在数据驱动和生命周期上。

3.1 页面结构:用WXML+WXSS实现药店专属UI,避开Flex布局陷阱

药店首页需要展示:搜索框、轮播图(促销药品)、分类导航(OTC/处方药/医疗器械)、热销药品列表。很多人用flex-direction: column堆叠,结果在iOS上滚动卡顿。正确做法是用小程序原生组件scroll-view包裹列表,并禁用其内部滚动:

<!-- index.wxml --> <view class="container"> <view class="search-bar"> <input bindinput="onSearchInput" placeholder="搜索药品名称或批号..." /> </view> <scroll-view scroll-y="true" style="height: calc(100vh - 120rpx);" bindscrolltolower="loadMore"> <!-- 轮播图 --> <swiper indicator-dots autoplay interval="3000"> <swiper-item wx:for="{{banners}}" wx:key="id"> <image src="{{item.imgUrl}}" mode="aspectFill" /> </swiper-item> </swiper> <!-- 分类导航 --> <view class="category-list"> <view wx:for="{{categories}}" wx:key="id" class="category-item" bindtap="switchCategory">/* index.wxss */ .container { height: 100vh; } .search-bar { padding: 20rpx; background: #f8f9fa; } .drug-list { padding: 0 20rpx 20rpx; } .drug-card { display: flex; padding: 20rpx 0; border-bottom: 1rpx solid #eee; } .drug-card image { width: 120rpx; height: 120rpx; margin-right: 20rpx; border-radius: 8rpx; } .drug-info { flex: 1; } .name { font-weight: bold; font-size: 32rpx; color: #333; } .spec { font-size: 26rpx; color: #666; margin-top: 10rpx; } .price { font-size: 36rpx; color: #e64340; margin-top: 10rpx; } .expired { color: #e64340 !important; font-weight: bold; }

为什么不用纯Flex布局?
小程序渲染层对flex: 1的计算在低端安卓机上存在兼容性问题,导致卡片高度塌陷。scroll-view是微信官方推荐的长列表容器,且支持bindscrolltolower懒加载,比监听页面滚动事件更稳定。

3.2 业务逻辑:扫码入库与处方药核验的双重校验链

药店核心风控点:非处方药扫码即入库,处方药必须上传处方照片并经药师审核。小程序端不能只做UI,得把校验逻辑前置:

// pages/scan/scan.js Page({ data: { scanResult: '', isPrescription: false, prescriptionImage: '' }, onScanCode() { wx.scanCode({ success: (res) => { const code = res.result; this.setData({ scanResult: code }); // 1. 调后端接口查药品是否为处方药 wx.request({ url: 'https://your-api.com/api/drug/check-type', method: 'POST', data: { barcode: code }, success: (res) => { if (res.data.data.isPrescription) { this.setData({ isPrescription: true }); wx.showToast({ title: '此为处方药,请上传处方', icon: 'none' }); } else { this.handleNormalInbound(code); // 直接入库 } } }); } }); }, choosePrescriptionImage() { wx.chooseMedia({ count: 1, mediaType: ['image'], sourceType: ['album', 'camera'], success: (res) => { const tempFile = res.tempFiles[0]; this.uploadImage(tempFile.tempFilePath); } }); }, uploadImage(filePath) { const uploadTask = wx.uploadFile({ url: 'https://your-api.com/api/upload/prescription', filePath: filePath, name: 'file', formData: { 'stockId': this.data.stockId, 'openid': getApp().globalData.openid }, success: (res) => { wx.showToast({ title: '处方已提交,等待药师审核' }); setTimeout(() => wx.navigateBack(), 1500); } }); } });

关键细节:

  • wx.scanCode()返回的res.result是条码原始字符串,需和服务端约定统一编码(如EAN-13),避免扫描枪扫出乱码;
  • 处方图片上传必须带stockId和openid,后端才能关联到具体药品和申请人;
  • wx.chooseMedia替代已废弃的wx.chooseImage,支持更高清图片,适配iOS 15+。

3.3 状态管理:用小程序Storage+内存缓存平衡性能与一致性

小程序没有Vuex或Redux,但药店场景需要频繁读取会员等级、积分、购物车。全靠API请求会卡顿,全放内存又易丢失。我的做法是分层缓存:

数据类型存储位置更新时机过期策略
用户基础信息(openid/level/points)wx.setStorageSync('userInfo', data)登录成功后写入,每次进入首页读取无过期,退出登录时清除
药品分类列表(10个以内)内存对象this.data.categoriesonLoad时请求一次,存入data页面销毁时自动释放
购物车(临时选品)wx.setStorageSync('cartItems', items)每次添加/删除立即写入用户结算后清空
// utils/cart.js class Cart { static getCart() { const cartStr = wx.getStorageSync('cartItems'); return cartStr ? JSON.parse(cartStr) : []; } static addToCart(drug) { const cart = this.getCart(); const exist = cart.find(item => item.stockId === drug.stockId); if (exist) { exist.quantity += 1; } else { cart.push({ ...drug, quantity: 1 }); } wx.setStorageSync('cartItems', JSON.stringify(cart)); } static clearCart() { wx.removeStorageSync('cartItems'); } } module.exports = Cart;

为什么不用云开发数据库存购物车?
云开发免费额度有限,且购物车是强时效性数据(用户可能加了又删),本地Storage响应更快,也避免因网络波动导致加购失败。


4. 避坑指南:药店管理系统开发中90%的人踩过的5个深坑

做毕业设计最怕的不是不会写,而是写了才发现逻辑错、数据丢、上线崩。以下是我在带学生做药店系统时,高频出现的5个真实翻车点,按「现象→原因→解决」给出可立即抄作业的方案。

4.1 现象:小程序扫码入库后,后台MySQL库存没变,但日志显示SQL执行成功

原因:MyBatis的update方法返回值是影响行数,不是布尔值。如果WHERE条件不匹配(如stock_id不存在或quantity不足),updated返回0,但代码没判断就当成功处理。
解决:所有UPDATE操作后必须检查返回值,且日志打印完整SQL和参数:

int updated = drugStockMapper.deductStock(params); if (updated != 1) { log.warn("库存扣减失败,stockId={}, expected=1, actual={}", params.get("stockId"), updated); throw new BusinessException("库存扣减失败,请重试"); }

4.2 现象:会员折扣计算错误,比如原价100元,8折后显示99.999999999元

原因:JavaScript浮点数运算精度丢失(0.1+0.2≠0.3),小程序端直接price * 0.8导致。
解决:价格相关计算一律用整数单位(分)处理:

// 正确:价格存为分,运算用整数 const priceInCents = 10000; // 10000分 = 100元 const discount = 20; // 20%折扣 const finalPrice = Math.round(priceInCents * (100 - discount) / 100); // 8000分 // 显示时转回元:(finalPrice / 100).toFixed(2) → "80.00"

4.3 现象:微信登录后,用户信息能查到,但再次进入页面时wx.getStorageSync('token')为空

原因:小程序onLaunch生命周期里异步请求token,但onShow时页面已渲染,getStorageSync还没写入。
解决:在app.js的onLaunch中用Promise封装登录逻辑,并全局挂载:

// app.js App({ globalData: { userInfo: null, token: '' }, onLaunch() { return this.login().then(res => { this.globalData.token = res.data.token; wx.setStorageSync('token', res.data.token); }); }, login() { return new Promise((resolve, reject) => { wx.login({ success: res => { wx.request({ url: '/api/login', data: { code: res.code }, success: resolve, fail: reject }); } }); }); } });

页面中通过getApp().globalData.token安全读取。

4.4 现象:药品效期预警失效,明明已过期,列表里还显示“30天后过期”

原因:MySQL的CURDATE()返回的是服务端日期,如果服务器时区是UTC而你的业务在中国,valid_date >= CURDATE()会漏判。
解决:统一用东八区时间,MySQL启动参数加--default-time-zone='+08:00',或SQL里用CONVERT_TZ(CURDATE(), '+00:00', '+08:00')。更稳妥的是后端Java层计算:

// Java中计算剩余天数 long days = ChronoUnit.DAYS.between(LocalDate.now(), stock.getValidDate().toLocalDate());

然后把days传给小程序,避免时区依赖。

4.5 现象:提交处方后,后台收到的图片文件大小为0KB,或格式为temp_*.jpg无法识别

原因:wx.uploadFile的filePath必须是临时路径(tempFilePath),但有些同学误传了path(沙箱路径)。
解决:严格使用res.tempFiles[0].tempFilePath:

wx.chooseMedia({ success: (res) => { const tempPath = res.tempFiles[0].tempFilePath; // ✅ 正确 // const wrongPath = res.tempFiles[0].path; // ❌ 错误,沙箱路径不可上传 this.uploadImage(tempPath); } });

5. 进阶验证:用三类真实数据跑通全流程,比写10页文档更有说服力

毕业设计答辩时,老师最想看的不是你画了多少UML图,而是系统能不能在真实场景下扛住一次完整的购药流程。我建议用以下三类数据做闭环验证,每类都附可执行的SQL和小程序操作步骤,确保你能当面演示不卡壳。

5.1 场景一:新用户首次购药(含微信登录+会员注册+扫码支付)

目标:验证从零开始的用户旅程是否丝滑。
准备数据(MySQL执行):

-- 插入测试药品(非处方药) INSERT INTO drug_info (name, spec, manufacturer, approval_no, valid_days) VALUES ('阿司匹林肠溶片', '100mg*30片', '华润双鹤', '国药准字H11020001', 1095); -- 插入对应库存(批号20240101,生产日期2024-01-01,效期3年) INSERT INTO drug_stock (drug_id, batch_no, produce_date, valid_date, quantity, lock_quantity, version) SELECT id, '20240101', '2024-01-01', DATE_ADD('2024-01-01', INTERVAL 1095 DAY), 100, 0, 0 FROM drug_info WHERE approval_no = '国药准字H11020001';

小程序操作链路:

  1. 清除微信开发者工具缓存 → 打开小程序首页;
  2. 点击“微信登录” → 授权 → 自动跳转至首页;
  3. 扫码输入20240101(模拟药品条码)→ 显示药品详情页 → 点击“加入购物车”;
  4. 进入购物车 → 点击“去结算” → 选择“微信支付” → 模拟支付成功;
  5. 查看MySQLsales_record表,确认新增一条记录,status=1(已支付),quantity=1,total_amount=12.50(假设单价)。

验证要点:

  • 登录后user_account表是否新增一行,level=0;
  • 支付成功后drug_stock.quantity是否减1;
  • sales_record的created_time是否为当前时间(非MySQL服务器时间)。

5.2 场景二:药师审核处方药(含图片上传+状态流转)

目标:验证强管控业务流程是否闭环。
准备数据(MySQL执行):

-- 插入处方药 INSERT INTO drug_info (name, spec, manufacturer, approval_no, valid_days, is_prescription) VALUES ('阿莫西林胶囊', '0.25g*24粒', '哈药集团', '国药准字H23020001', 1095, 1); -- 插入库存 INSERT INTO drug_stock (drug_id, batch_no, produce_date, valid_date, quantity, lock_quantity, version) SELECT id, '20240201', '2024-02-01', DATE_ADD('2024-02-01', INTERVAL 1095 DAY), 50, 0, 0 FROM drug_info WHERE approval_no = '国药准字H23020001'; -- 插入待审核处方记录(模拟小程序上传后) INSERT INTO prescription_apply (stock_id, openid, status, apply_time) VALUES (LAST_INSERT_ID(), 'test_openid_123', 0, NOW()); -- status=0:待审核

小程序操作链路:

  1. 用另一台手机(或清除缓存)登录,扫码20240201→ 提示“处方药,请上传处方”;
  2. 选择一张白纸照片上传 → 提交;
  3. 后台管理员登录PC端(可简单用Thymeleaf写个审核页),找到该申请 → 点击“通过”;
  4. 小程序端下拉刷新首页 → 查看该药品库存是否增加(因审核通过后才允许销售,此处应不变),但prescription_apply.status变为1。

验证要点:

  • prescription_apply表status是否从0变为1;
  • 审核通过后,该药品是否能在小程序搜索中显示(需后端API加AND (is_prescription=0 OR p.status=1)关联);
  • 若审核拒绝,status=2,小程序端应提示“处方审核未通过”。

5.3 场景三:库存预警与自动归档(用脚本模拟时间流逝)

目标:验证系统能否主动发现业务风险。
准备数据(MySQL执行):

-- 插入一批近效期药品(距今30天内过期) INSERT INTO drug_stock (drug_id, batch_no, produce_date, valid_date, quantity, lock_quantity, version) SELECT d.id, CONCAT('NEAR_', d.id), '2023-01-01', DATE_SUB(NOW(), INTERVAL 10 DAY), 10, 0, 0 FROM drug_info d LIMIT 3;

自动化验证脚本(Java定时任务):

@Component public class StockWarningJob { @Scheduled(cron = "0 0 9 * * ?") // 每天上午9点执行 public void checkNearExpiry() { List<DrugStock> nearExpiry = drugStockMapper.selectNearExpiry(30); // 查30天内过期 if (!nearExpiry.isEmpty()) { // 发送企业微信通知(或写入warning_log表) warningLogMapper.insertBatch(nearExpiry.stream() .map(s -> new WarningLog(s.getStockId(), "近效期预警", s.getValidDate().toString())) .collect(Collectors.toList())); // 同时更新药品状态为“近效期”(供小程序首页标红) nearExpiry.forEach(s -> { s.setStatus(2); // 2=近效期 drugStockMapper.updateById(s); }); } } }

小程序验证:

  • 打开首页 → 查看药品列表 → 所有valid_date在30天内的药品,valid-date文字是否为红色;
  • 进入“库存管理”页(管理员权限)→ 查看“预警药品”Tab → 是否列出上述3条记录。

关键技巧:

  • @Scheduled需在启动类加@EnableScheduling;
  • selectNearExpiry(30)的SQL必须用valid_date BETWEEN CURDATE() AND DATE_ADD(CURDATE(), INTERVAL 30 DAY),避免索引失效;
  • 预警日志表warning_log需加create_time字段,方便追溯。

最后说一句血泪经验:毕业设计答辩前,务必用真机(iOS+安卓各一台)跑通这三类场景。模拟器里一切正常,真机上可能因HTTPS证书、域名未备案、图片上传超时等问题当场崩溃。我见过太多同学在答辩现场点“微信登录”弹出“request:fail network error”,就是因为没在微信公众平台配置合法域名。把https://yourdomain.com加到「开发管理 > 服务器域名」里,哪怕只是用ngrok做的临时域名,也比localhost强。希望帮到你。

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

返回列表