☰
Vue3+UniApp 企业单据双端架构:PC 发起、移动审批、附件共享与状态回写如何保持一致
2026/10/13 3:14:38 网站建设 项目流程

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,可使用演示页提供的账号进入。建议不要只看列表,而是沿着本文主线完整走一次:

  1. PC 负责重录入,但提交的仍是统一保存契约
  2. 移动端详情是同一单据的轻量投影
  3. 附件共享靠身份关联,不靠复制 URL 字符串
  4. 审批结果回写业务状态,两个端都重新读取真相
资源地址适合谁
在线演示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(获取产品咨询)

打开演示地址直接查看系统。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询