SpringBoot3+AI Agent 智能报销:怎样把对话接进现有财务审批系统

一、先确定对话应该把员工带到哪里
一家企业已经有费用报销、票夹、预算和审批系统。员工又多了一个要求:"这两张票,帮我整理成差旅报销。"
真正要接入的不是一个聊天框,而是现有系统中的一张可继续办理、能回查来源的报销草稿。
员工不应重新填票号和金额;财务不能失去票据归属与占用规则;审批人仍应在正式流程中作决定;出纳仍应按资金权限办理付款。
RuoYi Office 的财务底座提供业务单据与流程入口;AgentOS 是独立加购扩展,采用独立源码仓、前后端服务和数据库,通过主线 API 衔接业务。
主线商业财务、AgentOS、模型与OCR配置分别确认交付范围。商业源码交付不等于免费开源,也不表示购买主线就附送智能扩展。
本文面向已有 Java/Vue 财务系统的研发与实施团队,重点是接口契约与失败恢复,不重复宏观路线,也不重写旧单票OCR教程。
核对基准为2026年11月1日本地源码。证据分三层:
| 等级 | 本文具体依据 | 能说明什么 |
|---|---|---|
| A:运行底座 | 本次独立浏览器读取既有报销、预算、凭证详情 | 财务业务字段和回查页面确实可见;不代表智能接入成功 |
| B:源码接入点 | 对话、OCR上下文、工具执行、确认服务、主线草稿API | 存在可接入实现,需版本、部署、配置和专项验收 |
| C:建设建议 | 确认原子抢占、请求内容一致性、超时对账、角色矩阵 | 本文建议的上线要求,不能写成当前已通过 |
本机现有PC5800、后端48080是资产教程运行包,未按当前源码重建。检查时未见AgentOS常用独立端口48091监听;没有部署或重启,不推断远程环境是否存在。
结论:接入智能报销的首个交付目标应是"本人可核对的真实草稿及回执",之后再由既有审批系统接力。工具清单、悬浮助手或一段成功话术都不能替代这个目标。
二、从真实报销详情反推接入契约
先从"费用管控→费用报销"打开现有记录,研发和财务一起确认哪些字段必须来自可信系统。

图2|本次真实只读截图,报销id=3、M5账套,500/200/300分别是报销额、冲抵额和应付额。它是既有演示单,不是AI生成草稿。页面虽显示审批通过,历史流程读取存在"流程实例不存在",不能据此宣称审批轨迹完整。右下角助手入口也不是智能调用成功证据。
这张详情决定了集成的第一条规则:票面价税合计、报销金额与实际应付不是同一个字段。
对话只得到"500元发票",不能自行推出"给员工付款500元",也不能擅自找一笔借款冲抵。既有单据上的200元冲抵有自己的来源和校验。
当前发票一体化接口的请求并没有借款冲抵明细。因此,不能因为AgentOS工具库还有"可冲抵借款查询",就写成这条建单链已经自动冲抵。
| 内容 | 可信来源 | 接入时的处理 |
|---|---|---|
| 员工、租户、公司与部门 | 登录身份、当前任职、员工档案 | 不接受模型自由指定申请人 |
| 银行账户 | 本人员工档案 | 确认卡脱敏;主线建单时重新取真实账户 |
| 票号、日期、销方、票面金额 | OCR上下文与原件 | 展示复核,缺项或金额关系不符就停止 |
| 账套、费用类型 | 当前公司有效配置及候选接口 | 确定性选账套;费用分类不确定时补问 |
| 报销事由、费用日期 | 员工输入与本人确认 | 允许范围内更改,最终值留在确认记录 |
| 状态、预算占用、审批实例 | 主线领域服务 | 草稿接口不能兼任审批或付款接口 |
设计上建议把一次接入拆成四个契约:身份契约、材料契约、确认契约、业务回执契约。每个契约都要能回答"谁提供、谁校验、失败退回哪里"。
| 决策 | 本文采用的接法 | 原因 |
|---|---|---|
| 不把财务库交给模型 | AgentOS调用确定性主线API | 复用财务状态与票据规则 |
| 不从自然语言抽取用户身份 | 请求入口固化员工上下文 | 工具会切换线程,不能临时再猜身份 |
| 不让模型重写票面 | ocrContextId转换成确定性请求 | 多轮补问后仍能引用同一份材料 |
| 不把"确认"扩成所有授权 | 仅执行一张确认单的目标动作 | 草稿、审批与资金责任分别存在 |
| 不用回复文字判断成功 | 保留真实业务主键、单号、状态与详情路径 | 能回查,才能处理响应丢失和异常 |
三、员工身份随工具走,补问才有边界
1. 会话上下文与业务上下文分开保存
会话用于保存消息、轮次和绑定智能体;OCR上下文用于保存附件、结构化票面、警告和选择结果;确认单用于固定即将执行的请求。
这三者不能都压在一段聊天历史里,否则"员工补了一句事由"可能诱使模型重新拼写票号和金额。
当前对话代码在进入模型前取得Token与任职信息,先加载历史,再保存本轮消息、运行记录和附件引用。续聊使用会话上已经绑定的智能体;会话服务检查用户和租户归属。
源码摘录1(连续24行):E:/ruoyi-office/ruoyi-office-addon/agentos/yudao-module-agentos/yudao-module-agentos-server/src/main/java/cn/iocoder/yudao/module/agentos/service/chat/AgentosChatServiceImpl.java,L160--183。
java
String accessToken = AgentosTokenHolder.currentToken();
AgentosActorDirectory.Actor actor = actorDirectory.load(accessToken);
String systemPrompt = buildSystemPrompt(agent, actor, reqVO.getPageContext());
String userContent = AgentosTextGuard.mask(buildUserContent(reqVO));
// 历史要在落本轮用户消息之前取,否则本轮提问会重复进上下文
AgentosChatHistory history = loadHistoryState(conversation.getId(), userId, tenantId);
saveUserMessage(conversation.getId(), userId, tenantId, userContent,
reqVO.getFileUrls(), reqVO.getFileNames());
Long runId = runService.startRun(conversation.getId(), userId, tenantId, userContent);
artifactService.recordUploads(reqVO, conversation.getId(), runId, userId, tenantId);
// 令牌必须在这里取:工具真正执行时已经切到响应式线程,那时请求上下文早就没了
AgentosExecutionContext context = new AgentosExecutionContext(
userId, tenantId, accessToken, runId);
context.setAttachmentUrls(reqVO.getFileUrls());
Sinks.Many<String> extraFrames = Sinks.many().unicast().onBackpressureBuffer();
context.setQuestion(reqVO.getContent());
context.setCompanyId(actor.companyId());
context.setCompanyName(actor.companyName());
context.setDeptId(actor.deptId());
context.setDeptName(actor.deptName());
context.setRoleIds(actor.roleIds());
context.setKnowledgeIds(agent == null ? null : agent.getKnowledgeIds());
这些代码说明了身份如何进入执行上下文,也说明发起对话本身就会产生消息、运行与附件记录。本文没有为了写稿发起真实智能对话。
附件URL优先从本轮上传上下文取值;OCR工具也允许从工具参数取附件,因此不能仅凭"优先"就宣称所有来源都已经按员工文件权限验收。
已核对的OCR工具检查数量上限10、允许主机、非成功状态,并在成功后写入AgentOS报销上下文。主线OCR另有识别记录写入,两条路径都不能叫"零写入识别"。
OCR工具旧注释"不落库"与当前调用reimburseContextService.create不同,本文以执行分支为准。
2. 把Token和租户传给业务系统
源码摘录2(连续17行):E:/ruoyi-office/ruoyi-office-addon/agentos/yudao-module-agentos/yudao-module-agentos-server/src/main/java/cn/iocoder/yudao/module/agentos/framework/executor/AgentosCapabilityExecutor.java,L365--381。
java
private String call(AgentosCapabilityDO capability, Map<String, Object> arguments,
AgentosExecutionContext context) {
HttpMethod method = HttpMethod.valueOf(capability.getHttpMethod());
boolean hasBody = HttpMethod.POST.equals(method) || HttpMethod.PUT.equals(method);
String uri = hasBody ? capability.getHttpPath() : buildQueryUri(capability.getHttpPath(), arguments);
RestClient.RequestBodySpec spec = executeRestClient.method(method)
.uri(uri)
.header(AgentosTokenHolder.TENANT_HEADER, String.valueOf(context.tenantId()))
.header("Authorization", "Bearer " + context.token());
if (hasBody) {
spec.contentType(MediaType.APPLICATION_JSON).body(JsonUtils.toJsonString(arguments));
}
String response = spec.retrieve().body(String.class);
validateMainlineResponse(response);
return response;
}
这里传递的是当前员工的Bearer Token和tenant-id,不是用一个财务管理员服务账号代替全员。
主线create-from-invoices有finance:reimburse:create权限,申请人从登录用户取得。/submit另有finance:reimburse:submit权限。
但"透传"不是"所有权限已验收"的同义词。普通详情GET与BPM专用详情的检查并不完全一样;账套校验器的validateEnabled检查存在与启用状态,不能单凭它证明当前任职公司和全部数据范围都已被检查。
实施时要补普通员工、同部门、跨部门、跨公司、跨租户的角色矩阵,不能只使用admin截图推导员工权限。
四、让工具白名单决定能做什么
工具白名单是服务端允许某员工在某个智能体中调用的能力集合,不是提示词里的一句"不要越权"。
当前装配路径先按租户取已上架能力,按启用授权匹配全员、用户、部门或角色,再与智能体能力范围相交,最后召回相关工具。
通用对话(未绑定智能体)传入只读模式;绑定了具体智能体,也仍需命中授权与能力范围。财务技能的最新配置要求默认使用finance_reimburse_create_from_invoices,不自由组合旧save/submit。
执行器按风险实际分支处理:
| 风险 | 当前源码路径 | 对业务的含义 |
|---|---|---|
| READ | 治理闸门放行,再执行 | 仍可能写运行日志;被标为read的OCR还会写扩展上下文 |
| WRITE | 首次挂起为待确认;确认执行时跳过再次弹卡 | 需要员工确认;不是直接拒绝所有写工具 |
| FUND | 拦截,提示去业务系统办理 | 不执行资金动作 |
| APPROVE | 拦截,要求审批人本人决定 | 不代同意或驳回 |
| 未识别风险、未上架、超工具步数 | 返回治理拒绝或停止提示 | 模型应解释原因,不能换参数绕开 |
旧"首版只放行只读"注释不能用来描述当前WRITE分支。反过来,WRITE分支存在,也不能证明当前部署已启用财务技能或已经通过确认集成。
结论:模型可以提出工具调用,服务端授权决定能否装配,执行器决定风险路径,财务API决定业务规则是否成立。四层需要分别验收。
用一个双票例子教员工怎样补问
下面是独立教学交互设计,不是真实聊天回放。选用交通和住宿两类材料,避免把一张OCR演示当成全部集成效果。
| 教学材料 | 不含税 | 税额 | 价税合计 | 待确认业务分类 |
|---|---|---|---|---|
| 交通票A | 100.00 | 9.00 | 109.00 | 交通费 |
| 住宿票B | 300.00 | 18.00 | 318.00 | 住宿费 |
| 合计 | 400.00 | 27.00 | 427.00 | 差旅报销;不假设存在借款 |
- 员工:"这两张票用于深圳项目出差,帮我整理成报销草稿。"
- 系统取得附件和员工身份,OCR产生票面候选及
ocrContextId。 - 确定性规则尝试选本公司最近有效账套;没有时选唯一有效默认账套,或仅有的有效账套。
- 助手:"报销类型拟为差旅;交通和住宿对应以下费用类型,请确认。费用发生日期与事由是否需要补充?"
- 员工补充用途;服务端据上下文构造请求,卡片显示两票、427.00元、账套、分类、原件与账户尾号。
- 员工确认只生成草稿;回执有真实单号后,进入详情核对并另行提交审批。
若账套无法唯一确定,先处理配置或按授权候选补问。代码默认选账套只在未传有效bookId时触发,因此显式选择的账套仍要独立验证权限和公司一致性。
若第二张票税额识成180.00而合计仍318.00,应停止并提示重新识别或走人工录入;不能为了生成427.00元草稿让模型修改税额。
当前上下文校验对0.02元以内差异有归一化,再交主线严格校验。这是源码规则,不是自动纠错准确率承诺;上线需核对原件、提示与确认卡最终金额是否一致。
五、确认单绑定什么,才不会变成一句"继续吗"
当前确认单保存tenantId、userId、runId、能力ID/代码、风险、摘要、预览、requestBody和过期时间,默认30分钟。
OCR上下文另有60分钟期限,状态为recognized → ready → created,并保存最终reimburseId。这是两套时钟,不能混写成"确认卡一小时有效"。

图3|源码关系示意,非系统截图。上下文与确认单通过转换后的请求、来源键逻辑关联;AgentOS与主线独立数据库,没有跨库物理外键。一个上下文可能生成多个待确认尝试,图中不虚构一对一约束。
确认状态为PENDING(0)、CONFIRMED(1)、REJECTED(2)、EXPIRED(3)。单纯看到CONFIRMED仍须核对业务回执,不能只读状态标签宣布建单成功。
源码摘录3(完整方法,连续26行):E:/ruoyi-office/ruoyi-office-addon/agentos/yudao-module-agentos/yudao-module-agentos-server/src/main/java/cn/iocoder/yudao/module/agentos/service/confirm/AgentosConfirmServiceImpl.java,L67--92。
java
@Override
public String confirm(Long confirmId, Long userId, Long tenantId, String token, Map<String, Object> overrides) {
AgentosConfirmDO confirm = getOwnedPending(confirmId, userId, tenantId);
if (confirm.getExpireTime() != null && confirm.getExpireTime().isBefore(LocalDateTime.now())) {
confirm.setStatus(AgentosConfirmStatusEnum.EXPIRED.getStatus());
confirmMapper.updateById(confirm);
throw exception(AGENTOS_CONFIRM_EXPIRED);
}
AgentosCapabilityDO capability = capabilityMapper.selectById(confirm.getCapabilityId());
if (capability == null) {
throw exception(AGENTOS_CAPABILITY_NOT_EXISTS);
}
@SuppressWarnings("unchecked")
Map<String, Object> stored = JsonUtils.parseObject(confirm.getRequestBody(), Map.class);
// 改过的值也回写确认单:审计要看到的是员工最终提交的内容,而不是模型最初的草稿
Map<String, Object> arguments = AgentosConfirmFields.merge(capability, stored, overrides);
confirm.setRequestBody(JsonUtils.toJsonString(arguments));
AgentosExecutionContext context = new AgentosExecutionContext(userId, tenantId, token, confirm.getRunId());
context.setSkipConfirm(true);
String result = executor.execute(capability, arguments, context);
confirm.setResultBody(result);
confirm.setStatus(AgentosConfirmStatusEnum.CONFIRMED.getStatus());
confirm.setConfirmTime(LocalDateTime.now());
confirmMapper.updateById(confirm);
return result;
}
这段实现先检查待确认单归属和期限,再从存储请求合并允许的修改,最后使用本次员工Token执行,保存结果。
允许修改不等于允许重写请求:AgentosConfirmFields忽略新增键、ID键、对象和数组;来源系统/来源请求号锁定;一体化草稿的收款信息及报销类型也锁定。
该能力的确认卡主要允许已有事由、费用日期等字段更改。票面、费用类型数组、目标账套需要回到业务流程重新选择并生成新确认,不能承诺直接在卡片里任意改金额。
必须把"已有防线"和"还需建设"分开
| 场景 | 当前源码可见行为 | 交付前还要验证或建设 |
|---|---|---|
| 跨员工、跨租户确认 | getOwnedPending比较用户与租户 |
用真实角色和隔离样本验证拒绝且主线无写入 |
| 取消 | PENDING改REJECTED,不调用执行器 | 与确认并发、断线后恢复;不能承诺所有竞态零写 |
| 过期 | 确认时检查30分钟期限并记EXPIRED | 过期边界、时钟、已转换请求与OCR期限关系 |
| 顺序重放 | 非PENDING拒绝 | 两个请求同时读到PENDING时的结果 |
| 参数篡改 | merge按允许字段过滤 | 原件、预览、最终参数一致性及审计可还原 |
| 工具下架或撤权 | 确认重取能力,执行器检查上架/风险 | 确认时未重新走完整buildTools授权装配;单独验证撤权生效 |
当前确认方法与Mapper没有看到基于PENDING的原子抢占或专用执行中状态,也没有把跨服务HTTP调用纳入分布式事务。不能把先查状态再更新写成"并发双击只会调用一次"。
此外,已转换完整请求会走复用分支,确认时不会重新调用OCR上下文的全部校验。buildCreateArguments核对用户、租户、期限,但未比较当前会话ID;不能宣称"同员工跨会话引用也已禁止"。
C级建议是在实施中定义:完整请求可信来源与内容摘要、关键参数变更需重新确认、原子执行抢占、确认前再授权,以及业务成功但确认回写失败的对账任务。
这些建议不是本次产品修复,也不是已经完成的安全认证。
六、主线接住的是结构化草稿请求
1. 先对齐版本,再上架场景
AgentOS的sql/optional/finance/README.md要求主线具备POST /admin-api/finance/reimburse/create-from-invoices、两个source字段和租户范围来源唯一索引;任何缺项都应停止启用。
配套preflight.sql只查两列与指定名称的唯一索引是否存在,不检测运行包是否真的暴露API,也不完整证明索引列顺序。应另核实际DDL、接口版本与当前构建对应关系。
源码增量脚本为ruoyi-office-db/update/202610/20261006_update/01_apply_必跑/111800_finance_reimburse_agentos_source.sql;本次没有执行SQL。目标索引列为tenant_id, source_system, source_request_id, deleted。
当前主线还存在yudao.finance.agentos-reimburse-enabled开关。默认配置、数据库脚本存在都不能代替目标环境已启用的证据。
2. 模型入参与主线入参不是一个形状
模型工具参数使用ocrContextId、可选bookId、reimburseType、单个或逐票expenseTypeId(s)、可选日期与事由。
防腐层把它变成主线请求:
| 主线字段 | 来源与约束 |
|---|---|
sourceSystem |
固定agentos,VO正则约束 |
sourceRequestId |
agentos-context-加OCR上下文ID;不是confirmId |
bookId、reimburseType |
必填;账套需有效,费用类型需属于账套并启用 |
expenseDate、remark |
可选;纯日期yyyy-MM-dd,事由最长500字符 |
invoiceItems |
必填1~10项,逐项校验 |
| 每项材料 | ocrResultId、fileUrl、invoiceNo、invoiceDate、sellerName |
| 每项金额/分类 | amount、taxAmount、priceTaxAmount、occupyAmount、expenseTypeId |
| 其他票面 | 可选票种、代码、销方税号和备注;不把对象当字符串透传 |
主线虽然VO有payeeName/bankName/bankAccount字段,但建单实际从本人员工档案重新取账户,不使用确认卡中的脱敏值作为真实收款账户。
ocrResultId在本路径被要求非空并作为来源引用保留,并不等于主线重新完成了OCR真实性查验;AgentOS独立OCR结果也不是主线识别记录的同一个ID。
3. 来源幂等与票据重复各管一件事
源码摘录4(连续19行):E:/ruoyi-office/ruoyi-office/yudao-module-finance/yudao-module-finance-server/src/main/java/cn/iocoder/yudao/module/finance/service/expense/FinanceReimburseServiceImpl.java,L172--190。
java
@Override
@Transactional(rollbackFor = Exception.class)
public FinanceReimburseCreateFromInvoicesRespVO createFromInvoices(
FinanceReimburseCreateFromInvoicesReqVO reqVO) {
if (!agentosReimburseEnabled) {
throw exception(REIMBURSE_SOURCE_DISABLED);
}
Long userId = SecurityFrameworkUtils.getLoginUserId();
FinanceReimburseDO existed = reimburseMapper.selectBySource(
reqVO.getSourceSystem(), reqVO.getSourceRequestId());
if (existed != null) {
if (!Objects.equals(existed.getApplicantId(), userId)) {
throw exception(REIMBURSE_SOURCE_REQUEST_INVALID);
}
return buildCreateFromInvoicesResp(existed,
invoiceMapper.selectListByBillId(existed.getId()).stream()
.map(FinanceReimburseInvoiceDO::getInputInvoiceId).toList(), 0, 0);
}
同来源请求已存在且申请人一致时返回已有单据和关联票据,不新建;这里的复用计数返回0/0,不能将它解释为重新导入了0张票且原单没有票。
该分支没有比较重试请求与原请求的全部业务字段摘要。同来源号改了事由或金额,也不能期待它覆盖原单或自动证明"相同请求内容"。
索引提供数据库去重约束;并发请求都在首次查询时未命中,后续仍可能在唯一键处失败。当前服务未见捕获唯一键冲突后再查询并返回已有回执的专用分支,因此不能承诺并发调用全部成功返回同一单号。
票据重复还有独立防线:同请求重复票号拒绝;既有票需属于本人、处于可用票夹状态、未被其他单据占用;费用类型需与账套匹配;金额加税额必须等于合计,本次占用必须等于票面合计。
已有票面的复用比较主要检查价税合计及双方非空销方税号,不是逐字段全面一致性校验;已有票据跨账套复用也应列入专项验收,不能只凭费用类型检查推导票据全部归属已验证。
@Transactional(rollbackFor = Exception.class)标在主线一体化入口,创建票据、报销、子表和占用沿该调用链进行。这是本地事务设计证据;本文没有故障注入验证回滚,也没有证明AgentOS与主线跨库原子提交。
结论:来源号解决"同一次办理是否又建一张单",票据规则解决"这张票还能不能用",确认状态解决"这次授权还能不能执行"。三者不能互相替代。
七、先返回草稿,再把审批还给业务系统
主线创建方法强制processStatus=NOT_START(-1)、billStatus=DRAFT(0)和paidAmount=0,保存来源字段,再保存子表与占用关系。
源码摘录5(连续16行):E:/ruoyi-office/ruoyi-office/yudao-module-finance/yudao-module-finance-server/src/main/java/cn/iocoder/yudao/module/finance/service/expense/FinanceReimburseServiceImpl.java,L347--362。
java
if (StrUtil.isBlank(bill.getPartnerName())) {
bill.setPartnerName(saveReqVO.getCreatorName());
}
bill.setProcessStatus(BpmProcessInstanceStatusEnum.NOT_START.getStatus());
bill.setBillStatus(FinanceReimburseBillStatusEnum.DRAFT.getStatus());
bill.setPaidAmount(BigDecimal.ZERO);
bill.setSourceSystem(sourceSystem);
bill.setSourceRequestId(sourceRequestId);
fillAmounts(saveReqVO, bill);
reimburseMapper.insert(bill);
validateLoanOffsets(saveReqVO, bill);
saveChildren(bill.getId(), saveReqVO);
occupyReimburseInvoices(bill.getId(), saveReqVO.getInvoices());
saveAttachments(bill.getId(), saveReqVO);
return bill.getId();
}
它没有启动流程。submitReimburse是另一个入口:检查必填与明细、保存单据、校验发票占用、检查流程可提交状态,执行预算占用,再启动流程并转为审批中。
因此,这里建议让员工打开草稿详情核对,再使用原业务系统提交。即便其他通用WRITE能力可以配置提交,也不能拿这点把本篇"只建草稿"的确认扩大成自动提交审批。

图4|本次真实只读截图,预算id=7,P1学习账套,展示审批接力时需要遵守的预算规则底座。它与图2的M5报销不是同一账套,不据此计算那张报销的可用预算,也不证明本次AI提交或预算并发测试通过。
预算强控不该由模型在文字里说"应该没问题"。提交动作需要由真实预算服务根据当前状态判断,草稿阶段能保存也不表示提交一定成功。

图5|B级源码接入时序及C级恢复要求。对话/OCR/确认各自可能落扩展记录;只有确认执行才请求主线草稿。人工提交审批是后续独立动作,失败与超时不画成成功箭头。
回执要能带员工走进真实业务
草稿API返回reimburseId、billCode、status、inputInvoiceIds、detailPath、reusedInvoiceCount和createdInvoiceCount。
AgentOS先检查空响应和CommonResult非零业务码,再精简结果;结果处理器按来源号定位上下文,写回created和reimburseId。运行步骤留存能力、风险、请求、结果、耗时和错误。
结果处理器回写异常被捕获为警告,避免把已成功的业务伪装成失败。这也意味着扩展上下文没变成created,不足以证明主线没建单。
当前回执校验兼容非CommonResult文本,没有对每条成功响应统一强制核验完整回执Schema;执行器某些治理拒绝也返回文本。实施时应把"CONFIRMED""调用返回""真实reimburseId存在"分开观察。

图6|本次真实只读截图,凭证id=32,借贷各500,来源FI504-2026090600008。它示范后续核算应保留的来源回查信息,与图2报销id=3不是同一记录;未由本次Agent创建,不表示本次完成制证、付款或过账。
对话的任务可以停在草稿,但业务不能停在"模型说完成"。后续审批、付款、凭证各有状态和来源,智能入口只应提供准确的交接点。
本环境P1 2026-08报表曾显示差额-500,原因尚未定性,不据此归因本篇草稿或凭证;M5银行调节-200是另一问题。本文不为展示闭环调整任何金额或账务。
八、把失败写进接口合同与验收卡
下面是待在隔离环境执行的验收清单,全部留空;不是本文测试通过记录。每项应记录输入、用户/租户、源码与运行版本、预期、实际结果及业务回查证据。
| 待验收场景 | 用户应得到什么 | 主线与恢复检查 |
|---|---|---|
| 缺报销类型、费用类型或员工账户 | 明确缺项并补问/回档案维护 | 不生成空确认或半张业务单 |
| OCR缺票号、日期错误、金额不符 | 显示原件与错误,重新识别或人工录入 | 不让模型补金额;区分扩展记录与主线写入 |
| OCR服务未配、超时、格式不支持 | 具体原因和回人工入口 | 不把工具返回的错误文本当成功票面 |
| 上下文过期、跨员工、跨租户 | 拒绝并要求重新提供本人材料 | 无主线创建,日志不泄露他人原件 |
| 同员工跨会话引用 | 按明确产品策略处理 | 当前方法未校验会话,验收不能预填"已拒绝" |
| 确认取消、过期、顺序重放 | 状态与原因可读 | 顺序路径无再次写;并发取消/确认另测 |
| 改账套、票面数组、来源号或账户 | 受限字段不进入最终请求 | 对照预览、最终审计请求与主线数据 |
| 能力下架、授权撤销、Token失效 | 停止办理并解释 | 分别测状态闸门、确认再授权及主线401/403 |
| 双击确认、两个确认单共用上下文 | 明确只有一张目标草稿或可解释冲突 | 主线来源唯一键、确认竞态与票据占用都查 |
| 同来源号不同参数 | 不误以为更新成功 | 当前复用旧单,补内容一致性策略再验收 |
| 主线业务码非零、空响应、异常返回 | 不显示虚假单号或"已成功" | 校验回执Schema及失败记录,不靠HTTP200 |
| 主线超时、响应丢失 | 显示"结果待核实",保留稳定来源号 | 先按来源/真实单据回查,不换来源号盲重试 |
| 业务成功、扩展回写失败 | 告知真实业务结果和待同步状态 | 以主线为准对账,不能再次无条件建单 |
| 主线事务中途失败 | 可解释失败并可回人工 | 隔离故障注入检查票据、子表、占用是否回滚 |
| 已有票据、跨账套与并发占用 | 复用或拒绝理由明确 | 检查所属、账套、状态与并发后的金额不变量 |
| 草稿完成后提交 | 员工另行核对并提交 | 预算不足或流程缺失时保留草稿及错误,绝不代批付款 |
对超时最容易犯的错是告诉员工"再点一下",同时悄悄生成新的来源号。这样会绕开原有幂等依据。
C级恢复方案应保留tenantId + userId + sourceSystem + sourceRequestId和已知业务主键,先通过授权查询或人工后台核实实际结果;当前没有专用来源查询接口的部署,应由实施补足受控回查能力,不能把它写成现成自动恢复。
不要让模型直接查询数据库"找一找",也不要因为某次错误没有返回单号就推断事务一定回滚。
九、接入评审可以怎样安排
只读盘点阶段
- 核对主线财务、AgentOS、OCR/模型的交付与授权范围。
- 对照当前运行包确认API、来源字段、完整唯一索引与开关。
- 从现有报销详情确认字段和状态,核对账套、员工档案与票据归属。
- 审查所有预览查询的副作用;GET也要检查初始化、ensureBuiltin等路径。
隔离集成阶段
- 建立独立测试租户与合成材料,执行上表正常和异常用例。
- 对照OCR原件、上下文、确认参数、主线单据与回执逐级核验。
- 对取消、过期、授权撤销、参数变更和并发重放分别留证。
- 明确成功、失败、结果未知三种提示与人工处理责任。
业务接力阶段
- 只把真实草稿回执交给员工;提交另有权限与确认意愿。
- 预算、流程和后续核算按原有责任体系验收。
- 模型服务故障仍能回到原业务表单;可停用某个工具。
- 不把审批和付款纳入模型自主决策范围。
本次为写稿只读源码和既有页面,未运行上述集成清单;也未执行隔离纯逻辑测试。代码核对和文章渲染检查不是产品集成测试。
常见问题
已有Spring Boot财务系统,要换掉原审批流程吗?
不需要把聊天层变成新审批引擎。先将智能入口接到受控草稿API,员工再通过原有提交入口启动流程。RuoYi Office当前源码里草稿与submit分离,是否适配企业既有流程仍需实施验收。
本人点确认以后,模型能顺便审批和付款吗?
不能。确认绑定目标动作;本篇目标是生成草稿。当前执行器拦截FUND和APPROVE,审批决定和资金办理留给对应责任人。
有唯一索引,就能保证所有重复请求都成功吗?
不能这样推导。来源查询可复用已存在单据,唯一索引约束重复落库;并发首次插入仍可能返回冲突,确认状态本身也未见原子抢占。需要单独测试结果返回与恢复路径。
OCR上下文过期后,旧确认卡一定不能执行吗?
不能只凭两个有效期下结论。初次转换检查OCR上下文60分钟期限,确认卡检查30分钟期限;已转换完整请求会复用存储参数,不重新执行全部OCR上下文校验。应明确产品期限策略后做隔离验证。
文章里的三张详情图证明智能报销已上线了吗?
没有。三张图证明现有财务底座的字段和回查落点。本次没有运行智能链,集成能力按B级源码接入点和C级建议描述;AgentOS仍是独立加购,不是免费开源或主线附送。
来源与交付边界
本篇事实来自本地Java源码、可选场景SQL说明、既有财务页面与接口只读实录,没有采用外部产品性能、客户或ROI数据。
完整路径、行号、源码SHA-256、图片来源与图注见来源与配图记录。源码摘录按连续原文保存;行号是本次本地版本定位,不是未来版本的固定行号。
下一步最有价值的工作,是带着一份合成双票材料,在隔离环境逐项验证"本人、同一份请求、真实草稿、可恢复回执"。你们现有系统最需要先补的是身份映射、确认契约,还是超时后的业务回查?
