ARTICLE DETAIL

资讯详情

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

Java开源采购系统实战:从Spring Boot到状态机与权限设计

Java开源采购系统实战:从Spring Boot到状态机与权限设计 1. 项目定位这个开源采购系统到底解决什么问题做Java开发这些年见过不少新手拿着图书管理、学生选课这类项目去面试十有八九会被面试官追问“你这项目有什么业务复杂度”。不是说那些项目不好而是它们基本只覆盖了增删改查连事务都很少碰到很难体现出对业务的理解。商品采购管理系统就不一样它天然带着多角色、多状态、金额计算、库存变动、审批流这些元素随便拆一个模块出来都能聊很久。这也是为什么我会特别推荐拿它来练手或者写进简历。这个开源项目的核心价值很直接把企业里“缺货了要补货、找谁买、多少钱买、买回来放哪、出了问题怎么退”这条完整链路用代码实现一遍。它不只是简单的商品管理加订单管理而是围绕采购业务做闭环覆盖供应商档案、商品信息、采购申请、订单审核、到货入库、退货退款、应付账款这些环节。你把它跑起来之后能很清楚看到一条采购数据是怎么从草稿状态一路流转到入库完成、再到财务对账的这对理解企业级系统非常有用。从受众来说大概分三类人。第一类是正在学JavaWeb、想找一个能写进简历的实战项目的在校生或转行人员第二类是准备面试的求职者需要一个业务逻辑能撑住深挖的项目来应对“数据一致性”“权限设计”“状态机”这类问题第三类是想快速搭一套内部采购工具的中小团队拿开源代码改改就能用。如果你属于这三种情况中的任意一种那这个项目都值得你花一到两周时间彻底吃透。我这边会直接从“把这个项目真正跑起来、真正理解透”的角度去拆不只是贴几个类名和表名而是把每一步背后的取舍和踩坑点都讲清楚让看完的人能直接上手复现拿去面试也能讲得有理有据。2. 技术选型与项目结构拆解2.1 后端技术栈Spring Boot MyBatis Plus为什么是这个组合现在开源社区里Java后端项目十有八九是Spring Boot MyBatis Plus这套组合这个采购管理系统也不例外。从这个选型就能看出它定位的是“实用工具型项目”而不是“研究型项目”。Spring Boot负责把配置简化掉让项目能快速启动MyBatis Plus则把单表CRUD的重复劳动降到最低。我实测下来使用MyBatis Plus的BaseMapper之后像供应商、商品这类基础档案的增删改查几乎不用写SQL省下的时间可以全部花在采购单这种核心业务逻辑上。很多人会问为什么不用Spring Data JPA我个人的观点是采购管理系统里报表统计和复杂查询特别多比如按月统计采购金额、按供应商汇总到货率、查询超期未入库的订单。这类需求用SQL表达最自然MyBatis可以让你精确控制每一条SQL而JPA在复杂查询上的学习成本和控制力都差点意思。MyBatis Plus相当于在MyBatis之上给了一个好用的MP抽象层但本质还是MyBatis所以上限没丢。另外要注意的是这个项目里通常还会用到Spring Security或Sa-Token做登录鉴权。如果你拿到的版本用的是Sa-Token那我建议你重点看一下它的拦截器注册方式因为相比Spring Security那套过滤器链Sa-Token的侵入性更小对新手理解起来也更友好。后面我也会讲到权限模型的设计这部分是面试高频区。2.2 前端与部署方式Vue Element Plus的经典搭配这个开源项目的前端一般是Vue 2或Vue 3搭配Element UI组件库通过axios调后端接口。如果你是从纯后端视角来看可能会觉得前端就是个“皮”但实际上RuoYi这类快速开发框架的很多设计思路在里面都能看到。菜单管理、角色分配、按钮权限控制这些功能在前端路由和侧边栏菜单渲染上都有对应的逻辑这比单独写CRUD接口要有意思得多。部署方式无非两种一种是把前端打包成dist目录放到Nginx下后端打成Jar包单独跑另一种是直接把前端静态资源塞进Spring Boot的resources/static目录里打成一个Fat Jar。我建议你本地开发用前后端分离模式部署到服务器时如果图省事可以用第二种方式把前端文件放到后端一起跑。开源项目的README里一般都会写清楚两种方式跟着做就行。这里有个非常关键的细节前端和后端联调的时候必然会遇到跨域问题。很多项目是在后端加一个CorsFilter或者CrossOrigin但如果通过Nginx部署更推荐在Nginx层配置反向代理把/api开头的请求转发到后端服务这样前端请求的都是同源地址跨域问题从根源上消失。这块网上配置很多我踩过坑后面在“常见问题”部分细说。2.3 代码分层与包结构从controller到mapper的职责边界标准的项目包结构大致是这样的controller负责接收参数和返回结果service里写业务逻辑mapper对应数据库操作entity或者domain放实体类另外还有dto数据传输对象、vo视图对象和common通用工具、统一返回结果、异常处理。这套分层的核心思想是“每一层只做自己该做的事”。我在实际review这个开源项目代码时发现很多做得好的模块controller里只有寥寥几行代码逻辑全在service层而service层又通过事务注解保证多个数据库操作要么全成功要么全回滚。千万别把业务逻辑写在controller里否则后面加一个需求比如“采购单审核通过后要同时更新库存快照”你就得在controller里改来改去迟早乱套。这里面有一个值得关注的细节实体类entity和视图对象vo不要混用。比如采购订单的查询结果里可能带一个供应商名称字段但采购单表里只有supplier_id如果你直接在entity里冗余一个supplierName字段用MyBatis Plus的自动映射时会很别扭。更规范的做法是单独定义一个PurchaseOrderVO在XML里用关联查询把供应商名称查出来映射进去。虽然代码量多一点但后期的可维护性完全不是一个级别。2.4 关于微服务架构的冷思考不少人在热词里看到“微服务架构”就不淡定了觉得单体项目很low。但我想泼一盆冷水采购管理系统这种业务规模用单体架构完全没有问题强行拆微服务反而会引入分布式事务、服务调用链路、配置中心一大堆复杂度。面试时如果你能把单体架构做好、说清楚什么时候该拆、怎么拆比那些只会背微服务概念的人强得多。这个开源项目如果后续要演进比较合理的拆分方式是以“采购域”“库存域”“财务域”为边界切分。采购服务负责申请、订单、审核库存服务负责入库、出库、库存查询财务服务负责应付账款和结算。服务间通过MQ或者OpenFeign通信分布式事务用Seata的AT模式处理。这些内容在老项目里都有对应的单体实现理解清楚之后再去学微服务你会豁然开朗。3. 数据库设计与核心业务模型3.1 核心表结构供应商、商品、采购订单、入库单、退货单做采购管理系统第一件事不是写代码而是把表结构理清楚。这个开源项目里常见的基础表有sys_user、sys_role、sys_menu这类权限表业务表则包括base_supplier供应商、base_product商品、purchase_order采购订单、purchase_order_item订单明细、stock_in_record入库记录、purchase_return退货单等。供应商表里一般有关联人、联系电话、地址、银行账户、合作状态这些字段。商品表里除了名称、编码、规格型号还有一个很关键的字段叫“默认供应商”这在采购申请转订单的时候可以直接带出报价。采购订单主表和明细表是典型的一对多关系主表存订单编号、供应商ID、订单总金额、状态、审核人、审核时间明细表存商品ID、采购数量、采购单价、金额小计。入库单和采购订单之间一般通过order_id关联一次采购订单可以分多次到货所以入库单表和采购订单是多对一。退货单同样要关联采购订单但还要记录退货原因、退货数量、退款金额。把这些关联想明白之后整个系统的数据流就清晰了——从下单到入库再到退货每一次数据变化都能找到源头。3.2 采购状态机设计从草稿到入库再到结算状态机是采购管理系统里最值得拿出来讲的部分。一张采购订单不可能只有“待审核”和“已完成”两个状态真实的业务流转通常是草稿DRAFT - 待审核PENDING - 审核通过APPROVED - 部分入库PART_IN - 已入库FINISHED - 已结算SETTLED。审核不通过则回到草稿或者标记为已驳回REJECTED。为什么要设计这么细因为每个状态都对应着不同的操作权限和数据可见性。比如草稿状态只有创建人自己能看到提交审核之后采购经理才能看到审核通过之后仓库人员才能执行入库操作。如果只用一个varchar字段存状态然后在代码里到处if判断后面改一个需求会有想死的感觉。我建议你在表里加一个state字段用字符串或者整数存状态值然后在Java代码里用一个枚举类去定义这些状态同时把“当前状态是否允许执行某操作”的判断收敛到一个方法里。比如public boolean canApprove() { return this.state PurchaseOrderState.PENDING; }这样比在controller里写if (order.getState() 3)要规范得多而且这样写的好处是所有状态流转逻辑都有据可查不容易出现“从已入库状态还能退回草稿”这种业务bug。3.3 金额与库存字段的类型选择金额字段必须用BigDecimal这是基本功double和float在二进制浮点运算里会产生误差三毛钱的尾差在财务对账时就是灾难。在Java里定义实体时用BigDecimal在MySQL里对应decimal(18,2)如果需要更高的精度可以decimal(18,4)对外展示时再四舍五入。这个项目如果连这个都做对了说明作者是有实际业务经验的你接手后也千万别改回double。库存字段要注意的是“库存数量”和“可用库存数量”其实是两个概念。可用库存等于总库存减去冻结库存。比如采购订单审核通过后系统可以先把这批在途商品数量冻结防止别人重复下单等到实际入库再释放冻结并增加总库存。这个细节做得好不好直接体现你的业务理解深度也是面试时能加分的点。另外凡是涉及数量的字段在数据库里统一用int或者bigint别用varchar存数字否则排序、求和、比较全是坑。我见过有人用varchar存库存数量结果库存不足判断变成字符串比较10 9成立差点造成超卖这个教训很深刻。3.4 数据字典与订单编号生成规则采购单编号不是简单的自增ID一般会遵循一套编码规则。比如格式为CG 年月日 流水号CG20250612001这样一看就知道是采购单而且知道是哪天下单的。还有的会带仓库代码、供应商类型等前缀。流水号可以用数据库自增也可以用Redis的INCR命令生成后者在高并发下性能更好。数据字典一般用sys_dict_type和sys_dict_data两张表来维护像订单状态、商品计量单位、退货原因这些枚举值都放到字典里前端通过接口拉取字典数据渲染下拉框。这样做的最大好处是改一个状态名称不用发版只要在数据库里改字典数据就行。这个开源项目如果没做数据字典你自己加一个也不难属于很常见的二次开发点。4. 核心流程实现从采购申请到入库结算4.1 采购申请与需求汇总让业务从源头就规范起来采购流程一般从采购申请开始。业务人员根据库存预警或销售需求在系统里提一张采购申请单内容包含需要采购的商品、数量、期望到货日期等。这个申请单本身也有状态草稿、待审批、已批准、已驳回。采购申请通过审批之后采购员才会根据它去生成正式采购订单。这里有一个设计点必须注意采购申请单里不应该直接写死供应商除非公司规定某些商品只能从指定供应商采购。更合理的做法是申请单里只表达“需要什么”供应商的选择放到生成采购订单时决定。这样就把需求端和采购执行端解耦了不同部门各司其职。代码实现上采购申请和采购订单是两张独立的表通过requisition_id建立关联一个申请单可以拆分成多个采购订单比如分批采购。需求汇总这块很多人会忽略但这是采购系统里非常实用的功能。你可以写一个SQL按商品编码、规格分组统计一段时间内所有已批准但还未生成采购订单的申请数量形成一张汇总表展示出来。采购员看一眼就知道近期要采购什么、数量多少不用手工Excel来回传。用MyBatis Plus的QueryWrapper加groupBy就能实现代码量不大但业务好感度提升明显。4.2 采购订单生成与多级审核工作流思路的轻量实现从采购申请单生成采购订单有两种实现方式一种是页面里勾选申请单然后自动生成另一种是手动选择商品明细再创建订单。开源项目里通常两种都支持但手动创建为主。生成订单时系统会把商品ID、商品名称、申请数量带出来采购员只需要填供应商和采购单价。接下来是审核环节。很多简单的项目审批就是一个人点通过但真实的采购业务往往需要两级审批采购经理审核价格是否合理、仓库主管审核到货计划是否可行。这个开源项目里如果实现了多级审核一般是在采购订单表里加一个audit_level字段或者用一张审批记录表把每次审核意见存下来。我强烈建议你注意项目里有没有审批记录表audit_record。这张表记录每一步操作是谁、在什么时间、做了什么操作、填了什么意见。有了这张表不仅采购过程可追溯而且面试时你可以很自然地说出“我设计了操作审计链保证业务数据在流转过程中有迹可循”这个点很受面试官认可。轻量级实现就是一个保存审核记录的方法加一个状态流转方法加一个事务注解把两个操作绑在一起。审核的操作逻辑大概是这样的Transactional public void approve(Long orderId, String opinion, String auditor) { PurchaseOrder order orderMapper.selectById(orderId); Assert.isTrue(order.getState() PurchaseOrderState.PENDING, 当前状态不能审核); order.setState(PurchaseOrderState.APPROVED); order.setAuditor(auditor); order.setAuditTime(new Date()); orderMapper.updateById(order); AuditRecord record new AuditRecord(); record.setOrderId(orderId); record.setAction(APPROVE); record.setOpinion(opinion); record.setOperator(auditor); auditRecordMapper.insert(record); }注意前置校验必须在事务里做防止并发情况下两个审核员同时审核同一张单子把状态更新成两次。如果你用的是MySQL的InnoDB除了状态判断外还可以在更新语句里加上where state PENDING通过受影响行数判断是否更新成功这是乐观锁的思路后面讲并发的时候会详细说。4.3 到货验收与入库逻辑库存更新的事务边界怎么划采购订单审核通过之后下一步是到货入库。仓库人员收到货后在系统里点“入库”选择对应的采购订单填写实际到货数量。这里就会遇到一个业务问题实到数量可能小于订单数量这叫“短装”。短装的处理方式有两种一种是允许分批入库一张采购单可以多次入库直到数量齐另一种是直接把差额标记为差异走异常处理流程。代码上入库操作至少要做三件事往入库流水表插一条记录、更新库存表数据、更新采购订单的已入库数量。这三个操作必须在一个事务里完成否则可能出现库存加了但订单状态没更新的情况。我见过最典型的bug就是入库单保存成功了但采购订单的“已入库数量”没变导致采购单永远卡在“部分入库”状态。这里还要想清楚一个边界问题订单金额和实际入库金额可能不一致。如果实到数量少了入库金额应该按实到数量乘以单价计算。所以采购订单明细里最好记录数量和单价入库单里记录实收数量应付账款按实收数量算这个逻辑财务最认。我建议你在实现入库时给采购订单明细加一个received_quantity字段每次入库都在事务里做UPDATE purchase_order_item SET received_quantity received_quantity #{qty} WHERE id #{itemId}然后判断如果received_quantity quantity就把主订单状态置为FINISHED。这个操作需要保证原子性所以把更新语句设计成原子自增是非常关键的做法。4.4 采购退货与财务对账闭环设计里的最后一块拼图采购退货是很多教材项目不会写的内容但它恰恰是采购闭环里不可缺少的。退货原因很多比如质量不合格、数量多发了、价格协商后取消部分订单等。退货单需要关联原采购订单明细记录退货数量、退货单价、退货原因并且要有一个状态表示退款是否完成。从实现上说退货操作和入库操作是对称的入库加库存退货减库存。所以退货事务里同样要保证生成退货单时同步扣减库存同时更新采购订单的已退货数量。如果退货发生在入库之前那就更简单了只需修改采购订单状态不需要动库存因为货根本没进来。财务对账这块采购订单审核通过后会在应付账款表里生成一条记录入库后再根据实际入库金额更新退货后再把退货金额抵消。这个模块做得好不好直接决定财务人员愿不愿意用这个系统。作为Java开发者你需要知道应付账款不是简单地把订单总金额copy过来而是要在每次入库、退货时重新计算。5. 权限、安全与多租户设计5.1 RBAC权限模型落地用户-角色-菜单的三层映射权限设计是这个系统的另一个重头戏。经典的RBAC模型用五张表实现用户表、角色表、菜单表、用户角色关联表、角色菜单关联表。用户不直接和权限挂钩而是通过角色间接获得权限。这样的好处是权限调整时只动角色和菜单的关联不用一个个改用户。在代码层面登录成功后后端会把当前用户的角色和权限标识比如perms字符串返回给前端前端在路由守卫和按钮级别做校验。比如一个用户拥有purchase:order:approve这个权限标识前端才能显示“审核”按钮后端接口也要通过注解或者拦截器再校验一次。前端权限控制提升用户体验后端鉴权才是真正的安全底线前后端都做才能防止绕过页面直接调接口的越权操作。这里有一个容易出问题的点如果项目用的是Spring Security那么接口权限通常通过PreAuthorize(hasAuthority(purchase:order:approve))来控制但是如果你忘了在启动类加上EnableGlobalMethodSecurity这个注解是不生效的。我见过不少项目代码里写了注解但没启用导致所有权限校验形同虚设这种坑一定要查。5.2 数据权限部门/仓库维度的隔离功能权限管的是“能不能操作”数据权限管的是“能看哪些数据”。比如采购经理应该能看到所有采购订单而普通采购员只能看到自己创建的单子。这个开源项目里常见做法是在业务表里加create_by或者dept_id字段查询时根据当前登录人的角色拼上过滤条件。MyBatis Plus的DataPermissionInterceptor就是干这个的它会在执行SQL时自动拼上数据权限条件不用每个Mapper的手写SQL里都加where。不过要注意自动拼接条件有时候会把统计SQL也过滤掉导致汇总数据不完整。所以我的建议是明细查询用数据权限拦截器报表统计单独走不受限制的查询接口并且在后端做好审计日志毕竟报表使用者通常是管理层。多租户的概念在需要SaaS化时才会涉及。如果你想从单系统演进到多租户最轻量级的方案是每个业务表加一个tenant_id字段所有查询强制带上租户条件DataSource也用路由方案去分库。不过说实话以采购管理系统的业务量大部分情况下单库加tenant_id就够了真到需要分库的那一天你也会比现在更清楚该怎么分。5.3 操作审计与日志防篡改正规一点的开源项目会有一张操作日志表或者登录日志表记录谁在什么时间干了什么事。采购管理系统里审计日志尤其重要因为涉及金额和库存出问题要能追溯。实现方式有两种一种是Service层手动记录好处是业务语义明确坏处是容易漏另一种是基于Spring AOP切面的注解式日志通过Log注解统一记录代码侵入小。我个人的做法是两种结合核心业务操作审核、入库、退货用AOP记录操作人、操作时间、请求参数、操作结果数据变更细节用数据库本身的高阶功能或者程序内的对比逻辑去记录。特别是订单金额、采购价这种敏感字段每次修改前把旧值和新值都存下来比单纯记录“修改了订单”要有用得多。还要注意一点日志表里的时间要统一用服务器时区最好存UTC时间或者时间戳展示时再转本地时区。如果服务器和数据库时区不一致你排查问题时会非常崩溃这个坑遇到过一次就再也不想遇到第二次。6. 并发、事务与常见性能陷阱6.1 并发下单与库存扣减的乐观锁方案采购管理系统虽然不像秒杀系统那样极端但并发场景还是有的比如多个采购员同时提交订单仓库多人同时执行入库操作。如果库存扣减不做并发控制就会出现“明明库存只有10件却同时入库了20件”的问题。最简单的方案是乐观锁。在库存表stock里加一个version字段更新时先查出版本号再执行UPDATE stock SET quantity quantity #{qty}, version version 1 WHERE id #{id} AND version #{oldVersion}如果更新影响行数为0说明版本已变需要重试或者提示操作失败。用乐观锁而不是悲观锁SELECT FOR UPDATE的原因很简单采购场景并发量不大锁冲突概率低乐观锁不需要一直持有数据库连接性能和代码复杂度上都更友好。但要注意库存充足校验和库存扣减必须放在同一个事务里并且扣减SQL必须是原子操作不要在Java里先查再算再更新否则还是会超卖。6.2 事务失效场景自调用、异常被吞、跨库事务事务注解Transactional用起来简单但失效场景极多。最常见的两个坑是同一个类里方法自调用导致事务失效以及捕获异常后没有抛出RuntimeException导致事务不回滚。举个例子你在PurchaseOrderServiceImpl里写了一个createOrderAndUpdateStock方法里面调用了同一个类里的updateStock方法updateStock上有Transactional注解但因为在类内部调用代理对象没有介入事务根本不生效。解决办法是把updateStock拆到另一个Service类里或者自己注入自己或者通过AopContext.currentProxy()调用。异常被吞的场景也很常见try { // 业务操作 } catch (Exception e) { log.error(操作失败, e); // 没有抛出异常事务依然提交 }Spring事务默认只在RuntimeException和Error时回滚checked exception不会触发回滚。如果你要处理受检异常并希望事务回滚必须在catch里抛出RuntimeException或者用Transactional(rollbackFor Exception.class)显式指定。做采购这种涉及金额的业务我建议你统一用rollbackFor Exception.class宁多勿少。跨库事务这个问题在单体项目里不会遇到但如果拆微服务就会面临。采购服务调用库存服务扣减库存两边各有数据库普通本地事务管不了。这时候要么用Seata这种分布式事务方案要么用本地消息表加MQ最终一致性。面试时聊到这里就很有深度了你可以说“单体阶段我用本地事务保证强一致微服务阶段我改成事务消息保证最终一致这是取舍”。6.3 金额计算用BigDecimal而不是double前面提过金额字段用decimal这里补充一下BigDecimal的用法。最容易踩的坑是使用BigDecimal构造函数的double参数比如new BigDecimal(0.1)它会得到0.1000000000000000055511151231257827021181583404541015625。正确做法是使用BigDecimal.valueOf(0.1)或者new BigDecimal(0.1)用字符串构造。采购系统里涉及单价、数量、折扣、税额、应付金额的乘法运算合理的方式是定义精度和舍入模式。比如金额保留两位小数四舍五入BigDecimal amount price.multiply(quantity).setScale(2, RoundingMode.HALF_UP);这里还有一个小细节税额计算的顺序会影响最终金额的尾差。有的系统先算不含税金额再乘以税率四舍五入得到税额最后相加有的系统直接算含税总价然后反推税额。两种方式在不同场景下都可能被财务接受但必须保持一致否则对账永远对不上。这个项目如果作者考虑到了这一点是会写一个统一的金额计算工具类的你看代码时留意一下。6.4 MyBatis Plus分页与大数据量优化采购订单列表通常要分页展示MyBatis Plus提供了分页插件用法是new Page(pageNum, pageSize)作为Mapper方法的参数。但分页插件有一个坑如果你在XML里写了多表关联查询分页插件会自动生成COUNT查询有时候这个COUNT语句效率极低甚至因为特殊SQL导致统计错误。我的经验是对于复杂的多表分页查询最好不要依赖分页插件自动COUNT而是自己写一个count查询单独执行或者用更轻量的方式——先分页查主表ID再根据ID查明细数据也就是俗称的“延迟关联”。比如先SELECT id FROM purchase_order WHERE ... LIMIT 20再用这些ID去查关联数据这样即使关联表很大也不会因为笛卡尔积导致分页查询变慢。另外采购订单明细查询时要注意避免N1问题。如果页面上需要显示每条订单对应的供应商名称和商品名称简单的代码可能是在循环里逐条查询这是典型的N1几百条数据就会明显卡顿。正确做法是一次IN查询把所有关联数据查出来再在内存里组装。面试时能主动说出“我用IN查询消除了N1问题”这个点会很加分。7. 面试官常问的八股与项目挂钩7.1 把项目包装成简历亮点的正确姿势很多人在简历上写“参与开发了商品采购管理系统”然后罗列Spring Boot、MyBatis、MySQL这些技术名词这等于什么都没写。面试官要看的是你到底解决了什么问题、方案为什么是这样。正确写法类似这样设计并实现了采购订单状态机支持草稿、待审核、部分入库、完成等7个状态的流转控制通过枚举封装状态判断逻辑避免业务规则散落在代码各处。基于用户-角色-菜单的RBAC模型实现功能权限和数据权限隔离核心接口使用Spring Security注解鉴权并设计操作审计表记录关键业务变更。通过乐观锁和事务边界控制解决并发入库场景下的库存一致性问题金额计算统一使用BigDecimal保证财务数据的精确性。这样的描述每一句都能引导面试官追问而且你确实能答得上来项目自然就成了加分项。最怕的是把Redis、MQ这些名词硬塞进去面试官一问细节就露馅。7.2 针对“数据一致性”的追问怎么答数据一致性是采购系统面试的高频问题。面试官可能会问“用户提交采购订单时如何保证订单数据和库存数据一致”回答的核心是“本地事务 边界控制”。先开启事务插入订单主表和明细表更新库存表和应付账款表全部成功则提交任何一个环节失败则回滚保证数据库层面强一致。他可能会接着问“如果是微服务架构呢”这个时候你不需要慌张可以分步骤回答先分析业务库存扣减和订单创建发生在不同服务就不能用本地事务了。可选方案包括Seata的AT模式适合中小团队对业务代码侵入小或者本地消息表加MQ保证最终一致。然后你可以补充一句“我在单体项目阶段已经把事务边界理清了后续拆服务时的分布式事务方案也有过预研”这样既有深度又有实战感。还有一个隐藏考点幂等性。因为采购订单提交、审核回调、入库操作都可能因为网络超时被前端重复请求后端接口要保证同一个操作只生效一次。最简单的方案是在业务表上建唯一索引比如入库流水表里加一个业务幂等键重复插入就会报错配合异常捕获就能避免重复处理。能讲到这个层面已经超出大多数候选人的水平了。7.3 单体项目如何讲清楚微服务演进路径面试官喜欢问微服务不一定是真要你做过而是想看你的架构思维。你可以这样回答目前的采购系统是单体架构模块划分已经按业务域做了边界controller、service、mapper三层清晰。如果业务规模变大我会优先做数据库的读写分离把查询压力分担到从库再往后把采购、库存、财务三个模块拆成独立服务服务间通过消息队列异步解耦比如订单审核通过后发一条消息库存服务消费后执行预占库存。分布式事务可以继续用Seata日志链路用Sleuth加Zipkin。但你一定要强调拆分的前提是业务确实需要不是为了微服务而微服务。这种回答既展示了你对微服务的了解又体现了工程判断力面试官通常会很认可。8. 踩坑记录与调试技巧8.1 开源项目拉下来跑不起来的排查清单从GitHub上拉下来的开源项目第一次跑不起来是常态千万别慌。我总结了一份排查顺序。第一检查JDK和Maven版本是否匹配。很多老项目用的是JDK 8新项目用JDK 17如果你的本地版本和项目的pom.xml不一致会报各种奇奇怪怪的编译错误。尤其是热词里提到的“源发行版17需要目标发行版17”这类提示本质就是JDK版本不匹配要么把本地JDK切到17要么在pom.xml里修改java.version。第二检查数据库版本和初始化脚本。项目一般会提供sql目录里面是建表和基础数据脚本。如果脚本是在MySQL 5.7上导出的MySQL 8.0可能因为字符集或者排序规则跑不过你在执行脚本的时候注意看报错信息就行。另外注意数据库连接串里的时区和编码参数serverTimezoneAsia/ShanghaicharacterEncodingutf8这两个参数一定要加上不然日期和中文都可能出问题。第三检查Redis是否启动。如果项目用了Redis做缓存或者存储验证码本地没装Redis的话登录都会失败。Windows上可以下载一个简易版RedismacOS直接brew install redis就行。这些都是基础环境问题按顺序排查基本都能解决。8.2 日期时区问题与JSON序列化循环引用日期问题在前后端联调时特别烦人。后端返回的Date类型默认序列化成一串时间戳前端拿到没法直接展示或者Jackson序列化LocalDateTime时格式不符合预期。解决方案是在application.yml里统一配置spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8然后实体类的时间字段上加JsonFormat(pattern yyyy-MM-dd HH:mm:ss)。这个配置看着简单但能省掉一堆联调时的破事。JSON序列化循环引用也是老问题比如采购订单里有供应商对象供应商对象里又有采购订单列表Jackson默认会报“could not write JSON: Infinite recursion”。解决办法是在关系字段上使用JsonIgnoreProperties或者JsonIgnore只保留单向序列化。很多开源项目刚开始没处理列表接口一调就崩你接手后注意检查这些注解。8.3 Flyway或Liquibase管理数据库变更开源项目如果提供了V1__init.sql、V2__add_column.sql这种命名规范的脚本目录那说明作者用了Flyway做数据库版本管理。Flyway的好处是每次启动应用时自动执行未执行过的迁移脚本保证所有开发人员和测试环境的数据结构一致。如果没有用Flyway我建议你自己上手加一下。把现有的初始化脚本整理成V1__init.sql放到resources/db/migration目录然后在pom.xml里引入flyway-core依赖。启动时会自动执行脚本并在flyway_schema_history表里记录执行记录。这样你再改表结构就新建一个V2__xxx.sql而不是直接在原有脚本上改团队协作时不会出现“为什么我这边表缺了字段”的奇葩问题。注意使用Flyway有一个前提如果项目已经在线运行Flyway默认会校验已执行的脚本是否被修改过如果你改了V1脚本启动就会报校验失败。解决办法是执行flyway repair命令或者重新建库。所以在项目早期就引入Flyway是成本最低的时机。8.4 用Arthas和IDEA Debug定位线上问题采购系统跑在线上偶尔会出现“某个入库操作很慢”或者“接口偶尔报错”的情况。这时候Arthas是个非常趁手的工具。用java -jar arthas-boot.jar启动然后select出你的应用进程在Arthas控制台里可以用trace命令跟踪一个方法的调用耗时用watch命令观察某个方法的入参和返回值还能在不用重启应用的情况下反编译看线上的代码逻辑。比如怀疑某个批量入库接口性能差可以先trace一下PurchaseOrderServiceImpl看哪一行SQL耗时最长然后对症优化。如果怀疑数据被并发写坏了可以用watch命令抓取更新前后的对象状态比瞎猜强得多。本地排查时用IDEA的Debug断点调试依然是最顺手的尤其是在多表关联的组装逻辑里。建议你在关键状态流转处都打上断点观察每一步的状态值变化能很快定位是哪个判断条件写得不对。如果线上环境不允许远程DebugArthas几乎是唯一的选择这个工具值得花半小时学一下。说到最后这个项目后续还能怎么扩展我个人的建议是先把现有代码按“业务域”重新梳理一遍尤其是把散落在service里的状态判断逻辑收敛到状态机里然后把应付账款模块做强增加账期、付款计划、发票管理这些功能。如果有一天想往SaaS方向走再考虑多租户改造。做这类项目最重要的是别贪多把一个闭环做透胜过开一堆虎头蛇尾的功能。
返回列表