我第一次在毕设选题表里看到“XX管理系统”这类题目时,心里多少有点抗拒。倒不是觉得管理系统没价值,而是这类系统太容易被做成千篇一律的增删改查:前台收银、商品管理、订单查询,页面换一换,数据库字段换一换,本质上一模一样。答辩时老师问一句“你的系统技术亮点在哪里”,很多人就卡住了。后来我把题目定成“基于SSM框架和协同过滤算法的药店销售管理系统”,这个问题才算彻底解决:SSM负责把业务系统做扎实,协同过滤负责让系统多出一个讲得出口的推荐模块,业务和算法兼得,工作量也足够撑起一篇完整毕设。
这篇文章就是来完整复盘这个项目的做法。从业务场景拆解、SSM工程搭建、数据库建模,到协同过滤算法落地、前后端联调、部署和答辩准备,我会把实际开发中踩过的坑、当时没想明白后来才懂的逻辑、以及可以直接照抄的配置一并写出来。适合三类人看:一是准备做Java方向毕设的同学,需要一套能讲清楚技术方案的管理系统;二是对协同过滤算法在真实场景落地感兴趣的人;三是刚拿到别人源码包但跑不起来,不知道从哪里下手的新手。
1. 先把题目掰开揉碎:药店场景里真正要解决的事
1.1 走一遍药店的日常:采购、库存、收银、报表
做系统之前一定要先去理解业务,而不是先想表结构。我那时专门找了一家小区周边的药店,观察了一个下午。药店的日常大概可以拆成四条线。
第一条线是采购入库。药店需要定期向供应商订货,货到了以后验货、清点数量,然后把药品信息录入系统,同时标记批号和有效期。这件事在Excel里做非常痛苦,尤其是药品品种一多,哪个批号还剩多少、哪批药快到有效期,全靠人脑记,记不住就会积压过期药。
第二条线是药品管理。药品和普通商品不一样,它有分类、厂家、规格、剂型、单位、进价、售价,还需要一个预警库存。比如某种降压药低于50盒就要提醒采购,否则断货会影响老客户。虽然我们做的是毕设项目,但字段设计必须向真实业务靠拢,答辩时才有话可说。
第三条线是前台收银。顾客来买药,可能是现金、扫码,也可能是老会员报手机号来结算。收银员需要快速找到药品,录入数量,计算总价,然后减库存。注册会员之后,用户每一次购买行为都会变成推荐模块的原始数据。
第四条线是统计报表。店长关心的是这个月卖了多少、哪些药卖得好、哪些药长期不动。系统里需要提供按月份、按药品分类、按会员维度的统计图表,这部分的SQL和可视化是论文里的常规亮点。
这四条线连起来,就是药店销售管理系统的核心闭环。需求分析阶段把这条业务流理清楚,后面所有设计都有依据。
1.2 为什么这类系统容易做“烂”,以及怎么加入亮点
我见过很多毕设管理系统的问题,大多不是功能不全,而是“没有数据深度”。所谓数据深度,就是系统里存的每一笔数据,它不仅仅被展示出来,还能被进一步分析和使用。
比如订单表,多数管理系统只用来查流水、算总金额,仅此而已。但在药店场景里,订单明细其实就是一份“用户-药品”的购买行为记录。我们能从里面算出:哪些药经常被一起购买,某些老会员的复购周期是什么,某种药最近30天的销量趋势如何。这些分析一旦做进去,系统就从“登记台账”变成了“数据工具”。
我加协同过滤模块的思路也是这样:不做大而全的推荐平台,只做一个小而清晰的推荐区——当收银员选中某个会员后,系统根据该会员的历史购买记录,自动展示“猜他需要”的几款药品。这个功能在论文里可以写一整章算法设计,在实际演示中也能直观看到效果。相比单纯堆功能,这种“一技之长”更容易拿到高分。
2. SSM框架选型与工程搭建:手写配置比想象中有价值
2.1 为什么是SSM,不是Spring Boot
很多同学一开始都问我:现在公司里都用Spring Boot,为什么毕设还要用SSM?
答案分两层。第一层是题目要求和技术评审。直到现在仍有相当多高校的毕设题目和课程设计沿用SSM,评审老师对这个组合非常熟悉,他们很容易判断你的代码结构是否标准、是否真的理解了框架原理。Spring Boot虽然省事,但大量配置被自动化了,反而容易让论文里的“关键技术”部分写不出深度。第二层是个人成长。手写SSM配置的过程,本质上是把Spring IoC、SpringMVC请求流程、MyBatis会话管理这些底层概念逐一实践一遍。等到你以后再切到Spring Boot,会理解得更透,学东西只是适应差异而已。
所以我的建议很明确:如果题目明确要求SSM,就踏踏实实写配置;如果题目没指定,也可以用Spring Boot,但论文里要补上“自动化配置的原理解析”来体现深度。这篇讲的是SSM路线。
2.2 目录结构、依赖版本和三个核心配置文件
SSM项目不像Spring Boot那样一键启动,它需要我们手动组织工程。我的目录结构大致是这样:
src/main/java ├─ com.pharmacy.controller // 控制层:接口入口 ├─ com.pharmacy.service // 业务层:接口 ├─ com.pharmacy.service.impl // 业务实现 ├─ com.pharmacy.dao // MyBatis Mapper接口 ├─ com.pharmacy.entity // 实体类 └─ com.pharmacy.common // 公共工具:MD5加密、结果包装类等 src/main/resources ├─ jdbc.properties // 数据库连接配置 ├─ mybatis-config.xml // MyBatis全局配置 ├─ spring-context.xml // Spring核心配置:数据源、事务、Mapper扫描 ├─ spring-mvc.xml // SpringMVC配置:注解驱动、视图解析器 └─ mapper // 各实体对应的Mapper XML文件 src/main/webapp ├─ static // CSS、JS、图片 ├─ WEB-INF │ ├─ web.xml // 启动配置、DispatcherServlet、编码过滤器 │ └─ views // JSP页面版本上我建议组合为:JDK 1.8、Spring 5.3.x、MyBatis 3.5.x、MySQL 8.0、Tomcat 9。这是最稳妥的组合,网上资料多,遇到问题容易查到。
核心依赖在pom.xml里大概是这样:
<dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>5.3.29</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> <version>5.3.29</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.5.13</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis-spring</artifactId> <version>2.1.1</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.33</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.2.20</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.15.2</version> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>jstl</artifactId> <version>1.2</version> </dependency> </dependencies>web.xml是SSM的入口,里面两个配置最重要。一个是字符编码过滤器,必须放在所有过滤器的最前面:
<filter> <filter-name>encodingFilter</filter-name> <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class> <init-param> <param-name>encoding</param-name> <param-value>UTF-8</param-value> </init-param> </filter> <filter-mapping> <filter-name>encodingFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>另一个是DispatcherServlet,它接管所有前端请求:
<servlet> <servlet-name>dispatcher</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <init-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:spring-mvc.xml</param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>dispatcher</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping>spring-mvc.xml里要开启注解驱动,配置视图解析器,并放行静态资源。spring-context.xml里则配置数据源、事务管理器和Mapper扫描。这里有一个容易忽略的点:MyBatis的Mapper接口扫描和Mapper XML扫描要配对配置,否则DAO层会注入不进去。
2.3 跑通“Hello World”前的版本对齐细节
第一次搭框架时,我栽在数据库驱动上。MySQL 8.0的驱动类名和MySQL 5.x不一样,jdbc.properties里必须写成:
jdbc.driver=com.mysql.cj.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/pharmacy_system?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai jdbc.username=root jdbc.password=123456如果还按老教程写com.mysql.jdbc.Driver,启动后直接报ClassNotFoundException。同理,Tomcat版本上我建议别用Tomcat 10,因为Tomcat 10把javax.servlet换成了jakarta.servlet,很多老项目跑不起来,Tomcat 9最省心。
3. 数据库设计:一张订单明细表是如何同时服务业务和算法的
3.1 核心表结构:从用户到销售的完整闭环
数据库是管理系统的地基,表设计好坏直接决定后面写SQL的难度。我梳理的核心表如下:
| 表名 | 作用 | 关键字段 |
|---|---|---|
| sys_user | 后台登录账号 | id, username, password, role |
| member | 会员信息 | id, name, phone, create_time |
| drug_category | 药品分类 | id, name, description |
| drug | 药品资料 | id, drug_code, name, category_id, manufacturer, specification, unit, purchase_price, sale_price, stock, warn_stock |
| supplier | 供应商 | id, name, contact, phone, address |
| stock_in | 入库记录 | id, drug_id, supplier_id, quantity, cost_price, batch_no, expire_date, create_time |
| sale_order | 销售订单主表 | id, order_no, member_id, emp_id, total_amount, create_time |
| sale_order_item | 销售订单明细 | id, order_id, drug_id, quantity, sale_price |
会员表单独建,是为了让推荐模块有“用户”维度。sys_user只负责后台登录,member才代表真实购买者。如果你只做管理员和收银员两种角色,也可以简化成一张user表,但会员那张表一定要独立,因为协同过滤的核心输入就是会员ID和药品ID的对应关系。
3.2 订单主表和订单明细为什么要分开
刚学设计时我一度想,为什么不能一张订单表把药品都拼成一个字符串存进去?做起来你就知道,这种设计在统计时几乎寸步难行。你想知道某个月某款药卖了多少,如果用拼接字符串,只能把每条记录拉出来拆字符串,数据量稍大就崩,SQL也丑。
订单主表存订单级别的信息:订单号、会员、收银员、订单总金额、下单时间。订单明细表存药品级别的信息:属于哪个订单、哪个药品、数量、当时的售价。主表一条记录对应明细表多条记录。这样设计之后,几十个SQL都变得非常顺:查订单列表是主表查询,查畅销药品是对明细表按drug_id分组求和,查会员关联购买记录是明细表关联member。
下单的完整链路包含查库存、扣库存、插订单主表、插订单明细四件事,它们必须在同一个事务里完成,否则可能出现“订单生成了但库存没扣”的严重数据错误。
3.3 库存、批号与有效期的设计取舍
药品库存管理有两个细节值得说一下。
第一个是库存数量放在哪里。毕设级别可以把当前库存直接放在drug表的stock字段里,同时设置warn_stock预警值。这样的优点是查询简单,列表页能直接过滤预警库存。缺点是没有任何库存流水,如果想追溯“某天库存为什么少了”,得靠订单去推。如果希望更完整,可以再加一张库存流水表inventory_log,每次出入库都写一条日志。我建议核心用drug表字段,流水作为论文的进阶设计来写。
第二个是批号和有效期。真实药店是按批入库的,不同批次进货价可能不同,有效期也不同。毕设阶段为了控制复杂度,可以在stock_in表里留下batch_no和expire_date两个字段,入库时记录,列表页可以根据过期日期筛选“临期药品”,但不需要单独建批次库存明细。这个粒度对毕设来说足够,论文里也能交代清楚。
3.4 留给推荐算法的“数据接口”
推荐模块要的数据不在额外的地方,就在sale_order_item里。表结构里只要保证member_id和drug_id能正确关联,我们就能源源不断地从历史订单中提取用户行为。
实际实现推荐服务时,我的思路是从sale_order_item关联sale_order表,查询近90天所有有会员身份的购买记录。为什么选90天?因为药品消费有时效性,感冒药买过一次可能半年都不会再买,但降血压药这种慢病药几乎每月复购。取近90天能兼顾时效性和数据量。这个口径如果在论文里写出来,会显得你是认真考虑过业务逻辑的。
4. 协同过滤推荐模块:在老客户的购买记录里找规律
4.1 选UserCF还是ItemCF:先想清楚推荐给谁
协同过滤分两大类:基于用户的协同过滤(UserCF)和基于物品的协同过滤(ItemCF)。UserCF的思路是“和你相似的人买了什么,就推荐给你”;ItemCF的思路是“你买过的物品相似的物品,才是你想买的”。
药店的业务场景有一个特点:会员群体相对固定,很多是慢性病患者,复购率高。从这个角度看,UserCF天然适合“老客户复购提醒”。但UserCF也有明显的毛病:需要实时计算用户之间的相似度,一旦会员数量增长,计算成本上升很快,而且新会员没有任何历史行为,根本找不到“相似的人”。
ItemCF的算法实现更直观:对每个药品,统计其被哪些人购买过;当用户已经买了A药品,系统就找出所有与A最相似的药品推荐给用户。比如买过感冒灵的人大概率也买过退烧药、维生素C,这和药店的销售直觉完全一致,解释起来非常自然。
所以我的推荐模块选择ItemCF为主算法,UserCF作为论文中的对比方案来写。这样既有实现的深度,又可以展开对比分析,工作量也更丰富。
4.2 药品相似度计算与推荐列表生成
ItemCF的核心分两步:求药品之间的相似度,然后根据用户已购买的药品生成推荐。
求相似度的方法有很多,常用的是余弦相似度。具体到药店场景,我们可以为每个药品构造一个“会员购买向量”,向量的维度是所有会员,值是该会员购买该药品的次数。两个药品的余弦相似度就是这两个向量夹角的cos值,越接近1表示越相似。
但全量计算所有药品两两相似度在毕设规模下可以承受,但也要注意效率。我的做法是先用一个Map统计“每个药品被哪些会员购买”,再只对“至少有几个共同购买者”的药品对计算相似度,避免把零相似度的数据也跑一遍。核心逻辑用伪代码表示如下:
1. 从sale_order_item join sale_order查询近90天的购买记录 2. 构建Map<drugId, Set<memberId>>:记录每个药品被谁买过 3. 遍历所有药品对(a, b): 公共购买者 = setA ∩ setB 如果公共购买者数量 < 2,跳过 计算sim = |setA ∩ setB| / sqrt(|setA| * |setB|) 保存到Map<a, List<b, sim>> 4. 对于目标用户已购买的药品集合P: 候选药品 = 所有与P中药品相似且用户未购买过的药品 推荐得分 = sum(药品相似度) 按得分降序取TopN这段逻辑写到Service里,用一个定时任务每天刷新一次相似度结果,存到缓存Map中。推荐接口只做查询和拼接,响应速度会很快。不要每个请求都重新算一轮算法,那是性能灾难。
4.3 冷启动:新会员和新药品怎么推荐
协同过滤最大的软肋是冷启动。一个新会员没有购买记录,任何推荐逻辑都拿不到输入特征。完全不给推荐又会显得系统很呆。我的解决方案是加一个兜底策略:没有历史行为时,直接按销量返回热门药品TopN。这类“热门榜”虽然个性化程度低,但至少页面有内容,用户也不会觉得突兀。
新药品的冷启动也同样处理。刚上架的药品没有购买记录,它永远不会进入相似度矩阵,自然也不会被推荐出来。我在列表页单独加了“新品上架”区域,让新药品先被看见,等积累了一定订单量后,再自动参与推荐。
4.4 算法性能优化和论文里可以写的指标
论文里如果只写“我用协同过滤实现了推荐”,是不太有说服力的。最好补上评价部分。离线评价最简单的做法是:把历史订单按时间切分,前70%作为训练集,后30%作为测试集,对测试集中的会员生成推荐,计算三个指标:
- 准确率:推荐列表中被用户实际购买的药品数 / 推荐列表总药品数
- 召回率:推荐列表中被用户实际购买的药品数 / 测试集中用户购买的所有药品数
- 覆盖率:推荐出的药品种类数 / 全部药品种类数
这些指标写出来,就是完整的“推荐算法实验”章节。不需要很复杂的数学推导,只要把计算方式讲清楚,再把结果列出来分析一遍,工作量就非常饱满。
5. 业务功能模块实现:从药品目录到销售报表的完整链路
5.1 商品维护与库存预警
药品管理模块就是常规的增删改查,但有几个点需要按真实药店来设计。药品编码drug_code应该设置为唯一索引,不能重复;药品名称、厂家、规格、单位、进价、售价、预警库存要齐全。录入页面需要做表单校验,售价和库存不能为负数,这是基础。
库存预警的SQL很简单:
SELECT drug_code, name, stock, warn_stock FROM drug WHERE stock <= warn_stock ORDER BY stock / warn_stock ASC;页面上的表现是我把库存数量做成红色高亮,一眼能看到哪些药该补货了。这个功能演示起来很直观,我准备演示数据时特意把几款常用感冒药的库存压到预警线以下。
5.2 收银下单:一个事务要保护的所有操作
收银模块是整个系统的核心,也是最容易写乱的地方。页面交互大致是:收银员在左侧搜索药品、点击加入购物车,右侧生成临时购物车列表,最后点击结算。
结算的后端方法必须用@Transactional注解,内部按以下顺序执行:
@Transactional public void createOrder(SaleOrderDTO dto) { // 1. 遍历购物车中的每个药品,校验状态和当前库存 // 2. 计算订单总金额 // 3. 插入sale_order主表,生成订单号 // 4. 遍历购物车写入sale_order_item明细 // 5. 执行update drug set stock = stock - quantity }这里的第一步非常重要。不能先扣库存再写明细,也不能先写订单再扣库存。只要中间任何一步抛异常,整个事务回滚,库存和订单保持一致。订单号的生成我采用一个简单办法:当前时间字符串加四位随机数,形如202501161523300123。毕设规模下基本够用。
5.3 入库管理与供应商台账
采购入库模块要和库存模块联动。入库时选择药品和供应商,填写数量、进货价、批号、有效期,保存后自动更新drug表的库存字段。同时把这次入库记录写到stock_in表里,形成完整的采购流水。
供应商是一个独立模块,维护供应商名称、联系人、电话、地址。设计上可以预留一个“按供应商查看入库记录”的功能,也就是从供应商维度反查历史采购情况,这个能体现多表查询能力,答辩时也容易讲。
5.4 统计报表的SQL和图表联调
统计报表是偏后的模块,但也是老师会重点看的模块。我做了三个方向:
按月销售趋势:
SELECT DATE_FORMAT(create_time, '%Y-%m') AS month, SUM(total_amount) AS total_amount, COUNT(id) AS order_count FROM sale_order GROUP BY DATE_FORMAT(create_time, '%Y-%m') ORDER BY month;畅销药排行:
SELECT d.name, SUM(oi.quantity) AS sale_count, SUM(oi.sale_price * oi.quantity) AS sale_amount FROM sale_order_item oi JOIN drug d ON oi.drug_id = d.id GROUP BY oi.drug_id ORDER BY sale_count DESC LIMIT 10;前端图表用ECharts比较多。后端把查询结果包装成List<Map<String, Object>>,通过@ResponseBody转成JSON,前端用Ajax取数后再渲染折线图和柱状图。这里要特别注意JSON内容的字段名要和前端一致,我一开始字段名没对准,图表白屏了半天,后来逐字段核对才发现问题。
6. 开发中真正折磨人的地方:联调、部署和接盘源码
6.1 数据库连接和时区的三连坑
这部分全是血泪。数据库连接过程最常见的三个坑:驱动类名写错、时区报错、中文乱码。前面已经提到驱动和后缀要写成MySQL 8.0对应的形式,这里再强调一遍。如果你拿到别人的源码一直启动报错,先看这三个地方:
- jdbc.properties的driver是不是com.mysql.cj.jdbc.Driver
- url里面有没有serverTimezone=Asia/Shanghai
- 数据库和项目连接字符集是不是都统一成了utf8
这三个问题排查完,数据库连接基本就稳了。
6.2 MyBatis映射与动态SQL的常见翻车点
MyBatis这一层最容易出的问题是实体类属性名和数据库列名对不上。解决方法是开启驼峰映射:
<settings> <setting name="mapUnderscoreToCamelCase" value="true"/> </settings>这样数据库里的create_time就会自动映射到实体类的createTime字段。如果没开,查出来之后属性全是null,前端就看不到数据。
多条件搜索我一般用动态SQL:
<select id="selectByCondition" resultType="com.pharmacy.entity.Drug"> SELECT * FROM drug <where> <if test="name != null and name != ''"> AND name LIKE CONCAT('%', #{name}, '%') </if> <if test="categoryId != null"> AND category_id = #{categoryId} </if> </where> ORDER BY id DESC </select>这里有一个细节:if判断里的参数名要和Mapper接口里的@Param注解一致,否则会报“There is no getter for property”的错误,这几乎是新手必踩。
6.3 前后端JSON交互与中文乱码
后端返回JSON时,Controller方法上要加@ResponseBody,或者类上直接用@RestController。返回类型可以是Map或List。前端Ajax请求时,如果传的是JSON格式的数据,记得设置:
$.ajax({ url: "/order/settle", contentType: "application/json;charset=UTF-8", data: JSON.stringify(cartList), type: "POST", success: function (res) { ... } });Controller接收时用@RequestBody注解:
public Result saveOrder(@RequestBody List<CartItemDTO> cartList) {经常遇到的情况是前端明明传了数据,后端却接收不到,十有八九是contentType没设置。这个问题排查了半小时才发现就少了这一行。
中文乱码则要三板斧齐做:第一,web.xml里加CharacterEncodingFilter;第二,数据库连接串带characterEncoding=utf8;第三,IDE的编码设置改成UTF-8。三处缺一不可。
6.4 拿到别人源码包后的通用启动排查步骤
这个题目带了“附源码”三个字,很多同学会直接买或下载源码包。我必须说一句:源码包能跑起来才是源码,跑不起来就是一堆代码文件。接手源码时按以下顺序排查,效率最高:
- 先看有没有数据库脚本(.sql文件),有就先在MySQL里执行,没有脚本就开始跑代码的基本都会失败。
- 检查pom.xml的依赖版本,执行mvn clean install把依赖拉下来,看有没有下载报错。
- 改jdbc.properties里的账号密码和URL,确认连接的是你自己的数据库。
- 用Tomcat 9启动项目,看日志里面有没有报错。第一轮常见错误是驱动、时区、端口占用。
- 如果项目起来了但页面空白,先看控制台有没有404,再打开浏览器F12看Network面板的请求状态,挨个接口排查。
- 资源文件里的图片、CSS、JS路径要注意,工程名改了之后,原来以/开头的绝对路径要跟着改。
7. 测试、演示与答辩:把毕设变成一个有说服力的项目
7.1 测试用例设计:正常、异常、边界和权限
毕设的测试章节不需要写得很高大上,但至少要有覆盖面。我习惯把测试用例按四类来组织:正常流程、异常流程、边界条件、权限控制。
| 模块 | 测试内容 | 输入/操作 | 预期结果 |
|---|---|---|---|
| 登录 | 正常登录 | 正确的用户名密码 | 跳转到首页,显示对应用户信息 |
| 登录 | 密码错误 | 错误密码 | 提示密码错误,停留登录页 |
| 药品管理 | 新增药品 | 填写完整合法数据 | 保存成功,列表出现新药品 |
| 药品管理 | 必填缺失 | 不填名称直接保存 | 表单校验提示,不写入数据库 |
| 收银下单 | 库存充足 | 添加数量小于库存 | 下单成功,库存减少 |
| 收银下单 | 库存不足 | 添加数量大于库存 | 下单失败,提示库存不足,库存不变 |
| 推荐模块 | 老会员有历史 | 选择购买过的会员 | 返回TopN推荐列表 |
| 推荐模块 | 新会员无历史 | 选择刚创建的会员 | 返回热门药品列表 |
| 权限管理 | 收银员访问管理员页 | 使用收银员账号访问 | 拦截器拒绝,跳回登录页 |
这些用例表直接放进论文附录或测试章节,非常加分。
7.2 环境部署与演示数据准备
演示前一定要准备好一份“有说服力”的种子数据。如果数据库里只有十几条记录,推荐模块基本测不出效果。我准备的是:20个会员、50种常用药品、大约3个月左右的订单记录、每月大概几百条明细。这样协同过滤才能真正计算出相似度和推荐结果。
部署打包用Maven来打:
mvn clean package打包成功后,target目录下会生成一个war包,把这个war包放到本地Tomcat的webapps目录下,启动Tomcat,访问http://localhost:8080就能看到项目。如果war包名是pharmacy-system,访问路径就是http://localhost:8080/pharmacy-system/。
7.3 答辩高频问题如何准备
答辩时老师最喜欢问的核心问题大概有五个,提前想好答案:
第一个:请简述SSM框架的请求流程。回答要点:浏览器发请求到DispatcherServlet,它通过HandlerMapping找到对应Controller方法,Controller调用Service处理业务,Service调用Mapper操作数据库,结果返回后经视图解析器渲染页面或返回JSON。
第二个:为什么选择协同过滤算法?回答要点:业务上有会员购买历史数据,适合做个性化推荐;选择ItemCF是因为药店商品间搭配购买关系明显,算法可解释性强,也方便冷启动兜底。
第三个:如果推荐结果不准怎么办?回答要点:冷启动问题用热门榜兜底;数据稀疏时只对共同购买者足够多的药品对计算相似度;还可以引入时间衰减因子,越近的购买记录权重越高。
第四个:系统的事务是怎么保证的?回答要点:Spring声明式事务,@Transactional注解标注在业务方法上,下单过程中任何异常都会导致整个事务回滚,库存和订单保持一致。
第五个:你的算法效果如何评估?回答要点:用训练集和测试集切分历史数据,计算准确率、召回率和覆盖率,离线实验评估。
7.4 论文章节安排与工作量分配
论文目录我会这样安排:第一章绪论写背景和意义,第二章相关技术写SSM、MySQL、协同过滤原理,第三章需求分析和可行性分析,第四章总体设计写系统架构和功能模块划分,第五章数据库设计,第六章系统实现配合截图和核心代码讲解,第七章重点写协同过滤推荐模块的算法设计和实验评估,第八章系统测试。这样的结构逻辑完整,工作量分配也均匀,不会出现前面写太多后面草草结尾的问题。
最后再说一点个人体会。做这类管理系统,真正拉开差距的不是你会多少新技术,而是你对一个简单业务有没有想清楚。把“订单从加入到扣库存”这条主链路跑通,再往里面加算法亮点,系统就会非常扎实。如果只想着堆页面、堆功能,最后大概率是花架子。准备答辩时给自己写一个“演示脚本”,固定用某个会员账号、展示某几个核心模块、点开某张报表,五分钟讲完主流程,剩下时间全部留给推荐模块讲解。这种方法我试过,现场效果很稳,也挺适合第一次做完整项目的同学参考。