فهرست منبع

Merge branch 'cooperation_video_candidate_pool_improved_lld_0509' of Server/growth-manager into master

liulidong 8 ساعت پیش
والد
کامیت
e44158a808

+ 29 - 0
api-module/src/main/java/com/tzld/piaoquan/api/common/enums/contentplatform/ContentPlatformAccountTypeEnum.java

@@ -0,0 +1,29 @@
+package com.tzld.piaoquan.api.common.enums.contentplatform;
+
+import lombok.Getter;
+
+@Getter
+public enum ContentPlatformAccountTypeEnum {
+    PARTNER(1, "合作方"),
+    INTERNAL(2, "内部账号"),
+    AGENT(3, "代理商"),
+
+    other(999, "其他");
+
+    private final int val;
+    private final String description;
+
+    ContentPlatformAccountTypeEnum(int val, String description) {
+        this.val = val;
+        this.description = description;
+    }
+
+    public static ContentPlatformAccountTypeEnum from(int val) {
+        for (ContentPlatformAccountTypeEnum typeEnum : ContentPlatformAccountTypeEnum.values()) {
+            if (typeEnum.getVal() == val) {
+                return typeEnum;
+            }
+        }
+        return other;
+    }
+}

+ 18 - 0
api-module/src/main/java/com/tzld/piaoquan/api/controller/contentplatform/ContentPlatformPlanController.java

@@ -132,6 +132,12 @@ public class ContentPlatformPlanController {
         return CommonResponse.success(planService.xcxPlanList(param));
     }
 
+    @ApiOperation(value = "小程序计划列表导出")
+    @PostMapping("/xcx/export")
+    public CommonResponse<String> xcxPlanExport(@RequestBody XcxPlanListParam param) {
+        return CommonResponse.success(planService.xcxPlanExport(param));
+    }
+
     @ApiOperation(value = "小程序计划 创建/更新")
     @PostMapping("/xcx/save")
     public CommonResponse<List<XcxPlanItemVO>> xcxPlanSave(@RequestBody XcxPlanSaveParam param) {
@@ -145,12 +151,24 @@ public class ContentPlatformPlanController {
         return CommonResponse.success();
     }
 
+    @ApiOperation(value = "小程序计划 多链接生成(同 video 复制 N 份)")
+    @PostMapping("/xcx/multiLink")
+    public CommonResponse<List<XcxPlanItemVO>> xcxPlanMultiLink(@RequestBody XcxPlanMultiLinkParam param) {
+        return CommonResponse.success(planService.xcxPlanMultiLink(param));
+    }
+
     @ApiOperation(value = "获取小程序分享二维码")
     @GetMapping("/xcx/getSharePic")
     public CommonResponse<String> getXcxSharePic(@RequestParam String pageUrl) {
         return CommonResponse.success(planService.getSharePic(pageUrl));
     }
 
+    @ApiOperation(value = "小程序投流-人群包列表")
+    @GetMapping("/xcx/getAudiencePackageList")
+    public CommonResponse<List<String>> getXcxAudiencePackageList() {
+        return CommonResponse.success(planService.getXcxAudiencePackageList());
+    }
+
     @ApiOperation(value = "获取分享二维码")
     @GetMapping("/getSharePic")
     public CommonResponse<String> getSharePic(@RequestParam String pageUrl) {

+ 3 - 0
api-module/src/main/java/com/tzld/piaoquan/api/dao/mapper/contentplatform/ext/ContentPlatformDemandVideoMapperExt.java

@@ -49,9 +49,12 @@ public interface ContentPlatformDemandVideoMapperExt {
                                                        @Param("driveDimensionTime") String driveDimensionTime,
                                                        @Param("category") String category,
                                                        @Param("matchMethod") String matchMethod,
+                                                       @Param("crowdPackage") String crowdPackage,
                                                        @Param("limit") int limit,
                                                        @Param("excludeSelfTitle") boolean excludeSelfTitle);
 
+    List<String> selectDistinctCrowdPackages(@Param("dt") String dt, @Param("channelName") String channelName);
+
     List<ContentPlatformDemandVideo> selectActiveVideos(@Param("dt") String dt);
 
     int updateStatusByVideoId(@Param("videoId") Long videoId, @Param("dt") String dt, @Param("status") Integer status, @Param("updateTimestamp") Long updateTimestamp);

+ 3 - 0
api-module/src/main/java/com/tzld/piaoquan/api/model/param/contentplatform/VideoContentListParam.java

@@ -24,4 +24,7 @@ public class VideoContentListParam extends PageParam {
 
     @ApiModelProperty(value = "公众号名称(对应 demand.channel_level3),仅 prior/posterior 路使用,无数据时退化为渠道粒度")
     private String ghName;
+
+    @ApiModelProperty(value = "人群包(对应 demand.crowd_package),小程序投流入口使用,过滤 demand 池候选")
+    private String crowdPackage;
 }

+ 6 - 0
api-module/src/main/java/com/tzld/piaoquan/api/model/param/contentplatform/XcxPlanListParam.java

@@ -13,4 +13,10 @@ public class XcxPlanListParam extends PageParam {
     @ApiModelProperty(value = "标题")
     private String title;
 
+    @ApiModelProperty(value = "创建时间开始")
+    private Long createTimestampStart;
+
+    @ApiModelProperty(value = "创建时间截止")
+    private Long createTimestampEnd;
+
 }

+ 14 - 0
api-module/src/main/java/com/tzld/piaoquan/api/model/param/contentplatform/XcxPlanMultiLinkParam.java

@@ -0,0 +1,14 @@
+package com.tzld.piaoquan.api.model.param.contentplatform;
+
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+@Data
+public class XcxPlanMultiLinkParam {
+
+    @ApiModelProperty(value = "源计划 id")
+    private Long planId;
+
+    @ApiModelProperty(value = "生成数量 (1-200)")
+    private Integer count;
+}

+ 36 - 0
api-module/src/main/java/com/tzld/piaoquan/api/model/vo/contentplatform/XcxPlanItemExportVO.java

@@ -0,0 +1,36 @@
+package com.tzld.piaoquan.api.model.vo.contentplatform;
+
+import com.alibaba.excel.annotation.ExcelProperty;
+import com.alibaba.excel.annotation.write.style.ColumnWidth;
+import com.alibaba.excel.annotation.write.style.ContentRowHeight;
+import com.alibaba.excel.annotation.write.style.HeadRowHeight;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@ColumnWidth(25)
+@HeadRowHeight(40)
+@ContentRowHeight(25)
+public class XcxPlanItemExportVO {
+
+    @ExcelProperty("人群包")
+    private String audiencePackage;
+
+    @ExcelProperty("视频标题")
+    private String title;
+
+    @ExcelProperty("视频封面")
+    private String cover;
+
+    @ExcelProperty("推送链接")
+    private String pageUrl;
+
+    @ExcelProperty("备注")
+    private String remark;
+
+    @ExcelProperty("创建时间")
+    private String createTime;
+}

+ 6 - 0
api-module/src/main/java/com/tzld/piaoquan/api/service/contentplatform/ContentPlatformPlanService.java

@@ -99,7 +99,13 @@ public interface ContentPlatformPlanService {
 
     Page<XcxPlanItemVO> xcxPlanList(XcxPlanListParam param);
 
+    String xcxPlanExport(XcxPlanListParam param);
+
     List<XcxPlanItemVO> xcxPlanSave(XcxPlanSaveParam param);
 
+    List<XcxPlanItemVO> xcxPlanMultiLink(XcxPlanMultiLinkParam param);
+
     void xcxPlanDelete(Long id);
+
+    List<String> getXcxAudiencePackageList();
 }

+ 169 - 15
api-module/src/main/java/com/tzld/piaoquan/api/service/contentplatform/impl/ContentPlatformPlanServiceImpl.java

@@ -25,6 +25,7 @@ import com.tzld.piaoquan.api.model.vo.contentplatform.GzhPlanVideoContentItemVO;
 import com.tzld.piaoquan.api.model.vo.contentplatform.QwPlanItemExportVO;
 import com.tzld.piaoquan.api.model.vo.contentplatform.QwPlanItemVO;
 import com.tzld.piaoquan.api.model.vo.contentplatform.VideoContentItemVO;
+import com.tzld.piaoquan.api.model.vo.contentplatform.XcxPlanItemExportVO;
 import com.tzld.piaoquan.api.model.vo.contentplatform.XcxPlanItemVO;
 import com.tzld.piaoquan.api.service.CgiReplyService;
 import com.tzld.piaoquan.api.service.GhDetailService;
@@ -57,6 +58,7 @@ import org.springframework.stereotype.Service;
 import org.springframework.util.StringUtils;
 
 import java.time.LocalDate;
+import java.time.ZoneId;
 import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
 import java.util.*;
@@ -575,8 +577,8 @@ public class ContentPlatformPlanServiceImpl implements ContentPlatformPlanServic
     private void updateGhDetail(ContentPlatformGzhAccount account, Integer selectVideoType, List<Long> videoIds,
                                 Map<Long, String> videoIdTestIdMap) {
         ContentPlatformAccount loginUser = LoginUserContext.getUser();
-        // 根据 content_platform_account 的 type 判断公众号类型:type=2 内部账号 -> 内部公众号,否则 -> 外部公众号
-        Integer ghType = Objects.equals(loginUser.getType(), 2) ? GhTypeEnum.GH.type : GhTypeEnum.THIRD_PARTY_GH.type;
+        // 根据 content_platform_account 的 type 判断公众号类型:内部账号 -> 内部公众号,否则 -> 外部公众号
+        Integer ghType = Objects.equals(loginUser.getType(), ContentPlatformAccountTypeEnum.INTERNAL.getVal()) ? GhTypeEnum.GH.type : GhTypeEnum.THIRD_PARTY_GH.type;
         GhDetail ghDetail = ghDetailService.getGhDetailByGhIdType(account.getGhId(), ghType);
         GhDetailVo detailVo = new GhDetailVo();
         Integer strategyStatus = selectVideoType == 0 ? StrategyStatusEnum.DEFAULT.status : StrategyStatusEnum.STRATEGY.status;
@@ -646,6 +648,7 @@ public class ContentPlatformPlanServiceImpl implements ContentPlatformPlanServic
     /** 服务号推送 / 公众号推送 走 Daily 人群_渠道,与即转稳定数据隔离 */
     private static final String CHANNEL_NAME_GZH_DAILY = "公众号合作-Daily-自选";
     private static final String CHANNEL_NAME_QW  = "群/企微合作-稳定";
+    private static final String CHANNEL_NAME_XCX = "小程序投流-稳定";
     private static final double PRIOR_GROUP_KEEP_RATIO = 0.5;
     /** posterior 按 demand_content_id 分组后保留 total_rov 排名前 50% 的需求组,
      * 砍掉群体表现弱的需求,避免低 total_rov 的 demand 带回来的相似变体稀释结果。 */
@@ -657,10 +660,10 @@ public class ContentPlatformPlanServiceImpl implements ContentPlatformPlanServic
     /**
      * 推导 channel_name(人群_渠道) 作为 demand 池强过滤。
      * 信号优先级:
-     *   1. type 明确时按 type 映射 — 0→公众号合作-即转-稳定;1/4→公众号合作-Daily-自选;2/3→群/企微合作-稳定
+     *   1. type 明确时按 type 映射 — 0→公众号合作-即转-稳定;1/4→公众号合作-Daily-自选;2/3→群/企微合作-稳定;5→小程序投流-稳定
      *   2. type=999/null 但带 ghName(公众号参数)→ 公众号入口,映射即转稳定(与历史一致)
      *   3. 否则 null,不限 channel_name(保留原行为)
-     * type 取值: 0-自动回复(公众号入口) / 1-服务号推送 / 2-企微-社群 / 3-企微-自动回复 / 4-公众号推送 / 999-不限。
+     * type 取值: 0-自动回复(公众号入口) / 1-服务号推送 / 2-企微-社群 / 3-企微-自动回复 / 4-公众号推送 / 5-小程序投流 / 999-不限。
      * 作用:解决 crowd_segment 跨渠道客户(如 gzyhc/wxm)在企微/公众号入口下被对侧数据污染的问题。
      */
     private String resolveChannelName(VideoContentListParam param) {
@@ -669,6 +672,7 @@ public class ContentPlatformPlanServiceImpl implements ContentPlatformPlanServic
             if (type == 2 || type == 3) return CHANNEL_NAME_QW;
             if (type == 1 || type == 4) return CHANNEL_NAME_GZH_DAILY;
             if (type == 0) return CHANNEL_NAME_GZH;
+            if (type == 5) return CHANNEL_NAME_XCX;
         }
         if (StringUtils.hasText(param.getGhName())) return CHANNEL_NAME_GZH;
         return null;
@@ -980,17 +984,18 @@ public class ContentPlatformPlanServiceImpl implements ContentPlatformPlanServic
         String ghName = StringUtils.hasText(param.getGhName()) ? param.getGhName() : null;
 
         String category = StringUtils.hasText(param.getCategory()) ? param.getCategory() : null;
+        String crowdPackage = StringUtils.hasText(param.getCrowdPackage()) ? param.getCrowdPackage() : null;
         // priorScene 池新识别:demand_strategy='人群需求' AND match_method='场景已看视频'(0519+ 起,旧 demand_strategy='人群需求-场景' 已迁走)
         List<ContentPlatformDemandVideo> rows = demandVideoMapperExt.selectForRecommend(
-                dt, channelName, crowdSegment, DEMAND_STRATEGY_PRIOR, null, null, null, ghName, null, category, MATCH_METHOD_PRIOR_SCENE, limit, false);
+                dt, channelName, crowdSegment, DEMAND_STRATEGY_PRIOR, null, null, null, ghName, null, category, MATCH_METHOD_PRIOR_SCENE, crowdPackage, limit, false);
         if (ghName != null && rows.isEmpty()) {
             rows = demandVideoMapperExt.selectForRecommend(
-                    dt, channelName, crowdSegment, DEMAND_STRATEGY_PRIOR, null, null, null, null, null, category, MATCH_METHOD_PRIOR_SCENE, limit, false);
+                    dt, channelName, crowdSegment, DEMAND_STRATEGY_PRIOR, null, null, null, null, null, category, MATCH_METHOD_PRIOR_SCENE, crowdPackage, limit, false);
         }
         // 跨渠道退化:channel_name 命中但 crowd_segment 在对侧渠道下 0 行(如公众号账号切到企微入口)→ 去 crowd_segment,只按 channel_name 拉通用数据
         if (channelName != null && rows.isEmpty()) {
             rows = demandVideoMapperExt.selectForRecommend(
-                    dt, channelName, null, DEMAND_STRATEGY_PRIOR, null, null, null, null, null, category, MATCH_METHOD_PRIOR_SCENE, limit, false);
+                    dt, channelName, null, DEMAND_STRATEGY_PRIOR, null, null, null, null, null, category, MATCH_METHOD_PRIOR_SCENE, crowdPackage, limit, false);
         }
         // 1. 同 video_id 取 total_rov 最大的代表行(SQL 已排序,putIfAbsent 保留首次)
         LinkedHashMap<Long, ContentPlatformDemandVideo> bestPerVideo = new LinkedHashMap<>();
@@ -1053,16 +1058,17 @@ public class ContentPlatformPlanServiceImpl implements ContentPlatformPlanServic
         int fetchLimit = Math.max(limit * 3, DEMAND_CANDIDATE_LIMIT);
 
         String category = StringUtils.hasText(param.getCategory()) ? param.getCategory() : null;
+        String crowdPackage = StringUtils.hasText(param.getCrowdPackage()) ? param.getCrowdPackage() : null;
         List<ContentPlatformDemandVideo> rows = demandVideoMapperExt.selectForRecommend(
-                dt, channelName, crowdSegment, DEMAND_STRATEGY_PRIOR, dimension, null, null, ghName, null, category, MATCH_METHOD_PRIOR, fetchLimit, false);
+                dt, channelName, crowdSegment, DEMAND_STRATEGY_PRIOR, dimension, null, null, ghName, null, category, MATCH_METHOD_PRIOR, crowdPackage, fetchLimit, false);
 
         if (ghName != null && rows.isEmpty()) {
             rows = demandVideoMapperExt.selectForRecommend(
-                    dt, channelName, crowdSegment, DEMAND_STRATEGY_PRIOR, dimension, null, null, null, null, category, MATCH_METHOD_PRIOR, fetchLimit, false);
+                    dt, channelName, crowdSegment, DEMAND_STRATEGY_PRIOR, dimension, null, null, null, null, category, MATCH_METHOD_PRIOR, crowdPackage, fetchLimit, false);
         }
         if (channelName != null && rows.isEmpty()) {
             rows = demandVideoMapperExt.selectForRecommend(
-                    dt, channelName, null, DEMAND_STRATEGY_PRIOR, dimension, null, null, null, null, category, MATCH_METHOD_PRIOR, fetchLimit, false);
+                    dt, channelName, null, DEMAND_STRATEGY_PRIOR, dimension, null, null, null, null, category, MATCH_METHOD_PRIOR, crowdPackage, fetchLimit, false);
         }
 
         rows = rows.stream()
@@ -1131,20 +1137,21 @@ public class ContentPlatformPlanServiceImpl implements ContentPlatformPlanServic
         int fetchLimit = Math.max(limit * 3, DEMAND_CANDIDATE_LIMIT);
 
         String category = StringUtils.hasText(param.getCategory()) ? param.getCategory() : null;
+        String crowdPackage = StringUtils.hasText(param.getCrowdPackage()) ? param.getCrowdPackage() : null;
         // posterior 池加 match_method='视频库_解构特征_向量相似匹配' 兜底,防止未来上游对优质相似分量出别的 match_method 值后污染本池
         // 优质相似池:drive_dimension_time 一律不限制(含主查与退化路径),避免仅「昨日」窗口召回过少。
         List<ContentPlatformDemandVideo> rows = demandVideoMapperExt.selectForRecommend(
-                dt, channelName, crowdSegment, DEMAND_STRATEGY_POSTERIOR, null, null, null, ghName, null, category, MATCH_METHOD_PRIOR, fetchLimit, true);
+                dt, channelName, crowdSegment, DEMAND_STRATEGY_POSTERIOR, null, null, null, ghName, null, category, MATCH_METHOD_PRIOR, crowdPackage, fetchLimit, true);
 
         // 退化:该 ghName 无数据 → 退回渠道粒度
         if (ghName != null && rows.isEmpty()) {
             rows = demandVideoMapperExt.selectForRecommend(
-                    dt, channelName, crowdSegment, DEMAND_STRATEGY_POSTERIOR, null, null, null, null, null, category, MATCH_METHOD_PRIOR, fetchLimit, true);
+                    dt, channelName, crowdSegment, DEMAND_STRATEGY_POSTERIOR, null, null, null, null, null, category, MATCH_METHOD_PRIOR, crowdPackage, fetchLimit, true);
         }
         // 跨渠道退化:channel_name 命中但 crowd_segment 在对侧 0 行 → 去 crowd_segment 拉通用数据
         if (channelName != null && rows.isEmpty()) {
             rows = demandVideoMapperExt.selectForRecommend(
-                    dt, channelName, null, DEMAND_STRATEGY_POSTERIOR, null, null, null, null, null, category, MATCH_METHOD_PRIOR, fetchLimit, true);
+                    dt, channelName, null, DEMAND_STRATEGY_POSTERIOR, null, null, null, null, null, category, MATCH_METHOD_PRIOR, crowdPackage, fetchLimit, true);
         }
 
         // 近 7 日 rov 下限,与 prior 池一致(cdjh 0514 验证 ≥0.02 保留 ~54%)
@@ -2058,10 +2065,62 @@ public class ContentPlatformPlanServiceImpl implements ContentPlatformPlanServic
         return result;
     }
 
+    /**
+     * 小程序投流计划导出:复用 xcxPlanList 的 filter(audiencePackage/title/时间范围),
+     * 不传时间范围时默认仅导出当天数据,最多导出 2000 条。
+     */
+    @Override
+    public String xcxPlanExport(XcxPlanListParam param) {
+        // 未传时间窗口时,默认仅导出当天数据(本地时区 00:00 ~ 次日 00:00)
+        if (param.getCreateTimestampStart() == null && param.getCreateTimestampEnd() == null) {
+            ZoneId zone = ZoneId.systemDefault();
+            long startOfToday = LocalDate.now().atStartOfDay(zone).toInstant().toEpochMilli();
+            long startOfTomorrow = LocalDate.now().plusDays(1).atStartOfDay(zone).toInstant().toEpochMilli();
+            param.setCreateTimestampStart(startOfToday);
+            param.setCreateTimestampEnd(startOfTomorrow);
+        }
+        // 强制分页参数:最多导出 2000 条
+        param.setPageNum(1);
+        param.setPageSize(2000);
+
+        ContentPlatformAccount loginAccount = LoginUserContext.getUser();
+        List<ContentPlatformXcxPlan> planList = planMapperExt.getXcxPlanList(param,
+                loginAccount.getId(), 0, param.getPageSize());
+        List<XcxPlanItemVO> list = buildXcxPlanItemVOList(planList);
+        return generateXcxPlanExcelFile(list);
+    }
+
+    private String generateXcxPlanExcelFile(List<XcxPlanItemVO> dataList) {
+        List<XcxPlanItemExportVO> list = new ArrayList<>();
+        if (CollectionUtils.isNotEmpty(dataList)) {
+            for (XcxPlanItemVO data : dataList) {
+                XcxPlanItemExportVO vo = new XcxPlanItemExportVO();
+                vo.setAudiencePackage(data.getAudiencePackage());
+                vo.setTitle(data.getTitle());
+                vo.setCover(data.getShareCover() != null ? data.getShareCover() : data.getCover());
+                vo.setPageUrl(data.getPageUrl());
+                vo.setRemark(data.getRemark());
+                if (Objects.nonNull(data.getCreateTimestamp())) {
+                    vo.setCreateTime(DateUtil.getDateString(data.getCreateTimestamp(), "yyyy-MM-dd HH:mm:ss"));
+                }
+                list.add(vo);
+            }
+        } else {
+            list.add(new XcxPlanItemExportVO());
+        }
+        ByteArrayOutputStream out = new ByteArrayOutputStream();
+        EasyExcel.write(out, XcxPlanItemExportVO.class).sheet("小程序投流计划").doWrite(list);
+        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(out.toByteArray());
+        String fileName = "小程序投流计划_" + System.currentTimeMillis() + ".xls";
+        AliOssFileTool.saveInPublicReturnHost(byteArrayInputStream, EnumPublicBuckets.PUBBUCKET, fileName, EnumFileType.TEMP_PICTURE);
+        return CdnUtil.DOWNLOAD_CDN_URL_HOST_PICTURE + fileName;
+    }
+
     @Override
     public List<XcxPlanItemVO> xcxPlanSave(XcxPlanSaveParam param) {
         ContentPlatformAccount loginUser = LoginUserContext.getUser();
-        if (!Objects.equals(loginUser.getType(), 2)) {
+        if (!Objects.equals(loginUser.getType(), ContentPlatformAccountTypeEnum.INTERNAL.getVal())
+                && !Objects.equals(loginUser.getType(), ContentPlatformAccountTypeEnum.AGENT.getVal())) {
             throw new CommonException(ExceptionEnum.XCX_PLAN_INTERNAL_ONLY);
         }
         Long now = System.currentTimeMillis();
@@ -2079,8 +2138,10 @@ public class ContentPlatformPlanServiceImpl implements ContentPlatformPlanServic
             String rootSourceId = MessageUtil.getRootSourceId(pageUrl);
             xcxPlan.setPageUrl(pageUrl);
             xcxPlan.setRootSourceId(rootSourceId);
+            xcxPlan.setStatus(PlanStatusEnum.NORMAL.getVal());
             xcxPlan.setCreateAccountId(loginUser.getId());
             xcxPlan.setCreateTimestamp(now);
+            xcxPlan.setUpdateTimestamp(now);
             planMapperExt.insertXcxPlanReturnId(xcxPlan);
             list.add(xcxPlan);
             // 保存视频内容
@@ -2091,8 +2152,13 @@ public class ContentPlatformPlanServiceImpl implements ContentPlatformPlanServic
 
     @Override
     public void xcxPlanDelete(Long id) {
+        ContentPlatformAccount loginUser = LoginUserContext.getUser();
+        if (!Objects.equals(loginUser.getType(), ContentPlatformAccountTypeEnum.INTERNAL.getVal())
+                && !Objects.equals(loginUser.getType(), ContentPlatformAccountTypeEnum.AGENT.getVal())) {
+            throw new CommonException(ExceptionEnum.XCX_PLAN_INTERNAL_ONLY);
+        }
         ContentPlatformXcxPlan plan = xcxPlanMapper.selectByPrimaryKey(id);
-        if (Objects.isNull(plan)) {
+        if (Objects.isNull(plan) || !Objects.equals(plan.getCreateAccountId(), loginUser.getId())) {
             throw new CommonException(ExceptionEnum.XCX_PLAN_NOT_EXISTS);
         }
         plan.setStatus(PlanStatusEnum.DELETED.getVal());
@@ -2100,6 +2166,94 @@ public class ContentPlatformPlanServiceImpl implements ContentPlatformPlanServic
         xcxPlanMapper.updateByPrimaryKeySelective(plan);
     }
 
+    @Override
+    public List<XcxPlanItemVO> xcxPlanMultiLink(XcxPlanMultiLinkParam param) {
+        ContentPlatformAccount loginUser = LoginUserContext.getUser();
+        if (!Objects.equals(loginUser.getType(), ContentPlatformAccountTypeEnum.INTERNAL.getVal())
+                && !Objects.equals(loginUser.getType(), ContentPlatformAccountTypeEnum.AGENT.getVal())) {
+            throw new CommonException(ExceptionEnum.XCX_PLAN_INTERNAL_ONLY);
+        }
+        if (Objects.isNull(param.getPlanId()) || Objects.isNull(param.getCount())
+                || param.getCount() < 1 || param.getCount() > 200) {
+            throw new CommonException(ExceptionEnum.PARAM_ERROR);
+        }
+        ContentPlatformXcxPlan src = xcxPlanMapper.selectByPrimaryKey(param.getPlanId());
+        if (Objects.isNull(src) || !Objects.equals(src.getCreateAccountId(), loginUser.getId())
+                || !Objects.equals(src.getStatus(), PlanStatusEnum.NORMAL.getVal())) {
+            throw new CommonException(ExceptionEnum.XCX_PLAN_NOT_EXISTS);
+        }
+        ContentPlatformXcxPlanVideoExample videoExample = new ContentPlatformXcxPlanVideoExample();
+        videoExample.createCriteria().andPlanIdEqualTo(src.getId());
+        List<ContentPlatformXcxPlanVideo> srcVideoList = xcxPlanVideoMapper.selectByExample(videoExample);
+        if (CollectionUtils.isEmpty(srcVideoList)) {
+            throw new CommonException(ExceptionEnum.XCX_PLAN_NOT_EXISTS);
+        }
+        ContentPlatformXcxPlanVideo srcVideo = srcVideoList.get(0);
+
+        Long now = System.currentTimeMillis();
+        List<ContentPlatformXcxPlan> created = new ArrayList<>();
+        try {
+            for (int i = 0; i < param.getCount(); i++) {
+                ContentPlatformXcxPlan xcxPlan = new ContentPlatformXcxPlan();
+                xcxPlan.setAudiencePackage(src.getAudiencePackage());
+                xcxPlan.setRemark(src.getRemark());
+                Staff staff = new Staff();
+                staff.setCarrierId("wxf7261ed54f2e450e");
+                staff.setRemark(src.getRemark());
+                String pageUrl = messageAttachmentService.getPageNoCache("pages/category", "touliu", "tencent", staff,
+                        srcVideo.getVideoId(), "小程序", null, null, srcVideo.getExperimentId());
+                String rootSourceId = MessageUtil.getRootSourceId(pageUrl);
+                xcxPlan.setPageUrl(pageUrl);
+                xcxPlan.setRootSourceId(rootSourceId);
+                xcxPlan.setStatus(PlanStatusEnum.NORMAL.getVal());
+                xcxPlan.setCreateAccountId(loginUser.getId());
+                xcxPlan.setCreateTimestamp(now);
+                xcxPlan.setUpdateTimestamp(now);
+                planMapperExt.insertXcxPlanReturnId(xcxPlan);
+                created.add(xcxPlan);
+
+                XcxPlanSaveVideoParam vp = new XcxPlanSaveVideoParam();
+                vp.setVideoId(srcVideo.getVideoId());
+                vp.setTitle(srcVideo.getTitle());
+                vp.setCover(srcVideo.getCover());
+                vp.setVideo(srcVideo.getVideo());
+                vp.setExperimentId(srcVideo.getExperimentId());
+                saveXcxPlanVideo(vp, xcxPlan.getId(), loginUser.getId());
+            }
+        } catch (Exception e) {
+            log.error("xcxPlanMultiLink failed at {}/{} for planId={}, rolling back {} created plans",
+                    created.size(), param.getCount(), param.getPlanId(), created.size(), e);
+            rollbackXcxPlans(created);
+            throw new CommonException(ExceptionEnum.SYSTEM_ERROR);
+        }
+        return buildXcxPlanItemVOList(created);
+    }
+
+    private void rollbackXcxPlans(List<ContentPlatformXcxPlan> created) {
+        if (CollectionUtils.isEmpty(created)) {
+            return;
+        }
+        Long now = System.currentTimeMillis();
+        for (ContentPlatformXcxPlan plan : created) {
+            try {
+                plan.setStatus(PlanStatusEnum.DELETED.getVal());
+                plan.setUpdateTimestamp(now);
+                xcxPlanMapper.updateByPrimaryKeySelective(plan);
+            } catch (Exception ex) {
+                log.error("xcxPlanMultiLink rollback failed for planId={}", plan.getId(), ex);
+            }
+        }
+    }
+
+    @Override
+    public List<String> getXcxAudiencePackageList() {
+        String dt = demandVideoMapperExt.getMaxDt(CHANNEL_NAME_XCX);
+        if (!StringUtils.hasText(dt)) {
+            return Collections.emptyList();
+        }
+        return demandVideoMapperExt.selectDistinctCrowdPackages(dt, CHANNEL_NAME_XCX);
+    }
+
     private List<XcxPlanItemVO> buildXcxPlanItemVOList(List<ContentPlatformXcxPlan> planList) {
         if (CollectionUtils.isEmpty(planList)) {
             return null;

+ 28 - 0
api-module/src/main/resources/mapper/contentplatform/ext/ContentPlatformDemandVideoMapperExt.xml

@@ -134,6 +134,16 @@
         <if test="matchMethod != null and matchMethod != ''">
             AND match_method = #{matchMethod}
         </if>
+        <if test="crowdPackage != null and crowdPackage != ''">
+            <choose>
+                <when test='crowdPackage == "泛人群"'>
+                    AND (crowd_segment IS NULL OR crowd_segment = '' OR crowd_segment = '-' OR crowd_segment = 'null')
+                </when>
+                <otherwise>
+                    AND crowd_segment = #{crowdPackage}
+                </otherwise>
+            </choose>
+        </if>
         <if test="excludeSelfTitle">
             AND (title IS NULL OR demand_content_title IS NULL OR title &lt;&gt; demand_content_title)
         </if>
@@ -141,6 +151,24 @@
         LIMIT #{limit}
     </select>
 
+    <select id="selectDistinctCrowdPackages" resultType="java.lang.String">
+        SELECT DISTINCT
+            CASE
+                WHEN crowd_segment IS NULL
+                  OR crowd_segment = ''
+                  OR crowd_segment = '-'
+                  OR crowd_segment = 'null'
+                THEN '泛人群'
+                ELSE crowd_segment
+            END AS cp
+        FROM content_platform_demand_video
+        WHERE dt = #{dt} AND status = 1
+        <if test="channelName != null and channelName != ''">
+            AND channel_name = #{channelName}
+        </if>
+        ORDER BY cp
+    </select>
+
     <select id="selectActiveVideos" resultType="com.tzld.piaoquan.api.model.po.contentplatform.ContentPlatformDemandVideo">
         SELECT DISTINCT video_id
         FROM content_platform_demand_video

+ 13 - 13
api-module/src/main/resources/mapper/contentplatform/ext/ContentPlatformPlanMapperExt.xml

@@ -437,17 +437,17 @@
         where create_account_id = #{createAccountId}
           and status = 1
         <if test="param.audiencePackage != null and param.audiencePackage != ''">
-            and audience_package like concat('%', #{param.audiencePackage}, '%')
+            and audience_package = #{param.audiencePackage}
         </if>
         <if test="param.title != null and param.title != ''">
             and id in (select plan_id from content_platform_xcx_plan_video where title like concat('%', #{param.title},
             '%'))
         </if>
-        <if test="param.startDate != null and param.startDate != ''">
-            and create_timestamp &gt;= UNIX_TIMESTAMP(#{param.startDate}) * 1000
+        <if test="param.createTimestampStart != null">
+            and create_timestamp &gt; #{param.createTimestampStart}
         </if>
-        <if test="param.endDate != null and param.endDate != ''">
-            and create_timestamp &lt; UNIX_TIMESTAMP(DATE_ADD(#{param.endDate}, INTERVAL 1 DAY)) * 1000
+        <if test="param.createTimestampEnd != null">
+            and create_timestamp &lt; #{param.createTimestampEnd}
         </if>
     </select>
 
@@ -458,27 +458,27 @@
         where create_account_id = #{createAccountId}
         and status = 1
         <if test="param.audiencePackage != null and param.audiencePackage != ''">
-            and audience_package like concat('%', #{param.audiencePackage}, '%')
+            and audience_package = #{param.audiencePackage}
         </if>
         <if test="param.title != null and param.title != ''">
             and id in (select plan_id from content_platform_xcx_plan_video where title like concat('%', #{param.title},
             '%'))
         </if>
-        <if test="param.startDate != null and param.startDate != ''">
-            and create_timestamp &gt;= UNIX_TIMESTAMP(#{param.startDate}) * 1000
+        <if test="param.createTimestampStart != null">
+            and create_timestamp &gt; #{param.createTimestampStart}
         </if>
-        <if test="param.endDate != null and param.endDate != ''">
-            and create_timestamp &lt; UNIX_TIMESTAMP(DATE_ADD(#{param.endDate}, INTERVAL 1 DAY)) * 1000
+        <if test="param.createTimestampEnd != null">
+            and create_timestamp &lt; #{param.createTimestampEnd}
         </if>
         order by create_timestamp desc
         limit #{offset}, #{pageSize}
     </select>
 
     <insert id="insertXcxPlanReturnId" useGeneratedKeys="true" keyProperty="id">
-        insert into content_platform_xcx_plan (audience_package, page_url, root_source_id, remark, create_account_id,
-                                              create_timestamp, update_timestamp)
+        insert into content_platform_xcx_plan (audience_package, page_url, root_source_id, remark, status,
+                                              create_account_id, create_timestamp, update_timestamp)
         values (#{record.audiencePackage}, #{record.pageUrl}, #{record.rootSourceId}, #{record.remark},
-                #{record.createAccountId}, #{record.createTimestamp}, #{record.updateTimestamp})
+                #{record.status}, #{record.createAccountId}, #{record.createTimestamp}, #{record.updateTimestamp})
     </insert>
 
 </mapper>