Просмотр исходного кода

Merge remote-tracking branch 'origin/master'

lining 1 год назад
Родитель
Сommit
ffb9735436
25 измененных файлов с 562 добавлено и 172 удалено
  1. 9 4
      job-bytedance/src/main/java/cn/com/ctop/job/bytedance/controller/BytedanceController.java
  2. 2 4
      job-bytedance/src/main/java/cn/com/ctop/job/bytedance/data/mapper/CPSReportMapper.java
  3. 2 2
      job-bytedance/src/main/java/cn/com/ctop/job/bytedance/data/mapper/xml/BytedanceProductUpdateMapper.xml
  4. 6 108
      job-bytedance/src/main/java/cn/com/ctop/job/bytedance/data/mapper/xml/CPSReportMapper.xml
  5. 7 2
      job-bytedance/src/main/java/cn/com/ctop/job/bytedance/data/mapper/xml/PromoteLinkRefreshMapper.xml
  6. 1 1
      job-bytedance/src/main/java/cn/com/ctop/job/bytedance/data/mapper/xml/ReportsAdPushMapper.xml
  7. 1 1
      job-bytedance/src/main/java/cn/com/ctop/job/bytedance/data/service/ICPSReportService.java
  8. 9 11
      job-bytedance/src/main/java/cn/com/ctop/job/bytedance/data/service/impl/BytedanceAccountReportServiceImpl.java
  9. 59 31
      job-bytedance/src/main/java/cn/com/ctop/job/bytedance/data/service/impl/BytedanceProductUpdateServiceImpl.java
  10. 21 1
      job-bytedance/src/main/java/cn/com/ctop/job/bytedance/data/service/impl/BytedancePromoteRefreshServiceImpl.java
  11. 2 2
      job-bytedance/src/main/java/cn/com/ctop/job/bytedance/data/service/impl/CPSReportServiceImpl.java
  12. 56 0
      job-bytedance/src/main/java/cn/com/ctop/job/bytedance/data/utils/JsonUtils.java
  13. 2 2
      job-bytedance/src/main/java/cn/com/ctop/job/bytedance/handler/BytedanceReportRetryJob.java
  14. 2 2
      job-bytedance/src/main/java/cn/com/ctop/job/bytedance/handler/CPSReportJob.java
  15. 1 0
      job-bytedance/src/main/java/cn/com/ctop/job/bytedance/handler/PromoteLinkRefreshJob.java
  16. 16 0
      job-bytedance/src/main/java/cn/com/ctop/job/bytedance/utils/doris_sink/DorisBufferConfig.java
  17. 45 0
      job-bytedance/src/main/java/cn/com/ctop/job/bytedance/utils/doris_sink/DorisConfiguration.java
  18. 41 0
      job-bytedance/src/main/java/cn/com/ctop/job/bytedance/utils/doris_sink/DorisMultiTableStreamLoadManager.java
  19. 7 0
      job-bytedance/src/main/java/cn/com/ctop/job/bytedance/utils/doris_sink/DorisSinkClient.java
  20. 17 0
      job-bytedance/src/main/java/cn/com/ctop/job/bytedance/utils/doris_sink/RecordWithMeta.java
  21. 86 0
      job-bytedance/src/main/java/cn/com/ctop/job/bytedance/utils/doris_sink/SimpleHttpDorisSinkClient.java
  22. 38 0
      job-bytedance/src/main/java/cn/com/ctop/job/bytedance/utils/doris_sink/StrictRedirectStrategy.java
  23. 96 0
      job-bytedance/src/main/java/cn/com/ctop/job/bytedance/utils/doris_sink/TableBufferManager.java
  24. 26 0
      job-bytedance/src/main/java/cn/com/ctop/job/bytedance/utils/doris_sink/TableKey.java
  25. 10 1
      job-bytedance/src/main/resources/application.yml

+ 9 - 4
job-bytedance/src/main/java/cn/com/ctop/job/bytedance/controller/BytedanceController.java

@@ -192,7 +192,7 @@ public class BytedanceController {
     }
 
     @RequestMapping("/adq/creatives")
-    public void adqCreatives(Long accountId,String startDate,String endDate) {
+    public void adqCreatives(Long accountId, String startDate, String endDate) {
         cdpProjectService.getAdqCreatives(accountId, startDate, endDate);
     }
 
@@ -203,15 +203,20 @@ public class BytedanceController {
     }
 
     @GetMapping("/adqWechatPages")
-    public void adqWechatPages(){
+    public void adqWechatPages() {
         List<JSONObject> iids = cdpProjectService.queryPageAdvertiserIds();
         if (!Check.isNull(iids)) {
             for (JSONObject iid : iids) {
                 Long pageId = iid.getLong("page_id");
                 Long advertiserId = iid.getLong("advertiser_id");
-                dailyRetrExecutorService.submit(() -> cdpProjectService.getAdqWechatPages(advertiserId,pageId));
+                dailyRetrExecutorService.submit(() -> cdpProjectService.getAdqWechatPages(advertiserId, pageId));
             }
         }
     }
 
-}
+
+    @GetMapping("/accountHour")
+    public void bytedanceAccountHour(Long advertiserId, String accessToken, String date) {
+        accountReportService.getBytedanceAccountHour(advertiserId, accessToken, date, date, 1, 0);
+    }
+}

+ 2 - 4
job-bytedance/src/main/java/cn/com/ctop/job/bytedance/data/mapper/CPSReportMapper.java

@@ -8,12 +8,10 @@ import java.util.List;
 
 public interface CPSReportMapper {
 
-    boolean cleanCPSPayDailyReports(@Param("nowDate") Integer nowDate);
-
-    boolean cleanCPSIaaDailyReports(@Param("nowDate") Integer nowDate);
+    boolean cleanCPSPayDailyReports(@Param("nowDate") String nowDate);
 
     @DS("selectdb")
-    List<JSONObject> queryCPSIaaDaily(@Param("nowDate") Integer nowDate);
+    List<JSONObject> queryCPSIaaDaily(@Param("nowDate") Integer nowDate, @Param("dateStr") String dateStr);
 
     boolean cleanCPSIaaDailyReportsDS(@Param("list") List<JSONObject> list);
 

+ 2 - 2
job-bytedance/src/main/java/cn/com/ctop/job/bytedance/data/mapper/xml/BytedanceProductUpdateMapper.xml

@@ -29,14 +29,14 @@
         FROM deepthink.bytedance_product_main_advertiser t1
         left join deepthink.bytedance_advertiser_list t2 on t1.advertiser_id=t2.advertiser_id
         left join deepthink.bytedance_auth_list t3 on t2.admin_advertiser_id=t3.admin_advertiser_id
-        where t1.`type`=2 and t1.`_status`=0 and platform_type=1
+        where t1.`type`=2 and t1.`_status`=0 and platform_type=1 and tb1.mini_instance_id is not null
     </select>
     <select id="getIAAPlatformList" resultType="com.alibaba.fastjson.JSONObject">
         select t1.*, t3.access_token
         FROM deepthink.bytedance_product_main_advertiser t1
         left join deepthink.bytedance_advertiser_list t2 on t1.advertiser_id=t2.advertiser_id
         left join deepthink.bytedance_auth_list t3 on t2.admin_advertiser_id=t3.admin_advertiser_id
-        where t1.`type`=1 and t1.`_status`=0 and platform_type=1
+        where t1.`type`=1 and t1.`_status`=0 and platform_type=1 and tb1.mini_instance_id is not null
     </select>
     <select id="getIAPVideoList" resultType="com.alibaba.fastjson.JSONObject">
         select third.album_id, third.platform_type, detail.platform_config, video.id as video_id, detail.id as video_detail_id, video.name as name, video.cover_img as img,ifnull(category.name, '其他') as 'theme', video.play_platform,

+ 6 - 108
job-bytedance/src/main/java/cn/com/ctop/job/bytedance/data/mapper/xml/CPSReportMapper.xml

@@ -90,10 +90,7 @@
                        LEFT JOIN thirtpart.ly_official_accounts t2 ON t0.official_account_id = t2.id
               WHERE official_account_id IS NOT NULL
               GROUP BY t1.link_id
-
-
-              union all
-
+              UNION ALL
               SELECT t3.date,
                      t0.official_account_id,
                      t0.responsibility_person_id,
@@ -113,7 +110,8 @@
               FROM (SELECT DATE_FORMAT(activation_time, '%Y%m%d') AS 'date', link_id,
                            count(DISTINCT uid) AS 'new_users'
                     FROM thirtpart.ly_user_activation_info
-                    WHERE DATE_FORMAT(activation_time, '%Y%m%d') = #{nowDate}
+                    WHERE created_at >= unix_timestamp(#{nowDate})
+                      AND created_at &lt; unix_timestamp(DATE_ADD(#{nowDate}, INTERVAL 1 day))
                     GROUP BY link_id) t3
                        LEFT JOIN thirtpart.ly_link t0 ON t0.id = t3.link_id
                        LEFT JOIN (SELECT consume_date,
@@ -165,107 +163,6 @@
         GROUP BY t.id
     </insert>
 
-    <insert id="cleanCPSIaaDailyReports">
-        replace
-        into thirtpart.cps_link_data_report
-        (
-                stat_date,
-                official_account_id,
-                responsibility_person_id,
-                ma_appid,
-                link_id,
-                new_users,
-                new_pay_users,
-                new_pay_counts,
-                new_order_counts,
-                new_pay_amount,
-                new_xn_pay_amount,
-                pay_amount,
-                xn_pay_amount,
-                pay_users,pay_counts,
-                order_counts,
-                new_iaa_users,
-                new_iaa_counts,
-                new_iaa_amount,
-                iaa_users,
-                iaa_counts,
-                iaa_amount,
-				active_users
-                )
-        SELECT IFNULL(t.date, t0.date),
-               tt.official_account_id,
-               tt.responsibility_person_id,
-               tt.app_id,
-               tt.id,
-               t1.new_users,
-               t3.new_pay_users,
-               t3.new_pay_counts,
-               t3.new_order_counts,
-               t3.new_pay_amount,
-               t3.new_xn_pay_amount,
-               t3.pay_amount,
-               t3.xn_pay_amount,
-               t3.pay_users,
-               t3.pay_counts,
-               t3.order_counts,
-               t0.new_iaa_users,
-               t0.new_iaa_counts,
-               t0.new_iaa_amount,
-               t0.iaa_users,
-               t0.iaa_counts,
-               t0.iaa_amount,
-               t.active_users
-        FROM (SELECT t0.id,
-                     t0.responsibility_person_id,
-                     t0.official_account_id,
-                     t2.app_id
-              FROM thirtpart.ly_link t0
-                       LEFT JOIN thirtpart.ly_official_accounts t2 ON t0.official_account_id = t2.id) tt
-                 LEFT JOIN
-             (SELECT t.date, t.link_id, COUNT(DISTINCT t.uid) as 'active_users'
-              FROM thirtpart.ly_user_login_record t
-              where date = #{nowDate}
-                AND link_id
-                  >0
-              GROUP BY link_id) t ON tt.id = t.link_id
-                 LEFT JOIN
-             (SELECT t.stat_date as 'date', t.link_id,
-                     count(distinct case
-                                        when SUBSTR(dying_time, 1, 10) = SUBSTR(event_time, 1, 10)
-                                            then uid end) 'new_iaa_users', sum(if(SUBSTR(dying_time, 1, 10) = SUBSTR(event_time, 1, 10), 1, 0)) 'new_iaa_counts', IFNULL(ROUND(
-                             sum(if(SUBSTR(dying_time, 1, 10) = SUBSTR(event_time, 1, 10), cost / 100000, 0)),
-                             2),0.00) 'new_iaa_amount', count(distinct uid) 'iaa_users', sum(1) 'iaa_counts', IFNULL(ROUND(sum(cost / 100000), 2),0.00) 'iaa_amount'
-              FROM media_api.bytedance_webhook_ma_ecpm t
-                       LEFT JOIN thirtpart.ly_link t0 ON t.link_id = t0.id
-              where t.stat_date = #{nowDate}
-              GROUP BY t.link_id
-              ORDER BY t.link_id) t0 ON tt.id = t0.link_id
-                 LEFT JOIN (SELECT t0.id,
-                                   DATE_FORMAT(t.activation_time, '%Y%m%d') as 'date', t0.official_account_id,
-                                   t0.responsibility_person_id,
-                                   count(DISTINCT t.uid) AS 'new_users'
-                            FROM thirtpart.ly_user_activation_info t
-                                     LEFT JOIN thirtpart.ly_link t0 ON t.link_id = t0.id
-                            WHERE DATE_FORMAT(t.activation_time, '%Y%m%d') = #{nowDate}
-                            GROUP BY t0.id) t1 ON tt.id = t1.id
-                 LEFT JOIN
-             (SELECT link_id,
-                     new_pay_users,
-                     new_pay_counts,
-                     new_order_counts,
-                     new_pay_amount,
-                     new_xn_pay_amount,
-                     pay_amount,
-                     xn_pay_amount,
-                     pay_users,
-                     pay_counts,
-                     order_counts
-              FROM thirtpart.cps_link_data_report
-              WHERE stat_date = #{nowDate}) t3 ON t3.link_id = tt.id
-        WHERE t0.iaa_amount is not null
-        GROUP BY tt.id
-    </insert>
-
     <select id="queryCPSIaaDaily" resultType="com.alibaba.fastjson.JSONObject">
         SELECT IFNULL(t.date, t0.date) AS 'date', tt.official_account_id,
                tt.responsibility_person_id,
@@ -294,7 +191,7 @@
                        LEFT JOIN tidb.thirtpart.ly_official_accounts t2 ON t0.official_account_id = t2.id) tt
                  LEFT JOIN (SELECT t.date, t.link_id, COUNT(DISTINCT t.uid) AS 'active_users'
                             FROM tidb.thirtpart.ly_user_login_record t
-                            WHERE date = #{nowDate} AND link_id > 0
+                            WHERE date = #{dateStr} AND link_id > 0
                             GROUP BY link_id, date) t ON tt.id = t.link_id
                  LEFT JOIN (SELECT t.stat_date AS 'date', t.link_id,
                                    count(DISTINCT CASE
@@ -313,7 +210,8 @@
                                    count(DISTINCT t.uid) AS 'new_users'
                             FROM tidb.thirtpart.ly_user_activation_info t
                                      LEFT JOIN tidb.thirtpart.ly_link t0 ON t.link_id = t0.id
-                            WHERE DATE_FORMAT(t.activation_time, '%Y%m%d') = #{nowDate}
+                            WHERE t.created_at >= unix_timestamp(#{nowDate})
+                              AND t.created_at &lt; unix_timestamp(DATE_ADD(#{nowDate}, INTERVAL 1 day))
                             GROUP BY 1, 2, 3, 4) t1 ON tt.id = t1.id
                  LEFT JOIN (SELECT link_id,
                                    new_pay_users,

+ 7 - 2
job-bytedance/src/main/java/cn/com/ctop/job/bytedance/data/mapper/xml/PromoteLinkRefreshMapper.xml

@@ -10,8 +10,11 @@
 
 
     <select id="refreshList" resultType="com.alibaba.fastjson.JSONObject">
-        select tb1.task_id, tb1.advertiser_id, tb1.promoter_id, tb1.link_uuid, tb4.expire_time, tb2.media_type, tb3.app_plat_type,
-        tb3.movie_id as video_id, tb5.id as video_detail_id, tb1.promoter_uri, tb2.app_id, tb6.id as applet_id
+        select tb1.task_id, tb1.advertiser_id, tb1.promoter_id, tb1.link_uuid, tb4.expire_time,
+        tb2.media_type, tb3.app_plat_type,
+        tb3.movie_id as video_id, tb5.id as video_detail_id,
+        tb1.promoter_uri, tb2.app_id, tb6.id as applet_id,
+        tb5.platform_config, tb5.sort, tb7.album_id
         from
         (select *
         from deepthink.bytedance_advertiser_task_link
@@ -26,6 +29,8 @@
         on tb3.movie_id = tb5.video_id and tb3.entry_chapter_count = tb5.sort
         left join thirtpart.ly_applet tb6
         on tb2.app_id = tb6.app_id
+        left join thirtpart.ly_third_videos tb7
+        on tb3.movie_id = tb7.video_id
         where tb4.expire_time &lt;= ${expireTime}
     </select>
 </mapper>

+ 1 - 1
job-bytedance/src/main/java/cn/com/ctop/job/bytedance/data/mapper/xml/ReportsAdPushMapper.xml

@@ -449,7 +449,7 @@
         on a.customer_user_name2=b.nick_name
         left join tidb.deepthink.sys_dept c
         on b.dept_id=c.dept_id
-        where stat_datetime=${date} and b.dept_id=${deptId}
+        where stat_datetime=${date} and b.dept_id=${deptId} and b.status=0 and b.del_flag=0
     </select>
     <select id="allBusinessPushData" resultType="java.util.Map">
         select

+ 1 - 1
job-bytedance/src/main/java/cn/com/ctop/job/bytedance/data/service/ICPSReportService.java

@@ -2,5 +2,5 @@ package cn.com.ctop.job.bytedance.data.service;
 
 public interface ICPSReportService {
 
-    void cleanCPSDailyReports(Integer nowDate);
+    void cleanCPSDailyReports(String nowDate);
 }

+ 9 - 11
job-bytedance/src/main/java/cn/com/ctop/job/bytedance/data/service/impl/BytedanceAccountReportServiceImpl.java

@@ -7,6 +7,8 @@ import cn.com.ctop.job.bytedance.data.utils.AlertUtils;
 import cn.com.ctop.job.bytedance.data.utils.Check;
 import cn.com.ctop.job.bytedance.data.utils.FieldsConfig;
 import cn.com.ctop.job.bytedance.data.utils.HttpUtils;
+import cn.com.ctop.job.bytedance.utils.doris_sink.DorisMultiTableStreamLoadManager;
+import cn.com.ctop.job.bytedance.utils.doris_sink.RecordWithMeta;
 import com.alibaba.fastjson.JSONArray;
 import com.alibaba.fastjson.JSONObject;
 import lombok.extern.slf4j.Slf4j;
@@ -24,15 +26,10 @@ import java.util.*;
 @EnableScheduling
 public class BytedanceAccountReportServiceImpl implements IBytedanceAccountReportService {
     @Autowired
-    private BytedanceAccountReportMapper accountReportMapper;
-
-    /*private static final int MAX_BATCH_SIZE = 5000;
-    private static final long MAX_WAIT_TIME_MS = 2 * 60 * 1000;
+    private DorisMultiTableStreamLoadManager dorisLoadManager;
 
-    private final List<BytedanceAdvertiserReportHour> advertiserReportHourBuffer = Collections.synchronizedList(new ArrayList<>());
-    private final List<BytedanceAdvertiserReportDaily> advertiserReportDailyBuffer = Collections.synchronizedList(new ArrayList<>());
-    private long advertiserReportHourLastFlushTime = System.currentTimeMillis();
-    private long advertiserReportDailyLastFlushTime = System.currentTimeMillis();*/
+    @Autowired
+    private BytedanceAccountReportMapper accountReportMapper;
 
 
     @Override
@@ -231,6 +228,8 @@ public class BytedanceAccountReportServiceImpl implements IBytedanceAccountRepor
                             advertiserReportHour.setStatTimeDay(statTimeDay);
                             advertiserReportHour.setHour(hour);
                             hours.add(advertiserReportHour);
+
+                            dorisLoadManager.add(new RecordWithMeta("media_api","bytedance_advertiser_report_hour",advertiserReportHour));
                         }
                     } else {
                         log.info("账户id:{},日期:{}广告时报(账户粒度)请求结果为空", advertiserId, startDate);
@@ -253,8 +252,7 @@ public class BytedanceAccountReportServiceImpl implements IBytedanceAccountRepor
             }
             if (!Check.isNull(hours)) {
                 accountReportMapper.replaceAccountReportHour(hours);
-                accountReportMapper.replaceAccountReportHourSD(hours);
-                //advertiserReportHourBuffer.addAll(hours);
+                //accountReportMapper.replaceAccountReportHourSD(hours);
             }
             if (hours.size() >= 100) {
                 try {
@@ -350,7 +348,7 @@ public class BytedanceAccountReportServiceImpl implements IBytedanceAccountRepor
                 } catch (InterruptedException e) {
                     throw new RuntimeException(e);
                 }
-                getBytedanceAccountHour(advertiserId, accessToken, startDate, endDate, page + 1, isLog);
+                getBytedanceAccountDaily(advertiserId, accessToken, startDate, endDate, page + 1, isLog);
             } else {
                 log.info("账户ID:{},日期:{},广告日报(账户粒度)数据请求成功", advertiserId, startDate);
             }

+ 59 - 31
job-bytedance/src/main/java/cn/com/ctop/job/bytedance/data/service/impl/BytedanceProductUpdateServiceImpl.java

@@ -35,7 +35,7 @@ public class BytedanceProductUpdateServiceImpl implements IBytedanceProductUpdat
     public void run() throws Exception {
         Set<String> errors = new HashSet<>();
         log.info("付费商品库更新中...");
-        iapProductUpdate(errors);
+//        iapProductUpdate(errors);
         log.info("付费商品库更新完成");
         log.info("免费商品库更新中...");
         iaaProductUpdate(errors);
@@ -76,18 +76,20 @@ public class BytedanceProductUpdateServiceImpl implements IBytedanceProductUpdat
                     String res = HttpUtils.httpPostRequest(url, body, headers);
                     System.out.println("授权结果: " + res);
                     JSONObject resObj = JSONObject.parseObject(res);
-                    int code = resObj.getIntValue("code");
+                    int code = resObj.getIntValue("err_no");
                     if (code != 0){
                         log.error("授权失败,code: " + code + ", msg: " + resObj.getString("message") + ", 社会信用代码: " + socialCreditCode);
                     }else {
                         JSONObject data = resObj.getJSONObject("data");
                         if (data.getBooleanValue("authed")){
-                            log.info("授权成功,社会信用代码: " + socialCreditCode + ", 企业名称: " + obj.getJSONArray("authed_corps").toJSONString());
+                            log.info("授权成功,社会信用代码: " + socialCreditCode + ", 企业名称: " + data.getJSONArray("authed_corps").toJSONString());
                             bytedanceProductUpdateMapper.updateMovieAuthStatus(obj.getString("platform_id"));
                         }
                     }
+                    Thread.sleep(4000);
                 }
             }catch (Exception e){
+                e.printStackTrace();
                 log.error("获取字节小程序access_token失败", e);
             }
         }
@@ -102,7 +104,7 @@ public class BytedanceProductUpdateServiceImpl implements IBytedanceProductUpdat
         List<JSONObject> iapPlatformList = bytedanceProductUpdateMapper.getIAPPlatformList();
         List<JSONObject> iaaPlatformList = bytedanceProductUpdateMapper.getIAAPlatformList();
         initNew(iaaPlatformList);
-        initNew(iapPlatformList);
+//        initNew(iapPlatformList);
         authMovieResource();
     }
 
@@ -146,18 +148,18 @@ public class BytedanceProductUpdateServiceImpl implements IBytedanceProductUpdat
 
             String res = HttpUtils.httpPostRequest(createLinkUrl, requestBody, headers);
             if (res == null || res.isEmpty()){
-                System.out.println("创建字节小程序失败");
-                return;
+                System.out.println("创建字节小程序失败,advertiser_id:"+advertiser_id);
             }else {
                 JSONObject jsonObject = JSONObject.parseObject(res);
+                System.out.println("创建字节小程序响应: " + jsonObject.toJSONString());
                 int code = jsonObject.getIntValue("code");
                 if (code != 0) {
-                    System.out.println("创建字节小程序失败,code: " + code + ", msg: " + jsonObject.getString("message"));
-                    break;
+                    System.out.println("创建字节小程序失败,advertiser_id: "+advertiser_id+",code: " + code + ", msg: " + jsonObject.getString("message"));
                 }else {
                     String instanceId = jsonObject.getJSONObject("data").getString("instance_id");
                     //更新表
                     bytedanceProductUpdateMapper.updateInstanceId(advertiser_id, instanceId);
+                    System.out.println("创建字节小程序成功,广告主ID: " + advertiser_id + ", 实例ID: " + instanceId);
                 }
             }
         }
@@ -178,11 +180,6 @@ public class BytedanceProductUpdateServiceImpl implements IBytedanceProductUpdat
             String advertiserId = obj.getString("advertiser_id");
             String accessToken = obj.getString("access_token");
             String instanceId = obj.getString("mini_instance_id");
-            if (instanceId == null){
-                errorBlanket.add("没有找到字节小程序的实例ID: " + advertiserId);
-                System.out.println("没有找到字节小程序的实例ID: " + advertiserId);
-                continue;
-            }
             List<JSONObject> iapVideoDouyinList = iapVideoList.stream().filter(obj1 -> "1".equals(obj1.getString("platform_type"))).collect(Collectors.toList());
             Map<String, String> albumIdToLinkMap = getBytedancePlayletPromoteLink(advertiserId, accessToken, instanceId, iapVideoDouyinList, errorBlanket);
             advertiserToLinkMap.put(advertiserId, albumIdToLinkMap);
@@ -271,15 +268,35 @@ public class BytedanceProductUpdateServiceImpl implements IBytedanceProductUpdat
         Map<String, String> linkObj = advertiserToLinkMap.get(platform.getString("advertiser_id"));
         String play_platform = album.getString("play_platform");
         List<String> ad_carrier = new ArrayList<>();
-        if (linkObj != null){
-            profession.put("micro_app_link", linkObj.get(album_id));
+//        if (linkObj != null){
+//            profession.put("micro_app_link", linkObj.get(album_id));
+//            ad_carrier.add("字节小程序");
+//            if (play_platform.contains("1")){
+//                if (bytedanceProductUpdateMapper.getWeixinAuditStatus(album.getLong("video_id"))>0){
+//                    ad_carrier.add("微信小程序");
+//                }
+//            }
+//        }else {
+//            profession.put("album_id", album_id);
+//            ad_carrier.add("微信小程序");
+//        }
+        Integer is_bytedance = platform.getInteger("is_bytedance");
+        Integer is_wechat = platform.getInteger("is_wechat");
+        Integer is_dys = platform.getInteger("is_dys");
+        if (is_bytedance == 1){
             ad_carrier.add("字节小程序");
-            if (play_platform.contains("1")){
-                ad_carrier.add("微信小程序");
-            }
+        }
+        if (is_wechat == 1){
+            ad_carrier.add("微信小程序");
+        }
+        if (is_dys == 1){
+            ad_carrier.add("端原生组件");
+        }
+
+        if (is_bytedance == 1){
+            profession.put("micro_app_link", linkObj.get(album_id));
         }else {
             profession.put("album_id", album_id);
-            ad_carrier.add("微信小程序");
         }
         profession.put("ad_carrier", JSON.toJSONString(ad_carrier));
 
@@ -315,11 +332,7 @@ public class BytedanceProductUpdateServiceImpl implements IBytedanceProductUpdat
             String advertiserId = obj.getString("advertiser_id");
             String accessToken = obj.getString("access_token");
             String instanceId = obj.getString("mini_instance_id");
-            if (instanceId == null){
-                errorBlanket.add("没有找到字节小程序的实例ID: " + advertiserId);
-                System.out.println("没有找到字节小程序的实例ID: " + advertiserId);
-                continue; //todo
-            }
+
             List<JSONObject> iaaVideoDouyinList = iaaVideoList.stream().filter(obj1 -> "1".equals(obj1.getString("platform_type"))).collect(Collectors.toList());
             Map<String, String> albumIdToLinkMap = getBytedancePlayletPromoteLink(advertiserId, accessToken, instanceId, iaaVideoDouyinList, errorBlanket);
             advertiserToLinkMap.put(advertiserId, albumIdToLinkMap);
@@ -387,18 +400,33 @@ public class BytedanceProductUpdateServiceImpl implements IBytedanceProductUpdat
         profession.put("membership_types", "5");
         profession.put("starting_unlock_episode", album.getString("starting_unlock_episode"));
         Map<String, String> linkObj = advertiserToLinkMap.get(platform.getString("advertiser_id"));
-        String play_platform = album.getString("play_platform");
         List<String> ad_carrier = new ArrayList<>();
-        if (linkObj != null){
-            profession.put("micro_app_link", linkObj.get(album_id));
+//        if (linkObj != null){
+//            profession.put("micro_app_link", linkObj.get(album_id));
+//            ad_carrier.add("字节小程序");
+//        }else {
+//            profession.put("album_id", album_id);
+//            ad_carrier.add("微信小程序");
+//        }
+        Integer is_bytedance = platform.getInteger("is_bytedance");
+        Integer is_wechat = platform.getInteger("is_wechat");
+        Integer is_dys = platform.getInteger("is_dys");
+        if (is_bytedance == 1){
             ad_carrier.add("字节小程序");
-            if (play_platform.contains("1")){
-                ad_carrier.add("微信小程序");
-            }
+        }
+        if (is_wechat == 1){
+            ad_carrier.add("微信小程序");
+        }
+        if (is_dys == 1){
+            ad_carrier.add("端原生组件");
+        }
+
+        if (is_bytedance == 1){
+            profession.put("micro_app_link", linkObj.get(album_id));
         }else {
             profession.put("album_id", album_id);
-            ad_carrier.add("微信小程序");
         }
+
         profession.put("ad_carrier", JSON.toJSONString(ad_carrier));
 
         product_info.put("profession", profession);

+ 21 - 1
job-bytedance/src/main/java/cn/com/ctop/job/bytedance/data/service/impl/BytedancePromoteRefreshServiceImpl.java

@@ -8,6 +8,7 @@ import cn.com.ctop.job.bytedance.utils.FeishuRobotUtil;
 import com.alibaba.fastjson.JSON;
 import com.alibaba.fastjson.JSONObject;
 import io.netty.util.internal.StringUtil;
+import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.lang.StringUtils;
 import org.springframework.data.redis.core.RedisTemplate;
 import org.springframework.stereotype.Service;
@@ -19,6 +20,7 @@ import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 
+@Slf4j
 @Service
 public class BytedancePromoteRefreshServiceImpl implements IBytedancePromoteRefreshService {
     @Resource
@@ -134,6 +136,20 @@ public class BytedancePromoteRefreshServiceImpl implements IBytedancePromoteRefr
                 }
                 String token = String.valueOf(tokenobj);
 
+                String serialNo = linkInfo.getString("sort");
+                String albumId = linkInfo.getString("album_id");
+                String episodeId = "";
+                try {
+                    String platform_config = linkInfo.getString("platform_config");
+                    if (!Check.isNull(platform_config)){
+                        JSONObject platformConfigJson = JSON.parseObject(platform_config);
+                        if (!platformConfigJson.containsKey("douyinyun")){
+                        }
+                        episodeId = platformConfigJson.getJSONObject("douyinyun").getString("episode_id");
+                    }
+                }catch (Exception e){
+                    log.error("解析平台配置失败: " + e.getMessage(), e);
+                }
                 //douyin
                 String path = promoterUri.split("\\?")[0];
                 String url = "https://open.douyin.com/api/apps/v1/url_link/generate/";
@@ -141,8 +157,12 @@ public class BytedancePromoteRefreshServiceImpl implements IBytedancePromoteRefr
                 query.put("link_id", String.valueOf(linkId));
                 query.put("video_id", String.valueOf(videoId));
                 query.put("video_detail_id", String.valueOf(videoDetailId));
-                query.put("type", String.valueOf(mediaType));
+                query.put("type", "1");
                 query.put("source", "link");
+                query.put("serialNo", serialNo);
+                query.put("app_id", appId);
+                query.put("tt_album_id", albumId);
+                query.put("tt_episode_id", episodeId);
                 Map<String, Object> params = new HashMap<>();
                 params.put("app_id", appId);
                 params.put("app_name", "douyin");

+ 2 - 2
job-bytedance/src/main/java/cn/com/ctop/job/bytedance/data/service/impl/CPSReportServiceImpl.java

@@ -13,10 +13,10 @@ public class CPSReportServiceImpl implements ICPSReportService {
     private CPSReportMapper cpsReportMapper;
 
     @Override
-    public void cleanCPSDailyReports(Integer nowDate) {
+    public void cleanCPSDailyReports(String nowDate) {
         try {
             if (cpsReportMapper.cleanCPSPayDailyReports(nowDate))
-                if (cpsReportMapper.cleanCPSIaaDailyReportsDS(cpsReportMapper.queryCPSIaaDaily(nowDate)))
+                if (cpsReportMapper.cleanCPSIaaDailyReportsDS(cpsReportMapper.queryCPSIaaDaily(Integer.valueOf(nowDate), nowDate)))
                     log.info("CPS数据清洗完成:{}", nowDate);
         } catch (Exception e) {
             e.printStackTrace();

+ 56 - 0
job-bytedance/src/main/java/cn/com/ctop/job/bytedance/data/utils/JsonUtils.java

@@ -0,0 +1,56 @@
+package cn.com.ctop.job.bytedance.data.utils;
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.PropertyNamingStrategy;
+import com.alibaba.fastjson.serializer.NameFilter;
+import com.alibaba.fastjson.serializer.SerializeConfig;
+import com.alibaba.fastjson.serializer.SerializerFeature;
+
+import java.util.regex.Pattern;
+
+public class JsonUtils {
+
+    // 正则预编译
+    private static final Pattern INVALID_PREFIX = Pattern.compile("^[^a-zA-Z0-9]+");
+    private static final Pattern INVALID_SUFFIX = Pattern.compile("[^a-zA-Z0-9]+$");
+    private static final Pattern LETTER_NUMBER = Pattern.compile("([a-zA-Z])([0-9])");
+    //private static final Pattern NUMBER_LETTER = Pattern.compile("([0-9])([a-zA-Z])");
+    private static final Pattern CAMEL_CASE = Pattern.compile("([a-z])([A-Z])");
+
+    // 全局 SerializeConfig
+    private static final SerializeConfig SNAKE_CASE_CONFIG = new SerializeConfig();
+
+    static {
+        SNAKE_CASE_CONFIG.propertyNamingStrategy = PropertyNamingStrategy.SnakeCase;
+    }
+
+    // NameFilter
+    private static final NameFilter CAMEL_TO_SNAKE_FILTER = (object, name, value) -> {
+        // 去除非法首尾字符
+        name = INVALID_PREFIX.matcher(name).replaceAll("");
+        name = INVALID_SUFFIX.matcher(name).replaceAll("");
+
+        // a3 -> a_3,3a -> 3_a
+        name = LETTER_NUMBER.matcher(name).replaceAll("$1_$2");
+        //name = NUMBER_LETTER.matcher(name).replaceAll("$1_$2");
+
+        // camelCase -> camel_case
+        name = CAMEL_CASE.matcher(name).replaceAll("$1_$2");
+
+        return name.toLowerCase();
+    };
+
+    /**
+     * 将对象转为 JSON 字符串,字段名转为下划线命名。
+     */
+    public static String toSnakeCaseJsonIgnoreNull(Object obj) {
+        return JSON.toJSONString(obj, SNAKE_CASE_CONFIG);
+    }
+
+    /**
+     * 将对象转为 JSON 字符串,字段名转为下划线命名
+     */
+    public static String toFilterJSON(Object obj) {
+        return JSON.toJSONString(obj, CAMEL_TO_SNAKE_FILTER, SerializerFeature.WriteMapNullValue, SerializerFeature.MapSortField);
+    }
+}

+ 2 - 2
job-bytedance/src/main/java/cn/com/ctop/job/bytedance/handler/BytedanceReportRetryJob.java

@@ -204,8 +204,6 @@ public class BytedanceReportRetryJob {
                 String dataDimension = jsonObject.getString("dataDimension");
                 Integer isSend = jsonObject.getInteger("isSend");
                 String accessToken = jsonObject.getString("accessToken");
-                Long taskId = jsonObject.getJSONObject("msg").getLong("taskId");
-
                 if ("xingtu_task_list".equalsIgnoreCase(dataDimension)) {
                     xingTuExecutorService.submit(new Runnable() {
                         @Override
@@ -214,6 +212,7 @@ public class BytedanceReportRetryJob {
                         }
                     });
                 } else if ("xingtu_task_info".equalsIgnoreCase(dataDimension)) {
+                    Long taskId = jsonObject.getJSONObject("msg").getLong("taskId");
                     xingTuExecutorService.submit(new Runnable() {
                         @Override
                         public void run() {
@@ -221,6 +220,7 @@ public class BytedanceReportRetryJob {
                         }
                     });
                 } else if ("xingtu_task_report".equalsIgnoreCase(dataDimension)) {
+                    Long taskId = jsonObject.getJSONObject("msg").getLong("taskId");
                     xingTuExecutorService.submit(new Runnable() {
                         @Override
                         public void run() {

+ 2 - 2
job-bytedance/src/main/java/cn/com/ctop/job/bytedance/handler/CPSReportJob.java

@@ -26,7 +26,7 @@ public class CPSReportJob {
         log.info("[dayCleanCPSDailyReports] 任务启动...");
         try {
             String nowDate = DateUtils.getNowDate("yyyyMMdd");
-            cpsReportService.cleanCPSDailyReports(Integer.valueOf(nowDate));
+            cpsReportService.cleanCPSDailyReports(nowDate);
             log.info("[dayCleanCPSDailyReports] 任务完成");
         } catch (Exception e) {
             e.printStackTrace();
@@ -43,7 +43,7 @@ public class CPSReportJob {
         try {
             String nowDate = DateUtils.getNowDate("yyyyMMdd");
             String yesterday = DateUtils.getNowAddDate("yyyyMMdd", -1);
-            cpsReportService.cleanCPSDailyReports(Integer.valueOf(yesterday));
+            cpsReportService.cleanCPSDailyReports(yesterday);
             log.info("[yesCleanCPSDailyReports] 任务完成");
         } catch (Exception e) {
             e.printStackTrace();

+ 1 - 0
job-bytedance/src/main/java/cn/com/ctop/job/bytedance/handler/PromoteLinkRefreshJob.java

@@ -43,6 +43,7 @@ public class PromoteLinkRefreshJob {
                 try {
                     JSONObject response = bytedancePromoteRefreshService.refreshLink(obj);
                     if (response.getInteger("code") != 0) {
+                        log.error("刷新链接失败: " + response.getString("msg"));
                         return response.getString("msg");
                     }
                 } catch (Exception e) {

+ 16 - 0
job-bytedance/src/main/java/cn/com/ctop/job/bytedance/utils/doris_sink/DorisBufferConfig.java

@@ -0,0 +1,16 @@
+package cn.com.ctop.job.bytedance.utils.doris_sink;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class DorisBufferConfig {
+    private long bufferFlushMaxRows;
+    private long bufferFlushMaxBytes;
+    private long bufferFlushIntervalMs;
+}

+ 45 - 0
job-bytedance/src/main/java/cn/com/ctop/job/bytedance/utils/doris_sink/DorisConfiguration.java

@@ -0,0 +1,45 @@
+package cn.com.ctop.job.bytedance.utils.doris_sink;
+
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+public class DorisConfiguration {
+    @Value("${spring.doris.fe-host}")
+    private String feHost;
+
+    @Value("${spring.doris.user}")
+    private String userName;
+
+    @Value("${spring.doris.password}")
+    private String password;
+
+    @Value("${spring.doris.label-prefix}")
+    private String labelPrefix;
+
+    @Value("${spring.doris.sink-threads}")
+    private int uploaderThreads;
+
+    @Value("${spring.doris.sink-batch-rows}")
+    private int flushMaxRows;
+
+    @Value("${spring.doris.sink-batch-mb}")
+    private long flushMaxMb;
+
+    @Value("${spring.doris.sink-batch-seconds}")
+    private long flushIntervalSeconds;
+
+    @Bean
+    public DorisMultiTableStreamLoadManager dorisStreamLoadManager() {
+        DorisBufferConfig defaultConfig = DorisBufferConfig.builder()
+                .bufferFlushMaxRows(flushMaxRows)
+                .bufferFlushMaxBytes(flushMaxMb * 1024 * 1024)
+                .bufferFlushIntervalMs(flushIntervalSeconds * 1000)
+                .build();
+
+        DorisSinkClient sinkClient = new SimpleHttpDorisSinkClient(feHost, userName, password, labelPrefix);
+
+        return new DorisMultiTableStreamLoadManager(uploaderThreads, defaultConfig, sinkClient);
+    }
+}

+ 41 - 0
job-bytedance/src/main/java/cn/com/ctop/job/bytedance/utils/doris_sink/DorisMultiTableStreamLoadManager.java

@@ -0,0 +1,41 @@
+package cn.com.ctop.job.bytedance.utils.doris_sink;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.scheduling.concurrent.CustomizableThreadFactory;
+
+import javax.annotation.PreDestroy;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+@Slf4j
+public class DorisMultiTableStreamLoadManager {
+    private final Map<TableKey, TableBufferManager> bufferManagers = new ConcurrentHashMap<>();
+    private final ExecutorService uploaderExecutor;
+    private final DorisSinkClient sinkClient;
+    private final DorisBufferConfig defaultBufferConfig;
+
+    public DorisMultiTableStreamLoadManager(int uploaderThreads,
+                                             DorisBufferConfig defaultBufferConfig,
+                                             DorisSinkClient sinkClient) {
+        log.info("init DorisMultiTableStreamLoadManager");
+        this.defaultBufferConfig = defaultBufferConfig;
+        this.sinkClient = sinkClient;
+        this.uploaderExecutor = Executors.newFixedThreadPool(uploaderThreads,
+                new CustomizableThreadFactory("doris-stream-uploader-"));
+    }
+
+    public void add(RecordWithMeta record) {
+        TableKey key = new TableKey(record.getDatabase(), record.getTable());
+        bufferManagers.computeIfAbsent(key, k -> new TableBufferManager(k, defaultBufferConfig, sinkClient, uploaderExecutor))
+                .add(record);
+    }
+
+    @PreDestroy
+    public void shutdown() {
+        log.info("Shutting down DorisMultiTableStreamLoadManager");
+        bufferManagers.values().forEach(TableBufferManager::flushNow);
+        uploaderExecutor.shutdown();
+    }
+}

+ 7 - 0
job-bytedance/src/main/java/cn/com/ctop/job/bytedance/utils/doris_sink/DorisSinkClient.java

@@ -0,0 +1,7 @@
+package cn.com.ctop.job.bytedance.utils.doris_sink;
+
+import java.util.List;
+@FunctionalInterface
+public interface DorisSinkClient {
+    void streamLoad(List<RecordWithMeta> batch) throws Exception;
+}

+ 17 - 0
job-bytedance/src/main/java/cn/com/ctop/job/bytedance/utils/doris_sink/RecordWithMeta.java

@@ -0,0 +1,17 @@
+package cn.com.ctop.job.bytedance.utils.doris_sink;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+
+@Data
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+public class RecordWithMeta {
+    private String database;
+    private String table;
+    private Object data;
+}

+ 86 - 0
job-bytedance/src/main/java/cn/com/ctop/job/bytedance/utils/doris_sink/SimpleHttpDorisSinkClient.java

@@ -0,0 +1,86 @@
+package cn.com.ctop.job.bytedance.utils.doris_sink;
+
+import cn.com.ctop.job.bytedance.data.utils.JsonUtils;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.http.HttpEntity;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpPut;
+import org.apache.http.entity.StringEntity;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClients;
+import org.apache.http.util.EntityUtils;
+
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
+import java.util.List;
+import java.util.UUID;
+import java.util.stream.Collectors;
+
+@Slf4j
+public class SimpleHttpDorisSinkClient implements DorisSinkClient {
+    private final String feHost;      // http://fe_host:8030
+    private final String user;        // doris 用户名
+    private final String password;    // doris 密码
+    private final String format;      // json/csv, 推荐 json
+    private final String labelPrefix; // stream load label 前缀(可选)
+
+    public SimpleHttpDorisSinkClient(String feHost, String user, String password, String labelPrefix) {
+        this.feHost = feHost;
+        this.user = user;
+        this.password = password;
+        this.format = "json";
+        this.labelPrefix = labelPrefix;
+    }
+
+    @Override
+    public void streamLoad(List<RecordWithMeta> records) throws Exception {
+        if (records == null || records.isEmpty()) return;
+
+        RecordWithMeta sample = records.get(0);
+        String db = sample.getDatabase();
+        String table = sample.getTable();
+
+        String label = labelPrefix + db + "_" + table + "_" + UUID.randomUUID();
+        String url = String.format("%s/api/%s/%s/_stream_load", feHost, db, table);
+
+        // 提取 data
+        List<Object> dataList = records.stream()
+                .map(RecordWithMeta::getData)
+                .collect(Collectors.toList());
+
+        String jsonBody = JsonUtils.toFilterJSON(dataList);
+        //log.info("jsonBody:{}", jsonBody);
+
+        HttpPut put = new HttpPut(url);
+        put.setHeader("Expect", "100-continue");
+        put.setHeader("label", label);
+        put.setHeader("Content-Type", "application/json");
+        if (user != null && password != null) {
+            String auth = Base64.getEncoder().encodeToString((user + ":" + password).getBytes(StandardCharsets.UTF_8));
+            put.setHeader("Authorization", "Basic " + auth);
+        }
+
+        put.setHeader("format", format); // json
+        put.setHeader("strip_outer_array", "true"); // json array
+        put.setHeader("fuzzy_parse", "true"); // json array
+
+        put.setEntity(new StringEntity(jsonBody, StandardCharsets.UTF_8));
+
+        try (CloseableHttpClient httpClient = HttpClients.custom()
+                .setRedirectStrategy(new StrictRedirectStrategy())  // 支持 307/301 重定向
+                .build();
+             CloseableHttpResponse response = httpClient.execute(put)) {
+            int code = response.getStatusLine().getStatusCode();
+            HttpEntity entity = response.getEntity();
+            String responseBody = EntityUtils.toString(entity, "UTF-8");
+            if (code != 200 && code != 201) {
+                throw new RuntimeException("Doris stream load failed. Status: " + code + ", response: " + responseBody);
+            }
+            // 可选: 解析返回内容判断 success
+            if (!responseBody.contains("\"Status\": \"Success\"")) {
+                throw new RuntimeException("Stream load returned non-success: " + responseBody);
+            }
+            log.info("Doris stream load success: {}", responseBody);
+        }
+    }
+}

+ 38 - 0
job-bytedance/src/main/java/cn/com/ctop/job/bytedance/utils/doris_sink/StrictRedirectStrategy.java

@@ -0,0 +1,38 @@
+package cn.com.ctop.job.bytedance.utils.doris_sink;
+
+import org.apache.http.HttpEntityEnclosingRequest;
+import org.apache.http.HttpRequest;
+import org.apache.http.HttpResponse;
+import org.apache.http.ProtocolException;
+import org.apache.http.client.methods.HttpPut;
+import org.apache.http.client.methods.HttpUriRequest;
+import org.apache.http.impl.client.DefaultRedirectStrategy;
+import org.apache.http.protocol.HttpContext;
+
+import java.net.URI;
+
+public class StrictRedirectStrategy extends DefaultRedirectStrategy {
+
+    @Override
+    protected boolean isRedirectable(String method) {
+        // 允许 GET, POST, PUT 等都可重定向
+        return true;
+    }
+
+    @Override
+    public HttpUriRequest getRedirect(HttpRequest request, HttpResponse response, HttpContext context)
+            throws ProtocolException {
+        URI uri = getLocationURI(request, response, context);
+
+        String method = request.getRequestLine().getMethod();
+        if (method.equalsIgnoreCase(HttpPut.METHOD_NAME)) {
+            HttpPut put = new HttpPut(uri);
+            put.setHeaders(request.getAllHeaders());
+            if (request instanceof HttpEntityEnclosingRequest) {
+                put.setEntity(((HttpEntityEnclosingRequest) request).getEntity());
+            }
+            return put;
+        }
+        return super.getRedirect(request, response, context);
+    }
+}

+ 96 - 0
job-bytedance/src/main/java/cn/com/ctop/job/bytedance/utils/doris_sink/TableBufferManager.java

@@ -0,0 +1,96 @@
+package cn.com.ctop.job.bytedance.utils.doris_sink;
+
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.scheduling.concurrent.CustomizableThreadFactory;
+
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicLong;
+
+@Slf4j
+public class TableBufferManager {
+
+    private final TableKey tableKey;
+    private final DorisBufferConfig config;
+    private final DorisSinkClient sinkClient;
+    private final ExecutorService uploaderExecutor;
+
+    private final List<RecordWithMeta> buffer = new ArrayList<>();
+    private final Object lock = new Object();
+    private final AtomicLong currentBytes = new AtomicLong(0);
+
+    private final ScheduledExecutorService scheduler =
+            Executors.newSingleThreadScheduledExecutor(new CustomizableThreadFactory("flusher-" + UUID.randomUUID()));
+
+    public TableBufferManager(TableKey tableKey,
+                              DorisBufferConfig config,
+                              DorisSinkClient sinkClient,
+                              ExecutorService uploaderExecutor) {
+        this.tableKey = tableKey;
+        this.config = config;
+        this.sinkClient = sinkClient;
+        this.uploaderExecutor = uploaderExecutor;
+
+        scheduler.scheduleAtFixedRate(this::tryFlush,
+                config.getBufferFlushIntervalMs(),
+                config.getBufferFlushIntervalMs(),
+                TimeUnit.MILLISECONDS);
+    }
+
+    public void add(RecordWithMeta record) {
+        if (record == null || StringUtils.isBlank(record.getDatabase()) || StringUtils.isBlank(record.getTable())) {
+            return;
+        }
+        synchronized (lock) {
+            buffer.add(record);
+            currentBytes.addAndGet(estimateSize(record));
+
+            if (buffer.size() >= config.getBufferFlushMaxRows() ||
+                    currentBytes.get() >= config.getBufferFlushMaxBytes()) {
+                flush();
+            }
+        }
+    }
+
+    public void tryFlush() {
+        synchronized (lock) {
+            if (!buffer.isEmpty()) {
+                flush();
+            }
+        }
+    }
+
+    public void flushNow() {
+        synchronized (lock) {
+            flush();
+        }
+        scheduler.shutdownNow();
+    }
+
+    private void flush() {
+        if (buffer.isEmpty()) return;
+
+        List<RecordWithMeta> toSend = new ArrayList<>(buffer);
+        buffer.clear();
+        currentBytes.set(0);
+
+        uploaderExecutor.submit(() -> {
+            try {
+                sinkClient.streamLoad(toSend);
+            } catch (Exception e) {
+                log.error("Stream load failed for {}.{}: {}", tableKey.getDatabase(), tableKey.getTable(), e.getMessage(), e);
+            }
+        });
+    }
+
+    private long estimateSize(RecordWithMeta record) {
+        return record.toString().getBytes(StandardCharsets.UTF_8).length;
+    }
+}

+ 26 - 0
job-bytedance/src/main/java/cn/com/ctop/job/bytedance/utils/doris_sink/TableKey.java

@@ -0,0 +1,26 @@
+package cn.com.ctop.job.bytedance.utils.doris_sink;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+
+import java.util.Objects;
+
+@Data
+@AllArgsConstructor
+public class TableKey {
+    private String database;
+    private String table;
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(database, table);
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) return true;
+        if (!(obj instanceof TableKey)) return false;
+        TableKey other = (TableKey) obj;
+        return Objects.equals(database, other.database) && Objects.equals(table, other.table);
+    }
+}

+ 10 - 1
job-bytedance/src/main/resources/application.yml

@@ -85,7 +85,7 @@ spring:
         master:
           url: jdbc:mysql://172.16.0.117:3390/media_api?characterEncoding=UTF-8&useUnicode=true&useSSL=false
           #url: jdbc:mysql://8.147.135.80:3390/media_api?characterEncoding=UTF-8&useUnicode=true&allowMultiQueries=true&useSSL=false
-          username: data
+          username: yanfa
           password: hr@2025..
           driver-class-name: com.mysql.jdbc.Driver
         selectdb:
@@ -134,6 +134,15 @@ spring:
       min-idle: 8
     port: 6379
 
+  doris:
+    fe-host: http://selectdb-cn-vym4at1qw04.selectdbfe.rds.aliyuncs.com:8080
+    user: data
+    password: hr@2025..
+    label-prefix: spring-bytedance
+    sink-threads: 3
+    sink-batch-rows: 50000
+    sink-batch-mb: 20
+    sink-batch-seconds: 20
 
 #mybatis plus 设置
 mybatis-plus: