287 lines
11 KiB
Go

package storage
import (
"context"
"errors"
"fmt"
driverMysql "github.com/go-sql-driver/mysql"
)
func (r *MySQLRepository) EnsureSchema(ctx context.Context) error {
if err := execAll(ctx, r.db, []string{
`CREATE TABLE IF NOT EXISTS dashboard_cdc_checkpoint (
service_name varchar(64) NOT NULL,
binlog_file varchar(255) NOT NULL,
binlog_pos int unsigned NOT NULL,
updated_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (service_name)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`,
`CREATE TABLE IF NOT EXISTS dashboard_cdc_event_dedup (
event_key varchar(255) NOT NULL,
source varchar(128) NOT NULL,
created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (event_key),
KEY idx_dashboard_cdc_event_dedup_created (created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`,
`CREATE TABLE IF NOT EXISTS dashboard_cdc_user_country (
user_id bigint NOT NULL,
sys_origin varchar(50) NOT NULL,
country_code varchar(30) NOT NULL,
country_name varchar(100) NOT NULL,
is_deleted tinyint(1) NOT NULL DEFAULT 0,
user_created_at datetime NULL,
user_updated_at datetime NULL,
updated_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (user_id),
KEY idx_dashboard_cdc_user_country_origin_country (sys_origin, country_code),
KEY idx_dashboard_cdc_user_country_origin_created (sys_origin, user_created_at, country_code)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`,
createDailyMetricTable,
createDailyUserMetricTable,
createPeriodMetricTable,
createPeriodUserMetricTable,
createGamePeriodMetricTable,
createGamePeriodUserMetricTable,
createRechargeDetailTable,
}); err != nil {
return err
}
return r.ensureMetricColumns(ctx)
}
const createDailyMetricTable = `CREATE TABLE IF NOT EXISTS country_dashboard_daily_metric (
id bigint NOT NULL AUTO_INCREMENT,
stat_date date NOT NULL,
date_number int NOT NULL,
sys_origin varchar(50) NOT NULL,
country_code varchar(64) NOT NULL,
country_name varchar(128) NOT NULL,
country_new_user bigint NOT NULL DEFAULT 0,
new_user_recharge decimal(20,2) NOT NULL DEFAULT 0,
official_recharge decimal(20,2) NOT NULL DEFAULT 0,
mifapay_recharge decimal(20,2) NOT NULL DEFAULT 0,
google_recharge decimal(20,2) NOT NULL DEFAULT 0,
dealer_recharge decimal(20,2) NOT NULL DEFAULT 0,
salary_exchange decimal(20,2) NOT NULL DEFAULT 0,
salary_transfer decimal(20,2) NOT NULL DEFAULT 0,
gift_consume decimal(20,2) NOT NULL DEFAULT 0,
lucky_gift_total_flow decimal(20,2) NOT NULL DEFAULT 0,
lucky_gift_user bigint NOT NULL DEFAULT 0,
lucky_gift_payout decimal(20,2) NOT NULL DEFAULT 0,
game_total_flow decimal(20,2) NOT NULL DEFAULT 0,
game_user bigint NOT NULL DEFAULT 0,
game_payout decimal(20,2) NOT NULL DEFAULT 0,
refreshed_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
UNIQUE KEY uk_daily_origin_country (stat_date, sys_origin, country_code),
KEY idx_daily_origin_date (sys_origin, stat_date)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`
const createDailyUserMetricTable = `CREATE TABLE IF NOT EXISTS country_dashboard_daily_user_metric (
id bigint NOT NULL AUTO_INCREMENT,
stat_date date NOT NULL,
sys_origin varchar(50) NOT NULL,
country_code varchar(64) NOT NULL,
country_name varchar(128) NOT NULL,
user_id bigint NOT NULL,
metric_type varchar(32) NOT NULL,
created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
UNIQUE KEY uk_daily_user_metric (stat_date, sys_origin, country_code, user_id, metric_type),
KEY idx_daily_user_metric (sys_origin, user_id, metric_type)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`
const createPeriodMetricTable = `CREATE TABLE IF NOT EXISTS country_dashboard_period_metric (
id bigint NOT NULL AUTO_INCREMENT,
period_type varchar(16) NOT NULL,
period_key varchar(32) NOT NULL,
stat_timezone varchar(64) NOT NULL DEFAULT 'Asia/Riyadh',
period_name varchar(64) NOT NULL,
period_start_date date NULL,
period_end_date date NULL,
sys_origin varchar(50) NOT NULL,
country_code varchar(64) NOT NULL,
country_name varchar(128) NOT NULL,
country_new_user bigint NOT NULL DEFAULT 0,
new_user_recharge decimal(20,2) NOT NULL DEFAULT 0,
official_recharge decimal(20,2) NOT NULL DEFAULT 0,
mifapay_recharge decimal(20,2) NOT NULL DEFAULT 0,
google_recharge decimal(20,2) NOT NULL DEFAULT 0,
dealer_recharge decimal(20,2) NOT NULL DEFAULT 0,
salary_exchange decimal(20,2) NOT NULL DEFAULT 0,
salary_transfer decimal(20,2) NOT NULL DEFAULT 0,
gift_consume decimal(20,2) NOT NULL DEFAULT 0,
lucky_gift_total_flow decimal(20,2) NOT NULL DEFAULT 0,
lucky_gift_user bigint NOT NULL DEFAULT 0,
lucky_gift_payout decimal(20,2) NOT NULL DEFAULT 0,
game_total_flow decimal(20,2) NOT NULL DEFAULT 0,
game_user bigint NOT NULL DEFAULT 0,
game_payout decimal(20,2) NOT NULL DEFAULT 0,
refreshed_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
UNIQUE KEY uk_period_origin_country (period_type, period_key, stat_timezone, sys_origin, country_code),
KEY idx_origin_period (sys_origin, stat_timezone, period_type, period_key)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`
func (r *MySQLRepository) ensureMetricColumns(ctx context.Context) error {
if err := r.addColumnIfMissing(ctx, "country_dashboard_daily_metric", "salary_transfer",
"ALTER TABLE country_dashboard_daily_metric ADD COLUMN salary_transfer decimal(20,2) NOT NULL DEFAULT 0 AFTER salary_exchange"); err != nil {
return err
}
if err := r.addColumnIfMissing(ctx, "country_dashboard_period_metric", "salary_transfer",
"ALTER TABLE country_dashboard_period_metric ADD COLUMN salary_transfer decimal(20,2) NOT NULL DEFAULT 0 AFTER salary_exchange"); err != nil {
return err
}
return r.addIndexIfMissing(ctx, "dashboard_cdc_user_country", "idx_dashboard_cdc_user_country_origin_created",
"ALTER TABLE dashboard_cdc_user_country ADD INDEX idx_dashboard_cdc_user_country_origin_created (sys_origin, user_created_at, country_code)")
}
func (r *MySQLRepository) addColumnIfMissing(ctx context.Context, table string, column string, ddl string) error {
var exists int
err := r.db.QueryRowContext(ctx, `
SELECT COUNT(1)
FROM information_schema.columns
WHERE table_schema = DATABASE()
AND table_name = ?
AND column_name = ?
`, table, column).Scan(&exists)
if err != nil {
return err
}
if exists > 0 {
return nil
}
if _, err := r.db.ExecContext(ctx, ddl); err != nil && !isDuplicateColumnError(err) {
return err
}
return nil
}
func (r *MySQLRepository) addIndexIfMissing(ctx context.Context, table string, indexName string, ddl string) error {
var exists int
err := r.db.QueryRowContext(ctx, `
SELECT COUNT(1)
FROM information_schema.statistics
WHERE table_schema = DATABASE()
AND table_name = ?
AND index_name = ?
`, table, indexName).Scan(&exists)
if err != nil {
return err
}
if exists > 0 {
return nil
}
if _, err := r.db.ExecContext(ctx, ddl); err != nil && !isDuplicateKeyError(err) {
return err
}
return nil
}
func isDuplicateColumnError(err error) bool {
var mysqlErr *driverMysql.MySQLError
if err == nil {
return false
}
if errors.As(err, &mysqlErr) {
return mysqlErr.Number == 1060
}
return fmt.Sprint(err) == "Error 1060 (42S21): Duplicate column name 'salary_transfer'"
}
func isDuplicateKeyError(err error) bool {
var mysqlErr *driverMysql.MySQLError
if err == nil {
return false
}
if errors.As(err, &mysqlErr) {
return mysqlErr.Number == 1061
}
return false
}
const createPeriodUserMetricTable = `CREATE TABLE IF NOT EXISTS country_dashboard_period_user_metric (
id bigint NOT NULL AUTO_INCREMENT,
period_type varchar(16) NOT NULL,
period_key varchar(32) NOT NULL,
stat_timezone varchar(64) NOT NULL DEFAULT 'Asia/Riyadh',
period_name varchar(64) NOT NULL,
period_start_date date NULL,
period_end_date date NULL,
sys_origin varchar(50) NOT NULL,
country_code varchar(64) NOT NULL,
country_name varchar(128) NOT NULL,
user_id bigint NOT NULL,
metric_type varchar(32) NOT NULL,
created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
UNIQUE KEY uk_period_user_metric (period_type, period_key, stat_timezone, sys_origin, country_code, user_id, metric_type),
KEY idx_origin_user_metric (sys_origin, stat_timezone, user_id, metric_type)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`
const createGamePeriodMetricTable = `CREATE TABLE IF NOT EXISTS country_dashboard_game_period_metric (
id bigint NOT NULL AUTO_INCREMENT,
period_type varchar(16) NOT NULL,
period_key varchar(32) NOT NULL,
stat_timezone varchar(64) NOT NULL DEFAULT 'Asia/Riyadh',
period_name varchar(64) NOT NULL,
period_start_date date NULL,
period_end_date date NULL,
sys_origin varchar(50) NOT NULL,
country_code varchar(64) NOT NULL,
country_name varchar(128) NOT NULL,
game_provider varchar(32) NOT NULL,
game_id varchar(128) NOT NULL,
game_name varchar(256) NOT NULL,
consume_amount decimal(20,2) NOT NULL DEFAULT 0,
payout_amount decimal(20,2) NOT NULL DEFAULT 0,
profit_amount decimal(20,2) NOT NULL DEFAULT 0,
user_count bigint NOT NULL DEFAULT 0,
order_count bigint NOT NULL DEFAULT 0,
refreshed_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
UNIQUE KEY uk_game_period (sys_origin, stat_timezone, period_type, period_key, country_code, game_provider, game_id),
KEY idx_game_period_game (sys_origin, stat_timezone, period_type, period_key, game_provider, game_id, country_code)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`
const createGamePeriodUserMetricTable = `CREATE TABLE IF NOT EXISTS country_dashboard_game_period_user_metric (
id bigint NOT NULL AUTO_INCREMENT,
period_type varchar(16) NOT NULL,
period_key varchar(32) NOT NULL,
stat_timezone varchar(64) NOT NULL DEFAULT 'Asia/Riyadh',
period_name varchar(64) NOT NULL,
period_start_date date NULL,
period_end_date date NULL,
sys_origin varchar(50) NOT NULL,
country_code varchar(64) NOT NULL,
country_name varchar(128) NOT NULL,
game_provider varchar(32) NOT NULL,
game_id varchar(128) NOT NULL,
user_id bigint NOT NULL,
created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
UNIQUE KEY uk_game_period_user (period_type, period_key, stat_timezone, sys_origin, country_code, game_provider, game_id, user_id),
KEY idx_game_user (sys_origin, stat_timezone, user_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`
const createRechargeDetailTable = `CREATE TABLE IF NOT EXISTS country_dashboard_recharge_detail (
id bigint NOT NULL AUTO_INCREMENT,
source_type varchar(32) NOT NULL,
record_id varchar(128) NOT NULL,
source_channel varchar(64) NOT NULL DEFAULT '',
sys_origin varchar(50) NOT NULL,
user_id bigint NOT NULL,
country_code varchar(64) NOT NULL DEFAULT 'UNKNOWN',
country_name varchar(128) NOT NULL DEFAULT 'UNKNOWN',
amount decimal(20,2) NOT NULL DEFAULT 0,
coin_quantity decimal(20,2) NULL,
remark varchar(512) NOT NULL DEFAULT '',
create_time datetime NOT NULL,
update_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (id),
UNIQUE KEY uk_recharge_source_record (source_type, record_id),
KEY idx_recharge_query (sys_origin, create_time, country_code),
KEY idx_recharge_user (user_id, create_time)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`