拓冰建站拓冰建站
首页 / 资讯中心 / 正文

SpringBoot+SSM实战:中药材店铺管理系统设计与实现

最近几年JavaWeb方向的项目需求基本被两类东西承包了一类是各种“管理系统”另一类还是各种“管理系统”。而中药材店铺管理系统算是这类项目里比较有代表性的一个。它表面上是一个进货、卖货、管库存的进销存系统实际却把权限管理、商品管理、订单流程、统计报表这些JavaWeb的核心知识点全串起来了所以不管是Java课程设计、毕业设计还是想练手SpringBootSSM整合的同学这个题目都挺值得细看的。这篇博文就围绕这个项目标题展开从需求拆解、技术选型、数据库设计、核心功能实现到本地部署和常见坑位一条线讲清楚。内容来源于我做同类项目时的实践经验代码风格和实现细节会偏“课设/毕设能用、能跑、能讲明白”的方向适合直接拿来参考。1. 项目整体拆解中药材店铺管理系统到底在管什么1.1 核心需求分析把项目标题里的几个词拆开看“中药材店铺管理系统”本质上是一个面向中小型中药材门店的进销存平台。它要解决的核心问题不是“药怎么种”而是“店怎么管”。中药材门店和普通超市、便利店比有几个很不一样的地方药材种类杂规格多。同一种药材可能有不同产地、不同等级、不同炮制规格比如黄芪就有生黄芪、炙黄芪之分价格差得还不少。库存有保质期和存储要求。药材怕潮、怕虫、怕串味库存周转和有效期管理比普通商品更敏感。进销存流程链条长。从供应商采购、入库验收、门店销售到库存盘点每一步都要留痕。日常经营需要看数据。哪些药材卖得快、哪些压在库里半年没动、这个月毛利是多少老板都得心里有数。所以一套完整的中药材店铺管理系统至少要覆盖用户登录、员工权限、药材信息管理、分类管理、供应商管理、采购入库、销售出库、库存预警、统计报表、操作日志这几个模块。模块之间不是孤立的而是通过订单和库存两条主线串联采购单影响入库和库存增加销售单影响库存减少和营业额累计。从代码层面看这个项目走的是典型的“表现层-业务层-数据层”三层结构。前端页面发起请求Controller接收参数Service层处理业务逻辑Mapper层和数据库打交道。这种结构在课程设计和实际企业项目里都是主流逻辑清楚也好答辩。1.2 适合谁来做、用来干什么这类项目最大的价值是它把JavaWeb开发里最常用的一整套技术栈实打实地串了一遍。如果你正处在下面这几个阶段这个项目是很好的练手对象正在准备Java课程设计或毕业设计需要一个能写、能跑、能讲的完整项目。Java基础已经学完但不知道SSM框架到底怎么配合工作想通过一个实际项目把Spring、SpringMVC、MyBatis的关系彻底搞懂。想快速体验SpringBoot的开发效率同时又想保留SSM的分层思路做一个不算太复杂但五脏俱全的Web应用。后续想找工作简历上缺一个能拿得出手、能讲清楚业务和数据表关系的项目。有一点要说清楚这个项目的难点不在于某个单一技术有多深而在于“整合”和“闭环”。SpringBoot负责自动装配SpringMVC负责请求分发MyBatis负责数据库操作三者怎么无缝衔接前端表单提交的数据怎么经过校验、业务处理最终落库一张销售单产生后库存是怎么被扣减的——把这条链路跑通你对JavaWeb的理解会上一个台阶。2. 技术栈选型的实际考量与数据库设计2.1 为什么是SpringBoot SSM这套组合很多初学者会有一个疑问SSM指的是Spring SpringMVC MyBatis而SpringBoot本身是用来简化Spring配置的这两个说法是不是冲突其实不冲突。SpringBoot并没有替代SpringMVC和MyBatis它做的是把以前需要大量XML配置的Spring生态“自动配置化”。你现在用SpringBoot建一个Web项目底层依然是Spring IOC和SpringMVC那套请求处理机制数据层依然可以用MyBatis来写SQL。所以“SpringBoot SSM”这个说法准确理解是用SpringBoot作为项目的基础框架和启动入口整合SpringMVC作为Web层整合MyBatis作为持久层。这么选最大的好处是两个字省事。以前搞一个SSM项目要手工配web.xml、Spring配置文件、SpringMVC配置文件、MyBatis配置文件还有各种包扫描、视图解析器、数据源配置半天才能跑一个HelloWorld。SpringBoot把这些默认配置都帮你准备好了你只需要在application.yml里写数据库连接信息再在启动类上标注SpringBootApplication项目就能起来。对中药材店铺管理系统这种业务型项目来说SpringBoot的价值还不只是开发爽更重要的是部署调试方便。项目内置Tomcat本地调试不需要单独装Tomcat再丢war包直接运行main方法就能看到效果。这在做课设和答辩演示时特别占优势——现场出问题概率降低一个档次。2.2 数据库设计数据表到底该怎么建数据表设计是这种管理系统的灵魂。表建得好后面写业务代码就是一把梭表建得乱业务逻辑写起来全是补丁。中药材店铺管理系统我建议至少建这几张表数据表核心字段作用userid, username, password, role, real_name, phone存储系统用户区分管理员和普通员工categoryid, name, parent_id, remark药材分类支持分级比如“补气药-黄芪”medicineid, name, category_id, origin, spec, unit, price, stock, warning_stock, create_time药材基本信息核心是库存和预警阈值supplierid, name, contact, phone, address, remark供应商信息purchase_orderid, order_no, supplier_id, total_amount, status, create_time采购单主表记录一次采购的整体信息purchase_itemid, purchase_id, medicine_id, quantity, price, amount采购单明细一种药材一条记录sale_orderid, order_no, user_id, total_amount, pay_type, create_time销售单主表sale_itemid, sale_id, medicine_id, quantity, price, amount销售单明细stock_logid, medicine_id, change_type, change_quantity, remain_stock, create_time库存变动流水所有出入库都留痕operate_logid, user_id, action, detail, create_time操作日志记录谁在什么时候做了什么这里有个很关键的设计思路采购和销售都拆成主表 明细表两张表。为什么要拆因为一张采购单里通常有多种药材如果只建一张表每种药材一条记录还要重复存储采购单号、供应商、总金额这些信息数据冗余而且修改其中一部分会很痛苦。拆成主表和明细表后主表存“这次采购是谁家的、总共多少钱”明细表存“具体买了哪几味药、各多少钱”通过外键关联符合第三范式的思路。库存预警阈值warning_stock这个字段也要单独说。它不是一个固定值而是每种药材根据自己的销售速度、采购周期单独设置。比如黄芪这种卖得快的阈值设高一点比如50公斤有些冷门药材设5公斤就够了。在查询药材列表时用stock warning_stock就能把需要补货的药材筛出来。3. 核心功能实现与关键代码解读3.1 用户登录与权限拦截是怎么做的管理系统的第一道关卡是登录认证。这个项目的用户表里有role字段用来区分管理员和普通员工。管理员的权限包括用户管理、供应商管理、采购入库这些普通员工主要负责销售开单和库存查询。登录的核心逻辑不复杂前端提交用户名和密码Controller调Service查询用户表比对密码是否一致一致就把用户信息放进Session然后跳转到首页。密码这块要注意不能明文存数据库建议用MD5加盐或者BCrypt加密存储防止数据库泄露后密码被直接看到。代码结构大致是这样的Controller public class LoginController { Autowired private UserService userService; PostMapping(/login) public String login(String username, String password, HttpSession session, Model model) { User user userService.login(username, MD5Util.md5(password)); if (user null) { model.addAttribute(error, 用户名或密码错误); return login; } session.setAttribute(loginUser, user); return redirect:/index; } RequestMapping(/logout) public String logout(HttpSession session) { session.invalidate(); return redirect:/login; } }登录之后的权限控制这个项目用拦截器就能搞定没必要上Spring Security这么重的框架。写一个LoginInterceptor实现HandlerInterceptor接口在preHandle方法里判断Session中是否有登录用户没有就重定向到登录页。public class LoginInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HttpSession session request.getSession(); User user (User) session.getAttribute(loginUser); if (user null) { response.sendRedirect(/login); return false; } return true; } }然后在SpringBoot的配置类里注册拦截器并配置排除路径比如登录接口、静态资源不拦截。Configuration public class WebConfig implements WebMvcConfigurer { Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new LoginInterceptor()) .addPathPatterns(/**) .excludePathPatterns(/login, /css/**, /js/**, /images/**); } }如果还要区分管理员和普通员工的操作范围可以再写一个AdminInterceptor专门拦截那些只有管理员能访问的URL比如用户管理、供应商管理这些。这种做法在答辩时也比较好讲先做登录认证再做功能授权两层控制思路清晰。3.2 库存管理入库、出库与预警阈值的计算逻辑库存是整个系统的核心资产入库和出库逻辑如果写不好库存数据就会变成一团乱账。这个项目的库存变动建议统一走stock_log流水表而不是散落在一段段零散的update语句里。先看采购入库。采购员录入一张采购单勾选几种药材填上数量和采购单价提交后系统要做两件事第一往purchase_order和purchase_item里插入采购单数据第二更新medicine表里对应药材的库存数量和最新采购价。这里最简单的实现方式是先插入主表拿到自增id再遍历明细列表插入子表同时更新库存。为了保证“采购单保存成功但库存没更新”这种情况不出错整个操作要加Transactional事务注解。一旦中间某一步失败全部回滚数据库不会留下半截数据。Transactional public void addPurchase(PurchaseOrder order, ListPurchaseItem items) { purchaseOrderMapper.insert(order); for (PurchaseItem item : items) { item.setPurchaseId(order.getId()); purchaseItemMapper.insert(item); // 更新库存和流水 medicineMapper.increaseStock(item.getMedicineId(), item.getQuantity()); stockLogMapper.insert(new StockLog(item.getMedicineId(), 采购入库, item.getQuantity())); } }销售出库的逻辑是镜像操作但多了一个校验卖出去的药库存不能是负数。所以在销售提交时要先查当前库存判断库存是否充足不足就直接抛出异常提示“库存不足”。public void addSale(SaleOrder order, ListSaleItem items) { for (SaleItem item : items) { Medicine medicine medicineMapper.findById(item.getMedicineId()); if (medicine.getStock() item.getQuantity()) { throw new RuntimeException(medicine.getName() 库存不足当前库存 medicine.getStock()); } } // 校验通过才开始插入销售单 saleOrderMapper.insert(order); for (SaleItem item : items) { item.setSaleId(order.getId()); saleItemMapper.insert(item); medicineMapper.decreaseStock(item.getMedicineId(), item.getQuantity()); stockLogMapper.insert(new StockLog(item.getMedicineId(), 销售出库, -item.getQuantity())); } }库存预警的SQL也简单查询的时候带上一个条件SELECT * FROM medicine WHERE stock warning_stock;如果老板想看到底哪些药材要补货可以在首页加一个“库存预警”列表把stock warning_stock的药材按缺口从小到大排列缺口就是warning_stock - stock。这个列表甚至可以做成醒目的红色高亮方便店员一眼看到。3.3 销售管理订单创建与收银台交互销售模块是整个系统里用户最常用、最影响体验的功能。现实中门店收银的场景是收银员扫一眼顾客拿的药材在系统里搜索药材名称加入订单列表改数量最后计算总价收钱。在Web页面实现这个流程一般是这样一个交互左侧是药材搜索框输入关键词实时联查中间是选中药材清单包含单价、数量、小计下方是总金额和结算按钮。前端用jQuery Ajax就能做得比较顺手不用上Vue这种重框架。后端接收销售订单时Controller里要处理两类数据订单头信息和商品明细列表。前端通过Ajax提交一个JSON结构大概是这样{ payType: 现金, items: [ { medicineId: 1, quantity: 2 }, { medicineId: 3, quantity: 1 } ] }Controller接收时用一个SaleVO对象来承接PostMapping(/sale/add) ResponseBody public Result addSale(RequestBody SaleVO saleVO, HttpSession session) { User loginUser (User) session.getAttribute(loginUser); SaleOrder order new SaleOrder(); order.setUserId(loginUser.getId()); order.setPayType(saleVO.getPayType()); saleService.addSale(order, saleVO.getItems()); return Result.success(); }注意这里要把当前登录用户也放到订单里这样后续查销售记录时能追踪到是哪位店员卖的做业绩统计和操作追溯都方便。销售单号我习惯生成一个带日期和随机码的单号比如“XS2025011516230001”就是“XS 年月日时分秒 随机四位”。这样有个好处销售单号本身就有时间信息不需要专门去查创建时间打印小票时也直观。3.4 统计报表让数据会说话管理系统如果不带统计功能老板用几天就会觉得“这系统就是个电子台账还不如我的Excel”。所以在报表这块至少要提供三个维度的数据第一是销售趋势。按天统计最近一个月的销售额可以画出柱状图或折线图看门店的生意波动。SQL用DATE_FORMAT函数按天分组就能搞定SELECT DATE_FORMAT(create_time, %Y-%m-%d) AS day, SUM(total_amount) AS amount FROM sale_order WHERE create_time #{startTime} GROUP BY day ORDER BY day;第二是热销药材排行。统计每种药材卖了多少数量、贡献了多少销售额看哪些是店里的顶梁柱。这个从sale_item里按medicine_id分组求和SELECT m.name, SUM(si.quantity) AS total_quantity, SUM(si.amount) AS total_amount FROM sale_item si LEFT JOIN medicine m ON si.medicine_id m.id GROUP BY si.medicine_id ORDER BY total_quantity DESC LIMIT 10;第三是库存周转情况。结合库存流水看看哪些药材只进不出、压在库里占资金。我的做法是把近三个月没有销售记录但库存大于预警值的药材筛出来单独做一个“滞销药材”列表。图表展示方面这个项目不需要接ECharts那样的大前端库用简单的CSS柱状图或者引入一个轻量的Chart.js就够。答辩时能展示出一张能随日期变化的销售趋势图已经比绝大多数同题目的作业要完整了。4. 项目跑起来环境准备与本地部署完整流程4.1 环境清单与版本匹配说句实话这个系统一类的问题十次里有七次是环境问题不是代码问题。版本不匹配、依赖下载不下来、数据库字符集不对都可能导致项目启动失败。所以先把环境版本定死。我推荐的环境组合是这样的软件推荐版本说明JDK1.8Java基础运行环境SpringBoot 2.x对JDK8支持最稳定Maven3.6.x依赖管理和项目构建MySQL5.7或8.0本地数据库字符集建议utf8mb4IDEIDEA 2020以上旗舰版自带SpringBoot插件社区版也能用SpringBoot2.5.x和JDK8配合最好不要用3.x3.x强制JDK17这里要提醒一句如果你的SpringBoot版本是2.5那就别再手贱把JDK换成11或者17虽然SpringBoot 2.5官方说支持JDK8到JDK17但实际开发中JDK8是最稳的。JDK版本过高反而容易出现一些奇怪的字节码兼容问题。4.2 从源码到本地运行的完整步骤拿到源码包之后不管是从什么地方下载的第一步一定是解压后先看目录结构。一个规范的项目压缩包应该包含这几部分project-root/ ├── src/ # 项目源代码 ├── sql/ # 数据库脚本文件 ├── README.md # 项目说明文档 ├── pom.xml # Maven配置 └── docs/ # LW论文或说明文档、调试文档等然后按照下面这个顺序操作第一步把项目导入IDEA。File - New - Project from Existing Sources选择项目根目录Maven会自动开始下载依赖。这个过程可能需要几分钟取决于网速和Maven镜像配置。如果下载很慢建议在settings.xml里配置阿里云镜像。第二步创建数据库。打开Navicat或者命令行工具执行sql目录里的database.sql脚本。脚本里通常包含建库、建表和初始化数据。注意执行前看一眼脚本的创建库语句确认数据库名称比如叫db_herb_store后面连接要用。第三步修改数据库连接配置。打开src/main/resources下的application.yml或application.properties文件修改数据库的host、端口、用户名、密码spring: datasource: url: jdbc:mysql://localhost:3306/db_herb_store?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 123456 driver-class-name: com.mysql.jdbc.Driver第四步启动项目。运行启动类通常在某个包下形如HerbStoreApplication的类右键Run。看到“Tomcat started on port(s): 8080”日志就说明启动成功了。第五步浏览器访问。打开http://localhost:8080使用初始化好的管理员账号登录。管理员账号密码一般在README或数据库脚本的注释里常见的是admin/admin123。4.3 调试文档和讲解视频应该怎么用这个项目标题里带了“LW调试文档讲解”这些附加资料的实际价值很多人其实没有吃到位。LW论文/说明文档的正确用法不是照着抄而是对照它理解系统设计思路。写毕业论文或课设报告时重点看它的摘要、需求分析、数据库设计、系统测试这几个章节怎么写因为这些东西老师最看重。代码实现相关的部分反而可以当作参考完全照搬反而容易被查重。调试文档是用来救命的。它通常记录了项目运行过程中可能出现的典型问题比如数据库连接失败怎么处理、端口被占用怎么解决、Maven依赖报红怎么处理。建议在启动项目前先把调试文档从头到尾扫一遍大概知道哪些是常见坑出了问题时按图索骥找答案比自己上网乱搜高效得多。讲解视频/讲解PPT的作用是帮助你在答辩时讲出逻辑来。很多人代码是自己写的但被老师一问“这个表的字段为什么这样设计”就卡壳。视频里通常会把设计思路、核心功能流程讲一遍把这块消化掉答辩就很难被问倒。5. 常见问题与排查技巧实录5.1 启动报错的几个高频原因做这个项目的人群主要集中在学生我见过的问题里启动阶段的高频bug基本固定这里直接整理成一张速查表症状原因解决方案启动报“Access denied for user”数据库用户名或密码错误对照application.yml检查账号密码启动报“Unknown database”数据库没建或名字不一致执行sql脚本创建数据库核对库名启动报“Port 8080 was already in use”8080端口被占用改用server.port: 8081或杀掉占用进程页面中文乱码数据库字符集和连接参数不一致url加characterEncodingutf8建库用utf8mb4Maven依赖报红依赖没下载成功配置阿里云镜像clean后重新import登录后跳转404页面路径或JSP位置不对检查Controller返回的视图名和页面文件位置是否匹配端口占用是最常遇到的。查看端口占用用一句话命令就能定位netstat -ano | findstr 8080这会列出所有监听8080端口的进程记下PID然后taskkill /PID 这里写PID号 /F如果项目用的JSP作为视图层还要特别注意JSP文件的位置。SpringBoot对JSP的支持和传统war包项目不一样JSP页面必须放在src/main/webapp/WEB-INF/views目录下并且pom.xml里要有对应的依赖否则会出现“Whitelabel Error Page”404。5.2 功能异常排查与避坑经验项目跑起来之后接踵而来的就是各种功能层面的bug。有几个问题属于“没人提醒就一定会踩”的级别。第一个是库存变负数。销售功能如果没有做库存校验或者校验和扣减不在一个事务里在高并发或快速点击的情况下库存很容易被扣成负数。解决办法就是前面代码里写的先循环校验库存再统一提交同时整个方法加上事务。第二个是新增药材时分类下拉框没有绑定两级分类。如果分类表里有父子关系但前端只查了顶级分类二级分类的药材就显示不出来。解决方法是分类下拉框做联动选中父分类后Ajax查询子分类。第三个是操作日志记录不完整。很多系统只做了登录日志忘了记录用户的新增、修改、删除操作。一旦数据出问题想追溯是哪位员工改的就无从下手。我的建议是做一个简单的AOP切面拦截所有带有Log注解的方法自动记录操作人和操作内容Aspect Component public class OperateLogAspect { Around(annotation(operateLog)) public Object around(ProceedingJoinPoint point, OperateLog operateLog) throws Throwable { long startTime System.currentTimeMillis(); Object result point.proceed(); long costTime System.currentTimeMillis() - startTime; // 从Session获取当前操作人 User loginUser (User) RequestContextHolder.getRequestAttributes().getAttribute(loginUser, 0); operateLogService.save(new OperateLog(loginUser ! null ? loginUser.getUsername() : 匿名, operateLog.value(), costTime ms)); return result; } }第四个是Redis或JWT相关的内容这类课设项目里大概率用不上。有些同学看到简历上的项目都写“基于SpringBootRedisJWT实现无状态登录”就把这个项目硬改成那样结果画虎不成反类犬。中药材店铺管理系统就是传统Session会话最适合的应用场景硬上JWT反而复杂化了答辩时还容易露怯。技术选型要匹配业务这点很重要。还有一个容易被忽略的是分页查询。药材数量一多不分页的话页面会卡。推荐用PageHelper插件在pom.xml引入依赖然后在Mapper查询前写一行PageHelper.startPage(pageNum, pageSize); ListMedicine list medicineMapper.selectByCondition(condition); PageInfoMedicine pageInfo new PageInfo(list);PageHelper的原理是通过MyBatis拦截器在SQL执行前自动拼接LIMIT语句。它把分页参数自动追加到第一条查询语句上所以PageHelper.startPage()这行代码必须紧跟查询语句中间不能穿插其他SQL查询否则会报“分页插件查询异常”或者分页失效。这个小细节很多人踩坑。最后说点我自己的体会。做这种管理系统类的项目最大的收获不是你背下来某个注解、某条SQL而是建立了一种“从需求到落库”的整体思维老板要什么功能对应到哪张表、哪个接口、哪个页面一条数据从产生到最后被统计经过了哪些环节、在哪里有可能出错。这套思维方式不管是继续做后端开发还是干别的需要统筹规划的活儿都通用。你在跑通这个项目的过程中每一次断点调试、每一回对着报错日志翻代码都是实打实在积累经验。如果是在做课程设计或毕业设计建议别把“跑起来”当终点试着改动一两个小需求比如给销售模块加个会员折扣、给库存预警接个邮件通知你会发现自己对这套代码的理解又上了一个台阶。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门