ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

基于Spring Boot的智能药箱系统:Java毕设全栈开发实战

基于Spring Boot的智能药箱系统:Java毕设全栈开发实战

每年到毕业设计的高峰期,总有人跑来问:“Java毕设做什么方向,不容易烂大街,还容易出效果?”我一般不建议去碰那些纯增删改查的管理系统——没有技术亮点,答辩三两句话就问到底,没什么可讲的。但如果把“管理系统”和“智能场景”结合起来,效果会明显不一样。这次要说的是基于Spring Boot的智能药箱系统,它表面上是药品信息管理平台,本质上是一套业务链完整、技术栈主流的JavaWeb应用,覆盖了用户认证、药品管理、服药计划、定时提醒、用药记录、统计分析、家庭共享等核心环节,用来做毕业设计非常合适。

这篇内容我会把整个系统从需求分析、技术选型、数据库设计、核心功能实现、前端联调、部署运行到最后答辩准备完整过一遍。正在准备Java毕设的同学,或者想用Spring Boot练手全栈开发的初学者,都能从中拿到可以直接参考的方案和代码思路。

1. 项目需求分析与功能模块拆解

1.1 智能药箱到底在解决什么问题

智能药箱这个词听起来像物联网硬件,但作为Java毕业设计课题,它更多是落在软件层面:把家庭里的用药行为管理起来,解决“吃什么药、什么时候吃、还剩多少、是否过期、谁没吃”这些靠人脑很难长期记清楚的问题。

拆开看,核心业务场景有三类:第一类,子女给家里老人建一个家庭药箱,把老人的慢性病药品录入系统,设置每天的服药计划,老人按时收到提醒;第二类,独居年轻人自己管理常用药,比如感冒药、消炎药、过敏药,系统记录库存和保质期,快过期时主动提示;第三类,结合智能硬件扩展(比如药盒传感器、摄像头识别)做联动,但作为毕设,先把软件链路打通就足够撑起整个项目了。

这个选题的优势在于业务闭环完整,从用户注册到药品管理,再到提醒、打卡、统计,数据之间环环相扣,不是孤立的CRUD。而且答辩时的“故事线”非常清晰——目标用户是谁、痛点是什么、系统怎么解决、效果如何衡量,都能讲得有理有据。

1.2 功能模块清单梳理

一个合格的智能药箱系统,至少需要拆出如下几个模块:

  • 用户模块:注册、登录、个人信息修改、密码管理。
  • 药品管理模块:药品信息的增删改查,字段包括药品名称、通用名、剂型、规格、生产厂家、分类、库存数量、生产日期、有效期、批号、药品描述。
  • 服药计划模块:为某个家庭成员创建服药计划,绑定药品、设置每次剂量、每日提醒时间和用药周期。
  • 提醒模块:系统定时扫描到点需要服药的计划,生成站内消息提醒,可扩展邮件或短信通知。
  • 用药记录模块:用户点击“已服药”完成打卡,系统记录实际服药时间,支持补录和漏服标记。
  • 统计分析模块:统计按时服药率、漏服次数、近30天用药趋势、药品库存分布、临期药品列表。
  • 家庭管理模块:用户可创建家庭组、邀请成员加入,家庭成员间共享药箱数据,一个家庭可设置多个被监护对象。

每个模块都不复杂,但串联起来就是一个完整的业务系统。做毕设的时候,千万不要只做前四个模块,统计和家庭共享恰恰是拉开项目档次的加分项。

1.3 为什么选 Spring Boot 作为核心框架

现在的Java毕设项目,Spring Boot基本上是事实标准。原因很实际:同学之间遇到问题方便交流,网上资料多,答辩老师也认可。技术上,Spring Boot通过自动配置大大减少了配置文件量,配合Maven的起步依赖,一个可运行的Web服务几分钟就能拉起来。

对比几条路:老的SSH组合(Spring + Struts + Hibernate)已经基本淡出企业使用场景,再选它没有实际意义;纯Servlet + JSP又太底层,开发效率低,而且在简历上没有优势;Spring Boot + MyBatis-Plus是目前中小型项目里最常见的一套组合,学习曲线平缓,开发效率高。选它不只是为了做毕设省事,更是因为你毕业之后进入企业开发,这一套东西依然通用。

2. 系统架构与核心设计思路

2.1 技术栈全景与选型逻辑

这个项目推荐采用前后端分离架构,整体技术栈如下:

  • 后端:Spring Boot 2.7.x,Spring MVC,MyBatis-Plus,JWT(认证),Druid连接池,Lombok,Hutool工具集。
  • 前端:Vue 3 + Element Plus + Axios + ECharts;如果不想上前后端分离,也可以用Thymeleaf + Bootstrap,部署更简单,但答辩时技术分略低。
  • 数据库:MySQL 8.0。
  • 定时任务:Spring自带的@Scheduled注解,简单场景完全够用。

前后端分离的意义,不只是“技术新”,更重要的是把项目结构讲清楚。前端只负责渲染和交互,后端只提供JSON接口,数据通过Restful API通信。答辩时可以从“接口设计”“跨域处理”“鉴权机制”三个角度展开,都是现成的技术话题。

有人可能会纠结缓存要不要用Redis,我的建议是:如果只是毕设,先不用。缓存是优化层面的东西,系统数据量不够大时加了反而增加复杂度。但可以把代码里预留缓存扩展点,比如在接口实现里写注释说“此处可引入Redis缓存热点药品数据”,答辩时主动提一句,老师会觉得你想过架构演进。

2.2 数据库设计核心思路

数据库设计是毕设答辩最容易深挖的地方,表结构设计得好,整个项目就稳了。核心表至少有这些:

用户表(user):

CREATE TABLE `user` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `username` varchar(50) NOT NULL COMMENT '登录账号', `password` varchar(100) NOT NULL COMMENT '密码,BCrypt加密', `nickname` varchar(50) DEFAULT NULL COMMENT '昵称', `phone` varchar(20) DEFAULT NULL COMMENT '手机号', `role` tinyint(4) DEFAULT 1 COMMENT '角色:1-普通用户,2-家庭管理员', `status` tinyint(4) DEFAULT 1 COMMENT '状态:1-正常,0-禁用', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';

药品表(drug):

CREATE TABLE `drug` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `drug_name` varchar(255) NOT NULL COMMENT '药品名称', `generic_name` varchar(255) DEFAULT NULL COMMENT '通用名', `dosage_form` varchar(50) DEFAULT NULL COMMENT '剂型', `specification` varchar(100) DEFAULT NULL COMMENT '规格', `manufacturer` varchar(255) DEFAULT NULL COMMENT '生产厂家', `category` varchar(50) DEFAULT NULL COMMENT '分类', `stock` int(11) DEFAULT 0 COMMENT '库存数量', `production_date` date DEFAULT NULL COMMENT '生产日期', `expiry_date` date DEFAULT NULL COMMENT '有效期至', `batch_no` varchar(100) DEFAULT NULL COMMENT '批号', `description` text COMMENT '药品描述', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_expiry_date` (`expiry_date`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='药品表';

给药计划表(medication_plan)和用药记录表(medication_record)也要重点设计,计划表关联用户、药品、被监护人和用药时间;记录表保存每次提醒对应的打卡情况,一个计划可以产生多条记录,是一对多关系。注意给记录表的plan_id和时间字段加索引,统计按时率的时候性能会差别很多。

设计表的时候有几点经验:第一,status这类状态字段不要用字符串,用tinyint加注释,简洁又高效;第二,凡是经常查询过滤的字段,比如expiry_date、plan_id,一定要建索引;第三,所有时间字段统一用datetime或date,不要混用,后面序列化才不会出幺蛾子。

2.3 角色权限模型怎么落地

智能药箱系统的权限不需要做得很重,但也不能没有。推荐“简单角色控制”方案:用户在user表里用role字段区分普通用户和家庭管理员,管理员可以管理整个家庭组的药品和成员,普通用户只能操作自己的数据和被授权的用药计划。

登录成功后,系统签发JWT令牌,令牌里带上userId和role;后端拦截器统一解析Token并校验权限。管理员接口校验时,从Token里取出role判断即可。这样一个不依赖Shiro或Spring Security的轻量权限模型,代码量少,逻辑透明,答辩时三句话就能讲清楚。

3. 核心功能模块的实现细节

3.1 注册登录与JWT认证

注册登录是整个系统的基础,也是最容易出问题的地方。密码存储必须加密,推荐用BCrypt,不要自己写MD5加盐这种方案,BCrypt本身就是行业标准做法。

JWT的实现步骤很清晰:用户登录成功,服务端生成Token返回给前端;前端每次请求在请求头带上Authorization: Bearer <token>;后端写一个拦截器,对所有需要登录的接口校验Token有效性,解析出用户信息后放入请求上下文。

JwtUtil工具类的核心代码示例:

public class JwtUtil { private static final String SECRET = "your-256-bit-secret"; private static final long EXPIRE_TIME = 24 * 60 * 60 * 1000L; // 24小时 public static String createToken(Long userId, String username, Integer role) { return Jwts.builder() .setSubject(username) .claim("userId", userId) .claim("role", role) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() + EXPIRE_TIME)) .signWith(SignatureAlgorithm.HS256, SECRET) .compact(); } public static Claims parseToken(String token) { return Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token).getBody(); } }

这里有几个坑要注意:SECRET不能写得太短,太短在HS256算法下会直接抛异常;过期时间设置1到3天比较合适,太短导致用户频繁登录,太长又有安全问题;Token生成以后,密码修改或账号禁用这类场景,Token仍然有效,所以管理员的禁用操作要和Token失效联动。

3.2 药品库存与保质期管理

药品模块是最典型的增删改查,但要在细节上做出亮点。新增药品时,要把药盒上的有效期、批号、生产厂家完整录入;列表页提供多条件组合查询,支持按名称模糊搜索、按分类筛选、按临期状态筛选;详情页展示药品说明书信息。

库存扣减不能做成盲减。推荐的做法是:在创建服药计划时绑定药品数量,每次打卡后自动扣减该计划对应的库存;同时保留手动盘点功能,实际清点药箱时修正库存值。每次扣减前检查库存是否充足,不足时返回提示“该药品库存不足,请先补录”。

临期药品功能需要写一个定时任务,每天凌晨扫描drug表,把有效期在30天内的药品筛选出来,写入提醒表,并在前端首页展示。这里用@Scheduled注解就能实现:

@Scheduled(cron = "0 0 1 * * ?") // 每天凌晨1点执行 public void scanExpiringDrugs() { LocalDate threshold = LocalDate.now().plusDays(30); List<Drug> expiringList = drugMapper.selectList( new LambdaQueryWrapper<Drug>() .le(Drug::getExpiryDate, threshold) .ge(Drug::getExpiryDate, LocalDate.now())); // 批量写入提醒记录 }

关键操作cron表达式必须自己会写,答辩时老师很可能问“这个定时任务多久跑一次、怎么控制并发”。一个基本逻辑是:定时任务里查询出的数据要加状态判断,防止同一计划被重复提醒;任务执行时间选在凌晨,不影响正常业务。

3.3 服药提醒的定时任务机制

服药提醒是智能药箱系统的灵魂功能,也是答辩时最出彩的点。

实现思路:定时任务每隔几分钟扫描一次服药计划表,找出当前时间处于提醒时间窗口内、且当日尚未生成提醒记录的未完成计划,为每条计划生成一条提醒消息。提醒消息写入message表,前端页面轮询或使用WebSocket推送新消息。

定时任务核心逻辑:

@Scheduled(cron = "0 */5 * * * ?") // 每5分钟扫描一次 public void scanMedicationRemind() { String nowTime = LocalTime.now().format(DateTimeFormatter.ofPattern("HH:mm")); List<MedicationPlan> duePlans = planMapper.selectDuePlans(nowTime); for (MedicationPlan plan : duePlans) { boolean alreadyReminded = remindRecordMapper.existsToday(plan.getId(), LocalDate.now()); if (alreadyReminded) { continue; } // 生成提醒记录 Message message = new Message(); message.setUserId(plan.getUserId()); message.setPlanId(plan.getId()); message.setContent("您有新的服药提醒:" + plan.getDrugName() + " " + plan.getDosage() ); messageMapper.insert(message); } }

这里有个细节:提醒时间要按照“分钟”维度匹配,查询时把当前时间转换成HH:mm字符串,和计划表中的remind_time直接比较。如果计划跨天,比如晚上10点提醒,第二天早上补提醒,需要额外处理,但不建议做得太复杂,保证基本打卡链路即可。

除了站内消息,可以预留邮件接口。Spring Boot里用JavaMailSender发送邮件很简单,准备一个工具类,在生成站内消息的同时触发邮件发送。答辩时提一句“当前使用站内消息,可以低成本扩展邮件或短信渠道”,就能体现出工程化的思维。

3.4 用药记录与漏服统计

用户收到提醒后,进入“我的用药任务”页面,点击“确认服药”,系统记录真实服药时间。如果用户忘记打卡,系统将该计划标记为漏服。打卡逻辑很简单,但统计逻辑要花点心思。

按时率计算的SQL思路如下:把每条用药记录的实际服用时间和计划提醒时间对比,误差在30分钟以内记为“按时”,超过则记为“延迟”。按时率的SQL示例:

SELECT plan_id, COUNT(*) AS total_times, SUM(CASE WHEN actual_taken_time <= DATE_ADD(remind_time, INTERVAL 30 MINUTE) THEN 1 ELSE 0 END ) AS on_time_times FROM medication_record GROUP BY plan_id;

这段SQL在答辩时非常有分量,因为考察了数据处理能力。实践的时候要注意DATE_ADD函数中remind_time字段类型是time还是datetime,如果只有time类型,需要注意跨日场景,建议在记录表同时冗余一个remind_time字段,避免复杂的JOIN。

3.5 数据可视化统计图表

光有数据没有可视化,系统会显得很单薄。统计模块推荐做四个图表:近30天每日服药次数柱状图、按时率环形图、药品分类占比饼图、临期药品列表。

后端聚合接口返回结构定义成一个DTO,例如:

{ "dates": ["2025-05-01", "2025-05-02"], "records": [12, 9], "onTimeRate": 0.86, "categoryData": [ {"name": "慢性病用药", "value": 34}, {"name": "常备药", "value": 17} ] }

前端拿到数据后,直接用ECharts渲染。做图表最省力的方式是前端一次性请求统计数据接口,后端用多个查询组装好数据后返回。不要在前端搞多次请求再拼接,既慢又难维护。另外,图表时间范围建议做近7天/近30天切换,这虽然增加了后端工作量,但很能体现细节。

4. 前端页面设计与接口联调经验

4.1 页面结构如何规划

采用Vue 3 + Element Plus时,页面规划如下:

  • 登录页:账号密码登录表单,登录成功后跳转首页。
  • 注册页:用户名、昵称、手机号、密码、确认密码。
  • 首页看板:欢迎信息,今日提醒数量、待办用药计划、临期药品提示,下方放统计图表。
  • 我的药品页:药品列表,支持搜索、筛选、新增、编辑、删除,点击详情查看完整信息。
  • 药箱管理页:家庭成员的药箱视图,管理员可查看全部成员药品。
  • 服药计划页:创建计划(选择家庭成员、选择药品、填写剂量、提醒时间、起止日期)、计划列表、启停计划。
  • 用药记录页:按日期查看打卡记录,可以补录打卡。
  • 家庭管理页:创建家庭组、生成邀请码、成员列表、移除成员。

页面数量不算少,但对Vue来说增量开发成本很低。关键是把前端项目结构组织好,router路由、api模块、views目录、components目录分开,源码看起来才会像企业项目。

4.2 API接口规范与前后端联调细节

接口统一使用/api前缀,返回格式统一如下:

{ "code": 200, "message": "success", "data": { } }

核心接口清单:

功能接口路径说明
用户登录POST /api/auth/login返回token和用户信息
注册POST /api/auth/register支持普通用户注册
药品分页GET /api/drug/page分页参数+查询条件
新增药品POST /api/drug/save管理员权限
计划列表GET /api/plan/list按用户和状态过滤
保存计划POST /api/plan/save创建或更新
打卡记录POST /api/record/take提交实际服药时间
统计概览GET /api/stats/overview首页图表数据
家庭组成员GET /api/family/members家庭角色管理

前后端分离最常遇到的坑是跨域。开发环境直接用Vue的代理解决,不需要后端配CORS。在vue.config.js里加:

module.exports = { devServer: { proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } } }

生产环境部署时,如果用Nginx,同样配置一个location /api/的反向代理即可。跨域问题解决之后,整个联调过程的体验会顺畅非常多。

5. 开发实测常见问题与排查技巧

5.1 数据库连接失败的经典错误

在本地跑项目时最容易遇到的就是数据库连接失败,控制台报错大概长这样:

Cannot create PoolableConnectionFactory (Communications link failure)

先别慌,按顺序排查:MySQL服务启动了吗?连接端口是3306吗?密码正确吗?数据库名存在吗?多数人卡在最后两个问题上。我在实际开发中遇到的频率最高的是URL参数配置问题,比如没加时区参数报错,或者useSSL参数不对。推荐直接复制下面这段配置:

spring: datasource: url: jdbc:mysql://localhost:3306/smart_medicine_box?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8mb4 username: root password: yourpassword driver-class-name: com.mysql.cj.jdbc.Driver

这段配置里同时兼容了SSL和时区问题,数据库版本是MySQL 8.0的话基本不会出幺蛾子。

5.2 日期格式序列化总是报错

LocalDateTime在Spring Boot默认序列化下会返回一大串数组格式,前端没法直接用。最简单的解决方案是全局配置Jackson格式,在application.yml里添加:

spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8

但这只对java.util.Date生效,LocalDateTime还需要配合JavaTimeModule,并在字段上手动加@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")。建议写一个全局的Jackson配置类一次性解决,避免每个DTO字段都加注解。这类问题在毕设联调阶段特别常见,早配置早省心。

5.3 拦截器把登录接口拦截了怎么办

Interceptor认证的配置里,最经典的问题是登录和注册接口还没拿到Token就被拦截,导致前端调用登录接口直接401。解决方法是注册拦截器时明确放行路径:

registry.addInterceptor(new JwtInterceptor()) .addPathPatterns("/**") .excludePathPatterns("/api/auth/login", "/api/auth/register", "/error");

如果用了Swagger或者Knife4j做接口文档,也要把/doc.html、/swagger-ui/**、/webjars/**等路径放行。不然打开文档页面也是空白的。写拦截器之前先想清楚哪些是公开接口,哪些是受保护接口,开发时能省很多事。

5.4 前端请求接口一直401或404

后端接口跑通了,前端却老是401,不外乎三种原因:Token没带上、Token过期、Interceptor路径写错。

Token没带上,检查axios请求拦截器:

axios.interceptors.request.use(config => { const token = localStorage.getItem('token'); if (token) { config.headers.Authorization = 'Bearer ' + token; } return config; });

Token过期,检查JwtUtil里的过期时间,以及前端统一处理401的响应拦截器,弹出一条“登录状态已过期,请重新登录”。拦截器路径写错,则要看excludePathPatterns和addPathPatterns的匹配规则,静态资源的路径也经常被误拦截。

5.5 统计数据图表加载不出来

图表页面空白时,优先打开浏览器开发者工具看Network面板,通常能看到某个统计接口报500。原因一般是后端聚合查询里有个别表join不上,或者某个字段为空导致前端渲染报错。

我的排查习惯是:先用Postman单独调接口,确认返回的JSON结构和预期一致;再从前端代码里看数据取用路径是否匹配。后端接口返回时尽量把为null的字段置为0或空数组,别给前端留坑。还有一个小经验:ECharts渲染之前最好先判断data不为空,空数据时显示“暂无数据”,避免控制台一片红。

6. 调试运行与部署上线经验

6.1 本地启动一步步走

拿到项目源码后第一次跑通,按下面步骤严格执行就不会卡壳:

  1. 确认本机环境:JDK 8或11、Maven 3.6+、MySQL 8.0、Node.js 14+(前端项目需要)。
  2. 创建数据库:执行项目提供的sql脚本,初始化表结构和测试数据。
  3. 修改application.yml中的数据源配置和JWT密钥。
  4. 启动后端:IDEA里直接Run启动类,看到“Started Application”表示成功。
  5. 启动前端:进入前端目录,执行npm install,再执行npm run dev,浏览器访问localhost端口即可。

第一次跑项目不要急着改业务代码,先保证数据能通、页面能开,再动手改造。

6.2 打包部署到服务器的流程

毕设如果需要演示给老师看线上版本,可以考虑部署到云服务器。后端打包用Maven命令:

mvn clean package -Dmaven.test.skip=true

打包完成后在target目录生成一个jar包,上传到服务器后执行:

nohup java -jar smart-medicine-box.jar --spring.profiles.active=prod &

如果服务器内存只有2G,建议加上JVM参数限制堆内存,避免OOM。数据量不大的系统,128M初始堆就足够跑演示了。前端项目先npm run build生成dist目录,再用Nginx指向dist目录,同时把/api反向代理到后端端口。

部署中最容易漏的是防火墙配置,服务器安全组和本地防火墙都要放开后端端口和前端端口,不然外部永远访问不到。

6.3 文档整理与演示要点

毕设文档建议包含:需求分析、可行性分析、系统设计(架构图、功能结构图、ER图、表结构说明)、核心功能实现、系统测试、总结展望。正文不需要太长,但图表一定要多,老师翻起来轻松。

答辩演示时准备一个“演示脚本”,按顺序走:登录页 → 首页看板 → 新增药品 → 创建服药计划 → 手动触发一次定时任务 → 展示提醒消息 → 打卡记录 → 统计图表 → 家庭管理。每一个动作对应一句讲解词,思路清晰不慌乱。

7. 答辩常问问题与扩展方向

7.1 高频答辩问题怎么答

答辩时老师问的问题集中在几个点上:

  • 为什么选Spring Boot?答:简化配置、快速起步、内置Tomcat、生态成熟,适合快速构建Web应用。
  • JWT相对Session的优势?答:无状态、天然支持分布式扩展,适合前后端分离项目。
  • 定时任务会不会重复提醒?答:记录表有每日唯一性判断,同一计划同一天只生成一次提醒。
  • 库存扣减会不会扣成负数?答:扣减前校验库存,不足时抛出业务异常并回滚事务。
  • 安全方面做了什么?答:密码BCrypt加密、JWT鉴权、参数校验、自定义异常全局处理。

回答的核心逻辑是“我用到了什么,解决什么问题,有没有想到边界情况”。不需要背定义,把代码里实实在在做的内容讲清楚,老师基本都会满意。

7.2 性价比高的扩展方向

如果时间充裕,可以往下面几个方向扩展:

  • Redis缓存:把热门的药品查询结果和Token会话放入Redis,讲解分布式场景下的优化思路。
  • WebSocket实时提醒:把轮询改造成服务端主动推送,这个功能很能体现技术深度。
  • 扫码录入药品:对接条码识别SDK,前端扫码自动填充药品信息,减少手动录入成本。
  • 智能药盒硬件联动:通过嵌入式开发板上报药盒重量,后端解析数据判断是否取药。这项改动较大,有硬件基础的同学可以考虑。

这些方向不一定要全部实现,挑一个做成亮点就够了。关键是让老师觉得你有独立思考和扩展能力,这是毕设答辩拿高分的核心。

我个人的体会是:毕设选智能药箱系统,真正选中的是“一个能讲清楚完整业务闭环的Web应用”。它不炫技、不堆框架,但每一层都有实实在在的业务逻辑支撑。答辩的时候老师问技术细节,你天天在写这些代码,心里完全不慌。如果你正准备做这个课题,我的建议是先把数据库表设计想明白,再把提醒定时任务跑通,剩下的事情会顺畅非常多。最后分享一个小细节:演示之前一定准备好一批真实感强的测试数据,有近几个月的历史打卡记录、有临期药品、有不同分类的药品,数据越丰富,系统看起来越完整,老师的第一印象就会好很多。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进