适用场景:Java 开发岗位面试复习。本文仅基于当前项目实际代码整理;“已实现”和“后续规划”明确区分,面试时不要把规划说成上线能力。
这是一个面向医药/医疗销售协同场景的 CRM 管理系统,包含市场维护、拜访管理、学术活动、付款流程、管理报表等模块。系统以医院、科室、医生为主数据,向拜访、学术活动、付款等业务模块提供关联数据。
项目采用前后端分离架构:
Vue 3 + TypeScript + Vite
|
/api 代理
|
Spring Boot 2.7.18 + Spring MVC
|
MyBatis-Plus + MySQL 8
|
市场 / 拜访 / 学术活动 / 付款 / 报表
当前后端运行在 8086,前端开发服务器默认使用 5173;Vite 将 /api 请求代理到后端,避免开发环境中的跨域问题。
| 层级 | 实际技术 | 面试关键词 |
|---|---|---|
| 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 辅助开发 | 需求拆解、代码审查、构建验证、人工验收 |
我参与开发了一个医疗销售 CRM 系统,主要解决医院、科室、医生档案维护,以及拜访、学术活动和付款审批的流程协同。后端采用 Java 8、Spring Boot、MyBatis-Plus 和 MySQL,按照 Controller、Service、Mapper、Entity、DTO 分层;前端用 Vue 3 和 TypeScript。
在实现上,我做了统一接口返回、参数校验、全局异常处理、跨域和钉钉用户上下文预留。业务上重点处理了主数据关联校验、付款和活动等状态流转、删除前的关联数据保护,以及报表聚合统计。开发中使用 AI Coding 辅助梳理需求和生成初稿,但所有改动都会结合现有代码审查,并通过构建和页面联调验证。
市场维护模块以医院、科室、医生三级关系为核心。创建科室前校验医院是否存在,创建医生前校验科室是否存在;删除医院或科室时,先检查其是否仍被下级档案、拜访记录或学术活动引用,存在关联时拒绝删除,避免业务孤儿数据。
付款流程和学术活动使用显式的状态字段实现轻量状态机。例如付款从草稿、待审批、待协议、财务处理中、待发票到归档,每一步先校验前置状态再允许变更。管理报表按日期范围汇总覆盖进度、拜访、活动和付款数据,并输出趋势、风险拜访和医院排行。
代码按业务模块组织,每个模块通常包含:
module/{业务模块}/
controller/ HTTP 协议、参数接收、调用 Service
service/ 业务规则、状态校验、跨表协调
mapper/ 数据访问,继承 MyBatis-Plus BaseMapper
entity/ 表映射实体
dto/ 接收请求参数与校验规则
可这样回答“为什么分层”:Controller 不直接处理复杂业务或 SQL,保证接口层轻;Service 聚合业务规则,便于复用和单测;Mapper 只处理持久化。这样后续替换数据源、增加审批规则或拆分服务时影响面更小。
@RestController 提供 JSON REST 接口,@RequestMapping 统一模块前缀。MarketService 注入多个 Mapper。相比字段注入,依赖关系更明确,也更利于单元测试。@Service 标识业务层组件,@Configuration 提供配置类,@Bean 注册 MyBatis-Plus 分页拦截器。application.yml 使用环境变量占位符管理端口、数据库和钉钉配置。application-local.yml 仅覆盖本地环境差异。追问:@Autowired 可以省略吗?
答:只有一个构造器时 Spring 能自动注入,当前项目采用显式构造器依赖,不依赖字段注入。
项目接口以资源为中心,例如:
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:触发付款申请提交这一动作。对于“提交、审批、签到、归档”等状态动作,使用资源子路径表达业务行为。这样比把状态直接暴露为任意可修改字段更容易在服务层校验状态机。
Controller 入参使用 @Valid @RequestBody,DTO 中可通过 @NotBlank、@NotNull、@DecimalMin 等 Bean Validation 注解约束数据。GlobalExceptionHandler 使用 @RestControllerAdvice 集中处理:
BusinessException:返回业务错误码与具体提示。MethodArgumentNotValidException:返回字段校验提示。所有接口使用统一结构:
{
"code": 0,
"message": "成功",
"data": {}
}
优点是前端能用统一拦截逻辑处理成功、业务失败和网络失败;缺点是不能只依赖 HTTP 200 判断业务成功,必须同时判断业务码。
实体使用 @TableName 对应数据库表,主键使用 @TableId(type = IdType.AUTO) 适配 MySQL 自增 ID。Mapper 继承 BaseMapper<T> 后具备常用 CRUD 能力。
查询使用 LambdaQueryWrapper,例如关键字检索:
wrapper.and(item -> item.like(MarketHospital::getName, keyword)
.or().like(MarketHospital::getRegion, keyword));
Lambda 写法的优点是字段改名时能获得编译期保护,减少字符串列名写错的问题。项目已注册 PaginationInnerInterceptor(DbType.MYSQL);当前市场维护前端采用本地分页,后续数据量增大时应将页码、页大小和筛选条件下推到后端,使用 MyBatis-Plus 的 Page<T> 避免全表加载。
医院、科室、医生是层级主数据。删除医院前,服务层检查:
存在引用时抛出业务异常,拒绝删除。科室和医生也使用同类保护。这体现了应用层引用完整性控制。
面试可进一步说明:数据库层也应补充外键或至少建立关联列索引;若业务要求保留历史,生产环境更适合逻辑删除(如 deleted、deleted_at)而非物理删除。当前项目是物理删除加关联拦截,属于初版实现。
付款模块状态大致为:
DRAFT
-> PENDING_APPROVAL
-> AGREEMENT_PENDING
-> FINANCE_PROCESSING
-> INVOICE_PENDING
-> ARCHIVED
\-> REJECTED
每个动作先校验当前状态,例如只有草稿可提交、只有待审批可审批、只有财务处理中可登记付款。学术活动也有草稿、待审批、已通过、已拒绝等流程状态,并维护总经理、市场、合规三方审批结果。
这是“状态机”而不是“任意更新状态字段”:状态转换规则集中在 Service,能避免前端绕过步骤直接归档。当前实现适用于固定流程;当角色、会签、加签、抄送、审批记录等需求变复杂时,应接入工作流引擎或将状态迁移表、审批任务表独立建模。
LocalDate、LocalDateTime 处理申请日期、付款时间、签到时间,避免旧版 Date 的可变性和时区混乱。BigDecimal 保存与汇总金额,避免 double 二进制浮点误差。Collectors.toMap 构建医院 ID 到名称的映射,减少报表组合数据时的重复遍历。追问:为什么金额不用 double?
答:double 不能精确表示很多十进制小数,累加可能出现 0.30000000000000004 一类误差。金额字段使用 MySQL DECIMAL(14,2),Java 中使用 BigDecimal,相加时使用 BigDecimal::add。
管理报表支持日期区间,默认最近 30 天,统计覆盖医院数、拜访完成数、活动状态、付款总额、趋势、风险拜访和医院排行。
当前实现先获取业务列表后在 Java Stream 中进行聚合,开发效率高、规则可读性好,适合初版和中小数据量。面试中应主动指出优化方向:
GROUP BY、聚合函数和时间范围条件下推数据库;(workflow_status, application_date);EXPLAIN 检查索引、回表和全表扫描情况。后端 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 或网关策略保护接口。
当前表设计中的实践:
id 主键、created_at、updated_at 审计字段;utf8mb4;MEDIUMTEXT,同时存储原文件名,方便预览和下载展示。需要注意:LIKE '%关键词%' 通常无法有效利用普通 B+Tree 索引。数据量很大时需要评估前缀匹配、全文索引、搜索引擎或专门的检索方案。
MarketView、VisitManagementView、AcademicActivityView、PaymentProcessView、ReportView 等单文件组件。/activity-check-in/:token。前端 request<T> 封装会先等待 HTTP 请求成功,再判断后端返回的 code 是否为 0。因此接口新增或调整时,需要同步考虑:请求方法、路径、DTO 字段、时间/金额格式、错误提示和列表刷新策略。
一次完整的联调排查顺序:
@RequestBody、DTO 校验和 CORS 配置。本项目没有把大模型能力直接提供给最终用户,也没有在业务代码中调用 OpenAI、通义或其他模型 API。AI 的使用方式是“开发助手”,主要用于:
明确需求和边界
-> 先阅读已有代码、表结构、接口约定
-> 让 AI 生成小范围候选改动
-> 人工审查业务规则、权限、删除与数据影响
-> 编译/构建/接口联调/页面回归
-> 记录变更与未覆盖风险
面试表达重点:AI 可以提高样板代码和排障效率,但不能替代工程判断。特别是金融金额、审批状态、权限、数据库迁移和删除操作,必须由开发者确认。
问:AI 生成代码如何保证质量?
答:我不会直接大段合入。先限定改动文件和验收条件,再检查是否符合本项目分层、接口约定和状态规则;之后执行编译或构建,完成接口和页面回归。重要逻辑要补充单元测试、边界测试和代码审查。
问:把代码或日志发给 AI 会不会泄露数据?
答:不能把生产数据库密码、Token、客户联系人、付款凭证和完整日志直接提交给不受控模型。应脱敏后只保留排障所需字段;密钥放到环境变量或密钥管理系统;企业场景优先使用经审批的企业版工具或内网模型。
问:AI 容易产生幻觉,如何处理?
答:把 AI 输出当作假设而不是事实。答案必须回到源码、框架文档、构建结果、接口响应和数据库结果验证。不能因为代码“看上去合理”就直接上线。
问:用 AI 后,开发者的核心价值是什么?
答:核心仍然是理解需求、划清边界、做架构取舍、识别数据与安全风险、设计可验证的方案,并对最终交付负责。AI 擅长加速,开发者负责正确性和结果。
| 题目 | 建议回答要点 |
|---|---|
| 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 原子递增或唯一索引重试。 |
如实说出不足,通常比回避更加分:
BigDecimal、LocalDateTime、Stream 在本项目中的真实使用点。sairui-server/pom.xmlsairui-server/src/main/java/com/sairui/config/WebConfig.javasairui-server/src/main/java/com/sairui/config/MybatisPlusConfig.javasairui-server/src/main/java/com/sairui/common/sairui-server/src/main/java/com/sairui/module/market/service/MarketService.javasairui-server/src/main/java/com/sairui/module/activity/service/AcademicActivityService.javasairui-server/src/main/java/com/sairui/module/payment/service/PaymentService.javasairui-server/src/main/java/com/sairui/module/report/service/ReportService.javasairui-web/src/api/http.ts、sairui-web/vite.config.ts