尧图精选

基于SSM的药品电子商城系统:从架构设计到在线诊断闭环实践

🕒 发布时间:2026/9/25 19:24:43 📁 来源:尧图网络
做过电商系统的朋友都知道药品商城跟普通卖衣服、卖数码产品的商城根本不是一回事。它的背后牵涉到药品信息合规展示、库存批次跟踪、处方药销售管控、甚至还有远程问诊开方的需求。而SSM也就是Spring SpringMVC MyBatis这套经典组合在这种业务复杂度中等但事务性极强的系统里反而是非常合适的选择。几天前我把一个基于SSM的药品电子商城系统完整跑通了一遍从数据库设计到在线诊断模块的联调前前后后踩了不少坑。如果你正准备做类似项目或者正在用SSM写医疗类电商系统这篇文章应该能帮你少走很多弯路。先简单说说它到底是个什么东西它本质上是一个B2C药品销售平台叠加了在线问诊与电子处方流转能力核心用户是普通消费者和坐诊药师代码结构上按SSM标准三层架构来组织。适合用SpringMVC做控制层、MyBatis做持久层、Spring做业务事务管理的中小型团队或个人开发者作为参考。系统设计与架构选型思路1.1 为什么药品商城仍适合用SSM而非微服务近几年微服务架构被炒得很热动不动就Spring Cloud、Dubbo但对于一个药品电子商城加在线诊断系统而言我反而坚持用SSM单体架构理由其实不复杂。首先药品商城这类系统的核心痛点不在于并发量而在于事务一致性和业务流程的准确性。一次下单动作可能同时要扣减药品库存、锁定优惠券、生成订单快照、记录操作日志如果这几步操作被拆到多个服务里就得引入分布式事务成本和复杂度陡然上升。而在SSM单体架构下一个Spring管理的Service方法里数据库事务天然就覆盖了这几个操作要么全部成功要么全部回滚逻辑极容易把控。其次SSM技术栈非常成熟招人容易、文档多、排错简单。MyBatis的灵活SQL非常适合医疗电商里大量动态查询和复杂报表统计的场景SpringMVC的注解开发让接口层代码一目了然Spring的IoC与AOP做权限拦截和事务管理也非常顺手。整个项目不需要额外引入注册中心、配置中心、消息队列对中小型团队来说维护成本便宜得多。1.2 药品电商的核心业务流程拆解我最初接手这个系统时第一件事不是看代码而是先把药品销售的合规路径梳理清楚。一个常规的药品电子商城其核心业务流程大致如下用户注册登录后浏览药品目录查询药品时不仅要看价格还要区分处方药与非处方药非处方药可以直接加入购物车下单购买处方药则必须先发起在线诊断或问诊由药师或医师确认后开具电子处方有了处方后系统校验处方有效性并允许提交订单订单支付后进入后台审核仓库按批次拣货出库物流完成后用户可确认收货整个流程闭环在线诊断模块之所以和商城深度绑定是因为处方药销售必须建立医生/药师与患者的互动记录。系统里诊断记录与处方单据需要存留可追溯的电子档案包括患者主诉、药师建议、处方药名称、用法用量等。这块在设计时我特意做了独立的业务表而没有把它塞进订单表里目的就是方便后续扩展随访功能。1.3 模块划分与代码包结构设计SSM项目最怕的就是代码层层堆在一起时间长了连自己都找不到是哪一层出问题。我在设计这个系统时严格按MVC三层加模块分包主要模块包括用户模块普通用户、药师、管理员三种角色药品模块药品分类、药品信息、药品规格与批次库存购物车与订单模块购物车管理、订单生成、支付回调在线诊断模块问诊会话、诊断病历、处方单系统管理模块角色权限、操作日志、数据统计对应的Java包结构大致是这样com.medical.mall ├── controller # 控制层接收前端请求 │ ├── UserController │ ├── DrugController │ ├── CartController │ ├── OrderController │ └── DiagnosisController ├── service # 业务层核心事务逻辑 │ ├── impl │ └── ... ├── dao # 数据访问层MyBatis Mapper接口 ├── pojo # 实体类、Vo、Dto ├── common # 公共工具类、常量、统一结果封装 └── config # Spring、MyBatis、SpringMVC配置这样的分包方式让我在开发在线诊断模块时可以直接复用用户管理和药品管理的基础能力同时又能保证业务隔离避免一个模块的改动牵连到其他模块。特别是涉及处方药这类敏感业务时独立模块也方便日后单独做权限收紧。数据库建模与核心表设计2.1 药品信息表的设计细节药品商品信息和普通商品不同它在建表时需要更多的字段来承载合规要求。我的t_drug表设计时除了基础的药品名称、通用名、生产厂家、批准文号之外还包含了剂型、规格、单位、存储条件、有效期、处方类型等字段。其中比较关键的几个字段有drug_type区分处方药与OTC前端展示时用来控制购买流程approval_number药品批准文号如国药准字H开头代表化学药品、Z代表中成药这个字段必须唯一stock_batch批次号对应仓库里实际入库的批次expiry_date有效期截止日期订单创建时必须校验require_diagnosis是否需要在线诊断通由drug_type推导但保留冗余字段建表时还有一个容易忽略的地方——药品图片与说明书附件不能直接存在主表里。我是用了单独的附件表来存储图片、PDF说明书等文件URL用drug_id关联这样查询药品列表时不会因为读取大字段拖慢接口速度。药品表的DDL核心部分大致如下CREATE TABLE t_drug ( id bigint(20) NOT NULL AUTO_INCREMENT, drug_code varchar(50) NOT NULL COMMENT 药品唯一编码, drug_name varchar(100) NOT NULL COMMENT 药品名称, generic_name varchar(100) DEFAULT NULL COMMENT 通用名, approval_number varchar(50) DEFAULT NULL COMMENT 批准文号, drug_type tinyint(4) NOT NULL COMMENT 1-处方药 2-OTC, dosage_form varchar(50) DEFAULT NULL COMMENT 剂型, specification varchar(100) DEFAULT NULL COMMENT 规格, unit varchar(20) DEFAULT NULL COMMENT 单位, price decimal(10,2) NOT NULL COMMENT 销售单价, stock_total int(11) NOT NULL DEFAULT 0 COMMENT 库存总量, require_diagnosis tinyint(1) NOT NULL DEFAULT 0 COMMENT 是否需诊断, status tinyint(4) NOT NULL DEFAULT 1 COMMENT 上下架状态, create_time datetime DEFAULT CURRENT_TIMESTAMP, update_time datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_drug_code (drug_code), KEY idx_drug_type (drug_type), KEY idx_status (status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;2.2 库存批次模型药品库存为何不能只存一个总数在做一个普通商品库存时可能一个stock字段就搞定了但药品库存一定要拆成批次来管理。原因很简单不同批次的药品生产日期和有效期不一样同一个药品不同批次到期时间不同的情况非常常见。如果只用一个总数拣货时判断近效期药品完全无从下手。为此我设计了t_drug_stock_batch表以batch_no为维度记录每个批次的入库数量、剩余数量、生产日期、有效期。订单发货时系统优先拣取有效期最近的批次也就是“先进先出”而到了更新库存时则需要同时更新批次表的剩余数和药品表的总库存数。这个过程我在事务里做保证两边数据一致。用了批次模型以后入库操作也变得清晰了。每次采购入库只需向批次表插入一条记录并累加药品主表库存每次用户下单则需要在事务里预占库存我这版用的是乐观锁方式更新批次剩余量时加WHERE remaining_qty #{qty}受影响行数为0则说明并发冲突重新尝试或提示用户库存不足。2.3 在线诊断与处方单的数据结构在线诊断模块的数据模型是我整个项目里花时间最多的部分因为它直接关系到合规性。诊断主表t_diagnosis记录一次问诊会话的基本信息包括患者用户ID、接诊药师ID、问诊状态、症状描述、初步诊断结果、创建时间。一次诊断会话可以对应多条消息记录也可以生成一张或多张处方单。处方单表t_prescription设计时我特意包含了处方号、诊断ID、患者ID、药师ID、处方类型、用法用量描述、有效天数、开方时间、过期时间、处方状态。这里有一个非常关键的字段是处方有效期。按照常规实践电子处方一般有效期为3天过期后系统应自动作废不允许用户继续用它结算购药。我在订单提交时做了一个校验方法专门判断处方是否处于有效期内。处方明细表t_prescription_item则以药品为粒度记录每种药的名称、剂量、用药频次、用药天数、总数量。处方明细与商城药品主表并不直接强关联外键因为处方历史是归档数据即使药品后来下架了也要保留当时的开方内容。这也是我在设计上坚持基本不动用物理外键的原因——历史数据完整性比关系完整性更重要。核心功能实现从药品检索到在线开方3.1 药品分类检索与条件筛选实现药品检索页的筛选条件往往比普通商品复杂。用户可能需要按药品种类中成药、化学药、保健品、按处方类型处方药、OTC、按疾病症状感冒、发烧、肠胃甚至按厂家来筛选。如果用普通拼接SQL方式写死后期扩展极为痛苦这里MyBatis的动态SQL就成了利器。我在Mapper中采用where标签加if标签动态拼条件比如select idsearchDrugs resultTypecom.medical.mall.pojo.Drug SELECT d.id, d.drug_code, d.drug_name, d.generic_name, d.specification, d.unit, d.price, d.drug_type, d.require_diagnosis, d.factory_name FROM t_drug d where d.status 1 if testkeyword ! null and keyword ! AND (d.drug_name LIKE CONCAT(%, #{keyword}, %) OR d.generic_name LIKE CONCAT(%, #{keyword}, %) OR d.factory_name LIKE CONCAT(%, #{keyword}, %)) /if if testdrugType ! null AND d.drug_type #{drugType} /if if testcategoryId ! null AND d.category_id #{categoryId} /if if testminPrice ! null AND d.price #{minPrice} /if if testmaxPrice ! null AND d.price lt; #{maxPrice} /if /where ORDER BY d.id DESC /select这里需要注意MyBatis中使用小于号时记得转义为lt;尤其写在XML里否则解析会直接报错。这种情况我在改生产SQL时碰到不止一次调了半天最后发现是特殊符号转义问题相当浪费时间。3.2 购物车与订单提交事务边界如何划购物车加购与下单流程中最容易出错的就是库存扣减与订单生成的原子性。我的处理方式是把购物车选中商品生成订单的过程做成一个Service方法加上Transactional注解内部依次做几件事——校验每个药品的上架状态、校验处方药是否携带有效处方、预扣库存、生成订单主表和明细表、清空购物车对应项。任何一步失败整个方法回滚。具体实现片段如下Transactional(rollbackFor Exception.class) public Order createOrder(OrderCreateDTO dto, Long userId) { // 1. 校验购物车项目 ListCartItem items cartMapper.selectSelectedItems(dto.getCartItemIds(), userId); if (CollectionUtils.isEmpty(items)) { throw new BusinessException(购物车选中项为空); } // 2. 校验药品状态及合规信息 for (CartItem item : items) { Drug drug drugMapper.selectById(item.getDrugId()); if (drug null || drug.getStatus() ! 1) { throw new BusinessException(药品不存在或已下架 item.getDrugName()); } if (drug.getRequireDiagnosis() 1) { // 处方药必须携带有效处方 Prescription pres prescriptionMapper.selectValidByUserAndId( dto.getPrescriptionId(), userId); if (pres null || pres.getStatus() ! 1) { throw new BusinessException(处方药必须凭有效处方购买); } } } // 3. 生成订单号并保存订单 String orderNo generateOrderNo(); Order order new Order(); order.setOrderNo(orderNo).setUserId(userId); order.setTotalAmount(calTotalAmount(items)); order.setStatus(0); // 待支付 orderMapper.insert(order); // 4. 保存订单明细 for (CartItem item : items) { OrderItem oi new OrderItem(); oi.setOrderId(order.getId()); oi.setDrugId(item.getDrugId()); oi.setDrugName(item.getDrugName()); oi.setPrice(item.getPrice()); oi.setQty(item.getQty()); orderItemMapper.insert(oi); } // 5. 预占库存并清空购物车 for (CartItem item : items) { int rows drugStockBatchMapper.deductStock(item.getDrugId(), item.getQty()); if (rows 0) { throw new BusinessException(库存不足 item.getDrugName()); } } cartMapper.removeByIds(dto.getCartItemIds(), userId); return order; }这里有一个细节值得单独说明事务方法内部如果catch了异常而不往外抛Spring的Transactional默认不会回滚。我一开始踩过这个坑后来定了一个团队规范——Service层只负责业务逻辑不吞异常所有异常在Controller层统一处理并转成前端可见的提示信息。这个用AOP可以做得很干净代码里也清爽不少。3.3 在线诊断的会话式交互与状态流转在线诊断模块我做成了一种类客服的会话模式。患者发起问诊请求后系统自动进入等待分配状态药师登录后台看到待接诊队列点击接诊后双方进入一对一会话。这里的核心是会话状态的流转管理我在t_diagnosis表里用status字段来表示0等待接诊、1问诊中、2药师已开方、3已完成、4已取消。患者发送消息时调用的Controller大致下面这样PostMapping(/api/diagnosis/send) public Result sendMessage(RequestBody DiagnosisMessageDTO dto, SessionAttribute(userId) Long userId) { Diagnosis diagnosis diagnosisService.getById(dto.getDiagnosisId()); if (diagnosis null || !diagnosis.getPatientId().equals(userId)) { return Result.error(会话不存在或无权限); } if (diagnosis.getStatus() ! 1) { return Result.error(当前会话不在问诊中); } diagnosisService.saveMessage(dto.getDiagnosisId(), userId, dto.getContent()); return Result.success(); }这段代码里有几个细节很容易被忽略。首先会话权限校验不只是判断用户是否登录还要判断当前用户是不是该会话的归属患者否则用户A把会话ID改成B就能看到B的病历信息这是医疗数据的大忌。其次状态校验放在Service边界之外只是控制层的快速失败策略真正严谨的判断必须在Service里再做一次。药师开方时系统支持三种操作开具处方、补充建议、结束会话。开具处方时会生成t_prescription记录并把诊断状态从问诊中改为“已开方”。开方后患者端可以立刻看到电子处方处方确认无误后就可以直接跳转购药。整个流程闭环做下来体验上比线下药店排队等候要顺畅得多。3.4 处方有效期校验与订单联动处方有效期是整个药品电子商城合规要求里特别容易忽略但特别重要的一环。我在t_prescription表设计时预留了effective_days和expire_time字段每次创建处方时自动计算过期时间。在订单提交Service里获取处方对象后还要再检查一下过期时间与当前时间的关系public Prescription checkValidPrescription(Long prescriptionId, Long userId) { Prescription prescription prescriptionMapper.selectById(prescriptionId); if (prescription null || !prescription.getPatientId().equals(userId)) { throw new BusinessException(处方不存在或与当前用户不匹配); } if (prescription.getStatus() ! 1) { throw new BusinessException(处方当前不可用); } if (prescription.getExpireTime().before(new Date())) { // 自动作废 prescriptionMapper.updateStatus(prescriptionId, 2); throw new BusinessException(处方已过期请重新问诊开方); } return prescription; }过期的处方自动作废这个逻辑我是在订单环节触发的而不是定时任务批量扫的。原因在于定时任务扫描有延迟如果用户正好卡在过期后的订单提交节点定时任务还没跑就会导致一张过期处方被继续使用。这里采用提交时实时校验并顺手作废的方式时效性更强。SSM各层协作与关键配置细节4.1 MyBatis Mapper接口与XML映射的协同SSM项目里MyBatis的使用是核心中的核心。我的习惯是Mapper接口只定义方法签名SQL写在对应的XML文件中原因很简单复杂SQL写在注解里会非常难看特别是有大量动态条件和多表关联时XML的include标签还能复用公共SQL片段比注解干净太多。这里有一个非常值得推荐的技巧——用MyBatis的sql标签把常用查询字段抽出来例如sql idBase_Column_List id, drug_code, drug_name, generic_name, approval_number, drug_type, dosage_form, specification, unit, price, stock_total, require_diagnosis, factory_name, status /sql然后用include refidBase_Column_List/去引用。这样每条SQL都不会因为字段过多导致可读性下降。另一个需要注意的点是结果映射如果表字段和下划线命名与Java驼峰命名不一致项目启动时一定要设置mapUnderscoreToCamelCase为true否则查出来的对象全是null以前调试这个问题时浪费了不少时间。4.2 Service层行为封装与事务控制Service层的设计原则我个人实践下来觉得有三条最重要。第一一个Service方法只做一个完整的业务动作不要把查询列表和下单分开又强行拼在一个事务里。第二事务边界要清晰读多写少的纯查询方法不需要加事务加了反而占用连接时间影响并发性能。第三Service内部调用同类中的其他方法时Transactional注解不生效这一点Spring代理机制决定跨类调用时切面才会介入。实际开发中我遇到过一件特别尴尬的事。当时我把库存扣减和订单生成放在两个不同Service类里在Controller里依次调用中间没有事务串联。结果库存更新成功后订单生成因为某个字段为空抛异常页面上提示下单失败但库存已经扣了。后来我把这两个操作挪到同一个Service类的方法里用Transactional(rollbackFor Exception.class)包裹起来才算彻底解决。这是一次代价不小的教训。4.3 SpringMVC拦截器与权限控制的落地药品商城有一个典型的权限控制场景普通用户不能访问药师后台药师不能操作管理员功能所有处方药购买流程用户必须完成在线诊断。这种场景用SpringMVC的拦截器做成统一验证是最合适的。我在spring-mvc.xml里配置了拦截器路径规则然后单独写了一个AuthInterceptor实现HandlerInterceptor接口。拦截器里从Session中获取当前登录用户根据请求URL前缀判断所需角色比如/api/admin/**要求管理员角色、/api/pharmacist/**要求药师角色。校验不通过时直接返回固定的JSON结果并中断请求。但要注意接口鉴权时千万不要只在拦截器里做角色判断因为拦截器的路径匹配规则很难覆盖所有URL变体Service层也要保留基础校验逻辑形成纵深防御。常见问题排查与性能优化实录5.1 MyBatis N1查询问题与结果集映射优化在线诊断模块开发到第二周时我遇到一个典型的性能问题。前端药师工作台需要展示每一个会话的最新一条消息预览我最初在循环里逐个查询会话的最后一条消息一次打开工作台就是几十条SQL接口响应时间慢得让人无法接受。排查思路很快就锁定到了N1查询上。解决办法是改用一条SQL按会话分组取最新消息。查所有会话基本信息和每个会话最新消息可以用子查询配合JOINSELECT d.*, m.content AS last_message FROM t_diagnosis d LEFT JOIN t_diagnosis_message m ON m.id ( SELECT msg.id FROM t_diagnosis_message msg WHERE msg.diagnosis_id d.id ORDER BY msg.create_time DESC LIMIT 1 ) WHERE d.status ! 4 ORDER BY d.create_time DESC这种方式在数据量不大的时候效率非常高而且避免了临时表带来的性能损耗。5.2 库存超卖问题乐观锁方案实测药品电商虽然不像秒杀系统那样有极端并发但遇到某个爆款药品促销时仍然可能产生并发下单。如果不做任何控制超卖几乎是必然的。我实现了一个非常经典的乐观锁做法在扣减批次库存的SQL上增加剩余量条件update iddeductStockByBatch UPDATE t_drug_stock_batch SET remaining_qty remaining_qty - #{qty} WHERE id #{batchId} AND remaining_qty #{qty} /update受影响行数等于0时说明该批次库存不足或已经有人抢先扣完直接抛出友好提示。这里我没有选择悲观锁SELECT ... FOR UPDATE因为药品商城整体并发并不高悲观锁会额外持有数据库连接锁资源而且需要特别注意事务的提交与释放顺序用乐观锁在大多数场景下更轻量。[\begin{lstlisting}[languagejava] // 伪代码片段说明乐观锁的调用流程 public void deductStock(Long drugId, Integer qty) { // 1. 查询有效期最近的批次 List batches stockBatchMapper.selectNearExpiryBatches(drugId); int totalDeducted 0; for (StockBatch batch : batches) { if (totalDeducted qty) break; int need Math.min(batch.getRemainingQty(), qty - totalDeducted); int rows stockBatchMapper.deductStockByBatch(batch.getId(), need); if (rows 0) continue; totalDeducted need; } if (totalDeducted qty) { throw new BusinessException(库存不足); } } \end{lstlisting}]5.3 在线诊断消息并发的幂等处理在线诊断的消息发送有一个容易被忽视的坑——患者连续点击发送按钮多次可能会因为前端没有及时禁用按钮而产生并发请求从而插入多条相同内容的消息。这个问题我一开始是在前端用loading标志解决但后来发现后端接口仍然需要做幂等控制兜底。我在消息表里加了一个client_msg_id字段由前端发送消息时生成一个唯一的UUID。后端插入消息时先检查这个client_msg_id是否已经存在如果存在则直接返回成功不重复插入。这样即使网络重传或者用户连点也不会产生重复消息记录。诊断与处方是严肃的医疗数据数据重复造成的后果远比普通聊天室严重。5.4 常见问题速查表问题现象根本原因解决方案查询药品列表返回null字段MyBatis驼峰映射未开启在mybatis-config.xml设置mapUnderscoreToCamelCasetrue事务方法中异常后数据未回滚Service方法内部catch了异常未向外抛出统一由Controller处理异常Service层不吞异常库存扣减出现负数扣减SQL未加剩余量条件使用乐观锁remaining_qty #{qty}条件处方药订单提交被拒处方已过期未做实时校验订单提交时实时检查过期时间并自动作废处方会话列表接口响应慢N1查询导致SQL过多使用子查询关联最新消息一次查出列表数据前端连点导致重复消息缺少幂等控制消息表增加client_msg_id唯一标识做去重拦截器放行了不该访问的接口路径匹配规则不够严谨增加更精确的URL前缀匹配Service层保留校验5.5 在线诊断轮询改WebSocket的实践最初在线诊断消息的获取方案我用的是前端每隔3秒轮询一次接口看有没有新消息。这种方式胜在实现简单但问题也不少。首先是轮询会产生大量无效请求药师端同时打开多个会话时服务器资源被白白消耗。其次是消息到达有最长3秒的延迟体验不够实时。后来我引入了WebSocket在SpringMVC配置类里注册一个TextWebSocketHandler按用户ID维护WebSocket会话集合。药师接诊、患者发消息、系统通知开方结果这些事件都通过WebSocket推送给对端接口从主动查询变成了事件通知。改造后效果很明显消息延迟几乎降到零服务器的HTTP请求压力也大幅下降。需要注意的是WebSocket连接要处理好登录鉴权问题我在握手拦截器中从查询参数里提取token并校验用户身份防止未授权连接。部署经验与扩展思考6.1 传统SSM项目的打包部署流程很多项目即使开发完了部署环节照样会让人焦头烂额SSM项目也不例外。我的部署流程一般分四步走在pom.xml里配置maven-war-plugin用mvn clean package打出war包把war包丢进Tomcat的webapps目录SSM传统方案基本都是这个套路修改jdbc.properties中数据库地址和账号密码保证连接的是生产库而不是本地库启动Tomcat后查看catalina.out日志确认Spring容器初始化成功这里有一个很关键的操作细节数据库连接信息抽取到独立的jdbc.properties文件中并在Spring配置里通过context:property-placeholder引入严禁硬编码在Java类中。否则每次环境切换都要重新编译代码非常痛苦。考虑到现在容器化越来越普及我也尝试过把TomcatSSM打成Docker镜像流程也不算复杂就是把war包拷贝到Tomcat镜像的webapps目录里启动时通过环境变量注入数据库连接参数迁移环境极其方便。6.2 从SSM向Spring Boot演进需要注意什么说实话如果让我从零开始重新做这个药品商城系统我大概率会直接用Spring Boot而非传统SSM。Spring Boot的自动配置和起步依赖让项目搭建节省大量时间内嵌Tomcat也省去了部署的烦恼。但这并不意味着SSM这套技术栈就过时了恰恰相反我的经验是先理解SSM的三层架构和核心思想再上手Spring Boot会事半功倍。MyBatis的操作方式基本不变SpringMVC的注解也完全兼容升级到Spring Boot更多是配置方式与依赖管理的转变。如果你手头这个SSM项目后续计划转型我建议从这几个点入手web.xml中的配置逐步迁移到Spring Boot的自动配置类中spring-mvc.xml、applicationContext.xml里的Bean定义改造为Configuration加Bean分页插件、通用Mapper等第三方集成尽量换成Spring Boot Starter版本这个演进过程并不需要重写业务代码重点工作量集中在配置迁移与依赖梳理上。6.3 合规审计视角下的日志与追溯设计药品商城业务跟普通商城有一个天壤之别就是药品销售数据需要满足可追溯的合规审计要求尤其是处方药。因此我在系统里单独添加了操作日志模块重点记录三类数据用户的登录日志包括登录时间、IP地址、终端设备药品订单的完整状态流转日志从下单、支付、审核、发货到签收在线诊断全流程的记录包括问诊发起、消息内容、处方开具时间这些日志不进入业务主流程而是通过Spring的AOP切面异步写入日志表。由于药品数据涉及用户隐私日志内容本身不存储明文敏感字段必要的关联通过用户ID进行。如果真的遇到审计需要再通过ID到业务表里查询详细数据。这种设计既满足了追溯需求又不会让日志表成为新的数据泄露点。6.4 关于这套系统我最后想说的几句话做药品商城这类项目最考验人的其实不是技术本身而是你能否真正理解业务背后的规则。SSM框架也好、Spring Boot也罢都只是实现手段真正决定项目价值的是那些看不见的细节处方药能不能凭一张截图就买走、库存批次是不是先进先出、诊断记录是否完整可追溯。我个人在实际开发中的体会是做这类系统时心里要始终绷着一根“合规”的弦。技术上的坑都好填比如N1查询、事务回滚失效、库存超卖这些东西网上有大把答案但业务上的疏忽比如处方校验不到位、药房资质信息缺失、售后退货不考虑药品特殊性才是一旦上线就可能带来真正麻烦的问题。希望这篇文章里的设计与实现思路能帮你把踩过的坑提前绕开。如果你也正在做类似项目欢迎在评论区聊聊你的方案互相补一补盲区。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →