Bläddra i källkod

Merge remote-tracking branch 'origin/master'

jiakai 1 vecka sedan
förälder
incheckning
a3b3504e53

+ 1 - 1
authentication/src/main/java/com/jzg/config/SecurityConfig.java

@@ -95,7 +95,7 @@ public class SecurityConfig {
                                 "/auth/login","/auth/captchaById","/auth/register","/sendSmsCode","/appLogin","/smsCodeLogin","/auth/publicKeyExchange","/appAuth/smsCodeLogin","/send/sendSmsCode",
                                 "/appAuth/appLogin","/auth/sendSmsCode","/auth/forgotPassword","/appAuth/partnerAppLogin",
                                 "/supplementOrder/getOrderDetail","/dict/get", "/getAllButtons","/order/qrCode/**","/appUser/userInfoGet",
-                                "/appUser/distributorInvite","/appUser/invitationRegister").permitAll()
+                                "/appUser/distributorInvite","/appUser/invitationRegister","/system/sysListAccountLinking","/signIn/updateSignCompleteRuleIn").permitAll()
                         .anyRequest().authenticated()  // 其他接口需认证
                 )
 //                .formLogin((form) -> form

+ 39 - 0
commons/src/main/java/com/jzg/commons/entity/dto/AgreementRulesAttrCopyParam.java

@@ -0,0 +1,39 @@
+package com.jzg.commons.entity.dto;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotEmpty;
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * 批量复制规则因子参数
+ *
+ * @author lipf
+ * @date 2026/9/2
+ */
+@Schema(description = "批量复制规则因子dto")
+@Data
+public class AgreementRulesAttrCopyParam {
+
+    @Schema(description = "要复制的规则因子关联记录ID列表(来源于getRuleInfo返回明细的id字段),源规则由所选因子记录所属规则确定")
+    @NotEmpty(message = "请指定要复制的规则因子")
+    private List<String> attrIds;
+
+    @Schema(description = "目标协议规则列表")
+    @NotEmpty(message = "目标协议不能为空")
+    private List<CopyTarget> targets;
+
+    @Schema(description = "复制目标")
+    @Data
+    public static class CopyTarget {
+
+        @Schema(description = "目标承保协议ID")
+        @NotBlank(message = "目标协议ID不能为空")
+        private String agreementId;
+
+        @Schema(description = "目标承保规则ID(可选);为空时若该协议下已有规则则复制到第一条规则,否则新增规则")
+        private String ruleId;
+    }
+}

+ 6 - 0
gateway/src/main/java/com/jzg/config/CorsConfig.java

@@ -43,6 +43,12 @@ public class CorsConfig {
         corsConfiguration.addAllowedOrigin("http://192.168.0.59:9000");
         corsConfiguration.addAllowedOrigin("http://192.168.1.61:9000");
         corsConfiguration.addAllowedOrigin("http://192.168.1.159:9000");
+        corsConfiguration.addAllowedOrigin("http://192.168.1.65:9010");
+        corsConfiguration.addAllowedOrigin("http://localhost:9010");
+        corsConfiguration.addAllowedOrigin("http://192.168.1.65:9020");
+        corsConfiguration.addAllowedOrigin("http://localhost:9020");
+        corsConfiguration.addAllowedOrigin("http://192.168.1.65:8080");
+        corsConfiguration.addAllowedOrigin("https://test.baoxianzhanggui.com");
         // 允许的方法
         corsConfiguration.addAllowedMethod("*");
         // 允许的头信息

+ 6 - 0
gateway/src/main/java/com/jzg/filter/AuthGlobalFilter.java

@@ -10,6 +10,7 @@ import org.springframework.core.Ordered;
 import org.springframework.core.annotation.Order;
 import org.springframework.core.io.buffer.DataBuffer;
 import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpMethod;
 import org.springframework.http.HttpStatus;
 import org.springframework.http.MediaType;
 import org.springframework.stereotype.Component;
@@ -94,6 +95,11 @@ public class AuthGlobalFilter implements GlobalFilter {
     public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
         String path = exchange.getRequest().getURI().getPath();
 
+        // 跳过 CORS 预检请求
+        if (HttpMethod.OPTIONS.matches(exchange.getRequest().getMethod().name())) {
+            return chain.filter(exchange);
+        }
+
         // 1. 白名单路径直接放行
         if (isWhitelisted(path)) {
             return chain.filter(exchange);

+ 11 - 0
gateway/src/main/java/com/jzg/filter/DecryptFilter.java

@@ -15,6 +15,7 @@ import org.springframework.cloud.gateway.filter.GlobalFilter;
 import org.springframework.core.io.buffer.DataBuffer;
 import org.springframework.core.io.buffer.DataBufferFactory;
 import org.springframework.core.io.buffer.DataBufferUtils;
+import org.springframework.http.HttpMethod;
 import org.springframework.http.server.reactive.ServerHttpRequest;
 import org.springframework.http.server.reactive.ServerHttpRequestDecorator;
 import org.springframework.stereotype.Component;
@@ -25,6 +26,7 @@ import reactor.core.publisher.Mono;
 import java.nio.ByteBuffer;
 import java.nio.charset.StandardCharsets;
 import java.security.PrivateKey;
+import java.util.List;
 
 
 @Component
@@ -41,8 +43,17 @@ public class DecryptFilter implements GlobalFilter {
     @Override
     public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
         ServerHttpRequest request = exchange.getRequest();
+        // CORS 预检请求直接放行,避免 NPE
+        if (HttpMethod.OPTIONS.matches(request.getMethod().name())) {
+            return chain.filter(exchange);
+        }
         boolean contains = request.getURI().getPath().contains("/publicKeyExchange");
         if(isEncrypted && !contains) {
+            // 空指针保护
+            List<String> authHeaders = exchange.getRequest().getHeaders().get("authorization");
+            if (authHeaders == null || authHeaders.isEmpty()) {
+                return chain.filter(exchange);
+            }
             String token = exchange.getRequest().getHeaders().get("authorization").get(0);
             String userName = baseController.getUserName(token);
             // 使用 ServerHttpRequestDecorator 包装请求

+ 10 - 1
tenant/organization/src/main/java/com/jzg/organization/controller/AgreementRulesController.java

@@ -1,10 +1,11 @@
 package com.jzg.organization.controller;
 
 import com.jzg.commons.aop.OperatorTrajectory;
+import com.jzg.commons.constants.OperationTypeEnum;
 import com.jzg.commons.core.base.BaseController;
 import com.jzg.commons.core.page.HttpResult;
-import com.jzg.commons.constants.OperationTypeEnum;
 import com.jzg.commons.entity.dto.AgreementRulesAddParam;
+import com.jzg.commons.entity.dto.AgreementRulesAttrCopyParam;
 import com.jzg.commons.entity.dto.AgreementRulesEditParam;
 import com.jzg.commons.entity.po.PtlAgreementUndwrtRules;
 import com.jzg.commons.entity.vo.AgreementRulesVo;
@@ -88,6 +89,14 @@ public class AgreementRulesController extends BaseController {
         return HttpResult.ok(rulesVos);
     }
 
+    @OperatorTrajectory(moduleName = "报价配置/协议管理/承保规则/批量复制规则因子", type = "C", OperationDict = OperationTypeEnum.COPY, mapperClass = PtlAgreementUndwrtRulesMapper.class, entityClass = AgreementRulesAttrCopyParam.class)
+    @Operation(summary = "批量复制规则因子:将源承保规则下的规则因子复制到目标协议承保规则下,目标协议无规则时新增")
+    @PostMapping("/rulesAttrBatchCopy")
+    public HttpResult rulesAttrBatchCopy(@Validated @RequestBody AgreementRulesAttrCopyParam param) {
+        return agreementRulesService.rulesAttrBatchCopy(param) ?
+                HttpResult.ok("批量复制规则因子成功") : HttpResult.error("批量复制规则因子失败");
+    }
+
     @Operation(summary = "获取承保规则明细")
     @GetMapping("getRuleInfo")
     public HttpResult getRuleInfo(String ruleId){

+ 11 - 1
tenant/organization/src/main/java/com/jzg/organization/service/PtlAgreementUndwrtRulesService.java

@@ -1,9 +1,10 @@
 package com.jzg.organization.service;
 
+import com.baomidou.mybatisplus.extension.service.IService;
 import com.jzg.commons.entity.dto.AgreementRulesAddParam;
+import com.jzg.commons.entity.dto.AgreementRulesAttrCopyParam;
 import com.jzg.commons.entity.dto.AgreementRulesEditParam;
 import com.jzg.commons.entity.po.PtlAgreementUndwrtRules;
-import com.baomidou.mybatisplus.extension.service.IService;
 import com.jzg.commons.entity.vo.AgreementRulesVo;
 
 import java.util.List;
@@ -70,4 +71,13 @@ public interface PtlAgreementUndwrtRulesService extends IService<PtlAgreementUnd
      * @return
      */
     boolean deleteById(String id);
+
+    /**
+     * 批量复制规则因子:将源承保规则下的某条或多条规则因子复制到某个或多个承保协议下的某条承保规则下,
+     * 若目标承保协议下没有规则则新增规则
+     *
+     * @param param 复制参数
+     * @return boolean 是否成功
+     */
+    boolean rulesAttrBatchCopy(AgreementRulesAttrCopyParam param);
 }

+ 119 - 4
tenant/organization/src/main/java/com/jzg/organization/service/impl/PtlAgreementUndwrtRulesServiceImpl.java

@@ -7,10 +7,7 @@ import com.alibaba.fastjson.JSONArray;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
-import com.jzg.commons.entity.dto.AgreementRulesAddParam;
-import com.jzg.commons.entity.dto.AgreementRulesEditParam;
-import com.jzg.commons.entity.dto.CostAgreementParam;
-import com.jzg.commons.entity.dto.RulesGroupParam;
+import com.jzg.commons.entity.dto.*;
 import com.jzg.commons.entity.po.*;
 import com.jzg.commons.entity.vo.AgreementRulesVo;
 import com.jzg.commons.entity.vo.CostListVo;
@@ -767,6 +764,124 @@ public class PtlAgreementUndwrtRulesServiceImpl extends ServiceImpl<PtlAgreement
         return newAttrList;
     }
 
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public boolean rulesAttrBatchCopy(AgreementRulesAttrCopyParam param) {
+        log.info("批量复制规则因子:param=[{}]", JSONUtil.toJsonStr(param));
+
+        // 按 attrIds 精确查询要复制的因子记录,源规则由因子记录所属规则反推
+        List<PtlAgreementUndwrtRulesAttrLink> sourceLinks = agreementUndwrtRulesAttrLinkService.list(
+                new LambdaQueryWrapper<PtlAgreementUndwrtRulesAttrLink>()
+                        .in(PtlAgreementUndwrtRulesAttrLink::getId, param.getAttrIds()));
+        if (CollUtil.isEmpty(sourceLinks)) {
+            throw new SystemException("源规则因子不存在");
+        }
+        if (sourceLinks.size() != new HashSet<>(param.getAttrIds()).size()) {
+            throw new SystemException("部分规则因子不存在或已被删除");
+        }
+
+        // 源因子所属规则必须一致(同一规则或同一批量组)
+        List<String> sourceRuleIds = sourceLinks.stream()
+                .map(PtlAgreementUndwrtRulesAttrLink::getRulesId)
+                .distinct()
+                .collect(Collectors.toList());
+        List<PtlAgreementUndwrtRules> sourceRuleList = baseMapper.selectBatchIds(sourceRuleIds);
+        if (CollUtil.isEmpty(sourceRuleList)) {
+            throw new SystemException("源规则因子所属承保规则不存在或已被删除");
+        }
+        PtlAgreementUndwrtRules sourceRules = sourceRuleList.get(0);
+        if (sourceRuleList.size() > 1) {
+            boolean sameGroup = sourceRuleList.stream().allMatch(r ->
+                    StrUtil.isNotBlank(r.getParentId()) && r.getParentId().equals(sourceRules.getParentId()));
+            if (!sameGroup) {
+                throw new SystemException("源规则因子分属不同规则组,无法批量复制");
+            }
+        }
+        List<PtlAgreementUndwrtRulesAttr> undwrtRulesAttrList = agreementUndwrtRulesAttrService.getUndwrtRulesAttrList();
+
+        // 遍历目标协议:每个目标协议下新增一条规则承接所选因子,
+        // 新增规则与目标协议已有规则共享 parentId(无已有规则则独立展示),前端按组合并成一条展示;源规则保持不变
+        for (AgreementRulesAttrCopyParam.CopyTarget target : param.getTargets()) {
+            String targetAgreementId = target.getAgreementId();
+
+            // 1. 定位锚定规则(用于确定归组):优先指定的规则,否则取该协议下第一条规则(序号最小)
+            PtlAgreementUndwrtRules anchor = null;
+            if (StrUtil.isNotBlank(target.getRuleId())) {
+                anchor = baseMapper.selectById(target.getRuleId());
+                if (anchor == null) {
+                    throw new SystemException("目标承保规则不存在:" + target.getRuleId());
+                }
+                if (!targetAgreementId.equals(anchor.getAgreementId())) {
+                    throw new SystemException("目标承保规则[" + target.getRuleId() + "]不属于协议[" + targetAgreementId + "]");
+                }
+            } else {
+                LambdaQueryWrapper<PtlAgreementUndwrtRules> wrapper = new LambdaQueryWrapper<>();
+                wrapper.eq(PtlAgreementUndwrtRules::getAgreementId, targetAgreementId)
+                        .eq(PtlAgreementUndwrtRules::getIsDelete, 0)
+                        .orderByAsc(PtlAgreementUndwrtRules::getOrderNum)
+                        .last("LIMIT 1");
+                anchor = baseMapper.selectOne(wrapper);
+            }
+
+            // 2. 获取该协议下最大序号,新规则追加在末尾
+            LambdaQueryWrapper<PtlAgreementUndwrtRules> maxWrapper = new LambdaQueryWrapper<>();
+            maxWrapper.eq(PtlAgreementUndwrtRules::getAgreementId, targetAgreementId)
+                    .eq(PtlAgreementUndwrtRules::getIsDelete, 0)
+                    .orderByDesc(PtlAgreementUndwrtRules::getOrderNum)
+                    .last("LIMIT 1");
+            PtlAgreementUndwrtRules maxRules = baseMapper.selectOne(maxWrapper);
+            int orderNum = Objects.nonNull(maxRules) ? maxRules.getOrderNum() + 1 : 1;
+
+            // 3. 新增规则(仅含本次所选因子),基础信息沿用源规则
+            PtlAgreementUndwrtRules targetRules = new PtlAgreementUndwrtRules();
+            targetRules.setId(IdGenerate.nextId());
+            targetRules.setAgreementId(targetAgreementId);
+            targetRules.setStartTime(sourceRules.getStartTime());
+            targetRules.setEndTime(sourceRules.getEndTime());
+            targetRules.setInputDescription(sourceRules.getInputDescription());
+            targetRules.setOrderNum(orderNum);
+            if (anchor != null) {
+                // 与锚定规则同组展示:锚定已有 parentId 则复用;锚定无 parentId 则生成新组ID并将锚定规则归入该组
+                String groupParentId = anchor.getParentId();
+                if (StrUtil.isBlank(groupParentId)) {
+                    groupParentId = IdGenerate.nextId();
+                    LambdaUpdateWrapper<PtlAgreementUndwrtRules> anchorUpdate = new LambdaUpdateWrapper<>();
+                    anchorUpdate.eq(PtlAgreementUndwrtRules::getId, anchor.getId())
+                            .set(PtlAgreementUndwrtRules::getParentId, groupParentId);
+                    baseMapper.update(null, anchorUpdate);
+                }
+                targetRules.setParentId(groupParentId);
+            }
+            baseMapper.insert(targetRules);
+
+            // 4. 复制所选因子到新规则(同一 attrCode 仅复制一次)
+            List<PtlAgreementUndwrtRulesAttrLink> copiedLinks = new ArrayList<>();
+            Set<String> copiedCodes = new HashSet<>();
+            for (PtlAgreementUndwrtRulesAttrLink sourceLink : sourceLinks) {
+                if (!copiedCodes.add(sourceLink.getAttrCode())) {
+                    continue;
+                }
+                PtlAgreementUndwrtRulesAttrLink newLink = new PtlAgreementUndwrtRulesAttrLink();
+                newLink.setRulesId(targetRules.getId());
+                newLink.setAttrId(sourceLink.getAttrId());
+                newLink.setAttrCode(sourceLink.getAttrCode());
+                newLink.setMin(sourceLink.getMin());
+                newLink.setOperator(sourceLink.getOperator());
+                newLink.setMax(sourceLink.getMax());
+                agreementUndwrtRulesAttrLinkMapper.insert(newLink);
+                copiedLinks.add(newLink);
+            }
+
+            // 5. 生成新规则描述
+            String ruleDescription = BuildDescUtils.buildDesc(copiedLinks, undwrtRulesAttrList);
+            LambdaUpdateWrapper<PtlAgreementUndwrtRules> updateWrapper = new LambdaUpdateWrapper<>();
+            updateWrapper.eq(PtlAgreementUndwrtRules::getId, targetRules.getId())
+                    .set(PtlAgreementUndwrtRules::getRuleDescription, ruleDescription);
+            baseMapper.update(null, updateWrapper);
+        }
+        return true;
+    }
+
     @Override
     public List<AgreementRulesVo> queryListByParentId(String parentId) {
         log.info("根据父ID查询协议承保规则列表。 parentId=[{}]", parentId);

+ 1 - 2
tenant/organization/src/main/java/com/jzg/organization/service/impl/PtlSchemeServiceImpl.java

@@ -40,7 +40,6 @@ import com.jzg.organization.client.OrgClient;
 import com.jzg.organization.client.PlatformClient;
 import com.jzg.organization.mapper.*;
 import com.jzg.organization.service.*;
-import io.seata.spring.annotation.GlobalTransactional;
 import org.apache.http.util.TextUtils;
 import org.redisson.api.RedissonClient;
 import org.slf4j.Logger;
@@ -184,7 +183,7 @@ public class PtlSchemeServiceImpl extends ServiceImpl<PtlSchemeMapper, PtlScheme
                 }
             }
         }
-        //校验驾意险座位数范围不可重复
+        // 自选驾意险,校验座位数范围不可重复
         if(!ptlSchemeSaveVo.getSchemeType().equals("2")) {
             validateDrivingIntentionSeatRange(ptlSchemeInsuranceDrivingIntentionList);
         }

+ 23 - 12
tenant/organization/src/main/resources/mapper/SysUserJzgInfoMapper.xml

@@ -126,38 +126,49 @@
         order by suji.create_time desc
     </select>
     <select id="queryBJYwy" resultMap="BaseResultMap">
-        select
+
+        WITH RECURSIVE leader_tree AS (
+        SELECT id, leader_id, 1 AS depth,system_code
+        FROM sys_user_jzg_info
+        WHERE leader_id = #{leaderId}
+        AND is_delete = 0
+        UNION ALL
+        SELECT t.id, t.leader_id, lt.depth + 1,t.system_code
+        FROM sys_user_jzg_info t
+        INNER JOIN leader_tree lt ON t.leader_id = lt.id
+        WHERE t.is_delete = 0
+        AND lt.depth <![CDATA[  <  ]]> 10
+        )
+        SELECT
         suji.id,
         suji.user_id,
         suji.work_number,
         su.username,
-        su.mobile ,
+        su.mobile,
         suji.name,
         suji.dept_id,
         sd.name AS dept_name,
         suji.status,
         suji.is_enable,
         lsu.name AS leader_name,
-        ( select  aa.mobile from sys_user aa
-        where aa.id = lsu.user_id AND aa.is_delete = 0 ) AS leader_id,
-        (select re.name from sys_user_jzg_info re where re.id = suji.referrer_id) referrer_name,
+        (SELECT aa.mobile FROM sys_user aa
+        WHERE aa.id = lsu.user_id AND aa.is_delete = 0) AS leader_id,
+        (SELECT re.name FROM sys_user_jzg_info re WHERE re.id = suji.referrer_id) AS referrer_name,
         suji.referrer_id,
         suji.create_time AS createTime,
         suji.create_by AS createBy
-        from
+        FROM
         sys_user_jzg_info suji
-        left join sys_user su on su.id = suji.user_id AND su.is_delete = 0
-        LEFT JOIN sys_user_jzg_info lsu on lsu.id = suji.leader_id AND lsu.is_delete = 0
-        left join sys_dept sd on sd.id = suji.dept_id AND sd.is_delete = 0
+        INNER JOIN leader_tree lt ON suji.id = lt.id
+        LEFT JOIN sys_user su ON su.id = suji.user_id AND su.is_delete = 0
+        LEFT JOIN sys_user_jzg_info lsu ON lsu.id = suji.leader_id AND lsu.is_delete = 0
+        LEFT JOIN sys_dept sd ON sd.id = suji.dept_id AND sd.is_delete = 0
         LEFT JOIN sys_dept d ON suji.dept_id = d.id AND d.is_delete = 0
         <where>
             suji.is_delete = 0
             and suji.id  in (
             select sujis.sys_user_jzg_info_id  from sys_user_jzg_info_salesman sujis  where  sujis.type_attr != 2 AND sujis.type_attr != 3
             )
-            <if test="leaderId != null and leaderId != ''">
-                and suji.leader_id = #{leaderId}
-            </if>
             <if test="sysUserInfoListVo.deptId != null and sysUserInfoListVo.deptId != ''">
                 and d.route LIKE CONCAT('%', #{sysUserInfoListVo.deptId}, '%')
             </if>

+ 82 - 7
协议承保规则接口对接文档.md

@@ -320,15 +320,87 @@
 
 ---
 
+## 八、批量复制规则因子【新增】
+
+**接口路径**:`POST /agreementRules/rulesAttrBatchCopy`
+
+### 功能说明
+
+将源承保规则下的某条或多条规则因子批量复制到某个或多个目标承保协议下:每个目标协议下 **新增一条规则**承接所选因子,并与该协议下已有规则共享
+`parentId`(前端按组合并成一条展示);目标协议下没有规则时,新增的规则独立展示。
+
+### 请求参数
+
+| 字段名    | 类型                | 必填 | 描述                                                                                                      |
+|-----------|---------------------|------|-----------------------------------------------------------------------------------------------------------|
+| `attrIds` | Array\<String\>     | 是   | 要复制的规则因子关联记录ID列表(来源于`getRuleInfo`返回明细的`id`字段),源规则由所选因子记录所属规则确定 |
+| `targets` | Array\<CopyTarget\> | 是   | 目标协议规则列表                                                                                          |
+
+### CopyTarget 结构
+
+| 字段名        | 类型   | 必填 | 描述                                                                                                                                |
+|---------------|--------|------|-------------------------------------------------------------------------------------------------------------------------------------|
+| `agreementId` | String | 是   | 目标承保协议ID                                                                                                                      |
+| `ruleId`      | String | 否   | 归组锚定规则ID:复制出的新规则与该规则共享 `parentId`(同组展示);为空时取该协议下第一条规则作为锚定,协议下无规则则新规则独立展示 |
+
+### 请求体示例
+
+```json
+{
+   "attrIds": [
+      "1692100000000000001",
+      "1692100000000000002"
+   ],
+   "targets": [
+      {
+         "agreementId": "123456",
+         "ruleId": "987655"
+      },
+      {
+         "agreementId": "123457"
+      }
+   ]
+}
+```
+
+### 业务逻辑说明
+
+1. **源规则确定**:源规则由 `attrIds` 所选因子记录所属规则反推;所选因子必须属于同一规则或同一批量规则组,否则返回失败
+2. **因子校验**:`attrIds` 中的ID必须全部有效(因子关联记录ID可通过`getRuleInfo`接口获取),存在无效ID时返回失败
+3. **锚定规则定位(用于确定归组)**:
+   - 指定 `ruleId`:校验该规则存在且属于对应协议,作为归组锚定
+   - 未指定 `ruleId`:取该协议下第一条规则(`orderNum` 最小)作为锚定
+   - 协议下无规则:无锚定,新增规则独立展示
+4. **新增规则与归组(每个目标协议均新增一条)**:
+   - 每个目标协议下新增一条规则承接所选因子(仅含本次所选 `attrIds` 因子),基础信息(生效时间、录入描述)沿用源规则,
+     `orderNum` 取该协议下最大序号 + 1
+   - 锚定规则存在时:新规则与锚定规则共享 `parentId`(前端按组合并展示);锚定规则自身无 `parentId` 时,生成新组ID并将锚定规则归入该组
+   - 锚定规则不存在(协议下无规则)时:新规则 `parentId` 为空,独立展示
+   - **源规则保持不变**(不修改源规则的 `parentId`、不复制源规则本身,因此不会出现"目标下多出源规则")
+5. **因子复制**:将所选因子直接插入新规则(新规则为全新记录,不覆盖任何已有因子);同一 `attrCode` 仅复制一条(批量组内不同子规则存在相同因子时去重)
+6. 复制完成后重新生成新规则的规则描述(`ruleDescription`)
+
+### 响应
+
+```json
+{
+   "code": 200,
+   "msg": "批量复制规则因子成功"
+}
+```
+
+---
+
 ## 变更总结
 
-| 接口 | 变更点 | 变更内容 |
-|---|---|---|
-| `POST /rulesAdd` | 请求参数新增 | `rulesGroups`(批量规则分组),支持一次创建多条规则 |
-| `POST /rulesAdd` | 逻辑变更 | 批量模式下所有子规则共享同一 `parentId`,按分组顺序生成 `orderNum` |
-| `GET /rulesList` | 返回值新增 | `parentId`(父规则ID) |
-| `GET /rulesGet` | 返回值新增 | `parentId`(父规则ID) |
-| `GET /rulesListByParentId` | **新增接口** | 根据父ID查询子规则列表 |
+| 接口                       | 变更点       | 变更内容                                                                                                   |
+|----------------------------|--------------|------------------------------------------------------------------------------------------------------------|
+| `POST /rulesAdd`           | 请求参数新增 | `rulesGroups`(批量规则分组),支持一次创建多条规则                                                        |
+| `POST /rulesAdd`           | 逻辑变更     | 批量模式下所有子规则共享同一 `parentId`,按分组顺序生成 `orderNum`                                         |
+| `GET /rulesList`           | 返回值新增   | `parentId`(父规则ID)                                                                                     |
+| `GET /rulesGet`            | 返回值新增   | `parentId`(父规则ID)                                                                                     |
+| `GET /rulesListByParentId` | **新增接口** | 根据父ID查询子规则列表                                                                                     |
+| `POST /rulesAttrBatchCopy` | **新增接口** | 批量复制规则因子到目标协议:每个目标协议下新增一条规则(仅含所选因子)并与已有规则同组展示,源规则保持不变 |
 
 ### 业务逻辑说明
 
@@ -337,6 +409,9 @@
 - 批量模式与单条模式互斥,优先处理 `rulesGroups`
 - 通过 `rulesListByParentId` 接口可按 `parentId` 查询同批次的所有子规则
 - 修改接口仅支持对单条规则进行编辑
+- `rulesAttrBatchCopy` 支持跨协议批量复制规则因子;源规则由所选因子(`attrIds`
+  )反推,所选因子须属于同一规则或同一规则组;每个目标协议下新增一条规则(仅含所选因子)并与该协议已有规则共享 `parentId`
+  (同组合并展示),源规则保持不变
 
 ### 数据库变更说明