package com.yunhai.flowcenter.entity; import com.yunhai.flowcenter.enums.FlwEnums; import lombok.Getter; import lombok.Setter; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.Index; import javax.persistence.Table; import javax.persistence.UniqueConstraint; import javax.persistence.Version; import java.time.LocalDateTime; /** * 办件实例(概览 §4.3 + V2.0 Part D):托管模式下业务对象留在业务方,本表只存 * biz_title/biz_url/biz_snapshot 快照与关联键。正式命令每轮独立实例,唯一域为 * (school, sourcePrincipalId, bizType, bizId, applyRound),单在途由坐标表事务指针保护。 * sourcePrincipalId=NULL / legacySlot=1 仅保留迁移前旧入口的一业务一行语义;正式模式关闭旧写入口。 * *

version 是乐观锁(推进串行化)兼回调 payload 的 instanceVersion(业务方按它丢弃乱序旧事件), * 由 JPA @Version 在 flush 时自增。 */ @Getter @Setter @Entity @Table(name = "flw_instance", uniqueConstraints = { @UniqueConstraint(name = "uk_flw_instance_legacy", columnNames = {"sch_id", "source_principal_id", "biz_type", "biz_id", "legacy_slot"}), @UniqueConstraint(name = "uk_flw_instance_round", columnNames = {"sch_id", "source_principal_id", "biz_type", "biz_id", "apply_round"})}, indexes = { @Index(name = "idx_flw_inst_submitter", columnList = "sch_id, submitter_id, status"), @Index(name = "idx_flw_inst_status_time", columnList = "sch_id, status, submit_time")}) public class FlwInstance extends BaseEntity { @Column(name="source_principal_id",length=36,updatable=false) private String sourcePrincipalId; /** 1 for legacy writers, NULL for formal commands. Migration must drop the old three-column UK. */ @Column(name="legacy_slot") private Integer legacySlot = 1; /** 租户键 */ @Column(name = "sch_id", nullable = false) private Long schId; /** 定义快照(发布版本 id;重提重启时刷新为当前已发布版本) */ @Column(name = "def_id", nullable = false) private Long defId; /** 定义编码快照 */ @Column(name = "def_code", nullable = false, length = 64) private String defCode; /** 定义版本快照 */ @Column(name = "def_version", nullable = false) private Integer defVersion; /** 表单提交快照(无表单托管模式为 NULL,透传存业务方 formData) */ @Column(name = "form_data_json", columnDefinition = "LONGTEXT") private String formDataJson; /** 发起时指派快照:{nodeId:[{userId,userName}]};NULL=无指派(全量候选现状行为) */ @Column(name = "assign_json", columnDefinition = "LONGTEXT") private String assignJson; /** CMS 事项快照(原生模式,M1 不启用恒 null) */ @Column(name = "item_code", length = 64) private String itemCode; @Column(name = "item_title", length = 200) private String itemTitle; /** 业务类型键(外部托管模式,如 svc_item) */ @Column(name = "biz_type", length = 32) private String bizType; /** 业务主键(如 svc-hall applyId) */ @Column(name = "biz_id", length = 64) private String bizId; /** 办件标题快照:待办列表/消息/回调 payload 统一携带 */ @Column(name = "biz_title", length = 200) private String bizTitle; /** 业务方详情页链接:办理页"查看业务详情"跳转入口 */ @Column(name = "biz_url", length = 500) private String bizUrl; /** * 代发起调用方 app_key 快照(验签上下文 yh.app.auth,2026-10-12):验签域身份, * 审计/对账用——与 flw_callback_reg/flw_def 的 app_key 同一口径。 */ @Column(name = "apply_app_key", length = 32) private String applyAppKey; /** * 代发起调用方 app_code 快照(apply 时按 app_key 查 UserBase /oapi/app/by-key 换得): * 消息 jump_app 的权威来源(路由/跳转/判权域标识,projman app.name 口径如 notice/svc-hall)。 * biz_url 页面即调用方页面,验签身份=页面属主由平台结构保证。UserBase 不可达时为空 * →消息 jumpApp 空、回落发送方归属(现状行为),不阻断 apply。 */ @Column(name = "apply_app_code", length = 32) private String applyAppCode; /** 业务摘要 JSON 快照:办理页展示用(只收摘要,不收富文本正文,概览 §十边界) */ @Column(name = "biz_snapshot", columnDefinition = "LONGTEXT") private String bizSnapshot; /** 提交人 id(外部模式=业务方当前登录人快照,本校校验在入口做) */ @Column(name = "submitter_id", nullable = false) private Integer submitterId; /** 提交人姓名快照 */ @Column(name = "submitter_name", nullable = false, length = 128) private String submitterName; /** 10 审批中 / 20 已完成 / 30 已拒绝 / 40 已撤回 */ @Column(name = "status", nullable = false) private Integer status = FlwEnums.InstanceStatus.APPROVING.getCode(); /** 当前活跃节点 id 集合(冗余逗号分隔,列表页免展开) */ @Column(name = "current_nodes", length = 200) private String currentNodes; @Column(name = "submit_time") private LocalDateTime submitTime; @Column(name = "finish_time") private LocalDateTime finishTime; /** * 乐观锁版本:推进串行化 + 回调 instanceVersion 单调递增。 * 注意不可初始化(=0 会让 Spring Data 判定非新实体走 merge,id 不回填); * insert 时由 Hibernate 写入 0。 */ @Version @Column(name = "version", nullable = false) private Integer version; /** * 最近一次成功受理代发起的请求标识(消费方持久化命令里带的 token,v2 §8,TASK_2 C.3)。 * *

它回答的是"这次 apply 是同一命令的重投,还是新一轮发起"——biz uk 只说得出 * "这个业务键已经有办件了",说不出这句。没有它时,消费方在响应丢失后重发同业务键, * 命中 30/40 会被 R4 语义重启推进,等于把网络重试当成用户重提。 * 同 token 一律只回读现状、不重启;换 token 才是新一轮。

*/ @Column(name = "apply_request_token", length = 64) private String applyRequestToken; /** * 当前发起轮次(1 起)。正式命令由服务端坐标锁内分配,不接受客户端 attempt 分轮。 * *

轮次守卫是单调的:请求带来的 attempt 低于行上的 applyRound 就是陈旧轮次的迟到重投, * 只回读现状。两侧(消费方命令行 + 本行)都持久化,崩溃恢复后仍能判出"哪一轮才是当前轮"。

*/ @Column(name = "apply_round") private Integer applyRound; /** * 受理时规范载荷指纹(2026-10-25 T04,SHA-256 十六进制):同 requestToken 必须同指纹—— * 同 ID 不同载荷是两份命令,撞在同一个 token 上按冲突拒绝,不伪装成幂等成功。 * 存量行(指纹列为 NULL)跳过该核对(守卫缺省是关,与 token/round 存量纪律一致)。 */ @Column(name = "apply_payload_hash", length = 64) private String applyPayloadHash; /** * 事件序号计数器(2026-10-25 T05):实例内事件事实 eventSeq 的发号器。发号时在事务内 * 悲观锁住本行(findByIdForUpdate 当前读)再 +1——REPEATABLE READ 下对聚合做快照读会 * 拿到陈旧 max(FlowEngineIT 会签并发实证),行上计数器+行锁才是串行安全的。 */ @Column(name = "event_seq_counter") private Integer eventSeqCounter; /** * 台账展示位(F11-4):该办件是否「路由挂起」(审批中且最新 attempt 的路由判断行无出口)。 * 非持久列——由管理端办件分页(FlowAdminController.instancePage)按页查询后填充, * 随实体序列化给前端画红标;业务代码不得读写该字段。 */ @javax.persistence.Transient private Boolean routeHung; }