TiDBColdStore.java 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573
  1. package com.adx.tencent.storage;
  2. import com.adx.tencent.storage.mapper.*;
  3. import com.adx.tencent.storage.model.*;
  4. import com.fasterxml.jackson.databind.ObjectMapper;
  5. import org.apache.ibatis.session.ExecutorType;
  6. import org.apache.ibatis.session.SqlSession;
  7. import org.apache.ibatis.session.SqlSessionFactory;
  8. import org.slf4j.Logger;
  9. import org.slf4j.LoggerFactory;
  10. import java.sql.Statement;
  11. import java.sql.Timestamp;
  12. import java.time.Instant;
  13. import java.util.ArrayList;
  14. import java.util.List;
  15. /**
  16. * 对应 Go internal/storage/tidb.go 的 TiDBColdStore。
  17. * 使用 MyBatis 操作 TiDB(MySQL 协议兼容)。
  18. * 功能:
  19. * 1. migrate() - 建表 DDL
  20. * 2. saveBid / saveTracking / saveConversion / saveMediaCallback
  21. * 3. successfulCallbackExists / successfulCallbackExistsForEvent
  22. * 4. pendingMediaCallbacks
  23. */
  24. public class TiDBColdStore {
  25. private static final Logger log = LoggerFactory.getLogger(TiDBColdStore.class);
  26. private static final int CONVERSION_BATCH_SIZE = 1000;
  27. private final SqlSessionFactory sqlSessionFactory;
  28. private final ObjectMapper objectMapper;
  29. public TiDBColdStore(SqlSessionFactory sqlSessionFactory, ObjectMapper objectMapper) {
  30. this.sqlSessionFactory = sqlSessionFactory;
  31. this.objectMapper = objectMapper;
  32. }
  33. // ─── DDL 迁移 ────────────────────────────────────────────────────────────
  34. public void migrate() {
  35. String[] ddls = {
  36. """
  37. CREATE TABLE IF NOT EXISTS tencent_ad_bid_events (
  38. id BIGINT AUTO_INCREMENT PRIMARY KEY,
  39. qk VARCHAR(128) NOT NULL,
  40. media VARCHAR(64),
  41. media_trace_id VARCHAR(255),
  42. platform VARCHAR(16),
  43. req_id VARCHAR(512),
  44. bid_id VARCHAR(128),
  45. creative_id VARCHAR(128),
  46. imp_id VARCHAR(128),
  47. tag_id VARCHAR(128),
  48. price BIGINT UNSIGNED,
  49. show_urls JSON,
  50. click_urls JSON,
  51. landing_page TEXT,
  52. app_store_link TEXT,
  53. package_name VARCHAR(255),
  54. media_params JSON,
  55. created_at TIMESTAMP(3) NOT NULL,
  56. UNIQUE KEY uk_tencent_ad_bid_events_qk (qk),
  57. KEY idx_tencent_ad_bid_events_media_trace (media, media_trace_id),
  58. KEY idx_tencent_ad_bid_events_platform_tag (platform, tag_id),
  59. KEY idx_tencent_ad_bid_events_created_at (created_at)
  60. )
  61. """,
  62. """
  63. CREATE TABLE IF NOT EXISTS tencent_tracking_reports (
  64. id BIGINT AUTO_INCREMENT PRIMARY KEY,
  65. qk VARCHAR(128),
  66. kind VARCHAR(32) NOT NULL,
  67. tracking_url TEXT NOT NULL,
  68. status INT NOT NULL,
  69. ok TINYINT(1) NOT NULL,
  70. created_at TIMESTAMP(3) NOT NULL,
  71. KEY idx_tencent_tracking_reports_qk_kind_created_at (qk, kind, created_at)
  72. )
  73. """,
  74. """
  75. CREATE TABLE IF NOT EXISTS tencent_baidu_conversions (
  76. id BIGINT AUTO_INCREMENT PRIMARY KEY,
  77. dedupe_key VARCHAR(255),
  78. qk VARCHAR(128),
  79. media VARCHAR(64),
  80. tag_id VARCHAR(128),
  81. date_value VARCHAR(16),
  82. appsid VARCHAR(128),
  83. customer_name VARCHAR(128),
  84. device_id VARCHAR(255),
  85. conv DOUBLE,
  86. payment DOUBLE,
  87. gmv DOUBLE,
  88. act INT,
  89. tu VARCHAR(128),
  90. clk_time VARCHAR(32),
  91. created_at TIMESTAMP(3) NOT NULL,
  92. UNIQUE KEY uk_tencent_baidu_conversions_dedupe (dedupe_key),
  93. KEY idx_tencent_baidu_conversions_qk_act_created_at (qk, act, created_at),
  94. KEY idx_tencent_baidu_conversions_tag_id (tag_id),
  95. KEY idx_tencent_baidu_conversions_device_id (device_id)
  96. )
  97. """,
  98. """
  99. CREATE TABLE IF NOT EXISTS tencent_media_callbacks (
  100. id BIGINT AUTO_INCREMENT PRIMARY KEY,
  101. dedupe_key VARCHAR(255),
  102. media VARCHAR(64) NOT NULL,
  103. qk VARCHAR(128),
  104. callback_url TEXT NOT NULL,
  105. event_type INT NOT NULL,
  106. event_time_ms BIGINT NOT NULL,
  107. purchase DOUBLE,
  108. status INT NOT NULL,
  109. ok TINYINT(1) NOT NULL,
  110. attempt INT NOT NULL DEFAULT 1,
  111. response_body TEXT,
  112. error_message TEXT,
  113. request_body TEXT,
  114. dispatch_status VARCHAR(32) NOT NULL DEFAULT 'SENT',
  115. tracking_version VARCHAR(16) NOT NULL DEFAULT '0',
  116. created_at TIMESTAMP(3) NOT NULL,
  117. KEY idx_tencent_media_callbacks_dedupe_ok_created_at (dedupe_key, ok, created_at),
  118. KEY idx_tencent_media_callbacks_qk_created_at (qk, created_at),
  119. KEY idx_tencent_media_callbacks_media_event_created_at (media, event_type, created_at)
  120. )
  121. """,
  122. """
  123. CREATE TABLE IF NOT EXISTS tencent_tag_event (
  124. tag_id VARCHAR(32) NOT NULL,
  125. baidu_act INT NOT NULL COMMENT '百度转化行为',
  126. tencent_action_type VARCHAR(128) NOT NULL COMMENT '腾讯回传行为',
  127. PRIMARY KEY (tag_id, baidu_act)
  128. ) COMMENT '竞价事件记录'
  129. """,
  130. """
  131. CREATE TABLE IF NOT EXISTS tencent_account_tag_event (
  132. tag_id VARCHAR(128) NOT NULL COMMENT '百度广告位ID',
  133. account_id VARCHAR(64) NOT NULL COMMENT '腾讯广告主ID',
  134. baidu_act INT NOT NULL COMMENT '百度转化行为',
  135. tencent_action_type VARCHAR(128) NOT NULL COMMENT '腾讯回传行为',
  136. tencent_custom_action VARCHAR(128) COMMENT '腾讯自定义行为(action_type=CUSTOM时使用)',
  137. created_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
  138. updated_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
  139. PRIMARY KEY (tag_id, account_id, baidu_act),
  140. KEY idx_tencent_account_tag_event_account_act (account_id, baidu_act)
  141. ) COMMENT '腾讯账户级广告位回传方式配置'
  142. """,
  143. """
  144. CREATE TABLE IF NOT EXISTS tencent_tag_event_profile (
  145. tag_id VARCHAR(128) NOT NULL COMMENT '百度广告位ID',
  146. callback_profile VARCHAR(64) NOT NULL COMMENT '回传方案',
  147. baidu_act INT NOT NULL COMMENT '百度转化行为',
  148. tencent_action_type VARCHAR(128) NOT NULL COMMENT '腾讯回传行为',
  149. tencent_custom_action VARCHAR(128) COMMENT '腾讯自定义行为(action_type=CUSTOM时使用)',
  150. enabled TINYINT(1) NOT NULL DEFAULT 1 COMMENT '是否启用',
  151. created_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
  152. updated_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
  153. PRIMARY KEY (tag_id, callback_profile, baidu_act),
  154. KEY idx_tencent_tag_event_profile_profile_act (callback_profile, baidu_act)
  155. ) COMMENT '腾讯profile级广告位回传方式配置'
  156. """
  157. };
  158. try (SqlSession session = sqlSessionFactory.openSession(true)) {
  159. Statement stmt = session.getConnection().createStatement();
  160. for (String ddl : ddls) {
  161. stmt.execute(ddl);
  162. }
  163. // 字段长度补丁(已存在的表可能是旧的 128)
  164. try {
  165. stmt.execute("ALTER TABLE tencent_ad_bid_events MODIFY COLUMN req_id VARCHAR(512)");
  166. } catch (Exception ignored) {}
  167. // platform 字段补丁
  168. try {
  169. stmt.execute("ALTER TABLE tencent_ad_bid_events ADD COLUMN platform VARCHAR(16) AFTER media_trace_id");
  170. } catch (Exception ignored) {}
  171. try {
  172. stmt.execute("ALTER TABLE tencent_ad_bid_events ADD KEY idx_tencent_ad_bid_events_platform_tag (platform, tag_id)");
  173. } catch (Exception ignored) {}
  174. try {
  175. stmt.execute("ALTER TABLE tencent_baidu_conversions ADD COLUMN gmv DOUBLE AFTER payment");
  176. } catch (Exception ignored) {}
  177. try {
  178. stmt.execute("ALTER TABLE tencent_baidu_conversions ADD COLUMN tag_id VARCHAR(128) AFTER media");
  179. } catch (Exception ignored) {}
  180. try {
  181. stmt.execute("ALTER TABLE tencent_baidu_conversions ADD KEY idx_tencent_baidu_conversions_tag_id (tag_id)");
  182. } catch (Exception ignored) {}
  183. try {
  184. stmt.execute("ALTER TABLE tencent_media_callbacks ADD COLUMN request_body TEXT AFTER error_message");
  185. } catch (Exception ignored) {}
  186. try {
  187. stmt.execute("ALTER TABLE tencent_media_callbacks ADD COLUMN dispatch_status VARCHAR(32) NOT NULL DEFAULT 'SENT' AFTER request_body");
  188. } catch (Exception ignored) {}
  189. try {
  190. stmt.execute("ALTER TABLE tencent_tracking_reports ADD KEY idx_tencent_tracking_reports_created_at_id (created_at, id)");
  191. } catch (Exception ignored) {}
  192. try {
  193. stmt.execute("UPDATE tencent_media_callbacks SET dispatch_status = 'SENT' WHERE dispatch_status IS NULL OR dispatch_status = ''");
  194. } catch (Exception ignored) {}
  195. try {
  196. stmt.execute("ALTER TABLE tencent_media_callbacks ADD COLUMN tracking_version VARCHAR(16) NOT NULL DEFAULT '0' AFTER dispatch_status");
  197. } catch (Exception ignored) {}
  198. try {
  199. stmt.execute("UPDATE tencent_media_callbacks SET tracking_version = '0' WHERE tracking_version IS NULL OR tracking_version = ''");
  200. } catch (Exception ignored) {}
  201. try {
  202. stmt.execute("ALTER TABLE tencent_tag_event ADD COLUMN baidu_act INT NULL COMMENT '百度转化行为' AFTER tag_id");
  203. } catch (Exception ignored) {}
  204. try {
  205. stmt.execute("ALTER TABLE tencent_tag_event CHANGE COLUMN event_type tencent_action_type VARCHAR(128) NOT NULL COMMENT '腾讯回传行为'");
  206. } catch (Exception ignored) {}
  207. try {
  208. stmt.execute("UPDATE tencent_tag_event SET baidu_act = 0 WHERE baidu_act IS NULL");
  209. } catch (Exception ignored) {}
  210. try {
  211. stmt.execute("ALTER TABLE tencent_tag_event MODIFY COLUMN baidu_act INT NOT NULL");
  212. } catch (Exception ignored) {}
  213. try {
  214. stmt.execute("ALTER TABLE tencent_tag_event DROP PRIMARY KEY");
  215. } catch (Exception ignored) {}
  216. try {
  217. stmt.execute("ALTER TABLE tencent_tag_event ADD PRIMARY KEY (tag_id, baidu_act)");
  218. } catch (Exception ignored) {}
  219. try {
  220. stmt.execute("ALTER TABLE tencent_account_tag_event ADD COLUMN tencent_custom_action VARCHAR(128) COMMENT '腾讯自定义行为(action_type=CUSTOM时使用)' AFTER tencent_action_type");
  221. } catch (Exception ignored) {}
  222. try {
  223. stmt.execute("""
  224. CREATE TABLE IF NOT EXISTS tencent_tag_event_profile (
  225. tag_id VARCHAR(128) NOT NULL COMMENT '百度广告位ID',
  226. callback_profile VARCHAR(64) NOT NULL COMMENT '回传方案',
  227. baidu_act INT NOT NULL COMMENT '百度转化行为',
  228. tencent_action_type VARCHAR(128) NOT NULL COMMENT '腾讯回传行为',
  229. tencent_custom_action VARCHAR(128) COMMENT '腾讯自定义行为(action_type=CUSTOM时使用)',
  230. enabled TINYINT(1) NOT NULL DEFAULT 1 COMMENT '是否启用',
  231. created_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
  232. updated_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
  233. PRIMARY KEY (tag_id, callback_profile, baidu_act),
  234. KEY idx_tencent_tag_event_profile_profile_act (callback_profile, baidu_act)
  235. ) COMMENT '腾讯profile级广告位回传方式配置'
  236. """);
  237. } catch (Exception ignored) {}
  238. stmt.close();
  239. } catch (Exception e) {
  240. throw new RuntimeException("migrate failed", e);
  241. }
  242. }
  243. // ─── SaveBid ─────────────────────────────────────────────────────────────
  244. public void saveBid(BidRecord record) {
  245. if (record.getQk() == null || record.getQk().isBlank()) {
  246. throw new IllegalArgumentException("qk is required");
  247. }
  248. if (record.getCreatedAt() == null) record.setCreatedAt(Instant.now());
  249. record.setMediaTraceId(record.getMediaTraceId());
  250. BidRecord cold = record.toColdRecord();
  251. String mediaParams = toJson(cold.getMediaParams());
  252. try (SqlSession session = sqlSessionFactory.openSession(true)) {
  253. BidEventMapper mapper = session.getMapper(BidEventMapper.class);
  254. mapper.insertOrUpdate(
  255. cold.getQk(), cold.getMedia(), cold.getMediaTraceId(),
  256. cold.getPlatform(), cold.getAccountId(), cold.getEventType(),
  257. cold.getTagId(), cold.getPrice() != null ? cold.getPrice() : 0L,
  258. cold.getPackageName(), mediaParams, Timestamp.from(cold.getCreatedAt())
  259. );
  260. }
  261. }
  262. public BidRecord getBidByQk(String qk) {
  263. if (qk == null || qk.isBlank()) return null;
  264. try (SqlSession session = sqlSessionFactory.openSession(true)) {
  265. BidEventMapper mapper = session.getMapper(BidEventMapper.class);
  266. return mapper.selectByQk(qk);
  267. }
  268. }
  269. public List<BidRecord> getBidsByQks(List<String> qks) {
  270. if (qks == null || qks.isEmpty()) return List.of();
  271. try (SqlSession session = sqlSessionFactory.openSession(true)) {
  272. BidEventMapper mapper = session.getMapper(BidEventMapper.class);
  273. return mapper.selectByQks(qks);
  274. }
  275. }
  276. // ─── SaveTracking ─────────────────────────────────────────────────────────
  277. public void saveTracking(TrackingRecord record) {
  278. if (record.getCreatedAt() == null) record.setCreatedAt(Instant.now());
  279. try (SqlSession session = sqlSessionFactory.openSession(true)) {
  280. TrackingReportMapper mapper = session.getMapper(TrackingReportMapper.class);
  281. mapper.insert(
  282. record.getQk(), record.getKind(), record.getUrl(),
  283. record.getStatus(), record.isOk() ? 1 : 0,
  284. Timestamp.from(record.getCreatedAt())
  285. );
  286. }
  287. }
  288. // ─── SaveConversion ──────────────────────────────────────────────────────
  289. public void saveConversion(ConversionRecord record) {
  290. if (record.getCreatedAt() == null) record.setCreatedAt(Instant.now());
  291. try (SqlSession session = sqlSessionFactory.openSession(true)) {
  292. ConversionMapper mapper = session.getMapper(ConversionMapper.class);
  293. mapper.insertOrUpdate(
  294. record.getDedupeKey(), record.getQk(), record.getMedia(), record.getTagId(),
  295. record.getDate(), record.getAppSid(), record.getCustomerName(),
  296. record.getDeviceId(), record.getConv(), record.getPayment(), record.getGmv(),
  297. record.getAct(), record.getTu(), record.getClkTime(),
  298. Timestamp.from(record.getCreatedAt())
  299. );
  300. }
  301. }
  302. public void saveConversions(List<ConversionRecord> records) {
  303. if (records == null || records.isEmpty()) return;
  304. long startedAt = System.currentTimeMillis();
  305. Instant now = Instant.now();
  306. for (ConversionRecord record : records) {
  307. if (record.getCreatedAt() == null) record.setCreatedAt(now);
  308. }
  309. try (SqlSession session = sqlSessionFactory.openSession(false)) {
  310. ConversionMapper mapper = session.getMapper(ConversionMapper.class);
  311. for (int start = 0; start < records.size(); start += CONVERSION_BATCH_SIZE) {
  312. int end = Math.min(start + CONVERSION_BATCH_SIZE, records.size());
  313. mapper.batchInsertOrUpdate(records.subList(start, end));
  314. }
  315. session.commit();
  316. long costMs = System.currentTimeMillis() - startedAt;
  317. int batchCount = (records.size() + CONVERSION_BATCH_SIZE - 1) / CONVERSION_BATCH_SIZE;
  318. log.info("[TiDBColdStore] saveConversions done: size={}, batches={}, batchSize={}, cost={}ms",
  319. records.size(), batchCount, CONVERSION_BATCH_SIZE, costMs);
  320. }
  321. }
  322. // ─── SaveMediaCallback ───────────────────────────────────────────────────
  323. public void saveMediaCallback(MediaCallbackRecord record) {
  324. if (record.getCreatedAt() == null) record.setCreatedAt(Instant.now());
  325. int attempt = record.getAttempt() <= 0 ? 1 : record.getAttempt();
  326. if (record.getDispatchStatus() == null || record.getDispatchStatus().isBlank()) {
  327. record.setDispatchStatus(MediaCallbackRecord.DISPATCH_STATUS_SENT);
  328. }
  329. if (record.getTrackingVersion() == null || record.getTrackingVersion().isBlank()) {
  330. record.setTrackingVersion(defaultTrackingVersion(record.getMedia()));
  331. }
  332. try (SqlSession session = sqlSessionFactory.openSession(true)) {
  333. MediaCallbackMapper mapper = session.getMapper(MediaCallbackMapper.class);
  334. mapper.insert(
  335. record.getDedupeKey(), record.getMedia(), record.getQk(),
  336. record.getCallbackUrl(), record.getEventType(), record.getEventTimeMs(),
  337. record.getPurchase(), record.getStatus(), record.isOk() ? 1 : 0,
  338. attempt, record.getResponseBody(), record.getErrorMessage(),
  339. record.getRequestBody(), record.getDispatchStatus(), record.getTrackingVersion(),
  340. Timestamp.from(record.getCreatedAt())
  341. );
  342. }
  343. }
  344. public void saveMediaCallbacks(List<MediaCallbackRecord> records) {
  345. if (records == null || records.isEmpty()) return;
  346. for (MediaCallbackRecord record : records) {
  347. if (record.getCreatedAt() == null) record.setCreatedAt(Instant.now());
  348. if (record.getAttempt() <= 0) record.setAttempt(1);
  349. if (record.getDispatchStatus() == null || record.getDispatchStatus().isBlank()) {
  350. record.setDispatchStatus(MediaCallbackRecord.DISPATCH_STATUS_SENT);
  351. }
  352. if (record.getTrackingVersion() == null || record.getTrackingVersion().isBlank()) {
  353. record.setTrackingVersion(defaultTrackingVersion(record.getMedia()));
  354. }
  355. }
  356. try (SqlSession session = sqlSessionFactory.openSession(ExecutorType.BATCH, false)) {
  357. MediaCallbackMapper mapper = session.getMapper(MediaCallbackMapper.class);
  358. mapper.batchInsert(records);
  359. session.commit();
  360. }
  361. }
  362. // ─── 查询:成功回调是否存在 ────────────────────────────────────────────────
  363. public boolean successfulCallbackExists(String dedupeKey) {
  364. if (dedupeKey == null || dedupeKey.isBlank()) return false;
  365. try (SqlSession session = sqlSessionFactory.openSession(true)) {
  366. MediaCallbackMapper mapper = session.getMapper(MediaCallbackMapper.class);
  367. int count = mapper.countSuccessfulByDedupeKey(dedupeKey);
  368. return count > 0;
  369. }
  370. }
  371. public boolean successfulCallbackExistsForEvent(String dedupeKey, String legacyDedupeKey, int eventType) {
  372. if (dedupeKey == null || dedupeKey.isBlank()) return false;
  373. try (SqlSession session = sqlSessionFactory.openSession(true)) {
  374. MediaCallbackMapper mapper = session.getMapper(MediaCallbackMapper.class);
  375. int count = mapper.countSuccessfulForEvent(dedupeKey, legacyDedupeKey, eventType);
  376. return count > 0;
  377. }
  378. }
  379. public List<String> successfulCallbackKeys(List<String> dedupeKeys) {
  380. if (dedupeKeys == null || dedupeKeys.isEmpty()) return List.of();
  381. try (SqlSession session = sqlSessionFactory.openSession(true)) {
  382. MediaCallbackMapper mapper = session.getMapper(MediaCallbackMapper.class);
  383. return mapper.selectSuccessfulDedupeKeys(dedupeKeys);
  384. }
  385. }
  386. public List<String> successfulCallbackKeysWithLegacy(List<String> dedupeKeys,
  387. List<String> legacyDedupeKeys,
  388. List<Integer> eventTypes) {
  389. if (dedupeKeys == null || dedupeKeys.isEmpty()) return List.of();
  390. try (SqlSession session = sqlSessionFactory.openSession(true)) {
  391. MediaCallbackMapper mapper = session.getMapper(MediaCallbackMapper.class);
  392. return mapper.selectSuccessfulKeysWithLegacy(dedupeKeys, legacyDedupeKeys, eventTypes);
  393. }
  394. }
  395. // ─── 查询:待重试的回调 ────────────────────────────────────────────────────
  396. public List<MediaCallbackRecord> pendingMediaCallbacks(String media, int limit) {
  397. if (limit <= 0) limit = 100;
  398. try (SqlSession session = sqlSessionFactory.openSession(true)) {
  399. MediaCallbackMapper mapper = session.getMapper(MediaCallbackMapper.class);
  400. return mapper.selectPendingCallbacks(media, limit);
  401. }
  402. }
  403. public MediaCallbackRecord getMediaCallbackById(long id) {
  404. if (id <= 0) return null;
  405. try (SqlSession session = sqlSessionFactory.openSession(true)) {
  406. MediaCallbackMapper mapper = session.getMapper(MediaCallbackMapper.class);
  407. return mapper.selectById(id);
  408. }
  409. }
  410. public void updateMediaCallback(MediaCallbackRecord record) {
  411. if (record == null || record.getId() == null || record.getId() <= 0) {
  412. throw new IllegalArgumentException("media callback id is required");
  413. }
  414. int attempt = record.getAttempt() <= 0 ? 1 : record.getAttempt();
  415. record.setAttempt(attempt);
  416. if (record.getDispatchStatus() == null || record.getDispatchStatus().isBlank()) {
  417. record.setDispatchStatus(MediaCallbackRecord.DISPATCH_STATUS_SENT);
  418. }
  419. if (record.getTrackingVersion() == null || record.getTrackingVersion().isBlank()) {
  420. record.setTrackingVersion(defaultTrackingVersion(record.getMedia()));
  421. }
  422. try (SqlSession session = sqlSessionFactory.openSession(true)) {
  423. MediaCallbackMapper mapper = session.getMapper(MediaCallbackMapper.class);
  424. mapper.updateById(record);
  425. }
  426. }
  427. private static String defaultTrackingVersion(String media) {
  428. return "tencent".equalsIgnoreCase(media) ? "0" : "v1";
  429. }
  430. // ─── 查询:广告位回传方式配置 ──────────────────────────────────────────────
  431. public List<TagEventRecord> listAllTagEvents() {
  432. try (SqlSession session = sqlSessionFactory.openSession(true)) {
  433. TagEventMapper mapper = session.getMapper(TagEventMapper.class);
  434. return mapper.selectAll();
  435. }
  436. }
  437. /**
  438. * 根据广告位ID查询回传方式配置(DB 兜底查询)。
  439. */
  440. public TagEventRecord getTagEvent(String tagId, int baiduAct) {
  441. if (tagId == null || tagId.isBlank()) return null;
  442. try (SqlSession session = sqlSessionFactory.openSession(true)) {
  443. TagEventMapper mapper = session.getMapper(TagEventMapper.class);
  444. return mapper.selectByTagIdAndAct(tagId, baiduAct);
  445. }
  446. }
  447. public TencentTagEventProfileRecord getTagEventProfile(String tagId, String callbackProfile, int baiduAct) {
  448. if (tagId == null || tagId.isBlank() || callbackProfile == null || callbackProfile.isBlank()) {
  449. return null;
  450. }
  451. String sql = """
  452. SELECT tag_id, callback_profile, baidu_act, tencent_action_type, tencent_custom_action
  453. FROM tencent_tag_event_profile
  454. WHERE tag_id = ? AND callback_profile = ? AND baidu_act = ? AND enabled = 1
  455. """;
  456. try (SqlSession session = sqlSessionFactory.openSession(true);
  457. var ps = session.getConnection().prepareStatement(sql)) {
  458. ps.setString(1, tagId);
  459. ps.setString(2, callbackProfile);
  460. ps.setInt(3, baiduAct);
  461. try (var rs = ps.executeQuery()) {
  462. if (!rs.next()) {
  463. return null;
  464. }
  465. TencentTagEventProfileRecord record = new TencentTagEventProfileRecord();
  466. record.setTagId(rs.getString(1));
  467. record.setCallbackProfile(rs.getString(2));
  468. record.setBaiduAct(rs.getInt(3));
  469. record.setTencentActionType(rs.getString(4));
  470. record.setTencentCustomAction(rs.getString(5));
  471. return record;
  472. }
  473. } catch (Exception e) {
  474. log.warn("[TiDBColdStore] 查询 tencent_tag_event_profile 失败 | tagId={} | profile={} | act={} | error={}",
  475. tagId, callbackProfile, baiduAct, e.getMessage());
  476. return null;
  477. }
  478. }
  479. public List<TencentTagEventProfileRecord> listAllTagEventProfiles() {
  480. String sql = """
  481. SELECT tag_id, callback_profile, baidu_act, tencent_action_type, tencent_custom_action
  482. FROM tencent_tag_event_profile
  483. WHERE enabled = 1
  484. """;
  485. try (SqlSession session = sqlSessionFactory.openSession(true);
  486. var ps = session.getConnection().prepareStatement(sql);
  487. var rs = ps.executeQuery()) {
  488. List<TencentTagEventProfileRecord> records = new ArrayList<>();
  489. while (rs.next()) {
  490. TencentTagEventProfileRecord record = new TencentTagEventProfileRecord();
  491. record.setTagId(rs.getString(1));
  492. record.setCallbackProfile(rs.getString(2));
  493. record.setBaiduAct(rs.getInt(3));
  494. record.setTencentActionType(rs.getString(4));
  495. record.setTencentCustomAction(rs.getString(5));
  496. records.add(record);
  497. }
  498. return records;
  499. } catch (Exception e) {
  500. log.warn("[TiDBColdStore] 查询 tencent_tag_event_profile 全量失败 | error={}", e.getMessage());
  501. return List.of();
  502. }
  503. }
  504. // ─── 查询:腾讯账户级广告位回传方式配置 ───────────────────────────────────
  505. public List<AccountTagEventRecord> listAllAccountTagEvents() {
  506. try (SqlSession session = sqlSessionFactory.openSession(true)) {
  507. AccountTagEventMapper mapper = session.getMapper(AccountTagEventMapper.class);
  508. return mapper.selectAll();
  509. }
  510. }
  511. /**
  512. * 根据广告位ID、腾讯广告主ID和百度行为查询账户级回传方式配置(DB 兜底查询)。
  513. */
  514. public AccountTagEventRecord getAccountTagEvent(String tagId, String accountId, int baiduAct) {
  515. if (tagId == null || tagId.isBlank()) return null;
  516. if (accountId == null || accountId.isBlank()) return null;
  517. try (SqlSession session = sqlSessionFactory.openSession(true)) {
  518. AccountTagEventMapper mapper = session.getMapper(AccountTagEventMapper.class);
  519. return mapper.selectByTagIdAndAccountIdAndAct(tagId, accountId, baiduAct);
  520. }
  521. }
  522. // ─── 工具方法 ─────────────────────────────────────────────────────────────
  523. private String toJson(Object value) {
  524. if (value == null) return "null";
  525. try {
  526. return objectMapper.writeValueAsString(value);
  527. } catch (Exception e) {
  528. throw new RuntimeException("JSON serialize failed", e);
  529. }
  530. }
  531. }