# 宁夏赛睿医疗 CRM 项目技术复盘与 AI Coding 实践 > 适用场景:Java 开发岗位面试复习。本文仅基于当前项目实际代码整理;“已实现”和“后续规划”明确区分,面试时不要把规划说成上线能力。 ## 1. 项目概述 这是一个面向医药/医疗销售协同场景的 CRM 管理系统,包含市场维护、拜访管理、学术活动、付款流程、管理报表等模块。系统以医院、科室、医生为主数据,向拜访、学术活动、付款等业务模块提供关联数据。 项目采用前后端分离架构: ```text Vue 3 + TypeScript + Vite | /api 代理 | Spring Boot 2.7.18 + Spring MVC | MyBatis-Plus + MySQL 8 | 市场 / 拜访 / 学术活动 / 付款 / 报表 ``` 当前后端运行在 `8086`,前端开发服务器默认使用 `5173`;Vite 将 `/api` 请求代理到后端,避免开发环境中的跨域问题。 ## 2. 技术栈速记 | 层级 | 实际技术 | 面试关键词 | | --- | --- | --- | | JDK | Java 8 | Lambda、Stream、Optional、`java.time` | | 后端框架 | Spring Boot 2.7.18、Spring MVC | 自动配置、依赖注入、RESTful、拦截器、全局异常处理 | | 持久层 | MyBatis-Plus 3.5.5 | `BaseMapper`、Lambda 条件构造器、分页插件、实体映射 | | 数据库 | MySQL 8、InnoDB、utf8mb4 | 索引、事务、外键关系设计、金额精度、慢查询优化 | | 构建 | Maven | 依赖管理、Profile、可执行 Jar | | 前端 | Vue 3、TypeScript、Vite、Vue Router、Axios、Pinia | 组合式 API、组件化、路由、响应式、接口封装 | | 集成预留 | 钉钉用户上下文、JS API 接入位 | 拦截器、Header 透传、用户上下文隔离 | | 工程协作 | AI Coding 辅助开发 | 需求拆解、代码审查、构建验证、人工验收 | ## 3. 推荐的项目介绍话术 ### 3.1 一分钟版本 我参与开发了一个医疗销售 CRM 系统,主要解决医院、科室、医生档案维护,以及拜访、学术活动和付款审批的流程协同。后端采用 Java 8、Spring Boot、MyBatis-Plus 和 MySQL,按照 Controller、Service、Mapper、Entity、DTO 分层;前端用 Vue 3 和 TypeScript。 在实现上,我做了统一接口返回、参数校验、全局异常处理、跨域和钉钉用户上下文预留。业务上重点处理了主数据关联校验、付款和活动等状态流转、删除前的关联数据保护,以及报表聚合统计。开发中使用 AI Coding 辅助梳理需求和生成初稿,但所有改动都会结合现有代码审查,并通过构建和页面联调验证。 ### 3.2 两分钟版本补充 市场维护模块以医院、科室、医生三级关系为核心。创建科室前校验医院是否存在,创建医生前校验科室是否存在;删除医院或科室时,先检查其是否仍被下级档案、拜访记录或学术活动引用,存在关联时拒绝删除,避免业务孤儿数据。 付款流程和学术活动使用显式的状态字段实现轻量状态机。例如付款从草稿、待审批、待协议、财务处理中、待发票到归档,每一步先校验前置状态再允许变更。管理报表按日期范围汇总覆盖进度、拜访、活动和付款数据,并输出趋势、风险拜访和医院排行。 ## 4. 后端核心知识点 ### 4.1 分层架构与职责边界 代码按业务模块组织,每个模块通常包含: ```text module/{业务模块}/ controller/ HTTP 协议、参数接收、调用 Service service/ 业务规则、状态校验、跨表协调 mapper/ 数据访问,继承 MyBatis-Plus BaseMapper entity/ 表映射实体 dto/ 接收请求参数与校验规则 ``` 可这样回答“为什么分层”:Controller 不直接处理复杂业务或 SQL,保证接口层轻;Service 聚合业务规则,便于复用和单测;Mapper 只处理持久化。这样后续替换数据源、增加审批规则或拆分服务时影响面更小。 ### 4.2 Spring Boot 与依赖注入 - `@RestController` 提供 JSON REST 接口,`@RequestMapping` 统一模块前缀。 - 构造器注入依赖,如 `MarketService` 注入多个 Mapper。相比字段注入,依赖关系更明确,也更利于单元测试。 - `@Service` 标识业务层组件,`@Configuration` 提供配置类,`@Bean` 注册 MyBatis-Plus 分页拦截器。 - `application.yml` 使用环境变量占位符管理端口、数据库和钉钉配置。`application-local.yml` 仅覆盖本地环境差异。 追问:`@Autowired` 可以省略吗? 答:只有一个构造器时 Spring 能自动注入,当前项目采用显式构造器依赖,不依赖字段注入。 ### 4.3 RESTful API 设计 项目接口以资源为中心,例如: - `GET /api/v1/market/hospitals`:查询医院,可带关键字。 - `POST /api/v1/market/hospitals`:新增医院。 - `PUT /api/v1/market/hospitals/{id}`:更新指定医院。 - `DELETE /api/v1/market/hospitals/{id}`:删除指定医院。 - `POST /api/v1/payments/{id}/submit`:触发付款申请提交这一动作。 对于“提交、审批、签到、归档”等状态动作,使用资源子路径表达业务行为。这样比把状态直接暴露为任意可修改字段更容易在服务层校验状态机。 ### 4.4 DTO 校验与统一错误输出 Controller 入参使用 `@Valid @RequestBody`,DTO 中可通过 `@NotBlank`、`@NotNull`、`@DecimalMin` 等 Bean Validation 注解约束数据。`GlobalExceptionHandler` 使用 `@RestControllerAdvice` 集中处理: - 业务异常 `BusinessException`:返回业务错误码与具体提示。 - 参数异常 `MethodArgumentNotValidException`:返回字段校验提示。 - 未预期异常:返回统一系统错误,避免直接向前端暴露堆栈。 所有接口使用统一结构: ```json { "code": 0, "message": "成功", "data": {} } ``` 优点是前端能用统一拦截逻辑处理成功、业务失败和网络失败;缺点是不能只依赖 HTTP 200 判断业务成功,必须同时判断业务码。 ### 4.5 MyBatis-Plus 实践 实体使用 `@TableName` 对应数据库表,主键使用 `@TableId(type = IdType.AUTO)` 适配 MySQL 自增 ID。Mapper 继承 `BaseMapper` 后具备常用 CRUD 能力。 查询使用 `LambdaQueryWrapper`,例如关键字检索: ```java wrapper.and(item -> item.like(MarketHospital::getName, keyword) .or().like(MarketHospital::getRegion, keyword)); ``` Lambda 写法的优点是字段改名时能获得编译期保护,减少字符串列名写错的问题。项目已注册 `PaginationInnerInterceptor(DbType.MYSQL)`;当前市场维护前端采用本地分页,后续数据量增大时应将页码、页大小和筛选条件下推到后端,使用 MyBatis-Plus 的 `Page` 避免全表加载。 ### 4.6 业务关联与删除保护 医院、科室、医生是层级主数据。删除医院前,服务层检查: - 是否存在所属科室; - 是否已有拜访记录; - 是否有关联学术活动。 存在引用时抛出业务异常,拒绝删除。科室和医生也使用同类保护。这体现了应用层引用完整性控制。 面试可进一步说明:数据库层也应补充外键或至少建立关联列索引;若业务要求保留历史,生产环境更适合逻辑删除(如 `deleted`、`deleted_at`)而非物理删除。当前项目是物理删除加关联拦截,属于初版实现。 ### 4.7 状态机与流程控制 付款模块状态大致为: ```text DRAFT -> PENDING_APPROVAL -> AGREEMENT_PENDING -> FINANCE_PROCESSING -> INVOICE_PENDING -> ARCHIVED \-> REJECTED ``` 每个动作先校验当前状态,例如只有草稿可提交、只有待审批可审批、只有财务处理中可登记付款。学术活动也有草稿、待审批、已通过、已拒绝等流程状态,并维护总经理、市场、合规三方审批结果。 这是“状态机”而不是“任意更新状态字段”:状态转换规则集中在 Service,能避免前端绕过步骤直接归档。当前实现适用于固定流程;当角色、会签、加签、抄送、审批记录等需求变复杂时,应接入工作流引擎或将状态迁移表、审批任务表独立建模。 ### 4.8 Java 8 在项目中的应用 - 使用 `LocalDate`、`LocalDateTime` 处理申请日期、付款时间、签到时间,避免旧版 `Date` 的可变性和时区混乱。 - 管理报表中用 Stream 完成筛选、分组、排序、聚合,例如统计已完成拜访数和付款金额。 - `BigDecimal` 保存与汇总金额,避免 `double` 二进制浮点误差。 - `Collectors.toMap` 构建医院 ID 到名称的映射,减少报表组合数据时的重复遍历。 追问:为什么金额不用 `double`? 答:`double` 不能精确表示很多十进制小数,累加可能出现 `0.30000000000000004` 一类误差。金额字段使用 MySQL `DECIMAL(14,2)`,Java 中使用 `BigDecimal`,相加时使用 `BigDecimal::add`。 ### 4.9 报表聚合 管理报表支持日期区间,默认最近 30 天,统计覆盖医院数、拜访完成数、活动状态、付款总额、趋势、风险拜访和医院排行。 当前实现先获取业务列表后在 Java Stream 中进行聚合,开发效率高、规则可读性好,适合初版和中小数据量。面试中应主动指出优化方向: - 大数据量场景必须用 SQL 的 `GROUP BY`、聚合函数和时间范围条件下推数据库; - 关联字段建立组合索引,例如 `(workflow_status, application_date)`; - 报表可使用按日汇总表、缓存或异步预计算; - 对慢 SQL 使用 `EXPLAIN` 检查索引、回表和全表扫描情况。 ### 4.10 CORS、反向代理与用户上下文 后端 `WebConfig` 对 `/api/**` 配置允许的来源、方法、Header 和凭证;本地环境补充了 `localhost`、`127.0.0.1` 的 5173/5174 端口以及局域网访问模式。前端 Vite 对 `/api` 设置开发代理,并在代理请求时移除 `Origin`,用于解决本地联调中的跨域 403 问题。 钉钉接入预留了 `DingTalkUserInterceptor`:从 `X-DingTalk-User-Id` 和 `X-DingTalk-User-Name` 读取用户信息,放入 `ThreadLocal` 上下文,并在 `afterCompletion` 中清理,避免线程复用导致用户信息串联。 注意:目前拦截器只读取 Header,不做签名校验或真实免登校验,属于本地开发兼容方案。生产应通过钉钉服务端鉴权获取可信用户身份,并采用 Spring Security/JWT 或网关策略保护接口。 ### 4.11 MySQL 表设计与索引 当前表设计中的实践: - 表统一有 `id` 主键、`created_at`、`updated_at` 审计字段; - 编号字段如付款申请号设置唯一索引; - 流程状态和申请日期建立组合索引,支持按状态和日期查询; - 收款单位建立索引,支持关键词检索的部分场景; - 使用 InnoDB,字符集使用 `utf8mb4`; - 文件内容/地址使用 `MEDIUMTEXT`,同时存储原文件名,方便预览和下载展示。 需要注意:`LIKE '%关键词%'` 通常无法有效利用普通 B+Tree 索引。数据量很大时需要评估前缀匹配、全文索引、搜索引擎或专门的检索方案。 ## 5. 前端与联调知识点 ### 5.1 Vue 3 与 TypeScript - 页面按业务模块拆分为 `MarketView`、`VisitManagementView`、`AcademicActivityView`、`PaymentProcessView`、`ReportView` 等单文件组件。 - Vue Router 管理模块路由与活动签到动态路由,如 `/activity-check-in/:token`。 - Axios 封装统一请求实例:设置基础地址、15 秒超时、请求 Header、响应错误转换和业务码判断。 - TypeScript 用接口约束 API 响应和前端模型,降低字段拼写错误。 - 前端使用响应式表单、搜索筛选、分页、详情弹窗、文件预览/下载,以及 PC 与移动端响应式布局。 ### 5.2 前后端接口约定 前端 `request` 封装会先等待 HTTP 请求成功,再判断后端返回的 `code` 是否为 `0`。因此接口新增或调整时,需要同步考虑:请求方法、路径、DTO 字段、时间/金额格式、错误提示和列表刷新策略。 一次完整的联调排查顺序: 1. 浏览器 Network 查看请求 URL、方法、状态码和请求体。 2. 检查 Vite 代理的目标端口是否与后端一致。 3. 检查后端 Controller 的路径、`@RequestBody`、DTO 校验和 CORS 配置。 4. 检查服务日志和数据库是否确实写入/更新。 5. 修复后重新构建前端并完成浏览器回归。 ## 6. AI Coding 实践:面试如何真实表达 ### 6.1 可以说的做法 本项目没有把大模型能力直接提供给最终用户,也没有在业务代码中调用 OpenAI、通义或其他模型 API。AI 的使用方式是“开发助手”,主要用于: - 根据会议纪要和既有 Demo,将模糊需求拆成实体、字段、状态、接口和页面清单; - 根据现有项目分层和命名规范,生成 DTO、Entity、Mapper、Controller、表单和基础样式初稿; - 辅助分析 403、请求参数缺失、代理地址不一致、保存覆盖等联调问题; - 生成模块 README、操作手册、周报和本技术复盘的初稿; - 对改动进行构建验证,并由开发者检查业务逻辑、数据安全和用户体验后再落地。 ### 6.2 高质量使用 AI Coding 的流程 ```text 明确需求和边界 -> 先阅读已有代码、表结构、接口约定 -> 让 AI 生成小范围候选改动 -> 人工审查业务规则、权限、删除与数据影响 -> 编译/构建/接口联调/页面回归 -> 记录变更与未覆盖风险 ``` 面试表达重点:AI 可以提高样板代码和排障效率,但不能替代工程判断。特别是金融金额、审批状态、权限、数据库迁移和删除操作,必须由开发者确认。 ### 6.3 常见追问与参考回答 **问:AI 生成代码如何保证质量?** 答:我不会直接大段合入。先限定改动文件和验收条件,再检查是否符合本项目分层、接口约定和状态规则;之后执行编译或构建,完成接口和页面回归。重要逻辑要补充单元测试、边界测试和代码审查。 **问:把代码或日志发给 AI 会不会泄露数据?** 答:不能把生产数据库密码、Token、客户联系人、付款凭证和完整日志直接提交给不受控模型。应脱敏后只保留排障所需字段;密钥放到环境变量或密钥管理系统;企业场景优先使用经审批的企业版工具或内网模型。 **问:AI 容易产生幻觉,如何处理?** 答:把 AI 输出当作假设而不是事实。答案必须回到源码、框架文档、构建结果、接口响应和数据库结果验证。不能因为代码“看上去合理”就直接上线。 **问:用 AI 后,开发者的核心价值是什么?** 答:核心仍然是理解需求、划清边界、做架构取舍、识别数据与安全风险、设计可验证的方案,并对最终交付负责。AI 擅长加速,开发者负责正确性和结果。 ## 7. 高频面试题清单 | 题目 | 建议回答要点 | | --- | --- | | Spring Boot 自动配置原理? | `@SpringBootApplication` 包含自动配置;按条件注解、classpath 和配置属性装配 Bean。可结合本项目 Web、校验、数据源自动配置说明。 | | 为什么使用 DTO 而不直接用 Entity 接收请求? | 防止客户端随意修改数据库字段;分离 API 模型与表模型;便于校验、兼容和隐藏内部字段。 | | `@ControllerAdvice` 有什么用? | 集中处理异常,统一错误格式,减少 Controller 重复 try-catch,并避免暴露内部异常细节。 | | MyBatis-Plus 的优缺点? | CRUD 和条件构造器效率高、开发快;复杂报表 SQL 不应强行用 Wrapper,应写 XML/注解 SQL 或专用查询层。 | | 事务何时需要? | 一个业务动作涉及多表写入且必须整体成功时,如新建活动同时保存议程和费用。当前初版应补上 `@Transactional`,避免中途失败产生部分数据。 | | 为什么当前活动保存存在风险? | 主表插入后,再依次删除/插入子表;若中间失败会部分成功。改进是 Service 方法加 `@Transactional(rollbackFor = Exception.class)`。 | | 什么是 ThreadLocal 泄漏? | 线程池会复用线程,未清理的用户上下文可能被下一个请求读取。当前在 `afterCompletion` 调用 `clear()`。 | | 403 与 401 区别? | 401 是未认证,403 是已认证但无权限或被跨域/安全策略拒绝。实际还要根据服务端日志和浏览器响应判断。 | | 如何处理并发审批? | 当前初版未处理。生产可加乐观锁版本号、条件更新、审批流水和幂等键,避免同一申请被重复审批。 | | 为什么编号生成不够可靠? | 当前用日期加毫秒尾数,极高并发下可能冲突。可改为数据库序列、雪花算法、Redis 原子递增或唯一索引重试。 | ## 8. 当前项目的不足与可升级方向 如实说出不足,通常比回避更加分: 1. 当前缺少自动化测试。应为状态流转、关联删除、金额与日期边界补 JUnit 5、Mockito 和集成测试。 2. 多表写入缺少显式事务注解。活动保存、付款关键操作需要补充事务与失败回滚验证。 3. 分页插件已配置,但多数列表目前是一次性查询、前端分页。应改为后端物理分页并限制页面大小。 4. 报表目前在 JVM 内存中聚合。数据量增长后应改为 SQL 聚合、汇总表或缓存。 5. 钉钉身份只完成开发期 Header 上下文预留,未完成真实免登、签名校验、角色权限和数据权限。 6. 文件目前以内容/地址字段存储,生产环境应接入对象存储,保存文件元数据、MIME 类型、大小、哈希和访问授权。 7. 审批流程为代码内固定状态机。复杂审批场景可演进为审批任务表、操作日志或 BPMN 工作流引擎。 8. 当前技术版本符合既定环境,但新项目可评估 Java 17/21、Spring Boot 3、Jakarta 命名空间和更完善的可观测性体系。 ## 9. 面试前检查清单 - 能画出本项目从 Vue 请求到 Controller、Service、Mapper、MySQL 的调用链。 - 能说清医院、科室、医生关联关系和删除保护的原因。 - 能完整讲出付款流程或学术活动流程的一次状态迁移。 - 能解释统一响应、参数校验、全局异常处理各自解决什么问题。 - 能说明 CORS 403 的常见排查顺序,以及 Vite 代理的作用。 - 能说明 `BigDecimal`、`LocalDateTime`、Stream 在本项目中的真实使用点。 - 能承认当前项目的事务、测试、分页、权限、并发方面的缺口,并给出可实施的优化路径。 - 能如实说明 AI Coding 是辅助开发和质量提升手段,不把它包装成项目已有的 AI 产品功能。 ## 10. 可重点查看的源码 - 后端依赖与版本:`sairui-server/pom.xml` - 接口与跨域配置:`sairui-server/src/main/java/com/sairui/config/WebConfig.java` - MyBatis-Plus 分页配置:`sairui-server/src/main/java/com/sairui/config/MybatisPlusConfig.java` - 统一响应和异常处理:`sairui-server/src/main/java/com/sairui/common/` - 市场维护业务规则:`sairui-server/src/main/java/com/sairui/module/market/service/MarketService.java` - 学术活动状态与签到:`sairui-server/src/main/java/com/sairui/module/activity/service/AcademicActivityService.java` - 付款状态机:`sairui-server/src/main/java/com/sairui/module/payment/service/PaymentService.java` - 报表聚合:`sairui-server/src/main/java/com/sairui/module/report/service/ReportService.java` - 前端请求封装与代理:`sairui-web/src/api/http.ts`、`sairui-web/vite.config.ts`