|
|
@@ -0,0 +1,129 @@
|
|
|
+package com.adx.tencent.httpapi;
|
|
|
+
|
|
|
+import org.springframework.http.HttpStatus;
|
|
|
+import org.springframework.lang.Nullable;
|
|
|
+import org.springframework.stereotype.Service;
|
|
|
+import org.springframework.web.server.ResponseStatusException;
|
|
|
+
|
|
|
+import javax.sql.DataSource;
|
|
|
+import java.io.BufferedWriter;
|
|
|
+import java.io.IOException;
|
|
|
+import java.io.OutputStream;
|
|
|
+import java.io.OutputStreamWriter;
|
|
|
+import java.nio.charset.StandardCharsets;
|
|
|
+import java.sql.Connection;
|
|
|
+import java.sql.PreparedStatement;
|
|
|
+import java.sql.ResultSet;
|
|
|
+import java.sql.SQLException;
|
|
|
+import java.time.LocalDate;
|
|
|
+import java.time.format.DateTimeFormatter;
|
|
|
+
|
|
|
+/** Streams the fixed Honor/jlt68 activation OAID export to an HTTP response. */
|
|
|
+@Service
|
|
|
+public class HonorOaidExportService {
|
|
|
+
|
|
|
+ public static final long MAXIMUM_OAIDS = 50_000_000L;
|
|
|
+
|
|
|
+ private static final String TAG_ID = "jlt68";
|
|
|
+ private static final String MEDIA = "honor";
|
|
|
+ private static final int ACTIVATE_ACT = 1;
|
|
|
+ private static final DateTimeFormatter DATE_VALUE_FORMAT = DateTimeFormatter.BASIC_ISO_DATE;
|
|
|
+
|
|
|
+ private static final String COUNT_SQL = """
|
|
|
+ SELECT COUNT(*)
|
|
|
+ FROM (
|
|
|
+ SELECT DISTINCT TRIM(device_id) AS device_id
|
|
|
+ FROM tencent_baidu_conversions
|
|
|
+ WHERE date_value = ?
|
|
|
+ AND tag_id = ?
|
|
|
+ AND act = ?
|
|
|
+ AND media = ?
|
|
|
+ AND device_id IS NOT NULL
|
|
|
+ AND TRIM(device_id) <> ''
|
|
|
+ ) export_oaids
|
|
|
+ """;
|
|
|
+
|
|
|
+ private static final String EXPORT_SQL = """
|
|
|
+ SELECT DISTINCT TRIM(device_id) AS device_id
|
|
|
+ FROM tencent_baidu_conversions
|
|
|
+ WHERE date_value = ?
|
|
|
+ AND tag_id = ?
|
|
|
+ AND act = ?
|
|
|
+ AND media = ?
|
|
|
+ AND device_id IS NOT NULL
|
|
|
+ AND TRIM(device_id) <> ''
|
|
|
+ """;
|
|
|
+
|
|
|
+ @Nullable
|
|
|
+ private final DataSource dataSource;
|
|
|
+
|
|
|
+ public HonorOaidExportService(@Nullable DataSource dataSource) {
|
|
|
+ this.dataSource = dataSource;
|
|
|
+ }
|
|
|
+
|
|
|
+ /** Validates the platform maximum before the servlet starts downloading bytes. */
|
|
|
+ public long countExportRows(LocalDate exportDate) {
|
|
|
+ DataSource source = requireDataSource();
|
|
|
+ try (Connection connection = source.getConnection();
|
|
|
+ PreparedStatement statement = connection.prepareStatement(COUNT_SQL)) {
|
|
|
+ bindExportFilters(statement, exportDate);
|
|
|
+ try (ResultSet result = statement.executeQuery()) {
|
|
|
+ if (!result.next()) {
|
|
|
+ return 0L;
|
|
|
+ }
|
|
|
+ long count = result.getLong(1);
|
|
|
+ if (count > MAXIMUM_OAIDS) {
|
|
|
+ throw new ResponseStatusException(HttpStatus.UNPROCESSABLE_ENTITY,
|
|
|
+ "OAID count " + count + " exceeds the upload limit of " + MAXIMUM_OAIDS);
|
|
|
+ }
|
|
|
+ return count;
|
|
|
+ }
|
|
|
+ } catch (SQLException e) {
|
|
|
+ throw new ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE,
|
|
|
+ "Unable to query OAID export data", e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Writes a headerless UTF-8 CSV. The MySQL streaming fetch mode prevents a
|
|
|
+ * large daily export from being loaded into application memory.
|
|
|
+ */
|
|
|
+ public void writeCsv(LocalDate exportDate, OutputStream outputStream) throws IOException {
|
|
|
+ DataSource source = requireDataSource();
|
|
|
+ try (Connection connection = source.getConnection();
|
|
|
+ PreparedStatement statement = connection.prepareStatement(EXPORT_SQL,
|
|
|
+ ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY)) {
|
|
|
+ bindExportFilters(statement, exportDate);
|
|
|
+ statement.setFetchSize(Integer.MIN_VALUE);
|
|
|
+ try (ResultSet result = statement.executeQuery();
|
|
|
+ BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(outputStream, StandardCharsets.UTF_8))) {
|
|
|
+ while (result.next()) {
|
|
|
+ String deviceId = result.getString(1);
|
|
|
+ if (deviceId == null || deviceId.indexOf('\r') >= 0 || deviceId.indexOf('\n') >= 0) {
|
|
|
+ throw new IOException("Invalid OAID value in export data");
|
|
|
+ }
|
|
|
+ writer.write(deviceId);
|
|
|
+ writer.newLine();
|
|
|
+ }
|
|
|
+ writer.flush();
|
|
|
+ }
|
|
|
+ } catch (SQLException e) {
|
|
|
+ throw new IOException("Unable to stream OAID export data", e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private static void bindExportFilters(PreparedStatement statement, LocalDate exportDate) throws SQLException {
|
|
|
+ statement.setString(1, DATE_VALUE_FORMAT.format(exportDate));
|
|
|
+ statement.setString(2, TAG_ID);
|
|
|
+ statement.setInt(3, ACTIVATE_ACT);
|
|
|
+ statement.setString(4, MEDIA);
|
|
|
+ }
|
|
|
+
|
|
|
+ private DataSource requireDataSource() {
|
|
|
+ if (dataSource == null) {
|
|
|
+ throw new ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE,
|
|
|
+ "TiDB datasource is not configured");
|
|
|
+ }
|
|
|
+ return dataSource;
|
|
|
+ }
|
|
|
+}
|