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

资讯详情

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

SSM+Vue实战:新型药物临床药品治疗方案信息管理系统开发

SSM+Vue实战:新型药物临床药品治疗方案信息管理系统开发 接到“新型药物临床药品治疗方案信息管理系统”这个需求时对方负责对接的同事直接甩过来一份共享表格——里面是过去一年用 Excel 管出来的临床药品台账。一打开我就知道问题不小同一个受试者的用药方案分散在好几个 Sheet 里方案版本改了三轮表格里看不出哪个版本生效药品批次信息没有和患者入组时间关联想知道某批药用到哪些人身上只能人工在“批号”列里一个个搜。这个系统到底是什么说白了它是给从事新型药物临床研究的管理人员、研究医生和药剂师用的一个内部管理平台核心是把药品信息、治疗方案、受试者随访、入组进度这四块业务从纸质记录和 Excel 中搬进数据库用系统来约束流程、留痕操作、提高数据的可追溯性。这类系统的难点不在于技术多前沿而在于业务边界非常明确。如果你直接问“我要不要做药品库存管理”答案不是简单的是或否而是要理解临床场景下药品管理的特殊规则试验用药不能像普通药品那样随意入库出库它要关联到具体的临床试验项目、受试者编号、用药方案版本每一步操作都要能追溯。这个认知差异会直接影响后面的数据库设计和业务流程编写。技术选型方面后端用 SSMSpring SpringMVC MyBatis前端用 Vue。这套组合在今天的 Java 开发圈不算新潮但在医疗信息化、政企类项目里存量非常庞大招聘容易、运维团队熟悉、遇到问题网上资料一抓一大把。如果你正打算用 SSM Vue 做类似的业务管理系统或者正在做一个临床药品相关的毕业设计、接单项目这篇文章能给你一套完整可参考的项目拆解从业务流程梳理到数据库设计从后端接口实现到前端联调再到部署验收我都会把实际做下来的思路和踩过的坑讲清楚。1. 接这个项目之前我先理清了临床药品管理的业务现场很多从代码入手的人会犯一个错误上来就建表、写接口结果做到一半发现业务逻辑根本撑不起来。我拿到这个项目后先花了整整两天去理解业务而不是直接打开 IDEA。原因很简单新型药物临床研究的数据关联复杂度远高于传统的进销存系统。药物临床试验有一套通用流程方案设计、伦理审批、受试者筛选、知情同意、入组、分配药物、定期访视、记录不良事件、数据统计。每一个环节都会产生数据而这些数据之间是强关联的。1.1 为什么传统的Excel管理扛不住新型药物临床研究我接触过不少做临床数据管理的用户他们的第一反应往往是“现有表格也能用”。确实在样本量小、项目周期短的时候Excel 能覆盖基本记录需求。但新型药物的临床研究有几个特征正好是表格工具的短板。数据关联复杂是第一个问题。一个受试者要经历筛选期、入组、治疗期、随访期每个阶段会对应不同的药品、剂量、访视记录。散落在多个 Sheet 中的数据很难维护一条完整的事件链。比如需要回答“这个患者入组后用了哪几个批次的药、每个批次还剩多少”Excel 要跨好几个文件折腾。方案版本变化频繁是第二个问题。一类创新药物在试验过程中会根据阶段性数据分析调整给药方案。方案改动了历史数据还得保留后续新入组受试者要用新方案这实际上是一个版本管理问题Excel 做版本管理非常笨重经常出现“改来改去最后不知道哪版是生效的”这种情况。操作留痕要求高是第三个问题。临床数据讲究原始数据可溯源谁在什么时间录入了什么内容、修改了哪条记录都需要留痕。表格的修改记录很难做到细粒度审计。而且一旦多人同时编辑误改、覆盖、误删都是常有的事。1.2 系统的三类用户和各自的核心诉求在设计功能模块之前先把用户角色拆清楚很关键。我最后在这个系统里设计了四种角色系统管理员、项目管理员、研究医生、药剂师。每一种角色关注的数据维度不一样这直接影响菜单设计和接口权限。系统管理员最关注账号、角色、菜单权限和操作日志不掺和业务数据。项目管理员是整个业务的统筹者最关心项目总进度、入组人数、方案版本变更记录、用药计划执行情况。研究医生负责受试者筛选、制定或调整治疗方案、记录不良事件是核心业务数据的生产者。药剂师则负责试验药物的接收、入库、发药、库存盘点关注药品批次和库存周转。角色划分清楚了后续的权限设计就顺理成章了。比如研究医生可以查看患者的完整治疗信息但不能看到药品库存的成本数据药剂师能看到发药记录但不能编辑治疗方案内容。这些边界必须在需求阶段就定清楚否则做到联调阶段再去改权限代价会非常大。2. SSMVue这套技术组合在医药管理系统里凭什么还能打技术选型阶段对方给了一个基本约束后端用 SSM前端用 Vue。说实话这个选型一眼就能看出来是从“有现成团队技术积累”出发的。SSM 在今天不算新潮但在很多企业内网环境里它的存量非常庞大而且这类系统通常不追求极致的并发性能更看重稳定、可控、易维护。2.1 后端为什么选SSM而不是Spring Boot这里要解释一下为什么现在做新系统还在用 SSM。很多刚入行的开发者会觉得 SSM 已经过时了应该用 Spring Boot。但从实际项目交付的角度看SSM 依然能打有几个很现实的原因。团队技能匹配是最关键的一点。接手的团队长期维护老项目SSM 的结构和配置习惯已经形成肌肉记忆强行上 Spring Boot 反而会增加沟通成本和维护负担。服务器环境兼容性也很重要。有些医院或研究机构的内网环境比较旧JDK 版本可能还停留在 1.8 甚至更低Tomcat 也可能是老版本。SSM 打包成 war 丢进现有 Tomcat 就能跑迁移成本低。另外 SSM 的 XML 配置虽然啰嗦但每一项配置都是显式的出了问题按图索骥定位很快。Spring Boot 的自动配置在企业内网环境里反而容易因为依赖版本差异踩坑。当然SSM 的缺点也很明显配置繁琐、依赖管理要靠 Maven 手动折叠、项目结构不统一。所以在项目里我做了两件事来规避一是用 Maven 统一管理依赖版本二是把 Spring、SpringMVC、MyBatis 的配置拆分成独立的配置文件并加上详尽注释方便后续维护的人快速接手。2.2 前端为什么用Vue3Element Plus页面和接口怎么分工前端选型我用了 Vue3 Vite Element Plus。如果用 Vue2 Element UI 也是完全可行的但考虑到这个项目生命周期会比较长以后可能要在 Vue3 生态里扩展功能干脆一步到位。Vue3 的组合式 API 在处理复杂表单数据、跨组件状态共享时明显比选项式 API 舒服代码组织更接近普通函数逻辑可读性也更好。前端和后端的分工很明确后端只负责提供 JSON 接口前端负责页面结构、交互、表单校验和状态管理。这种前后端分离的架构好处是前端开发和后端开发可以并行推进坏处是对接口约定要求很高。我在项目启动时先和后端统一了一套接口规范所有列表接口返回{ code, message, data }结构分页参数统一用pageNum和pageSize日期时间统一用yyyy-MM-dd HH:mm:ss字符串传输。这样约定到位联调阶段会省掉大量扯皮。3. 从药品到治疗方案再到入组随访核心模块的边界怎么划拿到需求后我没有立刻写代码而是先把业务流程画了一遍。这个项目的业务主线可以这样表述围绕一个临床试验项目管理试验药物、方案模板、受试者入组、用药执行和随访记录。下面按模块拆解边界这部分决定了整个系统的骨架。3.1 药品管理基础资料、审批状态、批次库存药品管理模块看起来像一个普通的药品增删改查但实际要复杂一些因为“试验药物”和“普通药品”的经营逻辑完全不同。我把它拆成两个层次。药品基础资料记录的是静态信息药品的通用名、商品名、剂型、规格、生产厂家、批准文号或试验药物编号、药物类别试验组/对照组/安慰剂、储存条件等。药品批次与库存则负责动态流转每次接收到一批试验药物要记录批号、生产日期、有效期、到货数量、接收人和接收时间。发药时则要记录消耗到哪个受试者身上剩余库存实时更新。这里有一个非常容易被忽略的点试验药物往往存在盲态管理需求。双盲试验里药师知道某位受试者用的是试验药还是安慰剂但研究者不能知道。所以系统里要对药盒编号和实际药品的映射做权限控制列表展示时不能把盲底信息直接显示出来。这个需求我放在了“角色可见字段”里控制药剂师角色能看到发药记录研究医生角色只能看到受试者编号和用药计划看不到具体药品的盲底分组。3.2 治疗方案管理方案模板、版本快照、剂量调整记录治疗方案是整个系统的中枢。方案基础字段包括方案名称、方案编号、适用的临床试验项目、治疗周期、给药途径、给药频次、单次剂量、剂量单位、疗程时长等。这些字段看起来像普通表单但它们之间是有业务约束的比如给药频次和单次剂量的组合决定了一个给药周期内的总用量而总用量又会影响药品库存的备货计划。治疗方案最容易出问题的地方是版本变更。试验方案不是一成不变的研究进行到中期数据安全监查委员会可能会建议调整剂量。如果方案变更后已经入组和开始治疗的受试者还要继续按旧方案执行而新入组受试者按新方案执行那么系统必须支持方案版本快照。我的做法是设计了一张treatment_plan_version表每次编辑方案并提交审核后当前版本号加 1旧的版本内容整体保存为历史快照受试者入组时绑定的是当时生效的方案版本。这样即使方案后面改了历史数据依然准确可追溯。这个设计思路其实和配置中心的版本管理很像核心原则是“不改历史只追加新版本”。3.3 受试者管理、入组与随访、不良事件受试者管理是临床上最敏感的部分涉及隐私和伦理要求。我在设计时做了两个层面的考虑。第一是数据脱敏展示。受试者列表默认只展示受试者编号、性别、年龄组、入组日期姓名需要点击详情并通过权限校验后才能查看。列表接口在数据层直接做了字段过滤尽量让最少量的敏感信息流到前端。第二是流程记录。受试者从筛选开始有筛选记录、入组记录、每次访视的记录、用药执行记录、不良事件记录这块我建了一套以入组记录为主表、多个从表挂接的数据结构。不良事件记录要单独提一下。临床试验中的不良事件记录本身就有一套规范事件名称、开始日期、严重程度分级、与试验药物的关系判断、处理措施、转归情况。系统里可以直接把这几个字段做成表单同时把“导致退出试验”“导致剂量调整”做成一键标记方便后续汇总统计。这个模块虽然只是一个表单但字段设计必须严谨因为后续统计分析全靠它。4. 数据库设计临床数据结构化是系统最不能省的一步数据库是这个系统的地基。我见过太多项目因为表结构设计粗糙后面写业务代码时不停加班改 SQL。临床药品管理系统最忌讳的就是“一张大宽表存所有业务”一会儿加个字段一会儿拆个表没两天就失控了。这里我把核心表结构的设计思路和字段规划分享出来。4.1 权限体系的三张核心表SSM 项目的权限体系通常用 RBAC基于角色的访问控制模型核心是五张表用户表、角色表、菜单表、用户角色关联表、角色菜单关联表。用户和角色、角色和菜单之间都是多对多关系。表名核心字段说明sys_userid, username, password, real_name, status, create_time用户登录账号密码使用 BCrypt 加密不存明文sys_roleid, role_name, role_code, description角色编码如 ADMIN、PROJECT_MANAGER、DOCTOR、PHARMACISTsys_menuid, parent_id, menu_name, path, component, perms, menu_type菜单与权限标识关联按钮级权限也挂在这里sys_user_roleuser_id, role_id用户和角色的关联sys_role_menurole_id, menu_id角色和菜单的关联为什么密码要用 BCrypt 而不是 MD5因为 MD5 是哈希算法不加盐时很容易被彩虹表破解即使加了固定盐同一密码的哈希值也完全相同安全性不够。BCrypt 自带随机盐同一个密码每次加密得到的字符串都不一样而且可以调整计算强度是目前做密码存储比较推荐的方式。菜单表的perms字段很重要它对应一个权限标识字符串比如drug:add、drug:edit、plan:audit。后端接口的拦截器在鉴权时会检查当前用户的所有角色对应的菜单权限集合里是否包含目标接口需要的perms这样就实现了接口级别的权限控制。4.2 药品、方案、受试者主表设计要点业务主表我分成四块药品、方案、受试者、业务过程记录。药品相关表有drug_info和drug_batch。drug_info是药品基础信息表包含 drug_code药品编码、drug_name、generic_name、specification、manufacturer、drug_type试验药/对照药/安慰剂、storage_condition、status草稿/审核通过/停用。drug_batch是药品批次表包含 batch_no批号、drug_id、quantity本次接收数量、remaining_quantity剩余库存、production_date、expire_date、receiver、receive_time。方案相关表有三张。treatment_plan是方案主表包含 plan_code方案编号、plan_name、project_id关联临床试验项目、approval_status草稿/待审核/已通过/已停用。treatment_plan_version是方案版本表包含 plan_id、version_no版本号从 1 开始递增、plan_content方案文本快照、total_cycles治疗周期数、status生效中/历史版本、create_time。plan_item是方案明细表包含 plan_version_id、drug_id、dosage单次剂量、dosage_unit、frequency给药频次、route给药途径、duration_days疗程天数。受试者相关表有patient_info、visit_record、adverse_event。patient_info是受试者主表包含 patient_code受试者编号、name、gender、birth_date、id_card身份证号加密存储、phone、enrollment_date、project_id、plan_version_id入组时绑定的方案版本、status筛选期/治疗期/随访期/完成/退出。visit_record是访视记录表包含 patient_id、visit_no第几次访视、visit_date、visit_type、生命体征字段、record_doctor_id、description。adverse_event是不良事件表包含 patient_id、event_name、start_date、grade严重程度分级、relation_to_drug与试验药物关系、action_taken、outcome、is_serious是否严重不良事件。这里要重点说一下patient_info表里的plan_version_id和id_card。临床数据讲究原始性和可追溯性受试者入组之后执行的方案就应该是入组那一刻方案版本的快照后面改方案不影响这条历史记录。身份证号属于敏感个人信息我在存储时用了 AES 加密列表页默认不返回该字段只有详情接口在权限通过时才解密返回。虽然会增加一点代码量但这是合规要求必须做。4.3 业务过程记录和时间字段的设计习惯临床系统的数据完整性还有一个重要抓手就是时间字段。我的设计习惯是每张业务表都带create_time、update_time两个字段create_time在插入时由 MyBatis 自动填入当前时间update_time在更新时利用数据库的ON UPDATE CURRENT_TIMESTAMP自动刷新。这样后面做“最近操作记录”和审计追溯时不用额外写逻辑。另外所有涉及“操作人”的记录比如发药记录、审核记录、不良事件上报都要单独带一个operator_id或create_by字段。原因很简单管理端系统一旦出现问题必须能定位到是谁在什么时间干了什么。这个在需求方验收时通常也是必查项我见过不少系统一开始没设计这个字段后面上线了被要求补审计日志改动成本非常高。5. 后端SSM落地登录鉴权、分页查询、事务处理的写法数据库设计完成后后端开发就相对快了。SSM 项目的开发节奏很固定先配好 Spring、SpringMVC、MyBatis 三份配置文件然后从实体类、Mapper 接口、Mapper XML、Service、Controller 一层一层往上写。下面分享几个关键实现特别是登录鉴权和并发控制这两块是这类系统最容易出问题的地方。5.1 SSM的配置文件怎么组织一个规范的 SSM 项目resources 目录下的核心配置一般是这样组织的resources/ ├── jdbc.properties ├── spring-mybatis.xml ├── spring-mvc.xml ├── mybatis-config.xml └── mapper/ ├── SysUserMapper.xml ├── DrugInfoMapper.xml ├── TreatmentPlanMapper.xml └── ...spring-mybatis.xml负责数据源、事务管理器、MyBatis 的 SqlSessionFactory 等。核心片段如下context:property-placeholder locationclasspath:jdbc.properties/ bean iddataSource classcom.alibaba.druid.pool.DruidDataSource property namedriverClassName value${jdbc.driver}/ property nameurl value${jdbc.url}/ property nameusername value${jdbc.username}/ property namepassword value${jdbc.password}/ /bean bean idsqlSessionFactory classorg.mybatis.spring.SqlSessionFactoryBean property namedataSource refdataSource/ property namemapperLocations valueclasspath:mapper/*.xml/ property nameconfigLocation valueclasspath:mybatis-config.xml/ /bean bean classorg.mybatis.spring.mapper.MapperScannerConfigurer property namebasePackage valuecom.example.ssm.mapper/ /bean bean idtransactionManager classorg.springframework.jdbc.datasource.DataSourceTransactionManager property namedataSource refdataSource/ /bean tx:annotation-driven transaction-managertransactionManager/这里有两个细节值得注意。数据源我用的是 Druid因为它自带监控页面后端起一个监控 servlet 就能看 SQL 执行情况和连接池状态对排查慢 SQL 很有帮助。事务管理在 SSM 中必须显式配置Service 层的Transactional注解才有效。很多人 SSM 写完后发现方法里某一个 SQL 失败不回滚八成就是漏了tx:annotation-driven这一行。spring-mvc.xml负责包扫描 Controller、配置视图解析器、静态资源映射、JSON 转换mvc:annotation-driven message-converters bean classorg.springframework.http.converter.json.MappingJackson2HttpMessageConverter/ /message-converters /mvc:annotation-driven context:component-scan base-packagecom.example.ssm.controller/JSON 转换是前后端分离项目的关键。SpringMVC 返回对象时通过MappingJackson2HttpMessageConverter自动序列化为 JSON 字符串这个转换器内部已经集成了 Jackson不需要额外导入太多依赖。控制层方法只需要加ResponseBody注解。5.2 token登录鉴权的实现SSM 项目做登录鉴权最经典的方案是 Session但前后端分离后我更推荐 Token。登录成功后后端返回一个 token 字符串前端每次请求在请求头带上Authorization: Bearer token后端用拦截器校验。token 的生成我用的是 JWTJSON Web Token特点是自包含、无状态服务端不保存 session校验时只需要验签和解码就能知道用户身份和过期时间。具体流程是用户提交用户名密码Controller 接收后调用登录服务先用 BCrypt 校验密码再查用户对应的角色和菜单权限生成 JWT 并返回。前端把 token 存在 localStorageaxios 请求拦截器在每次请求头上带上。后端写一个AuthInterceptor继承HandlerInterceptorAdapter在preHandle里解析 token、校验有效期、查询用户权限集合并放入 ThreadLocal方便 Controller 直接取当前用户。拦截器配置要分清楚哪些路径放行、哪些拦截。登录接口、静态资源、错误页面放行其余/api/**都进拦截器mvc:interceptors mvc:interceptor mvc:mapping path/api/**/ mvc:exclude-mapping path/api/login/ mvc:exclude-mapping path/api/captcha/ bean classcom.example.ssm.interceptor.AuthInterceptor/ /mvc:interceptor /mvc:interceptorsJWT 生成代码可以做成一个工具类public class JwtUtil { private static final String SECRET your-secret-key; private static final long EXPIRE 1000 * 60 * 60 * 12; public static String createToken(Integer userId, String username, String roleCode) { return Jwts.builder() .setId(String.valueOf(userId)) .setSubject(username) .claim(role, roleCode) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() EXPIRE)) .signWith(SignatureAlgorithm.HS256, SECRET) .compact(); } public static Claims parseToken(String token) { return Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token).getBody(); } }这里要注意JWT 的 secret 不能硬编码在代码里甚至不能提交到 Git。实践上我是读环境变量或者放在服务器上的独立配置文件里。因为一旦 secret 泄露攻击者就可以伪造任意身份的 token整个鉴权体系就形同虚设了。权限校验方面除了登录拦截器还加了一个基于RequiresPermission(drug:add)自定义注解的接口级权限。实现上不复杂在拦截器中拿到当前用户的角色编码对照菜单表里的perms集合做一次 contains 判断即可没有权限就返回 403 状态的 JSON。5.3 动态SQL和分页查询MyBatis 的动态 SQL 是 SSM 项目里最常用的技能。比如药品列表的模糊查询、多条件组合查询XML 里可以这样写select idselectDrugList resultTypemap SELECT d.id, d.drug_code, d.drug_name, d.specification, d.manufacturer, d.drug_type, d.status, COALESCE(SUM(b.remaining_quantity), 0) AS total_remaining FROM drug_info d LEFT JOIN drug_batch b ON b.drug_id d.id where if testdrugName ! null and drugName ! AND d.drug_name LIKE CONCAT(%, #{drugName}, %) /if if testdrugType ! null and drugType ! AND d.drug_type #{drugType} /if if teststatus ! null and status ! AND d.status #{status} /if /where GROUP BY d.id, d.drug_code, d.drug_name, d.specification, d.manufacturer, d.drug_type, d.status ORDER BY d.create_time DESC /select这种写法把动态拼接条件的逻辑放在 XML 里Service 层只看参数SQL 的变化完全隔离在 mapper 层维护起来很舒服。分页我用的是 PageHelper 插件在查询前调用PageHelper.startPage(pageNum, pageSize)后面紧跟的查询会自动拼接 LIMIT查询结果里能直接拿到 total。需要提醒的是PageHelper 的分页是“就近生效”的也就是说startPage之后的第一个查询语句会被分页。如果方法里先执行了其他查询再执行目标查询分页就会作用到错误的语句上这是 PageHelper 最常见的坑解决办法是确保startPage紧贴目标查询之前。5.4 事务处理和库存扣减的并发控制发药操作涉及多个表的更新扣减药品批次剩余库存、新增发药记录、更新受试者用药状态。这三步要么全部成功要么全部失败所以必须加事务。Transactional(rollbackFor Exception.class) public void dispenseDrug(DispenseRequest request) { drugBatchMapper.decreaseRemaining(request.getBatchId(), request.getQuantity()); dispenseRecordMapper.insert(request); patientMapper.updateTreatmentStatus(request.getPatientId(), ONGOING); }并发问题在发药场景很典型两个药师同时给不同患者发同一批号的药如果只是简单“先查剩余数量再扣减”就会出现超发。解决方式很直接用数据库的乐观锁或者 update 语句的原子扣减。MyBatis 的 XML 里这样写update iddecreaseRemaining UPDATE drug_batch SET remaining_quantity remaining_quantity - #{quantity}, update_time NOW() WHERE id #{batchId} AND remaining_quantity #{quantity} /update关键在于 SQL 里的AND remaining_quantity #{quantity}条件。这条 update 语句本身是原子的数据库的行级锁会保证同一时刻只有一个事务能成功更新这条记录如果剩余库存不足影响行数为 0Service 层判断影响行数后直接抛异常事务回滚。这种方式比“查询→判断→更新”的流程安全得多也避免了单独引入分布式锁的复杂度。6. Vue3前端与API联调从登录页到数据报表的实现要点前端这块我用的是 Vue3 Vite Element Plus Pinia Vue Router。项目初始化用npm create vitelatest创建 Vue 模板然后安装依赖。对于不熟悉 Vue3 组合式 API 的开发者需要习惯script setup的写法组件里定义的变量和方法直接暴露给模板不用像选项式那样写data()和methods。这种写法在维护复杂表单页面时会感觉特别顺手状态和逻辑的跳转更直观。6.1 前端工程化和目录结构一个职责清晰的 Vue 项目目录大概长这样src/ ├── api/ # 接口调用封装 │ ├── request.js # axios 实例 │ ├── auth.js │ ├── drug.js │ └── plan.js ├── router/ # 路由配置 ├── store/ # Pinia 状态管理 ├── views/ # 页面组件 │ ├── login/ │ ├── dashboard/ │ ├── drug/ │ ├── plan/ │ └── patient/ ├── components/ # 通用组件 └── utils/ # 工具函数把接口调用按模块拆分到api/目录里是我强烈建议的做法。页面组件只负责展示和交互不直接写 axios 请求。接口地址变了只需要改api/下的文件接口返回结构变了也只需要调整请求封装或全局处理。这个习惯在项目后期维护时会省下大量时间尤其是当后端返回结构需要调整时不用一个页面一个页面去改。6.2 axios封装和路由守卫axios 封装的核心是拦截器。请求拦截器负责带上 token响应拦截器负责统一处理错误码和 token 过期。// api/request.js import axios from axios import { ElMessage } from element-plus import router from /router import { useUserStore } from /store/user const request axios.create({ baseURL: /api, timeout: 15000, }) request.interceptors.request.use(config { const userStore useUserStore() if (userStore.token) { config.headers.Authorization Bearer ${userStore.token} } return config }) request.interceptors.response.use( response { const res response.data if (res.code ! 200) { ElMessage.error(res.message || 请求失败) return Promise.reject(new Error(res.message)) } return res }, error { if (error.response error.response.status 401) { const userStore useUserStore() userStore.clearToken() router.push(/login) } ElMessage.error(error.response?.data?.message || 网络异常) return Promise.reject(error) } )这里有个很容易踩的坑ElMessage提示未定义的问题。很多人用 Element Plus 时直接在页面里引入import { ElMessage } from element-plus这是没问题的。但如果使用了 unplugin-vue-components 自动按需导入组件ElMessage 这类命令式组件可能不会被自动引入导致报“ElMessage is not defined”。解决方式是手动 import或者在自动导入插件里配置 ElementPlusResolver 同时设置importStyle: false然后手动引入样式。这两种方式我都验证过推荐手动 import至少在排查问题时少一层魔法。路由守卫的逻辑是未登录用户只能访问登录页已登录用户按角色动态生成可访问的菜单和路由。为了让菜单显示更自然我在前端路由里按模块静态配了所有页面但请求接口时会受后端权限控制菜单则根据登录后返回的menus数组动态渲染。这里没有做特别复杂的前端路由权限因为真正的权限判断在后端接口上前端只做展示层的过滤。这个取舍在中小型系统里足够用代码复杂度也低。如果你对 Vue Router 的路由守卫和参数传递不太熟建议先搞清楚beforeEach的回调顺序以及query和params传参的差异这两个点是面试和实际开发都喜欢问的。6.3 数据报表和文件导出报表页是项目验收时最受关注的页面之一。我做了一个入组进度统计页左侧显示各项目入组人数的柱状图右侧显示各类药品的使用情况折线图。绘图库选了 ECharts和 Vue 没有强绑定通过echarts.init手动操作 DOM 实例。报表数据是通过后端接口返回的汇总数据前端把数据传给 ECharts 的 series 即可。代码不复杂但有一个必须注意的点图表容器在 Vue 中可能还没有渲染完成必须在onMounted之后初始化否则会拿到宽度为 0 的容器图表显示不出来。推荐加一个nextTick再初始化。另外如果页面里有 Tab 切换切回图表页时容器宽度可能从隐藏状态恢复需要调用chart.resize()重新计算尺寸。文件导出这块我用了两种方案一种是后端生成 Excel 文件返回二进制流前端用 Blob 下载另一种是纯前端表格数据用 SheetJS 客户端生成 Excel。库存台账和发药记录这类数据量大的导出用后端导出更稳避免前端拉取大量数据卡死页面。核心代码async function exportExcel(params) { const res await request({ url: /drug/export, method: get, params, responseType: blob }) const blob new Blob([res.data], { type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet }) const link document.createElement(a) link.href URL.createObjectURL(blob) link.download 药品库存台账_${Date.now()}.xlsx link.click() URL.revokeObjectURL(link.href) }这里同样有一个容易踩的坑如果请求被 axios 响应拦截器统一处理而后端返回的是文件二进制流而不是 JSON拦截器里不能强行JSON.parse否则会报错。所以导出接口要单独跳过 JSON 处理逻辑直接返回完整 response再手动处理 blob。7. 联调阶段踩过的坑跨域、时间格式、并发扣库存前后端分离项目联调阶段一定会遇到一堆“单测都正常一联调就炸”的问题。这一部分是最有参考价值的我把这次项目里实际踩过的坑按排查链路列出来希望能帮你少走弯路。7.1 跨域问题前后端分离第一个坎开发环境下Vite 默认跑在 5173 端口后端 Tomcat 跑在 8080 端口必然跨域。最简单可靠的方案不是在后端加 CORS 注解而是在 Vite 配置文件里做代理把/api前缀的请求转发到后端。// vite.config.js export default defineConfig({ server: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true, rewrite: path path.replace(/^\/api/, ) } } } })这样浏览器里所有请求都指向同源地址代码里不用处理跨域问题。生产环境部署时再用 Nginx 做同样的反向代理。这种方式比后端开 CORS 更干净因为线上如果前端静态文件和后端跑在同一个域名下代理配置也更好维护还避免了 CORS 预检请求带来的额外开销。7.2 MyBatis字段映射与时间时区列表页查出来的字段名和数据库列名对不上是最常见的低级错误。MyBatis 默认不会自动把数据库下划线字段映射成驼峰属性需要手动开启map-underscore-to-camel-case: true。开启前drug_code查出来是 null开启后自动映射到drugCode。这个配置我几乎每个项目都会写但每次接手新项目都要检查一遍。时间字段的坑更隐蔽。数据库 datetime 类型通过 JDBC 读取后转换为java.util.Date再经 Jackson 序列化时如果没有配置时间格式默认输出的是时间戳或国际标准格式前端显示出来会和小地时间差 8 小时。解决方式是统一在 Jackson 配置里设置日期格式Bean public Jackson2ObjectMapperBuilderCustomizer jacksonCustomizer() { return builder - { builder.simpleDateFormat(yyyy-MM-dd HH:mm:ss); builder.timeZone(TimeZone.getTimeZone(GMT8)); }; }前端展示层也统一处理所有时间字段用 Element Plus 的日期选择器配合value-formatYYYY-MM-DD HH:mm:ss传参列表展示用 dayjs 格式化。前后端约定的时间格式在联调阶段能省掉大量不必要的沟通。7.3 并发扣库存问题的实际排查过程这个坑是在测试阶段才暴露的。测试人员同时用两个账号对同一批号的药品发起发药请求本来库存只剩 5 支两个请求同时成功最后剩余库存变成了 -3。排查链路是这样的。第一步先看后端日志确认两个请求都通过了“剩余库存大于 0”的判断。问题就出在“查询→判断→更新”这个流程不是原子的两个事务可能同时读到remaining_quantity 5都判断可以发然后都执行 update后提交的事务覆盖了前一个事务的结果。第二步我一开始想用悲观锁给药品批次表加SELECT ... FOR UPDATE锁行。但这样要额外引入锁等待和超时处理而且发药业务本身耗时很短锁竞争不大有点小题大做。第三步最后决定用条件更新也就是第 5 部分展示的UPDATE drug_batch SET remaining_quantity remaining_quantity - #{quantity} WHERE id #{batchId} AND remaining_quantity #{quantity}。这条 SQL 能保证扣减操作的原子性影响行数为 0 时抛异常回滚。改造完再跑并发测试剩余库存没有再出现负数。这类问题的排查思路核心是先判断操作的原子性再考虑加锁的范围和粒度。能用原子 SQL 解决的就不要滥用悲观锁锁范围越大越容易把系统拖慢尤其是在这种管理后台系统里绝大多数操作的并发量其实很低用乐观锁和条件更新已经足够。7.4 token过期和401循环跳转联调阶段还有一个很烦的问题接口返回 401 后前端响应拦截器跳转登录页但跳转后的页面又发了一个需要认证的请求再次 401陷入死循环。排查后发现是路由守卫和响应拦截器互相打架响应拦截器跳转/login时路由守卫里又判断“当前没有 token 就 redirect 到 /login”两个逻辑叠加导致重复跳转。解法是加一个标志位。在响应拦截器里先判断当前路由是否已经是/login如果是就直接停止否则清除 token 再跳转if (error.response?.status 401 router.currentRoute.value.path ! /login) { userStore.clearToken() router.push(/login) }再加上路由守卫里判断to.path /login时直接放行问题就解决了。这类问题看起来小但不理清楚跳转链路很容易卡半天。排查的时候建议在响应拦截器和路由守卫里各加一个 console.log看跳转的完整链路一眼就能发现问题在哪。8. 打包部署与验收war包部署、环境配置、测试清单开发和联调完成之后还有一个不容忽视的环节部署和验收。临床药品管理系统最终会部署在医院或研究机构的内网服务器上环境不是我们说了算的。提前把部署方式准备好验收会顺利很多。8.1 Maven打包war并部署到TomcatSSM 项目用 Maven 打包成 war命令很简单mvn clean package -DskipTests打包完成后target 目录下会有一个ssm695.war。把这个 war 直接丢到 Tomcat 的webapps/目录下启动 Tomcat 后它会自动解压部署。如果项目里配置了 context path访问路径就是http://服务器IP:8080/ssm695/。需要注意SSM 项目打包前要把jdbc.properties里数据库的地址、账号、密码替换成生产环境的。我一般会建一个application-prod.properties打包时通过-Pprod和 Maven Profile 切换避免手动改配置出错。这个习惯很重要因为真实环境里经常出现开发库和生产库数据不一致的情况一旦连接了错误的库后果很严重。8.2 生产环境需要注意的配置项配置这一块有几个容易忽略的点。数据库连接池的初始化大小和最大连接数要根据并发量调整。Druid 的initialSize5、maxActive20对这类管理后台是够用的但如果报表页需要跑聚合查询连接池配太小会导致偶发超时。文件上传大小限制也要关注。系统中导入了药品图片、试验方案 PDFSpringMVC 默认上传文件大小限制是 1MB需要专门在配置里调大。我遇到过几次需求方上传方案文档失败的情况都是默认限制导致的。定时任务也要考虑。如果一个试验项目要定期生成随访提醒、逾期未发药提醒可以用 Spring 的Scheduled写定时任务。但要注意定时任务不要写在 Controller 里单独抽成 Task 类并且生产环境要确认只部署了一份节点否则多节点会重复执行产生重复的提醒数据。8.3 验收测试清单最后整理一份验收测试清单经历过几次项目验收后我现在都会把这类清单作为标准交付物之一。登录功能正确账号能登录错误密码有提示token 过期后跳转登录页。权限控制不同角色登录后看到不同菜单直接输入无权限接口地址返回 403。药品模块新增、编辑、停用、批量导入、批次入库、库存扣减、库存预警。方案模块新增方案、版本变更、旧版本详情查看、方案审核流程。受试者模块入组流程、访视记录新增、不良事件上报、受试者详情数据脱敏。统计报表报表数据与列表明细能对上导出 Excel 无乱码、无缺失。并发场景两个并发发药请求扣库存不超发同一用户重复提交发药不产生两条记录。浏览器兼容Chrome、Edge、内网常用浏览器下页面样式和功能正常。这份清单不只是给测试人员的也是给自己排查用的。每通过一项就在后台对应模块打一个勾验收时直接按清单过一遍能省不少沟通时间。做完这个项目我最大的体会是像临床药品管理这样的业务系统最怕的不是技术实现而是对业务规则理解不到位。先把“谁在什么阶段用哪个版本的方案、发药怎么扣库存、权限怎么控制敏感信息”这些问题想清楚后面的代码只是按图索骥。如果你也在用 SSM 或 Vue 做类似的业务管理后台希望我整理的这些踩坑经验和设计思路能让你在动手之前心里更有底。
返回列表