|
@@ -0,0 +1,270 @@
|
|
|
|
|
+package com.adx.tencent.report;
|
|
|
|
|
+
|
|
|
|
|
+import javax.sql.DataSource;
|
|
|
|
|
+import java.sql.Connection;
|
|
|
|
|
+import java.sql.PreparedStatement;
|
|
|
|
|
+import java.sql.ResultSet;
|
|
|
|
|
+import java.sql.Timestamp;
|
|
|
|
|
+import java.time.Instant;
|
|
|
|
|
+import java.time.LocalDate;
|
|
|
|
|
+import java.time.YearMonth;
|
|
|
|
|
+import java.time.ZoneId;
|
|
|
|
|
+import java.util.ArrayList;
|
|
|
|
|
+import java.util.List;
|
|
|
|
|
+import java.util.Locale;
|
|
|
|
|
+
|
|
|
|
|
+public class AdBidReportStore {
|
|
|
|
|
+
|
|
|
|
|
+ private static final ZoneId ZONE = ZoneId.of("Asia/Shanghai");
|
|
|
|
|
+
|
|
|
|
|
+ private final DataSource dataSource;
|
|
|
|
|
+ private final String mainSchema;
|
|
|
|
|
+ private final String reportSchema;
|
|
|
|
|
+ private final int retentionDays;
|
|
|
|
|
+ private final int archiveBatchSize;
|
|
|
|
|
+ private final int archiveMaxBatchesPerRun;
|
|
|
|
|
+ private final int statsLookbackDays;
|
|
|
|
|
+
|
|
|
|
|
+ public AdBidReportStore(DataSource dataSource,
|
|
|
|
|
+ String mainSchema,
|
|
|
|
|
+ String reportSchema,
|
|
|
|
|
+ int retentionDays,
|
|
|
|
|
+ int archiveBatchSize,
|
|
|
|
|
+ int archiveMaxBatchesPerRun,
|
|
|
|
|
+ int statsLookbackDays) {
|
|
|
|
|
+ this.dataSource = dataSource;
|
|
|
|
|
+ this.mainSchema = safeIdentifier(mainSchema);
|
|
|
|
|
+ this.reportSchema = safeIdentifier(reportSchema == null || reportSchema.isBlank() ? "adx_report" : reportSchema);
|
|
|
|
|
+ this.retentionDays = Math.max(retentionDays, 1);
|
|
|
|
|
+ this.archiveBatchSize = Math.max(archiveBatchSize, 100);
|
|
|
|
|
+ this.archiveMaxBatchesPerRun = Math.max(archiveMaxBatchesPerRun, 1);
|
|
|
|
|
+ this.statsLookbackDays = Math.max(statsLookbackDays, 1);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public void migrate() {
|
|
|
|
|
+ try (Connection conn = dataSource.getConnection();
|
|
|
|
|
+ PreparedStatement createDb = conn.prepareStatement(
|
|
|
|
|
+ "CREATE DATABASE IF NOT EXISTS `" + reportSchema + "` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci")) {
|
|
|
|
|
+ createDb.execute();
|
|
|
|
|
+ for (MediaTable media : MediaTable.values()) {
|
|
|
|
|
+ try (PreparedStatement ps = conn.prepareStatement(createDailyStatsDdl(media))) {
|
|
|
|
|
+ ps.execute();
|
|
|
|
|
+ }
|
|
|
|
|
+ ensureDailyStatsColumns(conn, media);
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ throw new RuntimeException("ad bid report migrate failed", e);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public RunSummary runOnce() {
|
|
|
|
|
+ RunSummary summary = new RunSummary();
|
|
|
|
|
+ Timestamp cutoff = Timestamp.from(Instant.now().minusSeconds(retentionDays * 24L * 3600L));
|
|
|
|
|
+ try (Connection conn = dataSource.getConnection()) {
|
|
|
|
|
+ conn.setAutoCommit(false);
|
|
|
|
|
+ for (MediaTable media : MediaTable.values()) {
|
|
|
|
|
+ summary.archivedRows += archiveMedia(conn, media, cutoff);
|
|
|
|
|
+ summary.statsRows += rollupStats(conn, media);
|
|
|
|
|
+ }
|
|
|
|
|
+ conn.commit();
|
|
|
|
|
+ return summary;
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ throw new RuntimeException("ad bid report run failed", e);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private long archiveMedia(Connection conn, MediaTable media, Timestamp cutoff) throws Exception {
|
|
|
|
|
+ List<YearMonth> months = listArchivableMonths(conn, media, cutoff);
|
|
|
|
|
+ long archived = 0L;
|
|
|
|
|
+ for (YearMonth month : months) {
|
|
|
|
|
+ ensureHistoryTable(conn, media, month);
|
|
|
|
|
+ Timestamp monthStart = Timestamp.valueOf(month.atDay(1).atStartOfDay());
|
|
|
|
|
+ Timestamp monthEnd = Timestamp.valueOf(month.plusMonths(1).atDay(1).atStartOfDay());
|
|
|
|
|
+ for (int i = 0; i < archiveMaxBatchesPerRun; i++) {
|
|
|
|
|
+ int inserted = insertHistoryBatch(conn, media, month, monthStart, monthEnd, cutoff);
|
|
|
|
|
+ int deleted = deleteHotBatch(conn, media, monthStart, monthEnd, cutoff);
|
|
|
|
|
+ if (deleted > 0) {
|
|
|
|
|
+ archived += deleted;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (inserted == 0 && deleted == 0) {
|
|
|
|
|
+ break;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return archived;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private List<YearMonth> listArchivableMonths(Connection conn, MediaTable media, Timestamp cutoff) throws Exception {
|
|
|
|
|
+ String sql = "SELECT DATE_FORMAT(created_at, '%Y%m') AS ym FROM `" + mainSchema + "`.`" + media.hotTable + "` " +
|
|
|
|
|
+ "WHERE created_at < ? GROUP BY ym ORDER BY ym ASC";
|
|
|
|
|
+ List<YearMonth> result = new ArrayList<>();
|
|
|
|
|
+ try (PreparedStatement ps = conn.prepareStatement(sql)) {
|
|
|
|
|
+ ps.setTimestamp(1, cutoff);
|
|
|
|
|
+ try (ResultSet rs = ps.executeQuery()) {
|
|
|
|
|
+ while (rs.next()) {
|
|
|
|
|
+ String ym = rs.getString("ym");
|
|
|
|
|
+ if (ym != null && ym.length() == 6) {
|
|
|
|
|
+ result.add(YearMonth.of(Integer.parseInt(ym.substring(0, 4)), Integer.parseInt(ym.substring(4, 6))));
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return result;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private void ensureHistoryTable(Connection conn, MediaTable media, YearMonth month) throws Exception {
|
|
|
|
|
+ String table = historyTable(media, month);
|
|
|
|
|
+ String sql = "CREATE TABLE IF NOT EXISTS `" + reportSchema + "`.`" + table + "` LIKE `" +
|
|
|
|
|
+ mainSchema + "`.`" + media.hotTable + "`";
|
|
|
|
|
+ try (PreparedStatement ps = conn.prepareStatement(sql)) {
|
|
|
|
|
+ ps.execute();
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private int insertHistoryBatch(Connection conn,
|
|
|
|
|
+ MediaTable media,
|
|
|
|
|
+ YearMonth month,
|
|
|
|
|
+ Timestamp monthStart,
|
|
|
|
|
+ Timestamp monthEnd,
|
|
|
|
|
+ Timestamp cutoff) throws Exception {
|
|
|
|
|
+ String table = historyTable(media, month);
|
|
|
|
|
+ String sql = "INSERT IGNORE INTO `" + reportSchema + "`.`" + table + "` " +
|
|
|
|
|
+ "SELECT * FROM `" + mainSchema + "`.`" + media.hotTable + "` " +
|
|
|
|
|
+ "WHERE created_at >= ? AND created_at < ? AND created_at < ? " +
|
|
|
|
|
+ "ORDER BY created_at ASC LIMIT ?";
|
|
|
|
|
+ try (PreparedStatement ps = conn.prepareStatement(sql)) {
|
|
|
|
|
+ ps.setTimestamp(1, monthStart);
|
|
|
|
|
+ ps.setTimestamp(2, monthEnd);
|
|
|
|
|
+ ps.setTimestamp(3, cutoff);
|
|
|
|
|
+ ps.setInt(4, archiveBatchSize);
|
|
|
|
|
+ return ps.executeUpdate();
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private int deleteHotBatch(Connection conn,
|
|
|
|
|
+ MediaTable media,
|
|
|
|
|
+ Timestamp monthStart,
|
|
|
|
|
+ Timestamp monthEnd,
|
|
|
|
|
+ Timestamp cutoff) throws Exception {
|
|
|
|
|
+ String sql = "DELETE FROM `" + mainSchema + "`.`" + media.hotTable + "` " +
|
|
|
|
|
+ "WHERE created_at >= ? AND created_at < ? AND created_at < ? " +
|
|
|
|
|
+ "ORDER BY created_at ASC LIMIT ?";
|
|
|
|
|
+ try (PreparedStatement ps = conn.prepareStatement(sql)) {
|
|
|
|
|
+ ps.setTimestamp(1, monthStart);
|
|
|
|
|
+ ps.setTimestamp(2, monthEnd);
|
|
|
|
|
+ ps.setTimestamp(3, cutoff);
|
|
|
|
|
+ ps.setInt(4, archiveBatchSize);
|
|
|
|
|
+ return ps.executeUpdate();
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private long rollupStats(Connection conn, MediaTable media) throws Exception {
|
|
|
|
|
+ LocalDate endExclusiveDate = LocalDate.now(ZONE);
|
|
|
|
|
+ LocalDate startDate = endExclusiveDate.minusDays(statsLookbackDays);
|
|
|
|
|
+ Timestamp start = Timestamp.valueOf(startDate.atStartOfDay());
|
|
|
|
|
+ Timestamp end = Timestamp.valueOf(endExclusiveDate.atStartOfDay());
|
|
|
|
|
+ String sql = "INSERT INTO `" + reportSchema + "`.`" + media.statsTable + "` " +
|
|
|
|
|
+ "(stat_date, platform, account_id, tag_id, event_type, bid_count, impression_count, click_count, unique_media_trace_count, price_sum, created_at, updated_at) " +
|
|
|
|
|
+ "SELECT DATE(created_at) AS stat_date, " +
|
|
|
|
|
+ "COALESCE(platform, '') AS platform, " +
|
|
|
|
|
+ "COALESCE(account_id, '') AS account_id, " +
|
|
|
|
|
+ "COALESCE(tag_id, '') AS tag_id, " +
|
|
|
|
|
+ "COALESCE(event_type, '') AS event_type, " +
|
|
|
|
|
+ "COUNT(*) AS bid_count, " +
|
|
|
|
|
+ "SUM(CASE WHEN COALESCE(event_type, '') = 'impression' THEN 1 ELSE 0 END) AS impression_count, " +
|
|
|
|
|
+ "SUM(CASE WHEN COALESCE(event_type, '') = 'click' THEN 1 ELSE 0 END) AS click_count, " +
|
|
|
|
|
+ "COUNT(DISTINCT CASE WHEN media_trace_id IS NULL OR media_trace_id = '' THEN NULL ELSE media_trace_id END) AS unique_media_trace_count, " +
|
|
|
|
|
+ "SUM(CASE WHEN COALESCE(price, 0) > 0 THEN 1 ELSE 0 END) AS price_sum, " +
|
|
|
|
|
+ "CURRENT_TIMESTAMP(3), CURRENT_TIMESTAMP(3) " +
|
|
|
|
|
+ "FROM `" + mainSchema + "`.`" + media.hotTable + "` " +
|
|
|
|
|
+ "WHERE created_at >= ? AND created_at < ? " +
|
|
|
|
|
+ "GROUP BY DATE(created_at), COALESCE(platform, ''), COALESCE(account_id, ''), COALESCE(tag_id, ''), COALESCE(event_type, '') " +
|
|
|
|
|
+ "ON DUPLICATE KEY UPDATE " +
|
|
|
|
|
+ "bid_count = VALUES(bid_count), " +
|
|
|
|
|
+ "impression_count = VALUES(impression_count), " +
|
|
|
|
|
+ "click_count = VALUES(click_count), " +
|
|
|
|
|
+ "unique_media_trace_count = VALUES(unique_media_trace_count), " +
|
|
|
|
|
+ "price_sum = VALUES(price_sum), " +
|
|
|
|
|
+ "updated_at = VALUES(updated_at)";
|
|
|
|
|
+ try (PreparedStatement ps = conn.prepareStatement(sql)) {
|
|
|
|
|
+ ps.setTimestamp(1, start);
|
|
|
|
|
+ ps.setTimestamp(2, end);
|
|
|
|
|
+ return ps.executeUpdate();
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private String historyTable(MediaTable media, YearMonth month) {
|
|
|
|
|
+ return media.historyPrefix + month.format(java.time.format.DateTimeFormatter.ofPattern("yyyyMM", Locale.ROOT));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private String createDailyStatsDdl(MediaTable media) {
|
|
|
|
|
+ return "CREATE TABLE IF NOT EXISTS `" + reportSchema + "`.`" + media.statsTable + "` (" +
|
|
|
|
|
+ "id BIGINT AUTO_INCREMENT PRIMARY KEY," +
|
|
|
|
|
+ "stat_date DATE NOT NULL COMMENT '统计日期'," +
|
|
|
|
|
+ "platform VARCHAR(16) NOT NULL DEFAULT '' COMMENT '平台(android/ios)'," +
|
|
|
|
|
+ "account_id VARCHAR(128) NOT NULL DEFAULT '' COMMENT '账户ID'," +
|
|
|
|
|
+ "tag_id VARCHAR(128) NOT NULL DEFAULT '' COMMENT '百度广告位ID'," +
|
|
|
|
|
+ "event_type VARCHAR(32) NOT NULL DEFAULT '' COMMENT '事件类型(impression/click)'," +
|
|
|
|
|
+ "bid_count BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '竞价事件数'," +
|
|
|
|
|
+ "impression_count BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '曝光事件数'," +
|
|
|
|
|
+ "click_count BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '点击事件数'," +
|
|
|
|
|
+ "unique_media_trace_count BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '去重媒体追踪ID数'," +
|
|
|
|
|
+ "price_sum BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT 'price>0记录数'," +
|
|
|
|
|
+ "created_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间'," +
|
|
|
|
|
+ "updated_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间'," +
|
|
|
|
|
+ "UNIQUE KEY `uk_" + media.statsTable + "_dims` (stat_date, platform, account_id, tag_id, event_type)," +
|
|
|
|
|
+ "KEY `idx_" + media.statsTable + "_tag_date` (tag_id, stat_date)," +
|
|
|
|
|
+ "KEY `idx_" + media.statsTable + "_account_date` (account_id, stat_date)" +
|
|
|
|
|
+ ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='" + media.comment + "日汇总统计'";
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private void ensureDailyStatsColumns(Connection conn, MediaTable media) {
|
|
|
|
|
+ tryAlter(conn, "ALTER TABLE `" + reportSchema + "`.`" + media.statsTable + "` " +
|
|
|
|
|
+ "ADD COLUMN impression_count BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '曝光事件数' AFTER bid_count");
|
|
|
|
|
+ tryAlter(conn, "ALTER TABLE `" + reportSchema + "`.`" + media.statsTable + "` " +
|
|
|
|
|
+ "ADD COLUMN click_count BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '点击事件数' AFTER impression_count");
|
|
|
|
|
+ tryAlter(conn, "ALTER TABLE `" + reportSchema + "`.`" + media.statsTable + "` " +
|
|
|
|
|
+ "ADD COLUMN unique_media_trace_count BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '去重媒体追踪ID数' AFTER click_count");
|
|
|
|
|
+ tryAlter(conn, "ALTER TABLE `" + reportSchema + "`.`" + media.statsTable + "` " +
|
|
|
|
|
+ "ADD COLUMN price_sum BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT 'price>0记录数' AFTER unique_media_trace_count");
|
|
|
|
|
+ tryAlter(conn, "ALTER TABLE `" + reportSchema + "`.`" + media.statsTable + "` " +
|
|
|
|
|
+ "MODIFY COLUMN price_sum BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT 'price>0记录数'");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static void tryAlter(Connection conn, String sql) {
|
|
|
|
|
+ try (PreparedStatement ps = conn.prepareStatement(sql)) {
|
|
|
|
|
+ ps.execute();
|
|
|
|
|
+ } catch (Exception ignored) {
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static String safeIdentifier(String value) {
|
|
|
|
|
+ if (value == null || !value.matches("[A-Za-z0-9_]+")) {
|
|
|
|
|
+ throw new IllegalArgumentException("invalid sql identifier: " + value);
|
|
|
|
|
+ }
|
|
|
|
|
+ return value;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public static class RunSummary {
|
|
|
|
|
+ public long archivedRows;
|
|
|
|
|
+ public long statsRows;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private enum MediaTable {
|
|
|
|
|
+ TENCENT("tencent_ad_bid_events", "tencent_ad_bid_events_history_", "tencent_ad_bid_events_daily_stats", "腾讯"),
|
|
|
|
|
+ HONOR("honor_ad_bid_events", "honor_ad_bid_events_history_", "honor_ad_bid_events_daily_stats", "荣耀"),
|
|
|
|
|
+ KUAISHOU("kuaishou_ad_bid_events", "kuaishou_ad_bid_events_history_", "kuaishou_ad_bid_events_daily_stats", "快手");
|
|
|
|
|
+
|
|
|
|
|
+ private final String hotTable;
|
|
|
|
|
+ private final String historyPrefix;
|
|
|
|
|
+ private final String statsTable;
|
|
|
|
|
+ private final String comment;
|
|
|
|
|
+
|
|
|
|
|
+ MediaTable(String hotTable, String historyPrefix, String statsTable, String comment) {
|
|
|
|
|
+ this.hotTable = hotTable;
|
|
|
|
|
+ this.historyPrefix = historyPrefix;
|
|
|
|
|
+ this.statsTable = statsTable;
|
|
|
|
|
+ this.comment = comment;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|