Vue3+UniApp 企业单据双端架构:PC 发起、移动审批、附件共享与状态回写如何保持一致
🌐文档地址:https://ruoyioffice.com
👇 文章底部获取源码和演示地址 👇
💬 :17156169080(获取产品咨询)
PC 适合完整录入、批量操作和打印,移动端适合查看、选人和审批;界面可以完全不同,但业务 ID、附件、状态、权限和日期语义必须只有一份。
▲ PC 与手机是同一业务单据契约的两种投影,状态回写后共同读取服务端事实。
引言:不是多做几个页面,而是统一事实
“一套后端支持 PC 和移动端”听起来简单,落地时却常变成两套 DTO、两套状态文案和两套附件上传逻辑。PC 保存后移动端找不到附件;移动端已经审批,PC 仍显示审批中;LocalDateTime 在 PC 是毫秒时间戳,在 H5 被转成字符串;两个端同时编辑,后提交的一方静默覆盖前者。
双端一致性不是要求 Vue3 和 UniApp 共用页面代码,而是建立稳定的服务端单据契约。端侧可以针对屏幕与操作场景裁剪视图,所有关键判断——当前状态、可办动作、字段权限、附件归属和并发版本——都由同一个业务服务提供。
| 常见做法 | 直接后果 | 本文主张 |
|---|---|---|
| PC 与移动端各定义状态枚举 | 同一单据出现两种文案和按钮 | 服务端返回稳定状态码与可办动作 |
| 附件 URL 直接塞表单字段 | 换端、换存储或权限校验困难 | 保存文件 ID 与业务关联 |
| 日期端侧自行格式化提交 | LocalDate/LocalDateTime 类型错位 | 契约明确字符串日期与毫秒时间戳 |
| 审批回调只更新流程实例 | 业务详情状态滞后 | 统一回调业务状态并发布刷新事件 |
| 双端同时保存 | 后写覆盖先写且无提示 | 版本号乐观锁与请求幂等 |
本文讨论的不是孤立按钮,也不会把前端、后端、表结构拆成三本说明书。我们先看产品闭环如何运转,再用 ER、时序和三段核心代码解释其中最关键的约束。文中的“现有实现”以当前系统与已提交能力为依据;需要因企业规则继续扩展的部分,会明确放在边界与对照中。
一、产品能力与特点
| 能力 | 主要角色 | 不止一张审批单的地方 |
|---|---|---|
| PC 发起 | 经办人 | 复杂表单、批量明细、关联选择、打印预览 |
| 移动发起/重提 | 外勤人员 | 精简字段、拍照上传、快速选择审批人 |
| 统一详情 | 所有角色 | 按业务 ID 返回字段、状态、附件和流程摘要 |
| 统一可办动作 | 审批人 | 服务端决定通过、驳回、撤回、编辑等动作 |
| 附件中心 | 经办人/审批人 | 不同终端共享业务附件与审批意见附件 |
| 状态回写 | 系统 | Flowable 结果回写业务单并通知双端刷新 |
这些能力的共同点,是每一步都留下可回查的业务身份、状态和时间。页面只是入口,真正决定系统可信度的是:上一环节的结果能否成为下一环节的明确输入;失败、撤回或重做时,能否沿原关系恢复,而不是人工改数。
二、业务流程怎么串
2.1 第一步:PC 负责重录入,但提交的仍是统一保存契约
PC 可以使用 Vben 表单、复杂表格和选择弹窗,保存时却不应提交页面组件状态,而是归一为业务 DTO:主单、明细、附件 ID、版本号和客户端请求号。这样移动端即使采用卡片式表单,也能生成同样的命令。
▲ PC 发起流程页面承载完整业务表单与流程上下文。
2.2 第二步:移动端详情是同一单据的轻量投影
移动端按小屏重新组织分组、折叠和底部操作,不复制业务判断。详情接口返回业务字段、流程摘要、字段权限与 action list,UniApp 只渲染。需要审批节点编辑的字段,通过 editableFields 明确下发。
▲ 移动端审批详情在小屏中组合业务内容、轨迹和底部动作。
2.3 第三步:附件共享靠身份关联,不靠复制 URL 字符串
上传完成得到 fileId,随后以 bizType、bizId、scene 建立关联。PC 上传的合同附件,移动端按业务 ID 查询即可看到;审批意见附件则关联 taskId/commentId,避免与业务正文附件混在一起。删除也先做权限与引用检查。
▲ 移动端表单可以直接拍照或选择文件,并归入统一附件关系。
2.4 第四步:审批结果回写业务状态,两个端都重新读取真相
Flowable 完成任务后调用业务状态网关。业务服务以 businessKey 找到单据,校验允许的状态迁移并更新版本号;随后发送站内消息或刷新事件。PC 标签页回到详情时重新请求,移动端操作成功后也刷新,避免长期依赖本地缓存。
▲ 移动端轨迹展示流程动作、时间与业务状态的对应关系。
走完这条主线,可以发现列表页只是入口。真正重要的是规则配置、业务表单、详情轨迹和最终台账,它们分别回答“为什么这样算”“这次提交了什么”“谁做了什么”“结果落在哪里”。
三、设计怎么落地:思路、ER、时序与核心代码
3.1 先把关键决策写清楚
| 决策点 | 落地方式 | 为什么这样做 |
|---|---|---|
| 共享命令而非共享页面 | PC/UniApp 归一成同一 SaveCommand | 允许交互差异,避免业务分叉 |
| 服务端下发可办动作 | 状态、身份、节点配置共同计算 | 端侧不写越权判断 |
| 附件只传 fileId | 附件关系表绑定业务与场景 | 跨端、迁移存储和鉴权更稳定 |
| 日期类型严格配对 | LocalDate 用 YYYY-MM-DD;LocalDateTime 用 epoch millis | 避免 date.locale 与 NaN |
| 并发显式失败 | version 乐观锁 + clientRequestId 幂等 | 不让静默覆盖破坏审批中的单据 |
3.2 核心实体只围绕闭环建模
▲ ER 图只保留支撑本文主链路的实体;字典、组织和通用审计表不在这里展开。
| 关键字段 | 业务含义 |
|---|---|
| id/bill_code | 稳定业务身份与人类可读编号 |
| process_instance_id | 关联流程实例 |
| process_status | 待提交、审批中、通过、驳回等稳定状态码 |
| version | 乐观锁版本 |
| client_request_id | 跨端提交幂等号 |
| file_id/biz_scene | 附件身份与业务场景 |
| editable_fields | 当前节点允许编辑字段 |
| available_actions | 当前用户可执行动作集合 |
3.3 一条主时序比十个接口列表更容易验收
▲ 从一次用户或调度动作出发,串起端侧、领域服务、流程或账本,失败补偿在状态与幂等规则中处理。
3.4 把第一条关键规则收进服务端
共享的是与 UI 无关的命令契约:纯日期为字符串,日期时间为毫秒数,金额用字符串避免 JS 浮点误差。
exportinterfaceBusinessBillCommand{id?:number;billCode?:string;version?:number;clientRequestId:string;businessDate:string;occurTime?:number;attachmentIds:number[];items:Array<{id?:number;lineNo:number;amount:string;remark?:string;}>;}exportfunctionnormalizeBill(form:BillForm):BusinessBillCommand{return{...form,attachmentIds:form.files.map(item=>item.id)};}3.5 让核心写入可重试也不重复
保存接口同时处理幂等、乐观锁、主子表和附件绑定;两个终端复用同一入口。
@Transactional(rollbackFor=Exception.class)publicLongsaveBill(LonguserId,BusinessBillSaveReqVOreq){StringrequestId=req.getClientRequestId();Longduplicated=requestLogService.findResult(userId,requestId);if(duplicated!=null){returnduplicated;}BusinessBillDObill=validateEditable(req.getId(),userId);if(bill!=null&&!Objects.equals(bill.getVersion(),req.getVersion())){throwexception(BILL_VERSION_CONFLICT);}LongbillId=billRepository.saveMasterAndItems(req);attachmentService.bind("BUSINESS_BILL",billId,"BODY",req.getAttachmentIds());requestLogService.recordSuccess(userId,requestId,billId);returnbillId;}3.6 让回调与派生结果可追溯
流程回调只提交稳定 businessKey 与状态,业务服务负责校验迁移、更新版本、审计和通知。
@Transactional(rollbackFor=Exception.class)publicvoidupdateProcessStatus(StringbusinessKey,Integerstatus){LongbillId=Long.valueOf(businessKey);BusinessBillDObill=billMapper.selectByIdForUpdate(billId);ProcessStatusnext=ProcessStatus.valueOf(status);stateMachine.checkTransition(ProcessStatus.valueOf(bill.getProcessStatus()),next);billMapper.updateProcessStatusAndVersion(billId,next.getStatus(),bill.getVersion());operationLogService.record(billId,"PROCESS_STATUS",next.name());domainEventPublisher.publish(newBillStatusChangedEvent(billId,next,bill.getCreator()));cacheService.evictBillDetail(billId);}以上代码是围绕业务约束裁剪后的核心摘录,省略了权限注解、转换器、日志和通用异常包装。落地时仍应沿用项目现有的 Controller、Service、Mapper 分层,以及租户、数据权限和操作日志约定。
四、边界与对照:系统真正容易出错的地方
| 边界场景 | 推荐处理 |
|---|---|
| 审批中编辑 | 默认只开放节点配置字段,其余字段只读 |
| 弱网重复提交 | 相同 clientRequestId 返回首次结果 |
| 附件删除 | 校验业务权限与引用,不能仅凭 fileId 删除物理文件 |
| 端侧缓存 | 缓存只做体验优化,操作后必须回源服务端 |
| 日期时区 | 业务日期不转时区;时间点统一毫秒并明确显示时区 |
| 版本冲突 | 提示用户刷新并对比,不自动吞掉服务端新版本 |
这里有一个通用判断:不要用“最终数值看起来对”替代过程可解释。企业系统迟早会遇到撤回、补录、跨期、重试和历史规则变化。只有保存来源、版本、状态动作和结果引用,系统才具备修复能力。
五、用四类契约把“双端一致”变成可验收项
1. 数据契约
同一字段在两端必须拥有相同类型和空值语义。纯日期使用 YYYY-MM-DD,LocalDateTime 使用毫秒时间戳;金额避免 number 累计误差,提交字符串或定点值;枚举传稳定 code,不把中文文案当协议。端侧 ViewModel 可以不同,但映射函数必须可单测。
2. 动作契约
详情接口返回 availableActions,例如 EDIT、SUBMIT、APPROVE、REJECT、WITHDRAW。每个动作还可以包含 requiresComment、targetNodeRequired、editableFields。这样移动端增加一个按钮不需要复制权限表达式,PC 也不会因为缓存状态显示已失效动作。
3. 附件契约
上传、绑定、查看和删除是四个动作。上传只产生 fileId;保存单据时才把 fileId 绑定到 bizType/bizId/scene;查看时服务端校验当前用户对业务单据的权限;删除先解除业务关系,物理清理交给生命周期任务。这个模型同时支持 PC 文件选择和移动端拍照。
4. 并发契约
| 冲突场景 | 服务端判断 | 用户可见结果 |
|---|---|---|
| 弱网重复点提交 | clientRequestId 已成功 | 返回首次结果,不新增单据 |
| PC 与移动同时改草稿 | version 不一致 | 明确提示刷新与对比 |
| 审批人同时操作任务 | Flowable 任务已完成 | 后到请求提示任务状态变化 |
| 审批回调重复到达 | businessKey + 目标状态幂等 | 不重复发消息或派生数据 |
| 附件上传成功但表单取消 | 尚未绑定业务 | 由临时文件清理任务回收 |
双端验收要走“交叉路径”
不要只测 PC 发起、PC 审批和移动发起、移动审批两条平行路径。更有价值的是 PC 保存草稿后移动端继续提交、移动端驳回后 PC 修改重提、PC 上传附件后移动端预览、两个端同时打开并制造版本冲突。交叉路径才能暴露隐藏的两套状态与序列化差异。
五、快速体验
在线演示地址:https://ruoyioffice.com/web,可使用演示页提供的账号进入。建议不要只看列表,而是沿着本文主线完整走一次:
- PC 负责重录入,但提交的仍是统一保存契约
- 移动端详情是同一单据的轻量投影
- 附件共享靠身份关联,不靠复制 URL 字符串
- 审批结果回写业务状态,两个端都重新读取真相
| 资源 | 地址 | 适合谁 |
|---|---|---|
| 在线演示 | https://ruoyioffice.com/web | 产品、实施、技术负责人先验证交互与业务闭环 |
| GitHub 源码 | https://github.com/yuqing2026/ruoyi-office | 需要阅读 Spring Boot、Vue3 与流程实现的开发者 |
| Gitee 源码 | https://gitee.com/yqzy1688/ruoyi-office | 国内网络环境下拉取与对照代码 |
常见问题(FAQ)
PC 和 UniApp 应该共用同一套 DTO 吗?
应共享业务契约,但可以有端侧 ViewModel。提交前统一映射到同一命令,返回也由同一详情 DTO 投影。
附件为什么不能只保存 URL?
URL 可能过期、迁移或需要鉴权。fileId 加业务关联可以统一权限、生命周期和存储切换。
移动审批后 PC 如何实时更新?
可靠底线是重新请求;在此基础上可通过 WebSocket 或站内消息提醒 PC 刷新。
LocalDateTime 为什么建议使用毫秒时间戳?
项目序列化配置对 LocalDateTime 使用 epoch millis,前端配 valueFormat=x 可避免不同端字符串解析差异。
结语
PC 适合完整录入、批量操作和打印,移动端适合查看、选人和审批;界面可以完全不同,但业务 ID、附件、状态、权限和日期语义必须只有一份。 好的企业系统不会把复杂性藏在一条超长 SQL、一个万能表或一堆端侧 if/else 里,而是把业务日期、来源身份、状态迁移、版本和幂等边界设计成可观察、可验证的契约。
RuoYi Office 把流程、人力、财务、合同、项目、资产与移动端放在同一套企业管理平台中,适合用来验证本文的完整闭环,也便于开发者继续阅读 Spring Boot 3、Vue3、Flowable 与 UniApp 的实现。
如果这篇对你有用,点个「在看」或收藏。
🌐演示地址:https://ruoyioffice.com/web
📦GitHub 源码:https://github.com/yuqing2026/ruoyi-office
📦Gitee 源码:https://gitee.com/yqzy1688/ruoyi-office
💬微信:17156169080(获取产品咨询)
打开演示地址直接查看系统。