1000.00元和1000.01元走两条审批路,飞算JavaAI能抓住金额边界吗?
同一套报销需求,只差 0.01 元就要切换审批路线。这次不看 AI 会不会生成 Controller,只看它能否抓住金额边界、部门权限与状态流转。
一、测试背景:为什么不再测普通 CRUD
这次飞算 JavaAI 模型升级,新增了智能路由与专家模型两种模式。官方 Brief 给出的方向一很直接:不要再用增删改查证明 AI 会写代码,而要把它放进权限链路、状态机和复杂业务规则里,看它能不能先“读懂”,再拆出合理的接口、数据结构与实现步骤。
1、项目选择:报销智审 ExpenseFlow
因此,我选择了一个很适合暴露逻辑问题的项目——报销智审 ExpenseFlow。它表面上只是企业费用报销,真正困难的却不是表单,而是下面这些互相咬合的规则:
-
员工只能处理自己的报销单,经理只能审批本部门申请
-
金额由后端按费用明细重新汇总,不能信任前端传来的总额
-
1000.00 元与 1000.01 元必须走不同审批路径
-
发票号码不能重复,驳回、撤回、打款都要遵守状态机
-
财务只能复审经理已通过的高金额单据,不能越级审批
-
管理员可以看全局台账,但不能绕过业务角色直接改状态
我把它设计成前后端分离项目,并且要求不仅给示例代码,还要具备登录、角色工作台、审批流、H2/MySQL 两种运行方式以及自动化测试。换句话说,这道题考的是业务建模,不是代码补全。
2、事实与证据边界
📌 事实说明: 前 6 张图记录的是飞算 JavaAI 的需求拆解、接口设计、表结构设计和代码生成计划;后 6 张图是随后对照该方案实现并复验的可运行参考项目。
二、测试环境与验证口径
1、环境配置
2、验证口径
本文采用三类证据:飞算 JavaAI 界面截图用于判断“是否抓住需求”;源码与自动化测试用于判断关键规则是否可实现;浏览器页面用于观察四类角色的数据范围与业务状态。没有计时器记录的指标,我一律不写“提升了多少”。
三、输入要求:我给 AI 的不是一句“做个报销系统”
1、工程与业务边界
为了避免需求含糊,我在提示词里先锁定了工程边界:根目录为 expense-flow,后端和前端分别放在 expenseflow-server 与 expenseflow-web,Java 基础包名为 com.expenseflow。更重要的是,我明确告诉它:项目重点是复杂业务规则理解,而不是普通 CRUD。

图 1:提示词开头明确了可运行前后端项目、目录结构和复杂业务目标;此时只是需求输入,并非已生成代码。
2、交付与验收标准
提示词末尾继续把交付标准钉死:README 必须写环境、启动命令、演示账号和审批规则;数据库既要支持零配置 H2,也要提供 MySQL 方案;生成后必须实际执行 mvn test 与 npm run build,还要跑通登录、低金额审批、高金额复审和确认打款。

图 2:提示词末尾定义了构建和业务验收要求。它证明“要求是什么”,不能替代后面的实际测试结果。
这一步很重要。复杂项目如果只写“实现报销审批”,AI 很容易给出几张表、几个 Controller 就宣布完成;而把失败路径和验收条件写进提示词,才能观察它究竟是在补全代码,还是在建立业务模型。
四、智能引导拆解结果
1、理解需求:41 个关键点
进入“理解需求”阶段后,界面显示这份长需求被拆成 41 个关键点。可见条目已经抓到了几类容易被遗漏的内容:
-
研发部与市场部两个部门,以及员工、经理、财务、管理员 6 个演示身份
-
800 元、1200 元、1500 元等不同状态的初始化单据
-
前后端跨域、H2/MySQL 双运行方式和 README 交付
-
500.00 + 500.00 = 1000.00 的服务端汇总
-
1000.00 与 1000.01 的审批边界
-
跨部门权限、重复发票、非法状态跳转、撤回后禁止继续审批、重复打款防护

图 3:首轮需求理解界面显示 41 个关键点,画面仅展示其中一部分。41 是拆解数量,不代表 41 项已经全部正确实现。
我的第一感受是,它没有把“报销系统”压扁成增删改查,而是把初始化数据、权限、边界值和负向测试都当成了需求。尤其是 1000.00/1000.01 这一分钱的差异被保留下来,说明金额分流没有在长文本里被稀释掉。
2、设计接口:15 个接口方案
到“设计接口”阶段,界面给出 15 个接口方案。比接口数量更值得看的是它如何描述边界:员工只能操作自己的报销,经理只能处理本部门待办,财务只处理高金额复审与打款,管理员只读查看全局数据;并且这些限制要在后端根据身份、角色、部门、申请人和当前状态共同校验,不能只靠前端隐藏按钮。

图 4:接口设计阶段显示 15 个方案,并把角色权限、统一响应、仪表盘统计、异常校验和自动化测试纳入设计。
这比“生成一个 ClaimController”更接近真实工程,因为权限并不是一个 role == MANAGER 判断就结束了。经理还必须同部门,财务还必须面对正确状态,列表查询参数也只能缩小权限范围,不能扩大后端由 JWT 决定的可见范围。
不过这里也出现了第一处需要人工对齐的地方:规划界面显示 15 个接口方案,而参考实现最终暴露了 16 个 HTTP 端点,多出的健康检查属于运行验证能力。因此,不能把“15”写成最终 API 的精确总数,更不能声称 15 个方案原样落地。
3、表结构设计:5 张表把审批轨迹单独建模
表结构阶段给出 5 张核心表,分别承载部门、用户、报销主单、费用明细和审批记录。截图展示的审批记录表包含申请单、操作人、动作、变更前状态、变更后状态、审批意见与操作时间,这个方向是对的:状态变化不能只覆盖主表当前值,还要留下可追溯的时间线。

图 5:智能引导阶段的 5 表数据库草案。右上个人标识已从交付图片中裁去。
但这张图只能代表设计草案,不是最终 DDL。参考实现中,审批表字段从 action_code/approval_comment/operation_time 收敛为 action/comment/created_at,并去掉了未使用的通用审计列。核心关系保留了,字段命名却发生了调整。这种规划与实现漂移并不罕见,也提醒我:AI 给出的表设计仍需要在生成后和实体、Mapper、初始化脚本做一次一致性检查。
4、代码生成计划:56 个计划项
“代码生成计划”被拆成 56 个计划项,画面可见的内容已经覆盖工程骨架、数据模型、H2/MySQL 初始化、统一异常、鉴权、接口、前端和分阶段验证。它还明确写出 Java 17、Spring Boot 3、Vue 3,以及先搭骨架、再验证、再进入下一层的节奏。

图 6:代码生成计划共 56 项;56 指计划条目,不是文件数、功能数或测试数。右上个人标识已从交付图片中裁去。
从“读懂需求”的角度看,这个过程有价值:41 个需求点、15 个接口方案、5 张表、56 个计划项构成了从业务到工程的中间层,用户可以在真正生成源码前纠正权限或状态设计。它不是一句 Prompt 之后的黑盒输出,而是允许在需求、接口、表结构和计划四个关口检查。
五、复杂业务落地验证
1、核心状态机:一分钱触发另一条审批路径
ExpenseFlow 的主状态机可以压缩成下面这张图。所有单据先交给部门经理,只有经理通过后,系统才根据金额决定是否进入财务复审;财务也只能处理已经进入 PENDING_FINANCE 的单据。
ExpenseFlow approval lifecycleExpense claim state machine showing manager approval for all claims and an additional finance review only when the server-calculated amount exceeds 1000 yuan#mermaid-svg-DuG9cAavEW8yBUoX{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-DuG9cAavEW8yBUoX .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-DuG9cAavEW8yBUoX .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-DuG9cAavEW8yBUoX .error-icon{fill:#552222;}#mermaid-svg-DuG9cAavEW8yBUoX .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-DuG9cAavEW8yBUoX .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-DuG9cAavEW8yBUoX .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-DuG9cAavEW8yBUoX .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-DuG9cAavEW8yBUoX .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-DuG9cAavEW8yBUoX .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-DuG9cAavEW8yBUoX .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-DuG9cAavEW8yBUoX .marker{fill:#333333;stroke:#333333;}#mermaid-svg-DuG9cAavEW8yBUoX .marker.cross{stroke:#333333;}#mermaid-svg-DuG9cAavEW8yBUoX svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-DuG9cAavEW8yBUoX p{margin:0;}#mermaid-svg-DuG9cAavEW8yBUoX defs #statediagram-barbEnd{fill:#333333;stroke:#333333;}#mermaid-svg-DuG9cAavEW8yBUoX g.stateGroup text{fill:#9370DB;stroke:none;font-size:10px;}#mermaid-svg-DuG9cAavEW8yBUoX g.stateGroup text{fill:#333;stroke:none;font-size:10px;}#mermaid-svg-DuG9cAavEW8yBUoX g.stateGroup .state-title{font-weight:bolder;fill:#131300;}#mermaid-svg-DuG9cAavEW8yBUoX g.stateGroup rect{fill:#ECECFF;stroke:#9370DB;}#mermaid-svg-DuG9cAavEW8yBUoX g.stateGroup line{stroke:#333333;stroke-width:1;}#mermaid-svg-DuG9cAavEW8yBUoX .transition{stroke:#333333;stroke-width:1;fill:none;}#mermaid-svg-DuG9cAavEW8yBUoX .stateGroup .composit{fill:white;border-bottom:1px;}#mermaid-svg-DuG9cAavEW8yBUoX .stateGroup .alt-composit{fill:#e0e0e0;border-bottom:1px;}#mermaid-svg-DuG9cAavEW8yBUoX .state-note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-DuG9cAavEW8yBUoX .state-note text{fill:black;stroke:none;font-size:10px;}#mermaid-svg-DuG9cAavEW8yBUoX .stateLabel .box{stroke:none;stroke-width:0;fill:#ECECFF;opacity:0.5;}#mermaid-svg-DuG9cAavEW8yBUoX .edgeLabel .label rect{fill:#ECECFF;opacity:0.5;}#mermaid-svg-DuG9cAavEW8yBUoX .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-DuG9cAavEW8yBUoX .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-DuG9cAavEW8yBUoX .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-DuG9cAavEW8yBUoX .edgeLabel .label text{fill:#333;}#mermaid-svg-DuG9cAavEW8yBUoX .label div .edgeLabel{color:#333;}#mermaid-svg-DuG9cAavEW8yBUoX .stateLabel text{fill:#131300;font-size:10px;font-weight:bold;}#mermaid-svg-DuG9cAavEW8yBUoX .node circle.state-start{fill:#333333;stroke:#333333;}#mermaid-svg-DuG9cAavEW8yBUoX .node .fork-join{fill:#333333;stroke:#333333;}#mermaid-svg-DuG9cAavEW8yBUoX .node circle.state-end{fill:#9370DB;stroke:white;stroke-width:1.5;}#mermaid-svg-DuG9cAavEW8yBUoX .end-state-inner{fill:white;stroke-width:1.5;}#mermaid-svg-DuG9cAavEW8yBUoX .node rect{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-DuG9cAavEW8yBUoX .node polygon{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-DuG9cAavEW8yBUoX #statediagram-barbEnd{fill:#333333;}#mermaid-svg-DuG9cAavEW8yBUoX .statediagram-cluster rect{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-DuG9cAavEW8yBUoX .cluster-label,#mermaid-svg-DuG9cAavEW8yBUoX .nodeLabel{color:#131300;}#mermaid-svg-DuG9cAavEW8yBUoX .statediagram-cluster rect.outer{rx:5px;ry:5px;}#mermaid-svg-DuG9cAavEW8yBUoX .statediagram-state .divider{stroke:#9370DB;}#mermaid-svg-DuG9cAavEW8yBUoX .statediagram-state .title-state{rx:5px;ry:5px;}#mermaid-svg-DuG9cAavEW8yBUoX .statediagram-cluster.statediagram-cluster .inner{fill:white;}#mermaid-svg-DuG9cAavEW8yBUoX .statediagram-cluster.statediagram-cluster-alt .inner{fill:#f0f0f0;}#mermaid-svg-DuG9cAavEW8yBUoX .statediagram-cluster .inner{rx:0;ry:0;}#mermaid-svg-DuG9cAavEW8yBUoX .statediagram-state rect.basic{rx:5px;ry:5px;}#mermaid-svg-DuG9cAavEW8yBUoX .statediagram-state rect.divider{stroke-dasharray:10,10;fill:#f0f0f0;}#mermaid-svg-DuG9cAavEW8yBUoX .note-edge{stroke-dasharray:5;}#mermaid-svg-DuG9cAavEW8yBUoX .statediagram-note rect{fill:#fff5ad;stroke:#aaaa33;stroke-width:1px;rx:0;ry:0;}#mermaid-svg-DuG9cAavEW8yBUoX .statediagram-note rect{fill:#fff5ad;stroke:#aaaa33;stroke-width:1px;rx:0;ry:0;}#mermaid-svg-DuG9cAavEW8yBUoX .statediagram-note text{fill:black;}#mermaid-svg-DuG9cAavEW8yBUoX .statediagram-note .nodeLabel{color:black;}#mermaid-svg-DuG9cAavEW8yBUoX .statediagram .edgeLabel{color:red;}#mermaid-svg-DuG9cAavEW8yBUoX #dependencyStart,#mermaid-svg-DuG9cAavEW8yBUoX #dependencyEnd{fill:#333333;stroke:#333333;stroke-width:1;}#mermaid-svg-DuG9cAavEW8yBUoX .statediagramTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-DuG9cAavEW8yBUoX :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
员工保存草稿
提交审批
经理通过且金额不高于1000.00
经理通过且金额高于1000.00
经理驳回
员工撤回
财务复审通过
财务驳回
员工撤回
财务确认打款
流程结束
流程结束
流程结束
Draft
PendingManager
Approved
PendingFinance
Rejected
Withdrawn
Paid
参考实现里最值得看的不是 Controller,而是下面这段服务层逻辑:
public static final BigDecimal FINANCE_REVIEW_THRESHOLD =
new BigDecimal("1000.00");
@Transactional
public ClaimVO submit(Long id, CurrentUser actor) {
ExpenseClaim claim = lockRequired(id);
requireOwner(claim, actor);
requireStatus(claim, ClaimStatus.DRAFT, ClaimStatus.PENDING_MANAGER);
List<ExpenseItem> items = itemMapper.selectList(
new LambdaQueryWrapper<ExpenseItem>()
.eq(ExpenseItem::getClaimId, id));
BigDecimal calculated = items.stream()
.map(ExpenseItem::getAmount)
.reduce(BigDecimal.ZERO, BigDecimal::add)
.setScale(2, RoundingMode.UNNECESSARY);
claim.setTotalAmount(calculated);
return transition(claim, ClaimStatus.PENDING_MANAGER,
actor, ApprovalAction.SUBMIT, "提交审批");
}
@Transactional
public ClaimVO approve(Long id, String comment, CurrentUser actor) {
ExpenseClaim claim = lockRequired(id);
if (actor.role() == UserRole.MANAGER) {
requireSameDepartment(claim, actor);
requireStatus(claim, ClaimStatus.PENDING_MANAGER, ClaimStatus.APPROVED);
ClaimStatus target = claim.getTotalAmount()
.compareTo(FINANCE_REVIEW_THRESHOLD) <= 0
? ClaimStatus.APPROVED
: ClaimStatus.PENDING_FINANCE;
return transition(claim, target, actor,
ApprovalAction.APPROVE, cleanOptional(comment));
}
// 财务只能复审已经进入 PENDING_FINANCE 的单据
if (actor.role() == UserRole.FINANCE) {
requireStatus(claim, ClaimStatus.PENDING_FINANCE, ClaimStatus.APPROVED);
return transition(claim, ClaimStatus.APPROVED,
actor, ApprovalAction.APPROVE, cleanOptional(comment));
}
throw forbidden("APPROVAL_FORBIDDEN", "当前角色不能审批报销单");
}
我认为这里有三个正确的工程选择:第一,用 BigDecimal.compareTo 比较金额,不用 double;第二,在提交时重新读取数据库明细并汇总,客户端即使伪造 totalAmount 也不能改变审批路线;第三,把“角色 + 部门 + 当前状态”放进同一条业务路径校验,避免财务跳过经理或经理跨部门审批。
2、登录页:六种演示身份进入不同工作区
参考项目提供 6 个演示身份,覆盖研发部员工/经理、市场部员工/经理、财务和管理员。它们进入的不是同一个页面换个名字,而是不同的数据范围和动作集合。

图 7:可运行登录页提供 6 种演示身份入口;统一密码仅供本地种子数据使用。
3、员工视角:只统计自己的报销记录
研发部员工视角只统计自己的草稿、审批中和已通过记录。截图中的“已通过 02”口径包含待打款与已打款,不能解读为两笔都已完成付款。

图 8:研发部员工工作台展示个人草稿、审批中、已通过及最近记录,并在侧栏提示 1000 元分界。
4、部门对照:高金额单也不能跳过经理
切换到市场部员工,页面只出现该员工的 1500.00 元待经理审批记录。高金额并不会因为最终要去财务就跳过经理;所有单据首先进入部门经理环节。

图 9:市场部员工的 1500 元单据仍处于待经理审批,页面数据与研发部员工不同。
5、经理视角:权限范围收窄到本部门
研发部经理看到的是本部门台账。截图时待办已经清零,但仍能看到 800.00 元已通过和 1200.00 元待财务复审两种结果。这与“低金额直接通过、高金额继续流转”的状态设计一致;不过截图没有记录点击审批的瞬间,因此这里把它视为状态快照,而不是完整操作录像。

图 10:研发部经理视角按部门收窄数据,待办已清零,后续流转中的本部门记录仍然可见。
6、财务视角:复审与打款分成两条队列
财务中心把“待财务复审”和“待确认打款”分成两条队列。1200.00 元单据进入复审区,说明它已经经过经理环节;财务可审阅、驳回或复审通过。该截图没有展示最终打款成功,所以我不会用它声称付款动作已经在画面里完成。

图 11:财务中心显示 1 笔待复审和 2 笔待确认打款;1200 元单据位于高金额复审队列。
7、管理员视角:全局查看但业务只读
管理员则是全局只读台账:可以跨部门筛选和查看,但审批、状态变化、打款仍须由对应业务角色执行。这里刻意没有给管理员“万能审批”能力,否则前面的状态机就失去了意义。

图 12:管理员可以查看和筛选企业总账,但不能直接绕过业务角色修改报销状态。
六、量化验证结果
1、规划与运行数据汇总
本轮可以直接复现的量化结果如下。规划阶段的数字说明拆解粒度;运行阶段的数字说明参考实现通过了什么验证,两者不能混为一谈。
2、测试覆盖与构建结果
20 个后端测试覆盖了金额重新汇总、伪造总额无效、重复发票、同部门与跨部门审批、员工数据隔离、财务越权、非法状态跳转、驳回原因、低/高金额完整流程、撤回后禁止审批以及重复打款防护。前端构建也成功完成,但有一个非阻塞警告:Element Plus vendor chunk 约 926.95 kB,超过 Vite 默认的 500 kB 提示线,后续可通过按需引入或更细的代码分包优化首屏资源。
七、真实评价与改进建议
1、优点:复杂业务主线没有丢失
这次最让我认可的,不是生成了多少行代码,而是飞算 JavaAI 在长需求里保住了业务主线。它把员工、经理、财务、管理员拆成不同权限范围;没有丢掉 1000.00/1000.01 的边界;把审批记录独立建模;也把负向场景写进自动化测试计划。这些都比一套标准 CRUD 更能体现“理解需求”的能力。
它的另一个优点是过程可检查。需求、接口、表结构、代码计划分成 4 个阶段展示,开发者有机会在源码生成前发现偏差。这对于复杂业务比“输入一句话,等待整仓输出”更稳妥。
2、不足:规划正确不等于工程完成
但不足也很明确:
-
规划数量不能替代正确率。 41、15、5、56 都是拆解结果,不是功能通过率
-
设计与实现需要二次对齐。 本次接口从规划的 15 项变为 16 个 HTTP 端点,审批表字段也做了收敛
-
复杂权限仍需负向测试。 仅凭不同角色页面不能证明后端隔离,必须补跨部门、越级审批和参数扩权测试
-
生成证据链还不完整。 现有截图只到代码生成计划,若要评价端到端源码生成质量,应补“生成源码”完成页、构建日志和实际修复记录
-
前端资源仍有优化空间。 生产构建通过,但大型 UI 依赖造成 vendor chunk 警告
3、建议:把自动化验证作为独立验收层
我的建议是:用飞算 JavaAI 先做复杂需求的结构化拆解,再把 mvn test、接口级 403/409、数据库约束和浏览器主流程作为独立验收层。对于金额、库存、余额等领域值,还可以在提示词里直接要求边界用例与服务端重算,避免“页面看起来对,后端规则却可绕过”。
八、结论:这次值得看的,是它没有把业务压扁
1、最终判断
如果只问“飞算 JavaAI 能不能写一个报销系统”,答案很难有意义,因为任何模型都能生成几组 Entity、Mapper 和 Controller。真正有价值的问题是:面对部门隔离、角色权限、金额分级和状态机,它能不能把规则拆对,并为后续实现留下可验证的路径。
就本次记录而言,飞算 JavaAI 的“读懂”能力是可感知的:41 个关键点覆盖了主流程与负向场景,接口和表结构设计抓住了后端权限与审批轨迹,56 个计划项也体现了分阶段工程思路。可运行参考项目与 20/20 后端测试进一步说明,这套业务模型具备落地条件。
但我不会据此下“零修改一次生成”或“比其他模型提升多少”的结论。现有证据更适合支持一个克制的判断:飞算 JavaAI 已经不只是把需求翻译成 CRUD,而是在尝试把复杂业务翻译成可检查、可实现、可测试的工程计划;真正交付前,开发者仍要用代码审查和自动化测试把最后一公里走完。
#飞算JavaAI #AI编程 #Java #Java代码生成 #AIcoding模型 #Java开发 #SpringBoot #CRUD
