352 lines
18 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package mysql
import (
"context"
"database/sql"
"errors"
"strings"
mysqlDriver "github.com/go-sql-driver/mysql"
)
func ensureResourceShopPurchaseOrderIndexes(ctx context.Context, db *sql.DB) error {
if _, err := db.ExecContext(ctx, `
ALTER TABLE resource_shop_purchase_orders
ADD INDEX idx_resource_shop_purchase_time (app_code, created_at_ms, order_id)`); err != nil && !isDuplicateKeyNameError(err) {
return err
}
if _, err := db.ExecContext(ctx, `
ALTER TABLE resource_shop_purchase_orders
ADD INDEX idx_resource_shop_purchase_resource_time (app_code, resource_id, created_at_ms, order_id)`); err != nil && !isDuplicateKeyNameError(err) {
return err
}
return nil
}
func ensureResourceGrantRevokeSchema(ctx context.Context, db *sql.DB) error {
if _, err := db.ExecContext(ctx, `
ALTER TABLE resource_grants
ADD COLUMN revoked_at_ms BIGINT NOT NULL DEFAULT 0 COMMENT '撤销时间UTC epoch ms' AFTER operator_user_id`); err != nil && !isDuplicateColumnError(err) {
return err
}
if _, err := db.ExecContext(ctx, `
ALTER TABLE resource_grants
ADD COLUMN revoked_by_user_id BIGINT NOT NULL DEFAULT 0 COMMENT '撤销操作人用户 ID' AFTER revoked_at_ms`); err != nil && !isDuplicateColumnError(err) {
return err
}
if _, err := db.ExecContext(ctx, `
ALTER TABLE resource_grants
ADD COLUMN revoke_reason VARCHAR(256) NOT NULL DEFAULT '' COMMENT '撤销原因' AFTER revoked_by_user_id`); err != nil && !isDuplicateColumnError(err) {
return err
}
return nil
}
func isDuplicateKeyNameError(err error) bool {
var mysqlErr *mysqlDriver.MySQLError
return errors.As(err, &mysqlErr) && mysqlErr.Number == 1061
}
func ensureHostSalarySettlementSchema(ctx context.Context, db *sql.DB) error {
if _, err := db.ExecContext(ctx, `
ALTER TABLE host_salary_settlement_records
ADD COLUMN trigger_mode VARCHAR(24) NOT NULL DEFAULT 'automatic' COMMENT '触发方式automatic/manual' AFTER settlement_type`); err != nil && !isDuplicateColumnError(err) {
return err
}
if _, err := db.ExecContext(ctx, `
ALTER TABLE host_salary_settlement_records
ADD COLUMN settlement_role VARCHAR(24) NOT NULL DEFAULT 'all' COMMENT '结算身份all/host/agency' AFTER trigger_mode`); err != nil && !isDuplicateColumnError(err) {
return err
}
return nil
}
func ensureExternalRechargeSchema(ctx context.Context, db *sql.DB) error {
if _, err := db.ExecContext(ctx, `
ALTER TABLE wallet_recharge_products
ADD COLUMN audience_type VARCHAR(32) NOT NULL DEFAULT 'normal' COMMENT '充值档位适用用户类型normal/coin_seller' AFTER app_code`); err != nil && !isDuplicateColumnError(err) {
return err
}
if _, err := db.ExecContext(ctx, `
ALTER TABLE wallet_recharge_products
ADD COLUMN app_visible BOOLEAN NOT NULL DEFAULT TRUE COMMENT '是否在 App 普通充值列表展示;不影响 Google Play 验单' AFTER status`); err != nil && !isDuplicateColumnError(err) {
return err
}
if _, err := db.ExecContext(ctx, `
CREATE TABLE IF NOT EXISTS third_party_payment_channels (
app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码,用于多租户隔离',
provider_code VARCHAR(32) NOT NULL COMMENT '三方支付渠道编码',
provider_name VARCHAR(64) NOT NULL COMMENT '三方支付渠道名称',
status VARCHAR(32) NOT NULL DEFAULT 'active' COMMENT '状态active/disabled',
sort_order INT NOT NULL DEFAULT 0 COMMENT '排序权重',
created_at_ms BIGINT NOT NULL COMMENT '创建时间UTC epoch ms',
updated_at_ms BIGINT NOT NULL COMMENT '更新时间UTC epoch ms',
PRIMARY KEY (app_code, provider_code),
KEY idx_third_party_payment_channels_status (app_code, status, sort_order)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='三方支付渠道配置表'`); err != nil {
return err
}
if _, err := db.ExecContext(ctx, `
CREATE TABLE IF NOT EXISTS third_party_payment_methods (
method_id BIGINT NOT NULL AUTO_INCREMENT COMMENT '支付方式 ID',
app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码,用于多租户隔离',
provider_code VARCHAR(32) NOT NULL COMMENT '三方支付渠道编码',
country_code VARCHAR(16) NOT NULL COMMENT 'MiFaPay countryCodeGLOBAL 表示全球方式',
country_name VARCHAR(64) NOT NULL DEFAULT '' COMMENT '国家/地区名称',
currency_code VARCHAR(8) NOT NULL COMMENT '下单币种',
pay_way VARCHAR(64) NOT NULL COMMENT 'MiFaPay payWay',
pay_type VARCHAR(64) NOT NULL COMMENT 'MiFaPay payType',
method_name VARCHAR(128) NOT NULL COMMENT '支付方式展示名',
logo_url VARCHAR(512) NOT NULL DEFAULT '' COMMENT '支付方式 logo',
status VARCHAR(32) NOT NULL DEFAULT 'active' COMMENT '状态active/disabled',
usd_to_currency_rate DECIMAL(24,8) NOT NULL DEFAULT 0 COMMENT '1 USD 可兑换本币金额0 表示未配置,不在 H5 展示',
sort_order INT NOT NULL DEFAULT 0 COMMENT '排序权重',
created_at_ms BIGINT NOT NULL COMMENT '创建时间UTC epoch ms',
updated_at_ms BIGINT NOT NULL COMMENT '更新时间UTC epoch ms',
PRIMARY KEY (method_id),
UNIQUE KEY uk_third_party_payment_method (app_code, provider_code, country_code, pay_way, pay_type),
KEY idx_third_party_payment_methods_country (app_code, provider_code, country_code, status, sort_order)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='三方支付国家和方式配置表'`); err != nil {
return err
}
if _, err := db.ExecContext(ctx, `
CREATE TABLE IF NOT EXISTS external_recharge_orders (
app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码,用于多租户隔离',
order_id VARCHAR(96) NOT NULL COMMENT '平台外部充值订单 ID',
command_id VARCHAR(128) NOT NULL COMMENT '客户端命令幂等 ID',
target_user_id BIGINT NOT NULL COMMENT '充值目标用户 ID',
target_region_id BIGINT NOT NULL COMMENT '充值目标区域 ID',
target_country_id BIGINT NOT NULL DEFAULT 0 COMMENT '充值目标国家 ID',
target_country_code VARCHAR(16) NOT NULL DEFAULT '' COMMENT '充值目标国家码',
audience_type VARCHAR(32) NOT NULL DEFAULT 'normal' COMMENT '商品适用用户类型',
product_id BIGINT NOT NULL COMMENT '充值商品 ID',
product_code VARCHAR(128) NOT NULL DEFAULT '' COMMENT '充值商品编码快照',
product_name VARCHAR(128) NOT NULL DEFAULT '' COMMENT '充值商品名称快照',
coin_amount BIGINT NOT NULL COMMENT '到账金币数量',
usd_minor_amount BIGINT NOT NULL COMMENT '美元最小单位金额',
provider_code VARCHAR(32) NOT NULL COMMENT '支付渠道mifapay/usdt_trc20',
payment_method_id BIGINT NOT NULL DEFAULT 0 COMMENT '三方支付方式 ID',
country_code VARCHAR(16) NOT NULL DEFAULT '' COMMENT '支付国家码',
currency_code VARCHAR(8) NOT NULL DEFAULT '' COMMENT '支付币种',
provider_amount_minor BIGINT NOT NULL DEFAULT 0 COMMENT '三方下单金额最小单位',
pay_way VARCHAR(64) NOT NULL DEFAULT '' COMMENT 'MiFaPay payWay',
pay_type VARCHAR(64) NOT NULL DEFAULT '' COMMENT 'MiFaPay payType',
pay_url TEXT NULL COMMENT '三方收银台地址',
provider_order_id VARCHAR(128) NOT NULL DEFAULT '' COMMENT '三方订单号',
tx_hash VARCHAR(128) NOT NULL DEFAULT '' COMMENT 'USDT-TRC20 交易哈希',
receive_address VARCHAR(128) NOT NULL DEFAULT '' COMMENT 'USDT 收款地址',
status VARCHAR(32) NOT NULL DEFAULT 'pending' COMMENT 'pending/redirected/credited/failed',
failure_reason VARCHAR(512) NOT NULL DEFAULT '' COMMENT '失败原因',
wallet_transaction_id VARCHAR(96) NOT NULL DEFAULT '' COMMENT '入账钱包交易 ID',
provider_payload JSON NULL COMMENT '三方原始响应或链上交易快照',
created_at_ms BIGINT NOT NULL COMMENT '创建时间UTC epoch ms',
updated_at_ms BIGINT NOT NULL COMMENT '更新时间UTC epoch ms',
PRIMARY KEY (app_code, order_id),
UNIQUE KEY uk_external_recharge_command (app_code, command_id),
KEY idx_external_recharge_user_time (app_code, target_user_id, created_at_ms),
KEY idx_external_recharge_provider_order (app_code, provider_code, provider_order_id),
KEY idx_external_recharge_wallet_tx (app_code, wallet_transaction_id),
KEY idx_external_recharge_reconcile (app_code, status, provider_code, updated_at_ms, created_at_ms),
KEY idx_external_recharge_tx (app_code, provider_code, tx_hash)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='H5 外部充值订单表'`); err != nil {
return err
}
if _, err := db.ExecContext(ctx, `
ALTER TABLE external_recharge_orders
ADD COLUMN target_country_id BIGINT NOT NULL DEFAULT 0 COMMENT '充值目标国家 ID' AFTER target_region_id`); err != nil && !isDuplicateColumnError(err) {
return err
}
return seedThirdPartyPaymentDefaults(ctx, db)
}
// ensureGooglePaymentPaidDetailsSchema 给 payment_orders 补 Google Orders API 实付明细列;
// 财务列表按 wallet_transaction_id 反查 Google 支付订单,需要独立索引避免回表扫描。
func ensureGooglePaymentPaidDetailsSchema(ctx context.Context, db *sql.DB) error {
alters := []string{
`ALTER TABLE payment_orders ADD COLUMN paid_currency_code VARCHAR(8) NOT NULL DEFAULT '' COMMENT '用户实付币种,来自 Google Orders API' AFTER amount_micro`,
`ALTER TABLE payment_orders ADD COLUMN paid_amount_micro BIGINT NOT NULL DEFAULT 0 COMMENT '用户实付总额微单位(实付币种)' AFTER paid_currency_code`,
`ALTER TABLE payment_orders ADD COLUMN paid_tax_micro BIGINT NOT NULL DEFAULT 0 COMMENT '订单税额微单位(实付币种)' AFTER paid_amount_micro`,
`ALTER TABLE payment_orders ADD COLUMN paid_net_micro BIGINT NOT NULL DEFAULT 0 COMMENT 'Google 扣费后开发者净收入微单位(实付币种)' AFTER paid_tax_micro`,
`ALTER TABLE payment_orders ADD COLUMN paid_synced_at_ms BIGINT NOT NULL DEFAULT 0 COMMENT '实付明细同步时间UTC epoch ms0 表示未同步' AFTER paid_net_micro`,
}
for _, stmt := range alters {
if _, err := db.ExecContext(ctx, stmt); err != nil && !isDuplicateColumnError(err) {
return err
}
}
if _, err := db.ExecContext(ctx, `
ALTER TABLE payment_orders
ADD INDEX idx_payment_orders_wallet_tx (app_code, wallet_transaction_id)`); err != nil && !isDuplicateKeyNameError(err) {
return err
}
return nil
}
func ensureWalletPolicyInstanceSchema(ctx context.Context, db *sql.DB) error {
_, err := db.ExecContext(ctx, `
CREATE TABLE IF NOT EXISTS wallet_policy_instances (
app_code VARCHAR(32) NOT NULL COMMENT '应用编码',
instance_code VARCHAR(96) NOT NULL COMMENT '后台策略实例编码',
template_code VARCHAR(96) NOT NULL COMMENT '模板编码快照',
template_version VARCHAR(64) NOT NULL COMMENT '模板版本快照',
region_id BIGINT NOT NULL DEFAULT 0 COMMENT '适用地区0 表示 App 兜底',
status VARCHAR(24) NOT NULL DEFAULT 'active' COMMENT 'active/disabled',
effective_from_ms BIGINT NOT NULL DEFAULT 0 COMMENT 'UTC epoch ms',
effective_to_ms BIGINT NOT NULL DEFAULT 0 COMMENT '0 表示长期有效',
host_point_asset_type VARCHAR(32) NOT NULL DEFAULT 'POINT' COMMENT '主播收益积分资产',
host_point_ratio_ppm BIGINT NOT NULL DEFAULT 700000 COMMENT '有效付费礼物转 POINT 比例ppm',
points_per_usd BIGINT NOT NULL DEFAULT 100000 COMMENT 'POINT/USD 展示换算比例',
withdraw_fee_bps INT NOT NULL DEFAULT 500 COMMENT '提现手续费 bps',
rule_json JSON NOT NULL COMMENT '完整政策快照',
published_by_admin_id BIGINT UNSIGNED NOT NULL DEFAULT 0,
published_at_ms BIGINT NOT NULL DEFAULT 0,
created_at_ms BIGINT NOT NULL,
updated_at_ms BIGINT NOT NULL,
PRIMARY KEY (app_code, instance_code, region_id),
KEY idx_wallet_policy_active (app_code, region_id, status, effective_from_ms, effective_to_ms)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='钱包运行侧收益政策实例表'`)
return err
}
func ensureGiftDiamondRatioSchema(ctx context.Context, db *sql.DB) error {
if _, err := db.ExecContext(ctx, `
CREATE TABLE IF NOT EXISTS gift_diamond_ratio_configs (
app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码,用于多租户隔离',
region_id BIGINT NOT NULL DEFAULT 0 COMMENT '房间可见区域0 表示全局默认',
gift_type_code VARCHAR(32) NOT NULL COMMENT '礼物类型编码',
status VARCHAR(32) NOT NULL DEFAULT 'active' COMMENT '业务状态',
ratio_percent DECIMAL(5,2) NOT NULL DEFAULT 100.00 COMMENT '房间贡献热度和主播周期钻石入账比例,百分比',
coin_return_ratio_percent DECIMAL(5,2) NOT NULL DEFAULT 0.00 COMMENT '收礼人金币返还比例,百分比',
created_by_admin_id BIGINT NOT NULL DEFAULT 0 COMMENT '创建后台用户 ID',
updated_by_admin_id BIGINT NOT NULL DEFAULT 0 COMMENT '更新后台用户 ID',
created_at_ms BIGINT NOT NULL COMMENT '创建时间UTC epoch ms',
updated_at_ms BIGINT NOT NULL COMMENT '更新时间UTC epoch ms',
PRIMARY KEY (app_code, region_id, gift_type_code),
KEY idx_gift_diamond_ratio_region (app_code, region_id, status)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='礼物钻石和房间贡献比例配置表'`); err != nil {
return err
}
if added, err := ensureGiftReturnCoinRatioColumn(ctx, db); err != nil {
return err
} else if added {
if err := seedGiftReturnCoinRatioDefaults(ctx, db); err != nil {
return err
}
}
_, err := db.ExecContext(ctx, `
INSERT IGNORE INTO gift_diamond_ratio_configs (
app_code, region_id, gift_type_code, status, ratio_percent, coin_return_ratio_percent,
created_by_admin_id, updated_by_admin_id, created_at_ms, updated_at_ms
) VALUES
('lalu', 0, 'normal', 'active', 100.00, 30.00, 0, 0, 0, 0),
('lalu', 0, 'lucky', 'active', 10.00, 10.00, 0, 0, 0, 0),
('lalu', 0, 'super_lucky', 'active', 1.00, 1.00, 0, 0, 0, 0)`)
return err
}
func ensureGiftReturnCoinRatioColumn(ctx context.Context, db *sql.DB) (bool, error) {
if _, err := db.ExecContext(ctx, `
ALTER TABLE gift_diamond_ratio_configs
ADD COLUMN coin_return_ratio_percent DECIMAL(5,2) NOT NULL DEFAULT 0.00 COMMENT '收礼人金币返还比例,百分比' AFTER ratio_percent`); err != nil {
if isDuplicateColumnError(err) {
return false, nil
}
return false, err
}
return true, nil
}
func seedGiftReturnCoinRatioDefaults(ctx context.Context, db *sql.DB) error {
_, err := db.ExecContext(ctx, `
UPDATE gift_diamond_ratio_configs
SET coin_return_ratio_percent = CASE gift_type_code
WHEN 'lucky' THEN 10.00
WHEN 'super_lucky' THEN 1.00
ELSE 30.00
END`)
return err
}
func ensureCoinSellerSalaryExchangeRateSchema(ctx context.Context, db *sql.DB) error {
_, err := db.ExecContext(ctx, `
CREATE TABLE IF NOT EXISTS coin_seller_salary_exchange_rate_tiers (
app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码,用于多租户隔离',
region_id BIGINT NOT NULL COMMENT '币商所属区域 ID',
tier_id BIGINT NOT NULL AUTO_INCREMENT COMMENT '区间 ID',
min_usd_minor BIGINT NOT NULL COMMENT '起始美元金额,单位美分,包含',
max_usd_minor BIGINT NOT NULL COMMENT '结束美元金额,单位美分,包含',
coin_per_usd BIGINT NOT NULL COMMENT '每 1 USD 工资可兑换的币商专用金币数量',
status VARCHAR(24) NOT NULL DEFAULT 'active' COMMENT '状态active/disabled',
sort_order INT NOT NULL DEFAULT 0 COMMENT '排序权重',
created_by_user_id BIGINT NOT NULL DEFAULT 0 COMMENT '创建人用户 ID',
updated_by_user_id BIGINT NOT NULL DEFAULT 0 COMMENT '更新人用户 ID',
created_at_ms BIGINT NOT NULL COMMENT '创建时间UTC epoch ms',
updated_at_ms BIGINT NOT NULL COMMENT '更新时间UTC epoch ms',
PRIMARY KEY (tier_id),
KEY idx_coin_seller_salary_rates_region (app_code, region_id, status, min_usd_minor, max_usd_minor),
KEY idx_coin_seller_salary_rates_sort (app_code, region_id, status, sort_order)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='币商工资兑换比例区间表'`)
return err
}
func ensureUserResourceEquipmentSchema(ctx context.Context, db *sql.DB) error {
if _, err := db.ExecContext(ctx, `
CREATE TABLE IF NOT EXISTS user_resource_equipment (
app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码,用于多租户隔离',
user_id BIGINT NOT NULL COMMENT '用户 ID',
resource_type VARCHAR(32) NOT NULL COMMENT '资源类型',
entitlement_id VARCHAR(96) NOT NULL COMMENT '权益 ID',
resource_id BIGINT NOT NULL COMMENT '资源 ID',
updated_at_ms BIGINT NOT NULL COMMENT '更新时间UTC epoch ms',
PRIMARY KEY (app_code, user_id, resource_type, entitlement_id),
KEY idx_user_resource_equipment_user (app_code, user_id, updated_at_ms)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户资源装备表'`); err != nil {
return err
}
primaryColumns, err := tablePrimaryKeyColumns(ctx, db, "user_resource_equipment")
if err != nil {
return err
}
if strings.Join(primaryColumns, ",") == "app_code,user_id,resource_type" {
_, err = db.ExecContext(ctx, `
ALTER TABLE user_resource_equipment
DROP PRIMARY KEY,
ADD PRIMARY KEY (app_code, user_id, resource_type, entitlement_id)`)
return err
}
return nil
}
func tablePrimaryKeyColumns(ctx context.Context, db *sql.DB, tableName string) ([]string, error) {
rows, err := db.QueryContext(ctx, `
SELECT COLUMN_NAME
FROM information_schema.KEY_COLUMN_USAGE
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = ?
AND CONSTRAINT_NAME = 'PRIMARY'
ORDER BY ORDINAL_POSITION ASC`, tableName)
if err != nil {
return nil, err
}
defer rows.Close()
columns := make([]string, 0, 4)
for rows.Next() {
var column string
if err := rows.Scan(&column); err != nil {
return nil, err
}
columns = append(columns, column)
}
return columns, rows.Err()
}