2026-08-04-duplicate-submit-remediation.md 23 KB

防重复提交整改方案

面向 AI 代理的工作者: 必需子技能:使用 superpowers:subagent-driven-development(推荐)或 superpowers:executing-plans 逐任务实现此计划。步骤使用复选框(- [ ])语法来跟踪进度。

目标: 修复系统防重复提交机制中的 5 类风险点 —— 补齐数据库唯一约束、统一订单号生成、修复先查后插竞态、补齐前端防重标志、消除弱随机订单号碰撞。

架构: 基于现有「前端体验层 + 后端状态机 + 数据库唯一约束」三层防御体系做定向加固,不引入分布式锁/幂等框架等重设施,保持改动最小化:

  1. 数据库层:guide_orderspayment_orders 补 UNIQUE 约束(最后防线)
  2. 业务层: 新建统一订单号生成器,替换各 Service 的弱随机订单号;修复 GrowthRecordService 先查后插竞态
  3. 前端层: 为 DAN 测评上传页补 submitting 防重标志

技术栈: Java 8 + Spring Boot 2.7.18 + MyBatis-Plus / MySQL 8.0 / uni-app Vue 2


背景:风险点清单(来源:2026-08-04 防重复提交机制分析)

编号 风险 严重度 位置
R1 guide_orders.order_no 无任何索引,重复下单无兜底 🔴 高 schema.sql:1427
R2 payment_orders.order_no 仅普通索引,支付回调重复有风险 🟠 中 schema.sql:3606
R3 订单号弱随机:PO+毫秒ORD+毫秒SUB+毫秒 同毫秒并发必撞 🟠 中 ProductOrderService:791MembershipService:699SubscriptionController:149MemberSubscriptionService:100
R4 GrowthRecordService 先查后插,并发下双查双插竞态 🟠 中 GrowthRecordService:286
R5 前端 DAN 测评上传页无防重标志 🟡 低 cfc-frontend/pages/dan-assessment/report-upload.vue

范围外: 支付回调状态机(handlePaymentSuccess"pending" 检查)已覆盖微信回调重复场景,无需整改。WechatService 的 synchronized 是 token 缓存保护,与防重复无关,不动。


文件结构

文件 职责 操作
cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java 运行时 DDL 迁移唯一入口 修改:追加迁移 158、159
cfc-backend/src/main/resources/schema.sql 建表完整快照 修改:同步唯一约束
cfc-backend/src/main/java/com/etotem/cfc/common/OrderNoGenerator.java 统一订单号生成工具(新建) 创建
cfc-backend/src/main/java/com/etotem/cfc/service/ProductOrderService.java 商城订单 修改:generateOrderNo() 改用生成器
cfc-backend/src/main/java/com/etotem/cfc/service/MembershipService.java 会员订单 修改:generateOrderNo() 改用生成器
cfc-backend/src/main/java/com/etotem/cfc/service/PackagePurchaseService.java 套餐订单 修改:generateOrderNo() 改用生成器
cfc-backend/src/main/java/com/etotem/cfc/service/PackagePaymentService.java 套餐支付订单 修改:generateOrderNo() 改用生成器
cfc-backend/src/main/java/com/etotem/cfc/service/AssessmentOrderService.java 测评订单 修改:generateOrderNo() 改用生成器
cfc-backend/src/main/java/com/etotem/cfc/controller/subscription/SubscriptionController.java 订阅下单(内联生成) 修改:改用生成器
cfc-backend/src/main/java/com/etotem/cfc/service/MemberSubscriptionService.java 订阅下单(内联生成) 修改:改用生成器
cfc-backend/src/main/java/com/etotem/cfc/service/GrowthRecordService.java 成长记录入库 修改:竞态修复
cfc-frontend/pages/dan-assessment/report-upload.vue DAN 测评报告上传 修改:补防重标志

任务 1:迁移 158 —— guide_orders.order_no 唯一约束

文件:

  • 修改:cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java(runMigrations() 末尾)

  • [ ] 步骤 1:确认当前最新迁移编号

运行:grep -n "// 迁移" cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java | tail -5 预期:最新编号为 // 迁移157(另有非编号 // 迁移20240709),新迁移从 158 开始。

  • 步骤 2:在 runMigrations() 末尾追加迁移 158

DatabaseInitializer.javarunMigrations() 方法末尾(// 迁移20240709 块之后)添加:

// 迁移158: guide_orders表order_no加唯一约束(防重复下单,整改R1)
try {
    // 先清理历史重复数据:每组order_no保留id最小的一行
    jdbcTemplate.execute("DELETE g1 FROM guide_orders g1 INNER JOIN guide_orders g2 " +
            "ON g1.order_no = g2.order_no AND g1.id > g2.id");
    log.info("已清理guide_orders重复order_no");
} catch (Exception e) {
    // 忽略:无重复数据时删除0行,不报错
}
try {
    jdbcTemplate.execute("ALTER TABLE guide_orders ADD UNIQUE KEY uk_order_no (order_no)");
    log.info("已为guide_orders.order_no添加唯一约束");
} catch (Exception e) {
    // 约束已存在,忽略
}
  • 步骤 3:编译验证

运行:cd cfc-backend && mvn clean compile 预期:BUILD SUCCESS


任务 2:迁移 159 —— payment_orders.order_no 升级为唯一约束

文件:

  • 修改:cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java(runMigrations() 末尾,任务 1 之后)

  • [ ] 步骤 1:在 runMigrations() 末尾追加迁移 159

    // 迁移159: payment_orders表order_no升级为唯一约束(防支付回调重复,整改R2)
    try {
    jdbcTemplate.execute("DELETE p1 FROM payment_orders p1 INNER JOIN payment_orders p2 " +
            "ON p1.order_no = p2.order_no AND p1.id > p2.id");
    log.info("已清理payment_orders重复order_no");
    } catch (Exception e) {
    // 忽略
    }
    try {
    // 先删除原普通索引,再加唯一索引(同名列不可并存)
    jdbcTemplate.execute("ALTER TABLE payment_orders DROP INDEX idx_order_no");
    jdbcTemplate.execute("ALTER TABLE payment_orders ADD UNIQUE KEY uk_order_no (order_no)");
    log.info("已为payment_orders.order_no添加唯一约束");
    } catch (Exception e) {
    // 索引已存在或已处理,忽略
    }
    

注意: 必须先 DROP 原 idx_order_no 普通索引,否则 MySQL 不允许同列同时存在普通索引与唯一索引。若迁移中途失败导致 idx_order_no 已删而唯一索引未建,重跑时会因 uk_order_no 存在而忽略,DROP 语句幂等性由 catch 保证。

  • 步骤 2:编译验证

运行:cd cfc-backend && mvn clean compile 预期:BUILD SUCCESS


任务 3:同步 schema.sql 快照

文件:

  • 修改:cfc-backend/src/main/resources/schema.sql

  • [ ] 步骤 1:更新 guide_orders 表定义(第 1427 行附近)

order_no VARCHAR(50) NOT NULL COMMENT '订单号', 行后追加唯一键声明,并删除结尾处若有冲突的旧索引行。修改后:

CREATE TABLE IF NOT EXISTS guide_orders (
    id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID',
    order_no VARCHAR(50) NOT NULL COMMENT '订单号',
    ...
    UNIQUE KEY uk_order_no (order_no)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='成长规划师订单表';
  • 步骤 2:更新 payment_orders 表定义(第 3591 行附近)

INDEX idx_order_no (order_no), 替换为 UNIQUE KEY uk_order_no (order_no),:

CREATE TABLE IF NOT EXISTS payment_orders (
    ...
    order_no VARCHAR(64) NOT NULL COMMENT '订单号',
    ...
    UNIQUE KEY uk_order_no (order_no),
    INDEX idx_family_id (family_id),
    INDEX idx_status (status)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='会员支付订单';
  • 步骤 3:验证

运行:cd cfc-backend && mvn clean compile 预期:BUILD SUCCESS(确认 schema.sql 改动未影响编译)


任务 4:创建统一订单号生成器 OrderNoGenerator

文件:

  • 创建:cfc-backend/src/main/java/com/etotem/cfc/common/OrderNoGenerator.java

  • [ ] 步骤 1:创建工具类

    package com.etotem.cfc.common;
    
    import java.util.UUID;
    import java.util.concurrent.atomic.AtomicLong;
    
    /**
    * 统一订单号生成器(整改R3)
    * 格式: 前缀 + 毫秒时间戳 + 自增序号(3位) + UUID短段(4位)
    * 前缀用于支付回调路由分派(PO=商城/其他=套餐),各业务前缀保持不变
    */
    public class OrderNoGenerator {
    
    private static final AtomicLong SEQ = new AtomicLong(0);
    
    private OrderNoGenerator() {
    }
    
    public static String generate(String prefix) {
        long seq = SEQ.incrementAndGet() % 1000;
        String uuid = UUID.randomUUID().toString().replace("-", "").substring(0, 4).toUpperCase();
        return prefix + System.currentTimeMillis() + String.format("%03d", seq) + uuid;
    }
    }
    

设计说明:

  • 同一毫秒内自增序号保证顺序唯一,4 位 UUID 段在进程重启后仍保证随机性,DB 唯一约束兜底最终一致性
  • 前缀参数化,保留 PO/ORD/SUB/PKG/A 等现有前缀,不影响 PaymentService.handleWechatNotify 按前缀路由的逻辑

  • [ ] 步骤 2:编译验证

运行:cd cfc-backend && mvn clean compile 预期:BUILD SUCCESS


任务 5:替换各 Service 的 generateOrderNo()

文件:

  • 修改:cfc-backend/src/main/java/com/etotem/cfc/service/ProductOrderService.java:791
  • 修改:cfc-backend/src/main/java/com/etotem/cfc/service/MembershipService.java:699
  • 修改:cfc-backend/src/main/java/com/etotem/cfc/service/PackagePurchaseService.java:202
  • 修改:cfc-backend/src/main/java/com/etotem/cfc/service/PackagePaymentService.java:247
  • 修改:cfc-backend/src/main/java/com/etotem/cfc/service/AssessmentOrderService.java:58
  • 修改:cfc-backend/src/main/java/com/etotem/cfc/controller/subscription/SubscriptionController.java:149
  • 修改:cfc-backend/src/main/java/com/etotem/cfc/service/MemberSubscriptionService.java:100

  • [ ] 步骤 1:替换 ProductOrderService.generateOrderNo()

将方法体替换为一行委托,并新增 import:

// 文件顶部新增 import
import com.etotem.cfc.common.OrderNoGenerator;

// generateOrderNo() 方法体替换为:
private String generateOrderNo() {
    return OrderNoGenerator.generate("PO");
}
  • [ ] 步骤 2:替换 MembershipService.generateOrderNo()

    // 新增 import com.etotem.cfc.common.OrderNoGenerator;
    
    private String generateOrderNo() {
    return OrderNoGenerator.generate("ORD");
    }
    
  • [ ] 步骤 3:替换 PackagePurchaseService.generateOrderNo()

    // 新增 import com.etotem.cfc.common.OrderNoGenerator;
    
    private String generateOrderNo() {
    return OrderNoGenerator.generate("PKG");
    }
    
  • [ ] 步骤 4:替换 PackagePaymentService.generateOrderNo()

    // 新增 import com.etotem.cfc.common.OrderNoGenerator;
    
    private String generateOrderNo() {
    return OrderNoGenerator.generate("PKG");
    }
    
  • [ ] 步骤 5:替换 AssessmentOrderService.generateOrderNo()

    // 新增 import com.etotem.cfc.common.OrderNoGenerator;
    // 若 generateOrderNo() 变为仅一行,可删除原 java.util.Random import(可选)
    
    private String generateOrderNo() {
    return OrderNoGenerator.generate("A");
    }
    
  • [ ] 步骤 6:替换 SubscriptionController 内联生成(第 149 行)

原文:String orderNo = "SUB" + System.currentTimeMillis(); 改为:

// 新增 import com.etotem.cfc.common.OrderNoGenerator;
String orderNo = OrderNoGenerator.generate("SUB");
  • 步骤 7:替换 MemberSubscriptionService 内联生成(第 100 行)

原文:String orderNo = "SUB" + System.currentTimeMillis(); 改为:

// 新增 import com.etotem.cfc.common.OrderNoGenerator;
String orderNo = OrderNoGenerator.generate("SUB");
  • 步骤 8:全量编译验证

运行:cd cfc-backend && mvn clean compile 预期:BUILD SUCCESS。若出现未使用 import 警告(Random),清理对应 import 后重新编译。

注意: 不要改动 PaymentService.handleWechatNotify 中按前缀分派路由的逻辑(PO 前缀 → 商城订单),订单号前缀保持不变。


任务 6:修复 GrowthRecordService 先查后插竞态

文件:

  • 修改:cfc-backend/src/main/java/com/etotem/cfc/service/GrowthRecordService.java:286

  • [ ] 步骤 1:替换 checkDuplicateAndInsert() 方法

原实现是先 selectOneinsert,并发下两个请求同时通过查询就会双插。改为直接插入 + 捕获唯一键冲突,依赖迁移已存在的 uk_growth_dedup_hash 唯一索引兜底:

// 文件顶部新增 import(若未引入)
import org.springframework.dao.DuplicateKeyException;

private void checkDuplicateAndInsert(GrowthRecord record) {
    String hash = record.getDedupHash();
    if (hash != null && !hash.startsWith("NO_DATE:")) {
        try {
            growthRecordMapper.insert(record);
        } catch (DuplicateKeyException e) {
            // 唯一索引 uk_growth_dedup_hash 兜底:并发重复插入被数据库拦截
            throw new DuplicateGrowthRecordException("Growth record already exists with hash: " + hash);
        }
    } else {
        growthRecordMapper.insert(record);
    }
}

行为说明:

  • AssessmentService:259 已捕获 DuplicateGrowthRecordException 并静默跳过,调用方无需改动
  • 无 hash 或 NO_DATE: 前缀的记录仍走原插入路径(不参与去重,行为不变)

  • [ ] 步骤 2:编译验证

运行:cd cfc-backend && mvn clean compile 预期:BUILD SUCCESS


任务 7:前端 DAN 测评上传页补防重标志

文件:

  • 修改:cfc-frontend/pages/dan-assessment/report-upload.vue

  • [ ] 步骤 1:添加 submitting 数据字段

data() 中新增(找到现有 loading 字段旁):

data() {
  return {
    // ...现有字段...
    submitting: false
  }
}
  • 步骤 2:提交入口加防重守卫

找到提交/上传处理方法(含 self.loading = true 的异步提交逻辑),在方法开头加:

submit: function() {
  if (this.submitting) return   // 防重复点击
  this.submitting = true
  // ...现有提交逻辑...
  // 在请求回调(成功/失败/完成)的 finally 位置:
  this.submitting = false
}

具体模式参照 cfc-frontend/components/feedback-popup.vue:51-71 的既有写法(if (this.submitting) return; this.submitting = true; ... .then(function(){ self.submitting = false }))。

  • 步骤 3:验证前端构建

运行:cd cfc-frontend && npm run build:mp-weixin 预期:构建成功,无编译错误


任务 8:整体回归验证

  • 步骤 1:后端全量编译

运行:cd cfc-backend && mvn clean compile 预期:BUILD SUCCESS

  • 步骤 2:检查路由无冲突(若改动涉及 Controller)

运行:

grep -rn '@Mapping' cfc-backend/src/main/java/com/etotem/cfc/controller/ | grep -oP '@\w+Mapping\("\K[^"]*' | sort -u | wc -l

预期:无新增路由,数量与改动前一致

  • 步骤 3:确认迁移幂等性

检查 DatabaseInitializer.java 中迁移 158/159 的 catch 分支:重复执行时 DROP/ADD 语句均应被 catch 吞掉或自然跳过。确认 DELETE 清理语句在无重复数据时执行 0 行不报错。

  • [ ] 步骤 4:git 提交(如需)

    git add cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java \
        cfc-backend/src/main/resources/schema.sql \
        cfc-backend/src/main/java/com/etotem/cfc/common/OrderNoGenerator.java \
        cfc-backend/src/main/java/com/etotem/cfc/service/ProductOrderService.java \
        cfc-backend/src/main/java/com/etotem/cfc/service/MembershipService.java \
        cfc-backend/src/main/java/com/etotem/cfc/service/PackagePurchaseService.java \
        cfc-backend/src/main/java/com/etotem/cfc/service/PackagePaymentService.java \
        cfc-backend/src/main/java/com/etotem/cfc/service/AssessmentOrderService.java \
        cfc-backend/src/main/java/com/etotem/cfc/controller/subscription/SubscriptionController.java \
        cfc-backend/src/main/java/com/etotem/cfc/service/MemberSubscriptionService.java \
        cfc-backend/src/main/java/com/etotem/cfc/service/GrowthRecordService.java \
        cfc-frontend/pages/dan-assessment/report-upload.vue
    git commit -m "fix: 防重复提交整改——补齐唯一约束/统一订单号/修复竞态"
    

整改后风险状态(初版)

编号 风险 整改后状态
R1 guide_orders 无唯一键 ✅ 迁移 158 补齐 UNIQUE KEY
R2 payment_orders 仅普通索引 ✅ 迁移 159 升级为 UNIQUE KEY
R3 订单号弱随机碰撞 ✅ OrderNoGenerator 统一生成(自增序号 + UUID 段)
R4 GrowthRecordService 竞态 ✅ 直接插入 + DuplicateKeyException 兜底
R5 前端 DAN 上传无防重 ✅ submitting 标志位

扩展部分:用户级高频创建页防重(R6)

经用户确认,仅整改订单类不足以覆盖"按钮连点插入两条"风险 —— 用户级创建页(任务、心愿、家庭成员、保险方案等)同样需要防重。已有 submitting 守卫的页面(如 pages/wishes/create.vuepages/wealth-sub/insurance-add.vue)无需改动;本任务覆盖经确认无 submitting 守卫的页面

已确认无 submitting 守卫的用户级创建页

文件 场景 按钮位置
cfc-frontend/pages/tasks/create-task.vue 创建任务(连点多孩会双倍创建) 第 78 行 <button @click="submit">
cfc-frontend/pages/family/add-member.vue 添加家庭成员 submit 方法
cfc-frontend/pages/assessment/apply.vue 测评申请(创建订单前) submit 方法
cfc-frontend/pages/assessment/purchase.vue 测评购买页 submit 方法
cfc-frontend/pages/guide/packages/create.vue 规划师创建套餐 submit 方法
cfc-frontend/pages/guide/register/index.vue 规划师注册申请 submit 方法

任务 9:批量补齐用户级创建页 submitting 守卫

文件:

  • 修改:cfc-frontend/pages/tasks/create-task.vue
  • 修改:cfc-frontend/pages/family/add-member.vue
  • 修改:cfc-frontend/pages/assessment/apply.vue
  • 修改:cfc-frontend/pages/assessment/purchase.vue
  • 修改:cfc-frontend/pages/guide/packages/create.vue
  • 修改:cfc-frontend/pages/guide/register/index.vue

统一改动模板(每个页面):

  • [ ] 步骤 1:data() 中新增 submitting: false

    data() {
    return {
    // ... 现有字段 ...
    submitting: false
    }
    }
    
  • [ ] 步骤 2:submit / submitForm 方法顶部加守卫

在方法最开头(参数校验之后、网络请求之前)插入:

async submit() {
  // ... 现有参数校验 ...
  if (this.submitting) return      // 重复点击直接拦截
  this.submitting = true
  uni.showLoading({ title: '提交中...' })
  try {
    // ... 现有请求逻辑 ...
  } catch (e) {
    // ... 现有错误处理 ...
  } finally {
    this.submitting = false         // 无论成功失败,解锁
    uni.hideLoading()
  }
}

注意: 若原方法没有 try/finally 结构,需将现有请求逻辑包裹进 try,并在 finally 中重置 submittingfinally 是必须的,否则网络失败时按钮永久禁用。

  • 步骤 3:模板按钮加 :loading:disabled

将原按钮:

<button @click="submit">创建任务</button>

改为:

<button @click="submit" :loading="submitting" :disabled="submitting">创建任务</button>

注意: 微信小程序 <button>:loading 属性仅在原生 <button> 上生效(uni-app 中有效),自定义 .btn-* 类样式不影响;:disabled 同步防止视觉/物理双击。

  • 步骤 4:逐文件实施

按以下顺序修改,每改一个文件立即 cd cfc-frontend && npm run build:mp-weixin 验证(单个文件改动较小,按文件级验证即可):

# 文件 重点
1 pages/tasks/create-task.vue submit 在第 259 行,含循环 for (memberId) 多孩创建,守卫至关重要
2 pages/family/add-member.vue 验证 submit 方法名(可能是 submitsubmitForm)
3 pages/assessment/apply.vue 测评申请会创建订单,需配合后端订单号唯一性
4 pages/assessment/purchase.vue 测评购买页(可能涉及支付,与任务 5 联动)
5 pages/guide/packages/create.vue 规划师创建套餐
6 pages/guide/register/index.vue 规划师入驻申请
  • 步骤 5:前端构建验证

运行:cd cfc-frontend && npm run build:mp-weixin 预期:构建成功,6 个文件无编译错误


任务 10:全量回归验证(含扩展)

  • 步骤 1:后端编译

运行:cd cfc-backend && mvn clean compile 预期:BUILD SUCCESS

  • 步骤 2:前端构建

运行:cd cfc-frontend && npm run build:mp-weixin 预期:构建成功

  • 步骤 3:路由冲突检查

运行:

grep -rn '@Mapping' cfc-backend/src/main/java/com/etotem/cfc/controller/ | grep -oP '@\w+Mapping\("\K[^"]*' | sort -u | wc -l

预期:数量与改动前一致(本次不新增 Controller 路由)

  • [ ] 步骤 4:迁移幂等性

  • DatabaseInitializer 迁移 158/159 的 catch 分支确保重复执行安全

  • schema.sql 与建表语句保持同步

  • [ ] 步骤 5:git 提交

    git add cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java \
        cfc-backend/src/main/resources/schema.sql \
        cfc-backend/src/main/java/com/etotem/cfc/common/OrderNoGenerator.java \
        cfc-backend/src/main/java/com/etotem/cfc/service/ProductOrderService.java \
        cfc-backend/src/main/java/com/etotem/cfc/service/MembershipService.java \
        cfc-backend/src/main/java/com/etotem/cfc/service/PackagePurchaseService.java \
        cfc-backend/src/main/java/com/etotem/cfc/service/PackagePaymentService.java \
        cfc-backend/src/main/java/com/etotem/cfc/service/AssessmentOrderService.java \
        cfc-backend/src/main/java/com/etotem/cfc/controller/subscription/SubscriptionController.java \
        cfc-backend/src/main/java/com/etotem/cfc/service/MemberSubscriptionService.java \
        cfc-backend/src/main/java/com/etotem/cfc/service/GrowthRecordService.java \
        cfc-frontend/pages/dan-assessment/report-upload.vue \
        cfc-frontend/pages/tasks/create-task.vue \
        cfc-frontend/pages/family/add-member.vue \
        cfc-frontend/pages/assessment/apply.vue \
        cfc-frontend/pages/assessment/purchase.vue \
        cfc-frontend/pages/guide/packages/create.vue \
        cfc-frontend/pages/guide/register/index.vue
    git commit -m "fix: 防重复提交整改——补齐唯一约束/统一订单号/修复竞态/批量前端守卫"
    

最终风险状态

编号 风险 整改后状态
R1 guide_orders 无唯一键 ✅ 迁移 158
R2 payment_orders 仅普通索引 ✅ 迁移 159
R3 订单号弱随机碰撞 ✅ OrderNoGenerator
R4 GrowthRecordService 竞态 ✅ DuplicateKeyException 兜底
R5 DAN 上传页无防重 ✅ submitting
R6 用户级创建页连点 ✅ 6 个高频页 submitting(任务 9)

遗留建议(本次不改,记录备查):

  1. 系统仍无分布式锁/幂等键表 —— 多实例部署的跨进程幂等需求另立计划
  2. 管理后台(Admin*)的 create 接口未覆盖 —— 通常管理后台连点概率低,但若发现批量创建类漏洞可补强
  3. 部分高频表的业务键 UNIQUE(如 tasks(creator_id, title, deadline))未强制 —— 若业务确认需要"同家庭同标题同日期任务唯一",另立迁移加 UNIQUE;当前接受业务上重复(同一任务可分发多个孩子)