尧图精选

基于SpringBoot+Vue的药店销售系统开发实战:库存批次与会员积分

🕒 发布时间:2026/10/2 2:57:32 📁 来源:尧图网络
1. 项目概述康健药店销售系统到底在解决什么问题做过药店信息化项目的朋友应该都清楚这类系统和普通的进销存软件有本质区别——它不光是管进货、卖货、库存更要面对药品批次追溯、近效期预警、处方药合规销售、会员积分体系这一堆垂直场景。我这次基于SpringBootVue做的康健药店销售系统就是冲着药店日常经营全流程管理去的从药品建档、库存批次管理、销售收银、退换货到会员营销和经营报表一条线全打通。这个系统适合谁参考如果你是正在做毕业设计或课设的计算机专业学生可以用它完整走一遍前后端分离项目的开发流程如果你是企业里需要快速交付一套药店管理系统的开发者可以直接把架构设计、数据库表结构和核心业务逻辑拿过去改改就用如果你只是对药品零售行业感兴趣想知道一个药店后台到底要管什么这篇也会用很直白的方式讲清楚。说句实在话市面上药店管理系统的教程和Demo很多但大多数停留在CRUD层面打开就几个增删改查表格。真正放到门店里用你会遇到批次怎么扣减、处方药怎么校验、小票格式怎么适配、会员积分怎么防套利这些现实问题。我这次把项目从零搭到能上线部署把过程中的关键设计逻辑和踩过的坑都整理出来希望能让后来者少走弯路。1.1 核心需求解析药店业务的特殊之处在动手写代码之前我花了不少时间蹲在药店看实际业务流程也翻了很多药店的运营资料和监管要求。整理下来药店销售系统的核心模块就六个用户与权限、药品信息、库存批次、销售收银、会员顾客、供应商采购。下面逐个拆解。用户角色与权限。药店通常有店长、店员、收银员、药师等角色不同岗位能干的活不一样。我把系统做成RBAC模型权限粒度控制在按钮级店长能看经营报表和库存盘点收银员只能操作收银台和会员结算药师可以审核处方药销售。这样职责边界清晰也方便以后做审计追溯。药品信息管理。药品是药店系统的核心基础数据字段必须严谨。我设计的主表字段包括药品编码唯一约束、通用名、商品名、生产厂家、批准文号、规格、单位、剂型、处方类型、存储条件、库存上下限、进价、零售价、医保编码。其中处方类型是合规红线——处方药必须走专门的销售校验流程OTC药才能直接售卖。库存批次管理。这是药店区别于普通进销存的关键点。同一款药品不同批次的进价可能不同、有效期不同销售时必须按先进先出原则扣减对应批次。我的方案是维护一个库存批次明细表记录每个批次的剩余数量销售单生成时按有效期升序锁定批次。销售与退货。销售单采用主表明细表结构覆盖金额、支付方式、收银员、顾客等维度。退货不是简单反向操作必须关联原销售单、校验退货数量、扣回会员积分整个流程是独立的事务闭环。会员积分体系。药店现在都在做会员营销积分等级体系是标配。我的设计是消费积分、积分抵现、等级折扣三联动同时把积分逆操作写进退货流程防止套利。供应商与采购。采购单关联供应商和批号到货后做验收入库数据进批次管理为后续FIFO扣减和近效期预警提供基础数据。1.2 技术选型的核心逻辑先亮结论工具各有边界组合起来才出效果。我见过很多开发者觉得一个框架打天下实际上在药店这种偏传统的信息化场景里务实比炫技重要。后端SpringBoot 2.7.x Java 8。为什么不用SpringBoot 3.x很多药店客户的内网环境还在用JDK 1.8部署兼容性是第一优先级。SpringBoot 2.7是目前最稳妥的版本社区资料多、坑少对初学者也更友好。前端Vue 2.7 Element UI。Vue 2.7是2.x系列最后的版本Composition API也能用生态成熟。药店后台管理系统的典型页面是大量表格表单弹窗Element UI这套组件玩熟之后开发效率极高。数据库MySQL 5.7/8.0ORM用MyBatis Plus。它对单表CRUD的加持太明显了药店系统90%的增删改查就是IService和LambdaQueryWrapper一行搞定剩下10%的复杂统计聚合自己写SQL。权限认证JWT Spring Security轻量、无状态适合前后端分离架构。文件存储MinIO。处方照片、资质文件、商品图片存本地磁盘不行——多机部署不同步、容易丢用MinIO做对象存储和SpringBoot集成简单也符合当前主流实践。这套组合的现实好处是pom.xml加几个依赖就能起服务Vue侧用脚手架初始化后组件即插即用可以把主要精力放在业务本身而不是环境配置上。2. 项目结构规划前后端分离如何组织代码很多初学者上来就毁在项目结构上包名乱、依赖乱、接口乱。药店销售系统的业务边界其实很好划分我建议直接用分模块的方式组织前后端各守其位互不干扰。2.1 后端工程结构springboot-medical-store/ ├── pom.xml ├── src/main/java/com/kangjian/ │ ├── KangjianApplication.java // 启动类 │ ├── config/ // 配置类WebMvc、Security、CORS、MyBatisPlus │ ├── controller/ // REST接口层 │ │ ├── admin/ // 管理端接口 │ │ ├── pharmacy/ // 门店业务接口 │ │ └── common/ // 通用接口登录、验证码、文件上传 │ ├── service/ // 业务逻辑层 │ ├── mapper/ // MyBatis Plus Mapper接口 │ ├── entity/ // 实体类 │ ├── dto/ // 入参和出参对象 │ ├── vo/ // 视图对象 │ ├── utils/ // 工具类JWT、Excel导出、日期处理 │ ├── exception/ // 全局异常处理 │ └── quartz/ // 定时任务库存预警、日结汇总 └── src/main/resources/ ├── application.yml ├── mapper/ // XML文件复杂SQL用 └── sql/ // 初始化脚本我的规矩是controller层只做参数校验和请求转发业务逻辑全部下沉到service。这个规矩从第一天就要立住不然项目到后期controller越写越臃肿维护成本直线上升。2.2 前端工程结构Vue 2.7 Element UIvue-medical-store/ ├── package.json ├── vue.config.js ├── src/ │ ├── main.js // 入口文件 │ ├── api/ // 统一封装axios接口 │ ├── router/ // 路由配置含动态路由 │ ├── store/ // Vuex状态管理 │ ├── views/ // 页面组件 │ │ ├── dashboard/ // 工作台、统计图表 │ │ ├── drug/ // 药品管理 │ │ ├── inventory/ // 库存管理 │ │ ├── sale/ // 销售收银 │ │ ├── member/ // 会员顾客管理 │ │ ├── supplier/ // 供应商与采购 │ │ ├── report/ // 报表中心 │ │ ├── system/ // 系统管理 │ │ └── login/ // 登录页 │ ├── components/ // 公共组件 │ ├── directives/ // 自定义指令 │ └── utils/ // 请求封装、权限校验前后端通过REST风格接口互通接口前缀统一用/api后面不管挂域名还是网关都好处理。2.3 数据库设计里的两个重要约定数据库设计是这类系统的灵魂我这里额外强调两个约定所有业务表都用逻辑删除。药店数据涉及合规审计物理删除一旦发生不可恢复后果严重。MyBatis Plus自带TableLogic逻辑删除配置一下就能自动处理。所有表必须有create_time、update_time、create_by、update_by、deleted五个通用字段。这样审计追溯时才不会抓瞎。我启用了MyBatis Plus的自动填充功能插入和更新时自动填值业务代码里不用每次手动set。另外数据库字符集要选utf8mb4。药品通用名里经常出现生僻字和特殊符号utf8存不下四字节字符我以前吃过亏只要看到乱码就明白是字符集问题。3. 环境准备踩过的最深的坑都在这在写业务代码之前环境搭建最磨人。我按实际流程把后端和前端环境完整梳理一遍重点标出容易翻车的点。3.1 后端环境清单JDK 1.8推荐1.8.0_231以上版本太低会有小毛病Maven 3.6.33.8.x也能用但有些插件版本不兼容IDEA 2021关键是配置Maven和JDKMySQL 5.7或8.0推荐8.0性能更好Redis 5做缓存和验证码存储MinIO对象存储离线包即可也可以用Docker跑3.2 前端环境清单Node.js 14.17Vue 2.7要求Node版本不能太低也不能太高。Node 18在某些情况下会有OpenSSL兼容问题构建报ERR_OSSL_EVP_UNSUPPORTED时别慌这是老版本Webpack的锅在package.json的scripts里加上NODE_OPTIONS--openssl-legacy-provider就能解决npm 6国内环境用cnpm或设置registry镜像不然装依赖等到哭Vue CLI 4.53.3 安装过程中的两个经典坑坑1Maven依赖下载慢/失败。解决方案是在settings.xml里配置镜像仓库。不配置镜像的话spring-boot-starter-parent这种超大依赖包能下载半小时还可能中断。mirrors mirror idaliyunmaven/id mirrorOfcentral/mirrorOf name阿里云公共仓库/name urlhttps://maven.aliyun.com/repository/public/url /mirror /mirrors坑2Vue依赖安装过程中的权限问题。遇到EACCES: permission denied时不要用sudo硬刚正确做法是清理npm缓存并在项目目录本地安装npm config set registry https://registry.npmmirror.com npm cache clean --force rm -rf node_modules package-lock.json npm install这里多说一句依赖尽量在项目本地安装不要全局安装。全局安装容易造成版本冲突尤其不同项目依赖同一个包的版本不一致时排查起来非常痛苦。3.4 数据库初始化建议我会把数据库脚本放在resources/sql/init.sql用Navicat或命令行执行。要点如下数据库名用kangjian_pharmacy字符集utf8mb4排序规则utf8mb4_general_ci初始化脚本包括建库、建表、初始数据管理员账号admin/admin123系统字典基础药品分类连接配置写在application.yml里注意MySQL 8.0的驱动类名要写成com.mysql.cj.jdbc.Driver老版本驱动类名在8.0下会直接连不上spring: datasource: url: jdbc:mysql://localhost:3306/kangjian_pharmacy?useUnicodetruecharacterEncodingutf8mb4useSSLfalseserverTimezoneAsia/ShanghaiallowMultiQueriestrue username: root password: yourpassword driver-class-name: com.mysql.cj.jdbc.DriverallowMultiQueriestrue这个参数容易忽略当你需要一次执行多条SQL比如初始化数据、批量更新时少了它就报错。3.5 MinIO集成仓库里最省心的对象存储MinIO接入比想象中简单核心就三步引入依赖、写配置类、提供上传下载接口。我用Docker方式启动docker run -p 9000:9000 --name minio \ -e MINIO_ROOT_USERminioadmin \ -e MINIO_ROOT_PASSWORDminioadmin \ -v /data/minio:/data \ minio/minio server /data --console-address :9001后端集成时在pom.xml引入MinIO的Java SDK。一个上传接口的示例Autowired private MinioClient minioClient; PostMapping(/upload) public RString upload(RequestParam(file) MultipartFile file) { String fileName UUID.randomUUID() - file.getOriginalFilename(); try { minioClient.putObject(PutObjectArgs.builder() .bucket(drug-images) .object(fileName) .stream(file.getInputStream(), file.getSize(), -1) .contentType(file.getContentType()) .build()); String url http://localhost:9000/drug-images/ fileName; return R.success(url); } catch (Exception e) { log.error(文件上传失败, e); return R.fail(文件上传失败); } }注意一个细节MinIO的地址不能写死localhost要在配置中心单独配否则项目部署到服务器后上传的图片全打不开。我建议把minio.endpoint、minio.accessKey、minio.secretKey、minio.bucket都放到application.yml里用ConfigurationProperties读取。4. 后端核心SpringBoot下的接口实现技巧环境跑通后就进入重头戏业务接口。药店系统的接口数量大概在80到120个之间如果一个个手写人会疯掉。好在有MyBatis Plus把重复CRUD的成本压到了最低。4.1 通用CRUD接口的懒人写法以药品分类表为例实体类继承ModelMapper继承BaseMapperService继承IServiceServiceImpl继承ServiceImpl一套下来连SQL都不用写public interface DrugCategoryService extends IServiceDrugCategory { } Service public class DrugCategoryServiceImpl extends ServiceImplDrugCategoryMapper, DrugCategory implements DrugCategoryService { }写到这里基础的增删改查方法就全有了。接下来在Controller里调用RestController RequestMapping(/api/drug-category) public class DrugCategoryController { Autowired private DrugCategoryService drugCategoryService; GetMapping(/list) public RListDrugCategory list() { return R.success(drugCategoryService.list( new LambdaQueryWrapperDrugCategory().orderByAsc(DrugCategory::getSort))); } PostMapping(/save) public RString save(RequestBody DrugCategory drugCategory) { drugCategoryService.save(drugCategory); return R.success(保存成功); } }一个小建议入参对象不要直接暴露Entity。虽然示例里图方便用了Entity但真实项目里建议用DTO接收前端参数再拷贝到Entity落库。原因很简单前端可能传多余字段直接落到实体上容易造成批量赋值漏洞或脏数据。4.2 分页查询的统一封装列表页面基本都要分页。MyBatis Plus提供一个分页插件配置一下就能用Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }分页查询的写法PageDrug page drugService.page( new Page(current, size), new LambdaQueryWrapperDrug() .like(StringUtils.hasText(name), Drug::getDrugName, name) .eq(categoryId ! null, Drug::getCategoryId, categoryId) .orderByDesc(Drug::getCreateTime) );这里的关键是把Page和QueryWrapper组合起来使用。条件拼接用lambda表达式做动态条件前端传哪个参数就拼哪个条件不传就不拼既灵活又避免SQL注入。这是MyBatis Plus最舒服的写法。4.3 JWT登录鉴权完整链路登录模块的技术点集中在两个地方生成Token和校验Token。生成Token。用户登录成功后我把用户ID、用户名、角色放进JWT的claims里设置过期时间为24小时String token Jwts.builder() .setSubject(user.getUsername()) .claim(userId, user.getId()) .claim(role, user.getRole()) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() 24*60*60*1000)) .signWith(SignatureAlgorithm.HS256, secretKey) .compact();校验Token。这里和Spring Security结合我编写一个JwtAuthenticationFilter继承OncePerRequestFilter从请求头Authorization: Bearer xxx中解析Token解析成功把userId和角色信息写入SecurityContextHolder解析失败或过期不设置认证信息由Spring Security的后续过滤器统一返回401有个细节很关键登录、验证码、注册、药品查询等接口需要放进白名单否则没登录用户连系统首页都进不去。我用一个数组统一配置匿名访问路径。4.4 Spring Security版本相关的坑如果你用SpringBoot 2.7.xSecurity依赖一般对应5.7.x里面有个大坑WebSecurityConfigurerAdapter已经被废弃网上大量老教程还在教继承这个类。虽然还能用但编译时会标灰警告。这里分享一份可直接使用的配置模板Configuration EnableWebSecurity public class SecurityConfig { Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http.csrf().disable() .cors().and() .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authorizeHttpRequests() .antMatchers(WHITE_LIST).permitAll() .anyRequest().authenticated() .and() .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); return http.build(); } }注意**authorizeHttpRequests和antMatchers的写法在不同版本有差异**。如果用5.7.x上面这段没问题如果升级到6.xantMatchers会变成requestMatchers路径匹配规则也不一样。为了稳妥建议锁版本SpringBoot定2.7Security就跟着默认依赖管理走别单独升级。4.5 药品销售事务的边界设计药品销售是整个系统的核心事务设计不好就会在并发下出现库存超卖、金额对不上。我建议按下面的边界来创建销售单插入销售主表明细表同一事务生成销售单号扣减库存同一事务内按批次FIFO扣减库存批次表更新实时库存计算积分同一事务内更新会员积分和等级为什么这三步必须在同一个事务因为如果先下单后扣库存网络抖动时可能出现卖了药但库存没减的情况。药店是实物业务台账和实物必须一致所以事务边界一定要清晰。Transactional注解加在service方法上默认只对RuntimeException回滚建议加上rollbackFor Exception.class把受检异常也纳入回滚范围Transactional(rollbackFor Exception.class) public SaleResult createSale(SaleOrderRequest request) { // 1. 保存销售单 // 2. 扣减库存逐条核对库存充足性不足则抛异常回滚 // 3. 更新会员积分 return saleResult; }再补充一个乐观锁的思路来防超卖库存批次表加version字段用UPDATE ... SET remaining remaining - #{num}, version version 1 WHERE batch_id #{id} AND version #{oldVersion}更新影响行数为0就重试或报错。这个在小型药店场景下完全够用性能开销也不大。5. 前端核心VueElementUI的高效实现路径前端的项目管理和操作界面我用Vue 2.7 Element UI。用户体验的打磨是药店系统的隐形竞争力——店员一天要开几十单页面响应慢、按钮找不到都会被抱怨。所以前端层面我特别在意响应速度和交互逻辑。5.1 动态路由与权限控制不同角色进来看到的菜单不一样。实现上没有用复杂方案直接在Vuex里存当前用户的权限标识用router.beforeEach做全局前置守卫检查。登录后拉取用户信息与菜单权限this.$store.dispatch(getUserInfo).then(() { this.$router.replace({ path: this.redirect || / }); });全局路由守卫里做登录态判断和动态菜单注册router.beforeEach((to, from, next) { const token localStorage.getItem(token); if (to.path /login) { next(); } else if (!token) { next(/login); } else { // 已登录根据用户角色动态添加路由 next(); } });注意在关键菜单比如库存管理里我还做了按钮级权限控制权限标识形如inventory:add、inventory:delete在按钮上用一个自定义指令v-permissioninventory:add控制显隐。这里要强调一下不显示按钮不代表安全后端接口同样要做权限校验前端只是提升体验。5.2 销售收银页面的高效交互收银是高频操作我专门优化了这个页面原则是输入步骤少、快捷键多、支持回车跳转。药品编码输入框支持扫码枪录入监听回车事件自动查询药品并在列表中勾选数量输入框回车后自动计算小计并跳到下一个商品结算按钮弹出付款对话框支持现金、微信、支付宝、医保卡支付支付完成后自动弹出小票打印窗口对接浏览器打印插件这个页面用Vue的响应式数据管理销售明细每行数据包含药品ID、数量、单价、小计。核心逻辑handleAddDrug(drug) { const existItem this.saleItems.find(item item.drugId drug.id); if (existItem) { existItem.quantity 1; // 同种药品自动累加数量 } else { this.saleItems.push({ drugId: drug.id, drugName: drug.drugName, specification: drug.specification, price: drug.retailPrice, quantity: 1, subtotal: drug.retailPrice, prescriptionType: drug.prescriptionType }); } this.calcTotal(); }这里有个细节同种药品累加而不是新增一行。店员实际操作中扫同一盒药扫两遍时页面会显示2个数量而不是两行视觉上更干净也方便核对。5.3 药品批量导入导出的Excel方案药店药品量动辄上千手工录入不现实。我在药品管理页做了Excel批量导入用xlsx库在前端解析Excel文件然后逐条调用后端接口。导出则直接调后端后端用EasyExcel生成文件流。一个导出示例后端GetMapping(/export) public void export(HttpServletResponse response) throws IOException { ListDrug list drugService.list(); response.setContentType(application/vnd.openxmlformats-officedocument.spreadsheetml.sheet); response.setCharacterEncoding(utf-8); String fileName URLEncoder.encode(药品信息, UTF-8); response.setHeader(Content-Disposition, attachment;filename fileName .xlsx); EasyExcel.write(response.getOutputStream(), Drug.class) .sheet(药品信息) .doWrite(list); }批量导入时需要注意唯一约束校验药品编码、药监码不能重复导入前先查一次数据库批量比对把重复的行标红提示用户而不是半路报错终止整个导入流程。5.4 跨域配置前后端联调的日常操作前后端分离开发时前端地址是localhost:8080后端接口是localhost:8081跨域必须解决。有两种主流方案后端CORS配置或前端代理。我推荐前端代理方案生产环境再把前后端都部署到同一域名下。Vue CLI的vue.config.js里这样配module.exports { devServer: { port: 8080, proxy: { /api: { target: http://localhost:8081, changeOrigin: true } } } };这样前端请求/api/drug/list时自动转发到后端的http://localhost:8081/api/drug/list前端代码里不用写绝对地址环境迁移时只需要改一处配置。如果后端自己不设置允许跨域前端代理就完全够用了如果后端还要给手机App或其他系统提供接口就再加CORS配置。两个一起配也不会冲突但注意CORS配置时不要用*允许所有来源——药店系统涉及用药信息来源限制越严格越安全。6. 关键业务场景的深度实现库存、销售、会员到这里基础框架已经能跑起来了但真正的业务灵魂在下面这三个模块。我把每个模块的落地细节和踩坑经验都写上。6.1 库存模块批次、预警、盘点的三重设计批次表核心字段批次ID、药品ID、批号、生产日期、有效期、入库数量、剩余数量、进价、供应商ID、入库时间。核心SQL是按FIFO扣减-- 按有效期排序找最早批次 SELECT * FROM inventory_batch WHERE drug_id #{drugId} AND remaining_count 0 ORDER BY expire_date ASC FOR UPDATE;注意FOR UPDATE是行级锁同一批次同时被扣减时不会超卖。扣减成功后要判断该批次剩余数量是否小于库存下限小于则触发预警。库存预警我设计了一个定时任务每天凌晨和上午各跑一次扫描inventory表的实时库存与min_stock、max_stock的关系生成预警记录库存低于min_stock生成采购建议库存高于max_stock提示滞销有效期在3个月内生成近效期预警预警记录进入一张stock_warning表前端在库存看板里展示。预警只看单据不看批次会出大事近效期预警必须精确到批次因为同一药品不同批次有效期可能差好几个月。盘点盘点时用盘点单先冻结当前库存快照然后逐条对比实盘数量。盘盈盘亏都生成单据要么自动调账要么人工确认。药店通常一个月盘点一次系统里我设计了盘点单状态流转待盘点 - 盘点中 - 待审核 - 已审核。6.2 销售模块从下单到小票的关键设计销售流程在代码层面是一条线串起来的前端提交商品列表 - 后端校验 - 生成单据 - 扣库存 - 返回结果。但我这里重点讲两个实操中容易忽视的细节1销售单号生成规则。销售单号是财务审计和店面对账的关键凭证我用前缀时间戳随机数生成格式类似XS202312150001001保证在分布式环境下也不会重复。不要用数据库自增主键当单号客户投诉时说单号3102134512根本无从查起。String orderNo XS LocalDateTime.now().format(DateTimeFormatter.ofPattern(yyyyMMddHHmmss)) String.format(%03d, (int)(Math.random() * 1000));2退货逆向流程。退货时不能简单把库存加回去必须先查原销售单再做退货单再按原批次加库存。而且退货是否允许改价格、是否允许退非原批次这些业务规则必须在代码里写死。我设计的是退货单必须关联原销售单号退货商品默认使用原销售价格退货数量不得超过原销售数量。需要手动改动的地方全部要有二次确认弹窗。3处方药校验。在创建订单时如果是处方药prescription_type 1必须要求前端传prescription_serial处方单号后端再校验该处方是否有效包括是否已使用、是否过期。这是硬性校验不能在SQL层绕过。早期我图省事只在前端限制结果上线后被药师批评绕过接口直接调用照样能下单前端限制等于没有。6.3 会员模块积分规则与等级体系药店会员的粘性一般靠积分和折扣。我实现了两套联动规则积分规则消费1元积1分充值赠送另算。积分可以抵现比例后台可配置等级规则普通会员、银卡、金卡、黑金卡升级条件是累计消费金额达标享受不同折扣率会员等级变动时前端应该收到即时提示并展示新等级。我用一张等级变更记录表做会员等级快照顾客在收银台结账时就能看到自己等级的变化。还有一个细节会员积分在退款时要做逆操作——退货必须扣回积分避免顾客先攒积分再全额退款套利。我在退货事务里一并处理积分扣回如果积分已经抵现使用过则现金抵扣部分按比例退回。7. 系统健壮性日志、缓存、定时任务一个都不能少系统上线后运维才是重头戏。药店系统每天几十上百张单据一旦出问题排查效率往往决定口碑。7.1 操作日志与审计追踪我用AOP切面的方式记录所有操作日志。自定义一个Log注解标记在Controller方法上切面里获取方法参数、返回值、操作人、操作时间插入日志表。日志级别区分为INSERT、UPDATE、DELETE、LOGIN、EXPORT等。药店场景里日志有几个特殊要求药品价格变动必须单独记录因为卖错价会引发客诉纠纷记录从A价改成B价谁改的什么时候改的是硬需求库存异动记录要求能按药品、批次、操作类型筛选方便审计表结构核心字段user_id、username、op_type、module、detailJSON、create_time。查询接口只给管理员开放。7.2 Redis缓存的使用边界药店系统的数据有个特点药品基础信息读多写少库存高频写。我建议这样用Redis药品列表、药品分类、系统字典缓存30分钟写操作时主动清理库存数量不缓存一律直接查数据库——因为库存是强一致的缓存过期时间差可能导致超卖或账实不符验证码存Redis5分钟过期用户TokenJWT本身无状态不需要存Redis但可以把在线状态、活跃时间放Redis方便管理员查看当前在店铺的用户滥用Redis是药店系统最常见的错误。不是所有数据都适合缓存特别是有余额、有数量的数据一旦缓存后写操作不谨慎就会出现缓存里是3数据库里是5的严重对账差异。7.3 定时任务日结、库存预警、会员过期我用了Spring自带的Scheduled轻量级场景够用三个核心任务每日日结任务凌晨0点把当天销售、退货、采购数据进行聚合生成销售日报表。聚合维度按药品、按收银员、按支付方式、按分类库存预警任务每天8点、16点扫描低库存和近效期批次生成预警记录并推送消息会员等级重算任务每周一根据累计消费金额重新计算会员等级日结任务有个容易踩的坑必须判断任务是否已经执行过否则服务重启时任务重复执行报表数据就会重复计算。我的方案是记录每次任务的执行流水用分布式锁Redis的SETNX保证同一时刻只有一个实例在执行。8. 部署上线从本地到服务器的完整打包方案项目开发完了部署是一道更现实的坎。我经历过不少同事本地跑得飞起、上服务器就各种404的情况总结出了最稳的路径。8.1 后端打包与部署SpringBoot项目用Maven打包成单个Jar包这是最省心的方式。关键是application.yml的配置不要写死建议用application-prod.yml把生产环境的数据库、Redis、MinIO配置外置打包时用--spring.profiles.activeprod启动mvn clean package -DskipTests java -jar target/kangjian-pharmacy-1.0.0.jar --spring.profiles.activeprod生产环境推荐用systemd管理Java进程或者直接用Docker。用systemd的话记得加入自动重启策略不然半夜进程挂了第二天到店发现系统瘫了就很尴尬。如果是Dockerdocker build -t kangjian-pharmacy . docker run -d -p 8081:8081 -v /opt/pharmacy/config:/config \ --name kangjian-pharmacy kangjian-pharmacy8.2 前端打包与Nginx配置Vue项目打包后生成dist目录上传到服务器用Nginx做静态资源服务。这里有一些非常容易出错的配置server { listen 80; server_name pharmacy.example.com; location / { root /opt/pharmacy/dist; index index.html; try_files $uri $uri/ /index.html; # 前端路由刷新404的问题 } location /api/ { proxy_pass http://127.0.0.1:8081/api/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }try_files那行是必须的没有它Vue Router用history模式时用户刷新页面就会出现404。很多人部署完发现首页没问题点进二级页面一刷新就白屏都是因为这个。8.3 HTTPS与敏感配置药店系统涉及患者用药信息HTTPS是底线。用Nginx配置SSL证书强制跳转HTTPS。同时注意数据库密码、MinIO密钥、JWT密钥不要写在代码里要写在环境变量或外置配置文件中并用ConfigurationProperties读取上线前修改默认管理员密码。我遇到过客户拿到系统后还留着admin/admin123被人扫到后台改药品价格这种事情真的是灾难级别9. 项目经验总结从能跑到好用的关键差异项目做到这里其实已经比大多数课程设计Demo强很多了。但如果你真的给药店用还有一个看不见的鸿沟从能跑到好用。9.1 药店场景里那些Demo不会告诉你的细节扫码枪兼容很多店员的扫码枪是老式USB HID设备模拟键盘输入监听回车事件必须做防抖否则连续扫两盒会被识别成一次操作并行会话限制同一账号多店登录是否允许我建议默认允许但记录登录设备信息。药店员工经常换店别把流程堵死打印样式小票格式每家店都不一样字号、行距、水印要求千奇百怪。代码里把打印样式做成可配置不要写死数据库备份药店每天都有单据数据自动备份任务必须加上。我用的是mysqldump加Linux crontab每天凌晨备份保留最近30天恢复演练至少一季度做一次9.2 系统的扩展方向如果这个项目后面要继续演进我建议按优先级排对接药品监管平台很多省市已要求药店销售数据实时上报这个接口属于合规需求越早做越好移动端H5收银/盘点店员手持PDA盘点和收银效率能提升很多智能药柜和硬件联动售药机自动捡药这是未来的方向数据分析基于销售数据做顾客画像、滞销预测、促销推荐从记录系统升级为经营系统9.3 最后的一点点实际感受药店销售系统这类项目技术栈真的不是最难的——SpringBoot加Vue的生态太成熟了任何有基本编程能力的人都能在两周内搭出原型。真正的门槛在于你懂不懂药店怎么经营、销售怎么合规、库存怎么管理。我见过太多开发者在技术选型上绞尽脑汁最后需求评审时被业务方问得哑口无言。所以如果你想复现或扩展这个项目我的建议是先去一个药店待半天看看店员怎么收银、药师怎么发药、店长怎么盘点。业务懂了技术才有意义。系统不是用来炫技的它是实实在在地帮人省时间、少出错、多赚钱的工具这个定位想清楚了项目就成功了一半。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →