☰
SpringBoot实战:从零构建诊所管理系统全程解析
2026/10/14 5:27:11 网站建设 项目流程

一个诊所管理系统,看上去只是又一个“增删改查”的Web项目,但真正动手做过的人都知道,这里面藏着不少东西。业务上要管病人、管医生、管排班、管药品、管收费,还有一堆状态流转和权限细节;技术上要选型、搭架构、设计表结构、处理并发挂号,哪一步没想清楚,后面都要返工。这篇文章基于我用SpringBoot从零实现一套诊所管理系统的完整过程来写,从需求拆解、技术选型、数据库设计,到核心业务链路和排查实录,一次性讲透。

如果你正准备做类似的管理系统——无论是毕设、课设,还是小诊所的真实信息化改造需求,这篇文章都能帮你少走很多弯路。我会把每个关键决策背后的“为什么”都讲清楚,也会把实际开发中踩过的坑直接摆出来,方便你直接“抄作业”。

1. 项目整体设计与需求拆解

1.1 诊所管理系统的核心痛点是什么

很多人一上来就想着把界面做得多么花哨,功能堆得多么齐全,结果做出来一个“四不像”的万能系统。实际上,诊所管理系统的核心痛点非常集中:患者信息散落、挂号排队靠人工吼、医生开药靠手写、收费对账靠计算器、药品库存靠记忆。

这些东西听起来原始,但在多数小诊所里就是日常。真正要解决的问题不是“把线下流程搬到线上”这种高大上的说法,而是建立一条清晰的主数据链路:患者建档 -> 挂号/预约 -> 医生接诊 -> 开具处方 -> 收费结算 -> 药房发药/库存扣减。所有功能模块都要围绕这条链路来展开,脱离了这条主线的功能基本都是多余的。

我做项目第一步不是写代码,而是把这条链路上涉及的每一个角色和动作梳理清楚。诊所里的角色大概有四类:前台人员负责建档和挂号收费,医生负责接诊和开处方,药房人员负责发药和库存管理,管理员负责系统配置和统计查看。每一类角色对应的功能边界必须清晰,这决定了后面的权限设计怎么做。

1.2 功能模块划分的底层逻辑

按照主数据链路,我把系统划分成这几个核心模块:

  • 患者管理:建档、查询、修改联系方式、历史就诊记录查看。患者信息是所有业务的起点,没有这个模块,后面全都玩不转。
  • 预约挂号管理:支持现场挂号和线上预约两种方式,需要处理号源锁定、取消挂号、退号退款(如果涉及在线支付的话)等场景。
  • 医生工作站:查看待接诊患者列表、书写诊断结果、开具处方(包含药品明细、用法用量)、查看历史病历。
  • 收费管理:根据处方自动生成收费单,支持现金、扫码等支付方式的记录,收费完成后才能进入药房环节。
  • 药房库存管理:药品入库、库存查询、库存预警、发药扣减库存、效期管理。
  • 系统管理:用户管理、角色权限分配、登录日志、基础数据字典(科室、药品分类等)。

注意,这些模块不是孤立存在的,它们之间有严格的状态依赖关系。比如一个患者必须完成挂号才能被医生接诊,接诊后必须开处方才能生成收费单,收费完成后药房才能发药。我在设计时就给每个核心环节定义了状态字段(如挂号状态:待就诊、已就诊、已取消;处方状态:待收费、已收费、已发药),通过状态机的流转把整个业务串起来。

1.3 为什么选择单体架构而不是微服务

这个项目我明确选择了SpringBoot单体架构,原因很直接:诊所管理系统的业务规模、并发量、数据量都处于一个单体架构完全能承载的范围内。一个日门诊量几百人次的诊所,高峰期也就几十个并发操作,单体架构不仅开发效率高、运维简单,而且事务的一致性处理天然比微服务容易得多。

微服务的好处是弹性伸缩和独立部署,但代价是分布式事务、服务治理、链路追踪等一堆复杂度。对这个项目来说,微服务带来的复杂度远超它带来的收益,属于过度设计。我见过不少人在毕设或者中小型项目中强行上微服务,最后光是一个“下单扣库存”的分布式事务问题就折腾好几周——完全没有必要。

技术栈方面,我是这么选的:

  • 后端框架:SpringBoot 2.7.x。稳定、社区资料多、出了问题随便搜就能找到答案,而且对JDK 8的支持非常成熟。
  • ORM框架:MyBatis-Plus。相比原生MyBatis,它内置了通用Mapper和分页插件,单表CRUD根本不用手写SQL,能省下大量重复工作。复杂查询还是可以自己写SQL,灵活度也够。
  • 数据库:MySQL 8.0。免费、主流、资料多,InnoDB引擎支持事务,满足诊所系统的数据一致性要求。
  • 前端:Vue 2 + Element UI。单体架构下的经典组合,不用Node服务端渲染,构建完的静态文件直接扔进SpringBoot的static目录就能跑。当然,如果你不擅长前端,用服务端模板渲染Thymeleaf或者干脆用JSP(老项目里很常见)也行,核心逻辑都一样。
  • 鉴权方案:JWT + 拦截器。无状态、适合前后端分离场景,实现简单。

选型这件事没有绝对的对错,关键是匹配业务复杂度。SpringBoot单体能解决90%的诊所业务问题,那它就是最合理的方案。

2. 数据库设计与核心表结构解析

2.1 从业务链路推导表结构

数据库设计是整个系统最核心的部分,表结构设计得不好,后面写业务代码会处处难受。我的习惯是:先画业务流程,再推导表结构,而不是拍脑袋想“一个系统大概需要哪些表”。

从患者进门那一刻开始推演:

  • 患者第一次来,前台录入患者信息,产生patient表的数据。
  • 患者挂号,产生registration表的数据,记录是哪个患者、挂哪个科室、哪个医生、什么时间段。
  • 医生接诊,写下诊断结论,产生diagnosis表的数据(或者叫病历主表)。
  • 医生开药,产生prescription主表和prescription_item明细表。为什么拆成两张?因为一张处方包含多行药品,主表存处方号、总金额、开具医生、患者ID,明细表存每一行药品的名称、数量、用法用量。
  • 收费人员根据处方金额收费,产生payment表的数据,处方状态从“待收费”变为“已收费”。
  • 药房发药,扣减库存,drug_stock表的数据发生变化,处方状态变为“已发药”。

这样推下来,核心表基本就浮出水面了。再补充一些辅助表:sys_user(系统用户)、sys_role(角色)、sys_user_role(用户角色关联)、drug(药品基础信息)、drug_stock_log(库存变动日志)、doctor_schedule(医生排班表,用于预约挂号场景)。

2.2 核心表的字段设计要点

主数据链路中的表,每个字段设计背后都有讲究。我挑几张关键表来说。

patient 患者表

CREATE TABLE `patient` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID', `name` varchar(50) NOT NULL COMMENT '患者姓名', `gender` tinyint(1) DEFAULT NULL COMMENT '性别:0未知 1男 2女', `age` int(11) DEFAULT NULL COMMENT '年龄', `phone` varchar(20) DEFAULT NULL COMMENT '联系电话', `id_card` varchar(18) DEFAULT NULL COMMENT '身份证号', `medical_record_no` varchar(32) DEFAULT NULL COMMENT '病历号', `allergy_history` varchar(255) DEFAULT NULL COMMENT '过敏史', `create_time` datetime DEFAULT NULL COMMENT '建档时间', `update_time` datetime DEFAULT NULL COMMENT '更新时间', PRIMARY KEY (`id`), UNIQUE KEY `uk_medical_record_no` (`medical_record_no`), KEY `idx_phone` (`phone`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='患者信息表';

几点经验:病历号不能依赖数据库自增ID直接暴露给用户,我用的是“日期+流水号”的格式,比如202409150001,这样既直观又不容易泄露系统存量信息。身份证号加了唯一索引防止重复建档,但要注意身份证号属于敏感信息,查询接口返回时要做脱敏处理。电话建索引是因为患者查询按电话检索的频率非常高。

registration 挂号表

挂号表是整个链路里状态变化最复杂的表。核心字段包括:患者ID、科室ID、医生ID、挂号日期、时间段(上午/下午)、号源序号、挂号类型(现场/预约)、状态(待就诊/已就诊/已取消)、挂号费用、创建时间。

这里有一个关键设计:号源数量不等于无限制创建挂号记录。预约场景下,医生排班产生的号源是有限的(比如上午放30个号),挂号必须在号源未满的前提下才能成功。我用排班表的remaining_count字段来控制并发情况下的挂号数量,扣减时用乐观锁或者UPDATE ... WHERE remaining_count > 0的方式避免超挂。

prescription 处方表

CREATE TABLE `prescription` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID', `prescription_no` varchar(32) NOT NULL COMMENT '处方号', `registration_id` bigint(20) NOT NULL COMMENT '关联挂号记录ID', `patient_id` bigint(20) NOT NULL COMMENT '患者ID', `doctor_id` bigint(20) NOT NULL COMMENT '开方医生ID', `total_amount` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '总金额', `status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '状态:0待收费 1已收费 2已发药 3已退药', `diagnosis_content` varchar(500) DEFAULT NULL COMMENT '诊断结果', `create_time` datetime DEFAULT NULL COMMENT '开方时间', `pay_time` datetime DEFAULT NULL COMMENT '收费时间', PRIMARY KEY (`id`), UNIQUE KEY `uk_prescription_no` (`prescription_no`), KEY `idx_registration_id` (`registration_id`), KEY `idx_patient_id` (`patient_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='处方主表';

处方主表和明细表分开设计的原因,除了字段数量差异外,还有一个业务含义:一张处方是“一次开药行为”的记录,明细是这次行为下的每一行药品。以后如果要做处方点评、药品使用统计,按主表维度聚合就行。

金额字段用decimal(10,2)而不是float/double,这是必须遵守的规矩。浮点数在计算机里是近似存储,做金额计算会出现0.1+0.2不等于0.3的尴尬,一旦涉及钱,精确度是底线,不能用浮点型。

2.3 数据库设计中的三个关键禁忌

第一,不要过度冗余。比如直接在挂号表里冗余一个“患者姓名”字段,表面上看查询省了一次关联,但如果患者改名了,历史数据咋办?除非有明确的性能瓶颈,否则宁可多用Join。

第二,不要不用外键。很多开发为了图省事,表之间靠代码逻辑维护“虚拟外键”,数据库层面完全不约束。这样一旦代码有bug,很可能产生孤儿数据。我个人的实践是:主链路表(比如prescription关联registration_id)加上外键约束,但索引一定要建。外键确实会带来一点性能开销,可诊所系统的写入量根本不构成压力,一致性远比那点性能重要。

第三,不要忽略软删除。业务数据(比如药品、患者)的删除如果直接物理删除,历史对账和审计就全完了。我习惯在每个表加deleted字段,用MyBatis-Plus的@TableLogic注解搞定逻辑删除,查询时自动过滤。这个习惯在后续做“数据统计报表”的时候会回来感谢自己。

3. 核心技术实现与业务链路实操

3.1 项目分层架构与代码组织

项目结构上,我采用的是标准的分层架构,职责边界非常明确:

com.clinic ├── controller # 接口层:接收请求,参数校验,返回结果 ├── service # 业务层:核心业务逻辑,事务控制 │ └── impl # 服务实现类 ├── mapper # 数据访问层:MyBatis-Plus的Mapper接口 ├── entity # 数据库实体类 ├── dto # 数据传输对象:接口入参/出参的封装 ├── vo # 视图对象:前端展示数据的聚合封装 ├── config # 配置类:拦截器、跨域、MyBatis-Plus分页等 ├── common # 通用类:统一返回结果、异常处理、常量定义 └── utils # 工具类:JWT工具、日期工具等

为什么不直接在Controller里写业务逻辑?这是很多新手最容易犯的错误。Controller的角色应该是“接收请求并返回结果”,一旦把业务逻辑写进去,会造成两个后果:一是多个接口复用同一业务逻辑时只能复制粘贴,改一处忘一处;二是从Service到Mapper的事务控制无从谈起——如果业务逻辑散布在Controller中,遇到“先更新处方状态再扣减库存”这种需要原子性的操作,就不知道该在哪里加@Transactional了。

正确的做法是一切业务逻辑下沉到Service层。Controller层的代码要精简到一眼能看明白“这个接口在做什么”,业务层关注“这个业务是怎么做的”,数据层关注“这条SQL该怎么写”。这样分层下来,每层职责单一,改起来不慌。

3.2 核心业务链路的代码实现

我挑两个最有代表性的业务场景来讲具体实现。第一个是挂号场景,第二个是开方收费发药场景。这两个场景覆盖了状态流转、事务控制、库存扣减这类典型问题。

挂号业务实现

挂号业务的核心逻辑是:先判断号源是否充足,再创建挂号记录,最后扣减号源数量。这三个动作必须在一个事务里,否则就会出现“记录创建成功了但号源没有扣减”的数据不一致。

@Service public class RegistrationServiceImpl implements RegistrationService { @Autowired private RegistrationMapper registrationMapper; @Autowired private ScheduleMapper scheduleMapper; @Override @Transactional(rollbackFor = Exception.class) public Long createRegistration(RegistrationCreateDTO dto) { // 1. 查询排班信息 Schedule schedule = scheduleMapper.selectById(dto.getScheduleId()); if (schedule == null) { throw new BizException("排班信息不存在"); } // 2. 检查号源是否充足(乐观锁扣减) LocalDateTime now = LocalDateTime.now(); if (schedule.getStartTime().isBefore(now)) { throw new BizException("该时段已过号,请选择其他时段"); } int updateCount = scheduleMapper.decreaseRemainingCount(schedule.getId()); if (updateCount == 0) { throw new BizException("该时段号源已满,请选择其他时段"); } // 3. 创建挂号记录 Registration registration = new Registration(); BeanUtils.copyProperties(dto, registration); registration.setRegistrationNo(RegNoGenerator.generate()); registration.setStatus(RegistrationStatus.WAITING.getCode()); registration.setCreateTime(now); registrationMapper.insert(registration); return registration.getId(); } }

对应的Mapper方法:

@Update("UPDATE doctor_schedule SET remaining_count = remaining_count - 1 " + "WHERE id = #{scheduleId} AND remaining_count > 0") int decreaseRemainingCount(Long scheduleId);

这里有两个非常关键的设计点。第一,扣减号源用UPDATE ... WHERE remaining_count > 0这种条件更新代替先查询再判断的流程,这一步直接避免了并发场景下多个请求同时读到“剩余1个号”然后同时扣减变成负数的问题。数据库的行锁保证了同一时刻只有一个事务能成功更新这一行,更新影响行数为0就说明号源已经没了。第二,@Transactional保证了这个方法内所有数据库操作要么全部成功提交,要么全部回滚。一旦中途抛出异常,前面扣减的号源也会自动回滚,不会出现“号扣了但挂号记录没建出来”的问题。

我在开发过程中发现很多人在这一步会忽略事务,导致测试时一切正常,一旦并发跑起来,数据就乱了。只要一个业务操作涉及两张以上的表,就一定要考虑事务。

开方、收费、发药的串联实现

医生开方并提交后,处方状态初始为“待收费”。收费完成后要同时更新处方状态并生成收费记录,这个可以放在收费Service中实现:

@Override @Transactional(rollbackFor = Exception.class) public PaymentResult pay(PaymentDTO dto) { // 1. 查询处方并校验状态 Prescription prescription = prescriptionMapper.selectById(dto.getPrescriptionId()); if (prescription == null) { throw new BizException("处方不存在"); } if (!prescription.getStatus().equals(PrescriptionStatus.PENDING_PAYMENT.getCode())) { throw new BizException("该处方当前状态不可收费"); } // 2. 创建支付记录 Payment payment = new Payment(); payment.setPaymentNo(PaymentNoGenerator.generate()); payment.setPrescriptionId(prescription.getId()); payment.setAmount(prescription.getTotalAmount()); payment.setPayType(dto.getPayType()); payment.setCreateTime(LocalDateTime.now()); paymentMapper.insert(payment); // 3. 更新处方状态为已收费 prescription.setStatus(PrescriptionStatus.PAID.getCode()); prescription.setPayTime(LocalDateTime.now()); prescriptionMapper.updateById(prescription); return PaymentResult.success(payment); }

注意这里支付记录和处方状态更新在同一个事务里。如果先插入支付记录、更新处方状态的某一步失败了,之前插入的支付记录就变成了一笔没有对应的脏数据。放在同一个事务里,保证“支付记录生成 + 处方状态变为已收费”是一个不可分割的原子操作。

药房发药时的库存扣减,逻辑和挂号扣号源一样,使用条件更新防止库存变成负数:

@Update("UPDATE drug_stock SET stock_count = stock_count - #{count} " + "WHERE drug_id = #{drugId} AND stock_count >= #{count}") int decreaseStock(Long drugId, Integer count);

发药完成后再更新处方状态为“已发药”,同时写一条库存变动日志到drug_stock_log表。这个日志表非常有用,后面做药品消耗统计、盘点对账全靠它。

3.3 前端接口联调与统一返回格式

前后端分离项目里,接口联调环节最容易出的问题就是“各说各话”。后端返回的数据结构、字段命名、分页参数格式,如果没有统一约定,联调阶段会浪费大量时间改来改去。

我提前定义了一套统一返回格式:

{ "code": 200, "message": "操作成功", "data": { } }

code为200表示成功,其他值表示业务异常。前端只认这个包裹结构,具体业务数据全放data里。分页接口的返回结构也固定为{ total: 总条数, records: 当前页数据 },前端组件(比如Element UI的el-table和el-pagination)直接就能对接。

接口出参尽量用VO对象,避免直接把数据库实体抛给前端。原因很简单:实体里有一些字段不能暴露(比如密码、逻辑删除标识),有一些字段格式需要转义(比如时间格式、性别编码转文字)。在VO层做一次转换,接口的返回结构才能真正贴合前端的展示需求。

我封装了一个Result<T>类作为所有Controller的返回类型,配合全局异常处理器@RestControllerAdvice,让业务异常和系统异常都能统一转换为Result结构返回,而不是把一堆堆栈信息直接抛给前端。整个系统的异常处理路径非常干净——Controller的代码基本都是:调用Service、返回Result。

4. 权限控制与系统安全设计

4.1 RBAC模型:用户-角色-权限三层关系

诊所系统里的角色大致分管理员、医生、收费员、药房人员。不同角色的操作边界完全不同:医生看不到收费和库存管理的菜单,收费员看不到诊断和处方录入的按钮,药房人员只处理发药和库存。这种场景天然适合用RBAC(基于角色的访问控制)模型。

我实现了三张核心表:sys_user(用户表)、sys_role(角色表)、sys_user_role(用户-角色关联表),另外配合菜单权限表sys_menu和角色-菜单关联表sys_role_menu。

权限校验的粒度和策略是:登录时把该用户的角色和对应菜单权限加载出来,生成JWT令牌;前端根据接口返回的菜单树动态渲染侧边栏,后端在拦截器里校验每次请求的URL是否在用户权限集合中。前端控制“看不看得到入口”,后端控制“能不能调接口”。这两者缺一不可——前端控制纯属用户体验,后端拦截才是安全底线。

4.2 JWT登录鉴权与密码加密

登录流程不复杂:用户提交用户名密码,后端校验通过后生成JWT返回给前端,前端把JWT存在localStorage里,之后每次请求在Header中携带Authorization: Bearer <token>。后端用一个拦截器统一解析token,并把用户ID和角色信息放入ThreadLocal供后续业务使用。

有两个坑我需要特别提醒。

第一个坑是JWT密钥硬编码。开发阶段图省事把签名密钥直接写在代码里,一旦代码泄露或者发布到公共仓库,任何人都能伪造合法token,相当于系统大门敞开。正确做法是把密钥放到配置文件里,并且使用足够长的随机字符串(至少32字节),部署时通过环境变量注入。

第二个坑是密码明文存储。这已经不是技术问题而是底线问题了。用户密码必须经过哈希处理再入库,我使用的是BCrypt算法(Spring Security框架里自带BCryptPasswordEncoder,单独引入spring-security-crypto依赖即可使用)。BCrypt的好处是哈希过程中自动加入随机盐,同样的密码每次加密结果都不一样,还能通过调高强度参数来对抗暴力破解。

// 密码加密 String encodedPassword = new BCryptPasswordEncoder().encode(rawPassword); // 密码校验 boolean matches = new BCryptPasswordEncoder().matches(rawPassword, encodedPassword);

4.3 接口层面还需要做什么防护

除了登录鉴权,接口层面还有几个细节容易被忽略。

一是操作日志与登录日志。关键操作(挂号、收费、发药、修改药品信息)需要记录操作人、操作时间、操作内容。审计日志的意义在于:一旦出现数据异常(比如收费金额不对,或者处方被修改),可以溯源到具体是谁在什么时间做了什么操作。

二是参数校验。很多人对参数校验的态度是“前端已经做了,后端不用重复做”。这是完全错误的观念。前端校验只是用户体验,真正守住安全底线的是后端的参数校验。比如收费接口传过来的amount,如果前端被绕过,直接调用接口传入负数怎么办?我的做法是使用JSR-303校验注解(@NotNull、@DecimalMin等)对入参DTO做校验,在进入业务逻辑前把不合法参数挡在门外。

三是敏感数据脱敏。患者身份证号、电话号码在列表查询接口中,我会在VO层做脱敏处理(比如身份证只显示前4后4位),只有在详情接口且当前用户有足够权限时才返回完整信息。这也是医疗数据隐私合规的基本要求。

5. 实际开发中遇到的坑与排查实录

5.1 事务失效的经典场景

我在开发“收费后更新处方状态”时,遇到过一次事务失效的问题。现象是:支付记录插入成功,但处方状态没有更新,而且没有抛出任何异常。排查了半天,最后发现是我在Service内部写了一个私有方法,私有方法上加了@Transactional注解,然后在同一个类里的公有方法调用了它。

这是Spring事务的一个经典陷阱。Spring的@Transactional是基于AOP代理实现的,只有通过代理对象调用方法时,事务增强才会生效。同一个类内部方法之间的“自调用”走的是this引用,不经过代理,注解自然就失效了。

解决方式很简单:要么把两个方法拆分到不同的Service类中,通过注入另一个Service来调用;要么直接在公有方法上统一加事务注解,不要在私有方法上单独加。我在后面的开发中统一约定:事务注解只加在Service的公有方法上,标识该方法是“一个完整的业务操作单元”。

5.2 MyBatis-Plus分页查询的隐性坑

MyBatis-Plus提供了Page对象配合分页插件做分页查询,使用非常简单。但有一个非常隐蔽的坑:分页插件不生效。

我排查过的场景是这样的:查询挂号记录列表时,Page对象返回了,但total一直是0,records返回所有满足条件的数据。翻了一下配置,发现问题在于我忘了在配置类中注册MybatisPlusInterceptor分页插件。虽然Mapper层的方法写全了,但没有插件的情况下Page参数会被当成普通参数传进SQL,分页逻辑实际上没有生效。

正确的配置如下:

@Configuration public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }

另外还有一个经验:多表关联查询时,分页要格外小心。如果SQL里有多表Join,分页插件会自动在原SQL外面包一层SELECT COUNT(*)进行总数统计。如果原SQL里有DISTINCT或者GROUP BY,有时候统计出来的总数会不对。我遇到这种情况时,会在Mapper里手写一个count方法,单独处理总数查询,确保分页数据准确。

5.3 金额计算的精度问题

还有一个让我印象深刻的bug出现在统计报表上。某天财务反馈说某天的收费总额和处方明细对不上,差了0.1元。查根因,是某个统计SQL用了AVG和SUM函数,配合浮点字段计算导致精度丢失。

这个问题的解决之道其实从设计阶段就能规避:金额字段一律使用DECIMAL类型,Java代码中使用BigDecimal进行计算,避免任何double或float出现在金额相关的逻辑里。BigDecimal在构造时务必使用字符串构造方法new BigDecimal("0.1"),而不是new BigDecimal(0.1)——后者底层仍然是一个不精确的二进制浮点数,依然会传精度丢失。

5.4 前端跨域与静态资源访问问题

开发调试阶段前后端是分开跑的:前端Vue工程占用8080端口,后端SpringBoot占用8081端口。前端通过axios请求http://localhost:8081/api/...必然触发跨域问题。

后端解决跨域有两种方式:一是加全局CORS配置类,二是在每个Controller上加@CrossOrigin注解。我推荐前者,集中配置,不需要每个接口重复标注。

@Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOriginPatterns("*") .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") .allowedHeaders("*") .allowCredentials(true) .maxAge(3600); } }

另外一个是静态资源访问问题。前端构建完的dist目录文件要放到SpringBoot的src/main/resources/static下面,把前端请求刷新时出现404。我采取的方式是配置一个简单的路由转发:SPA的请求路径(除/api/**外)都转发到index.html,避免前端路由刷新后找不到对应资源。

6. 常见问题与避坑速查表

把我在整个开发过程中整理的排查经验,浓缩成一张速查表,方便你对照自查。

问题现象可能原因解决方案
@Transactional不生效私有方法自调用;类未被Spring管理;异常被catch吞掉事务加在公有方法;确认Service类被@Component扫描;异常抛出而非截获
分页total始终为0MyBatis-Plus分页拦截器未注册配置MybatisPlusInterceptor并添加分页插件
金额有0.01的误差使用了float/double统一使用BigDecimal(字符串构造)
数据库库存变负数并发扣减未做条件限制UPDATE ... WHERE stock_count >= count
JWT登录后接口仍401token过期;拦截器放行路径配置错误排查WebMvcConfig中的addInterceptors放行路径
跨域请求失败CORS配置缺少;预检请求OPTIONS未放行全局CorsConfig;允许OPTIONS请求
身份证/手机号泄露接口直接返回实体Controller返回VO对象并脱敏
删除数据后历史报表对不上物理删除导致关联数据缺失使用逻辑删除(deleted字段)
密码明文入库未加密或加密方式弱BCrypt加密存储
超卖/超挂先查后改导致并发下数据错乱使用乐观锁或条件更新

再补充一个独家避坑技巧:当你开发一个“修改+审核+确认”的多步骤业务时,每一步操作前都要重新查询一次当前状态,而不是使用从列表页带过来的旧状态。这听起来是常识,但实际操作中很容易想当然地认为“我列表页显示的是待收费,那它一直就是待收费”。实际上另一条业务线可能已经改变了这个状态。稳妥的做法是:每次修改状态前,先根据主键查一次最新记录,再基于最新状态做校验和更新。配合事务,才能保证状态流转不出错。

7. 一些实操感悟与后续可扩展的方向

整个项目做下来,我最大的体会是:管理系统的复杂度从来不在于某个技术点有多难,而在于业务链路的梳理是否清晰、状态流转是否严密、数据一致性是否有保障。SpringBoot只是工具,真正考验功底的是“把复杂业务拆解成清晰模块”的能力和“对数据一致性保持敬畏”的态度。

对于打算参考这个项目来做的朋友,有几条建议送给你:

第一,不要一上来就写代码。花两三天把业务流程画清楚,把表结构设计好,想清楚每个状态怎么流转,后面写代码的速度会快得多。先写代码再补设计的做法,大概率会在开发过程中推翻重来。

第二,从核心主链路做起。做管理系统最容易陷入的功能堆砌陷阱,就是先做了一些周边功能(比如系统管理、数据字典),主链路还没走通。我自己的经验是:先把“建患者 -> 挂号 -> 接诊 -> 开方 -> 收费 -> 发药”这条主链路完整跑通,再往上面添加权限、统计、排班、预约这些周边功能。主链路通了,系统的骨架就立住了。

第三,业务场景的扩展空间。这个系统做完之后,可以扩展的方向很多:对接微信公众号实现线上预约和查询报告、引入定时任务做药品近效期预警、用ECharts做门诊量和收入的可视化统计、把诊断记录标准化打造电子病历模块,甚至在数据量增长后引入缓存提升查询性能。每一步扩展,都能让系统从“能跑”变成“好用”。

最后再分享一个小技巧:数据库设计文档一定要保存好,表结构、字段注释、状态枚举值都记录下来。过了一个月再回来看代码,你大概率不会记得status=2代表什么。一份好的设计文档,比任何代码注释都管用。这也是我这次做完最大的收获之一。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询