跳转到正文

关系绑定与任务

应用需要维护通用关联表或合并后台处理触发时,可以复用本页抽象。它们没有业务路由、权限策略或存储表,具体实现仍需完成基本服务装配

选择关系服务

抽象适用任务子类职责
AbstractBaseBindOneService通过一个外键查关联对象、解除该外键的关联实现 getFkExtractor(),定义模型与 Mapper
AbstractBaseBindTwoService维护主侧 ID 与关联侧 ID 的关系实现 getPkExtractor()getFkExtractor()buildPo(pk, fk)
AbstractBaseBindBeanService用完整 PO 表达更复杂的绑定键实现 getBindPo、单条/批量 bind、unbind;基类没有这些操作的默认逻辑

三种服务都复用六泛型的服务基类,沿用 public 构造器要求。这里的 pk 表示关系主侧业务 ID,不等同于关系记录自己的 BaseEntityPo.id

单外键关系

getPo(bindId) 按外键 selectOne,未找到时返回 null;它不同于强制存在的 getPoById。外键存在多条记录时也不能当作任意取一条使用。unbindunbindBatch 删除相应外键记录并返回成功 UpdateModel,不检查实际影响行数。

该抽象没有提供单外键 bind 方法;新增流程由应用选择普通 CRUD 或自己定义的业务入口。

双外键关系

默认单条 bind(pk, fk) 先检查相同关系是否存在,存在则报 ALREADY_BIND_DATA,否则直接插入 buildPo 的结果。unbind 在关系不存在时报 ALREADY_UNBIND_DATA,存在则删除。checkBindPo 用于要求关系存在,缺失时报 NO_BIND_DATA;普通 getBindPo 则可返回 null。

这些方法直接调用 Mapper,不经过普通新增/删除 Hook。应在 buildPo 中完成所需字段初始化,应用校验两端对象存在且当前用户有权操作。默认查询直接构造 QueryWrapper,不复用 Service 的 Query 权限过滤;先查后插也不能防止并发重复,数据库和应用需设计关系唯一性。

绑定接口上的事务声明及双外键实现中的 @Transactional(rollbackFor=Exception.class) 需要 Spring 事务代理和事务管理器生效。返回 UpdateModel(false) 不会像异常一样自动触发回滚;也没有跨服务、跨数据库事务保证。

批量操作与迁移的当前边界

使用前应针对实际实现处理以下情况:

方法当前实现与接入影响
两种 bindBatch 重载从传入 ID 列表中移除已绑定 ID,会修改调用方列表;空余集合报 NO_BIND_DATA,剩 1 项调用单条 bind,其余使用 insertBatch
批量去除已有关系使用 parallelStream 对传入列表执行 remove;不可变列表可能抛异常,普通 ArrayList 也没有并行修改保证,不能仅复制成 ArrayList 就视为已解决并发问题
批量输入重复 ID没有完整去重;应用应校验并使用安全的差集实现替代直接依赖默认筛选
unbindBatch(pk, ids)多条匹配分支将筛出的外键 ID 用在 getPkExtractor() 的 IN 条件中,可能删不到目标或删除范围不符;应重写并验证使用外键条件后再开放该分支
changeBindByPk / changeBindByFk先删原关系,再构造 Stream.toList() 的不可变列表传给 bindBatch;目标存在重合关系时可能因 remove 抛异常,迁移前需修正集合处理和冲突规则
deleteByPk / deleteByFk删除该侧全部关系,返回成功标识但不核对影响行数;不删除两端业务实体

批量插入还要求扩展 SQL 已装配,见Mapper 配置。双外键服务不自动刷新权限缓存、维护双向对象、触发外部通知或级联清理。具体业务模块可能重写这些行为,不能用基类语义替代其实际接口说明。

合并后台任务触发

AbstractAtomicQueueService 适合表达“已有待处理工作,启动一个循环持续取任务”的本地触发方式。应用注册具体子类,实现 invoke(),并准备自己的待处理存储;基类只管理运行标记,不保存任务元素。

默认执行顺序为:

  1. 调用 start(),对当前实例的 AtomicInteger 增加计数;原值大于 0 时直接返回。
  2. 首次触发向 Base 的 ExecutorUtil 提交任务,默认先等待 1000 毫秒,可重写 sleepTime()
  3. 每轮输出从 0 开始的处理日志,再依次调用 before()invoke()after()
  4. invoke() 返回 true 时继续循环,false 时结束,最后将计数清零。

同一实例运行中的多次 start 不代表等量排队任务;是否继续取决于 invoke() 的返回值。没有定时调度、跨实例互斥、持久化、重试或完成 Future,应用不能据 start() 返回判断工作已经完成。运行标记按对象实例隔离,分布式执行需要额外协调。

异常与结束竞态

计数清零不在 finally 中。提交被拒绝、等待被中断、日志或任一 Hook 抛异常时,标记可能一直保持非零,后续 start 不再提交;基类没有公开的重置入口。任务结束与新 start 之间也存在竞态,不能据原子计数保证每次触发都被消费。

需要可靠处理时,应由应用明确任务存储、恢复和重试方案,或修正执行生命周期后再使用。底层默认线程池可能采用 CallerRunsPolicy,饱和时等待和循环可能在调用线程执行;容量和上下文行为见 Base 异步任务

导入导出需要应用实现

BaseImportService.importPo(dto, file)BaseExportService.exportToWord(query)exportToExcel(query) 的默认实现都只返回 new UploadModel()。它们不解析文件、不生成 Word/Excel、不上传对象,也不登记下载记录。

若应用对外提供这些入口,应先重写为完整的文件处理流程,再返回可用元数据;具体文件生成、存储和访问授权属于调用应用的职责。生成完成后如需记录用户下载任务,可另接入下载中心