547 lines
24 KiB
Go
547 lines
24 KiB
Go
package mysql
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"encoding/json"
|
|
"errors"
|
|
"strings"
|
|
"time"
|
|
|
|
_ "github.com/go-sql-driver/mysql"
|
|
"hyapp/pkg/appcode"
|
|
"hyapp/pkg/xerr"
|
|
gamedomain "hyapp/services/game-service/internal/domain/game"
|
|
gameservice "hyapp/services/game-service/internal/service/game"
|
|
)
|
|
|
|
// Repository 是 game-service 的 MySQL 持久化入口。
|
|
type Repository struct {
|
|
db *sql.DB
|
|
}
|
|
|
|
// Open 创建连接池并执行幂等迁移,保证本地已有数据库也能补齐游戏表。
|
|
func Open(ctx context.Context, dsn string) (*Repository, error) {
|
|
if strings.TrimSpace(dsn) == "" {
|
|
return nil, xerr.New(xerr.InvalidArgument, "mysql_dsn is required")
|
|
}
|
|
db, err := sql.Open("mysql", dsn)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := db.PingContext(ctx); err != nil {
|
|
_ = db.Close()
|
|
return nil, err
|
|
}
|
|
repo := &Repository{db: db}
|
|
if err := repo.Migrate(ctx); err != nil {
|
|
_ = db.Close()
|
|
return nil, err
|
|
}
|
|
return repo, nil
|
|
}
|
|
|
|
func (r *Repository) Close() error {
|
|
if r == nil || r.db == nil {
|
|
return nil
|
|
}
|
|
return r.db.Close()
|
|
}
|
|
|
|
func (r *Repository) Ping(ctx context.Context) error {
|
|
if r == nil || r.db == nil {
|
|
return xerr.New(xerr.Unavailable, "mysql repository is not configured")
|
|
}
|
|
return r.db.PingContext(ctx)
|
|
}
|
|
|
|
// Migrate 与 initdb 保持同构;开发阶段不保留旧 schema 兼容分支。
|
|
func (r *Repository) Migrate(ctx context.Context) error {
|
|
statements := []string{
|
|
`CREATE TABLE IF NOT EXISTS game_platforms (
|
|
app_code VARCHAR(32) NOT NULL,
|
|
platform_code VARCHAR(64) NOT NULL,
|
|
platform_name VARCHAR(128) NOT NULL,
|
|
status VARCHAR(32) NOT NULL,
|
|
api_base_url VARCHAR(512) NOT NULL DEFAULT '',
|
|
callback_secret_ciphertext VARCHAR(1024) NOT NULL DEFAULT '',
|
|
callback_ip_whitelist JSON NULL,
|
|
adapter_config JSON NULL,
|
|
sort_order INT NOT NULL DEFAULT 0,
|
|
created_at_ms BIGINT NOT NULL,
|
|
updated_at_ms BIGINT NOT NULL,
|
|
PRIMARY KEY(app_code, platform_code)
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`,
|
|
`CREATE TABLE IF NOT EXISTS game_catalog (
|
|
app_code VARCHAR(32) NOT NULL,
|
|
game_id VARCHAR(96) NOT NULL,
|
|
platform_code VARCHAR(64) NOT NULL,
|
|
provider_game_id VARCHAR(128) NOT NULL,
|
|
game_name VARCHAR(128) NOT NULL,
|
|
category VARCHAR(64) NOT NULL DEFAULT '',
|
|
icon_url VARCHAR(512) NOT NULL DEFAULT '',
|
|
cover_url VARCHAR(512) NOT NULL DEFAULT '',
|
|
launch_mode VARCHAR(32) NOT NULL DEFAULT 'h5_popup',
|
|
orientation VARCHAR(32) NOT NULL DEFAULT 'portrait',
|
|
min_coin BIGINT NOT NULL DEFAULT 0,
|
|
status VARCHAR(32) NOT NULL,
|
|
sort_order INT NOT NULL DEFAULT 0,
|
|
tags JSON NULL,
|
|
created_at_ms BIGINT NOT NULL,
|
|
updated_at_ms BIGINT NOT NULL,
|
|
PRIMARY KEY(app_code, game_id),
|
|
UNIQUE KEY uk_game_provider(app_code, platform_code, provider_game_id),
|
|
KEY idx_game_platform_status(app_code, platform_code, status, sort_order)
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`,
|
|
`CREATE TABLE IF NOT EXISTS game_display_rules (
|
|
app_code VARCHAR(32) NOT NULL,
|
|
rule_id VARCHAR(96) NOT NULL,
|
|
game_id VARCHAR(96) NOT NULL,
|
|
scene VARCHAR(64) NOT NULL,
|
|
region_id BIGINT NOT NULL DEFAULT 0,
|
|
language VARCHAR(32) NOT NULL DEFAULT '',
|
|
platform VARCHAR(16) NOT NULL DEFAULT '',
|
|
visible TINYINT(1) NOT NULL DEFAULT 1,
|
|
enabled TINYINT(1) NOT NULL DEFAULT 1,
|
|
sort_order INT NOT NULL DEFAULT 0,
|
|
created_at_ms BIGINT NOT NULL,
|
|
updated_at_ms BIGINT NOT NULL,
|
|
PRIMARY KEY(app_code, rule_id),
|
|
KEY idx_game_display(app_code, scene, region_id, visible, sort_order)
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`,
|
|
`CREATE TABLE IF NOT EXISTS game_launch_sessions (
|
|
app_code VARCHAR(32) NOT NULL,
|
|
session_id VARCHAR(96) NOT NULL,
|
|
user_id BIGINT NOT NULL,
|
|
display_user_id VARCHAR(32) NOT NULL,
|
|
room_id VARCHAR(96) NOT NULL DEFAULT '',
|
|
scene VARCHAR(64) NOT NULL,
|
|
platform_code VARCHAR(64) NOT NULL,
|
|
game_id VARCHAR(96) NOT NULL,
|
|
provider_game_id VARCHAR(128) NOT NULL,
|
|
launch_token_hash VARCHAR(128) NOT NULL,
|
|
status VARCHAR(32) NOT NULL,
|
|
expires_at_ms BIGINT NOT NULL,
|
|
created_at_ms BIGINT NOT NULL,
|
|
updated_at_ms BIGINT NOT NULL,
|
|
PRIMARY KEY(app_code, session_id),
|
|
KEY idx_game_launch_user(app_code, user_id, created_at_ms),
|
|
KEY idx_game_launch_token(app_code, launch_token_hash)
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`,
|
|
`CREATE TABLE IF NOT EXISTS game_orders (
|
|
app_code VARCHAR(32) NOT NULL,
|
|
order_id VARCHAR(96) NOT NULL,
|
|
platform_code VARCHAR(64) NOT NULL,
|
|
provider_order_id VARCHAR(160) NOT NULL,
|
|
provider_round_id VARCHAR(160) NOT NULL DEFAULT '',
|
|
game_id VARCHAR(96) NOT NULL,
|
|
provider_game_id VARCHAR(128) NOT NULL,
|
|
user_id BIGINT NOT NULL,
|
|
room_id VARCHAR(96) NOT NULL DEFAULT '',
|
|
op_type VARCHAR(32) NOT NULL,
|
|
coin_amount BIGINT NOT NULL,
|
|
status VARCHAR(32) NOT NULL,
|
|
wallet_transaction_id VARCHAR(96) NOT NULL DEFAULT '',
|
|
wallet_balance_after BIGINT NOT NULL DEFAULT 0,
|
|
request_hash VARCHAR(128) NOT NULL,
|
|
raw_payload_ref VARCHAR(256) NOT NULL DEFAULT '',
|
|
failure_code VARCHAR(64) NOT NULL DEFAULT '',
|
|
failure_message VARCHAR(256) NOT NULL DEFAULT '',
|
|
created_at_ms BIGINT NOT NULL,
|
|
updated_at_ms BIGINT NOT NULL,
|
|
PRIMARY KEY(app_code, order_id),
|
|
UNIQUE KEY uk_game_provider_order(app_code, platform_code, provider_order_id),
|
|
KEY idx_game_order_user_time(app_code, user_id, created_at_ms),
|
|
KEY idx_game_order_round(app_code, platform_code, provider_round_id)
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`,
|
|
`CREATE TABLE IF NOT EXISTS game_callback_logs (
|
|
app_code VARCHAR(32) NOT NULL,
|
|
callback_id VARCHAR(96) NOT NULL,
|
|
platform_code VARCHAR(64) NOT NULL,
|
|
operation VARCHAR(64) NOT NULL,
|
|
request_id VARCHAR(96) NOT NULL DEFAULT '',
|
|
provider_request_id VARCHAR(160) NOT NULL DEFAULT '',
|
|
signature_valid TINYINT(1) NOT NULL DEFAULT 0,
|
|
request_hash VARCHAR(128) NOT NULL,
|
|
response_code VARCHAR(64) NOT NULL DEFAULT '',
|
|
response_body_hash VARCHAR(128) NOT NULL DEFAULT '',
|
|
status VARCHAR(32) NOT NULL,
|
|
created_at_ms BIGINT NOT NULL,
|
|
PRIMARY KEY(app_code, callback_id),
|
|
KEY idx_game_callback_platform_time(app_code, platform_code, created_at_ms)
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`,
|
|
`CREATE TABLE IF NOT EXISTS game_repair_orders (
|
|
app_code VARCHAR(32) NOT NULL,
|
|
repair_id VARCHAR(96) NOT NULL,
|
|
platform_code VARCHAR(64) NOT NULL,
|
|
provider_order_id VARCHAR(160) NOT NULL,
|
|
reason VARCHAR(128) NOT NULL,
|
|
status VARCHAR(32) NOT NULL,
|
|
operator_admin_id BIGINT NOT NULL DEFAULT 0,
|
|
attempts INT NOT NULL DEFAULT 0,
|
|
next_run_at_ms BIGINT NOT NULL DEFAULT 0,
|
|
last_error_code VARCHAR(64) NOT NULL DEFAULT '',
|
|
created_at_ms BIGINT NOT NULL,
|
|
updated_at_ms BIGINT NOT NULL,
|
|
PRIMARY KEY(app_code, repair_id),
|
|
UNIQUE KEY uk_game_repair_provider_order(app_code, platform_code, provider_order_id)
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`,
|
|
}
|
|
for _, statement := range statements {
|
|
if _, err := r.db.ExecContext(ctx, statement); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return r.seedDefaults(ctx)
|
|
}
|
|
|
|
func (r *Repository) seedDefaults(ctx context.Context) error {
|
|
if _, err := r.db.ExecContext(ctx,
|
|
`INSERT INTO game_platforms (app_code, platform_code, platform_name, status, api_base_url, sort_order, created_at_ms, updated_at_ms)
|
|
VALUES ('lalu', 'demo', 'Demo Games', 'active', 'https://game.example.local/h5', 10, 0, 0)
|
|
ON DUPLICATE KEY UPDATE platform_name = VALUES(platform_name)`); err != nil {
|
|
return err
|
|
}
|
|
if _, err := r.db.ExecContext(ctx,
|
|
`INSERT INTO game_catalog (
|
|
app_code, game_id, platform_code, provider_game_id, game_name, category, icon_url, cover_url,
|
|
launch_mode, orientation, min_coin, status, sort_order, tags, created_at_ms, updated_at_ms
|
|
) VALUES (
|
|
'lalu', 'demo_rocket_001', 'demo', 'rocket_001', 'Rocket', 'casual',
|
|
'https://cdn.example.com/game/rocket.png', 'https://cdn.example.com/game/rocket-cover.png',
|
|
'h5_popup', 'portrait', 100, 'active', 10, JSON_ARRAY('demo','voice_room'), 0, 0
|
|
) ON DUPLICATE KEY UPDATE game_name = VALUES(game_name)`); err != nil {
|
|
return err
|
|
}
|
|
_, err := r.db.ExecContext(ctx,
|
|
`INSERT INTO game_display_rules (
|
|
app_code, rule_id, game_id, scene, region_id, language, platform, visible, enabled, sort_order, created_at_ms, updated_at_ms
|
|
) VALUES (
|
|
'lalu', 'demo_rocket_voice_default', 'demo_rocket_001', 'voice_room', 0, '', '', 1, 1, 10, 0, 0
|
|
) ON DUPLICATE KEY UPDATE visible = VALUES(visible), enabled = VALUES(enabled)`)
|
|
return err
|
|
}
|
|
|
|
func (r *Repository) ListGames(ctx context.Context, query gameservice.ListGamesQuery) ([]gamedomain.AppGame, error) {
|
|
rows, err := r.db.QueryContext(ctx,
|
|
`SELECT c.game_id, c.platform_code, c.game_name, c.icon_url, c.cover_url, c.category,
|
|
c.launch_mode, c.orientation, c.min_coin, c.status, p.status,
|
|
MIN(CASE WHEN r.sort_order = 0 THEN c.sort_order ELSE r.sort_order END) AS display_sort
|
|
FROM game_catalog c
|
|
JOIN game_platforms p ON p.app_code = c.app_code AND p.platform_code = c.platform_code
|
|
JOIN game_display_rules r ON r.app_code = c.app_code AND r.game_id = c.game_id
|
|
WHERE c.app_code = ?
|
|
AND c.status IN ('active', 'maintenance')
|
|
AND p.status IN ('active', 'maintenance')
|
|
AND r.scene = ?
|
|
AND r.enabled = 1
|
|
AND r.visible = 1
|
|
AND (r.region_id = 0 OR r.region_id = ?)
|
|
AND (r.language = '' OR r.language = ?)
|
|
AND (r.platform = '' OR r.platform = ?)
|
|
GROUP BY c.game_id, c.platform_code, c.game_name, c.icon_url, c.cover_url, c.category,
|
|
c.launch_mode, c.orientation, c.min_coin, c.status, p.status, c.sort_order, p.sort_order
|
|
ORDER BY display_sort ASC, p.sort_order ASC, c.sort_order ASC, c.game_id ASC`,
|
|
appcode.Normalize(query.AppCode), query.Scene, query.RegionID, query.Language, query.ClientPlatform,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
items := []gamedomain.AppGame{}
|
|
for rows.Next() {
|
|
var item gamedomain.AppGame
|
|
var gameStatus, platformStatus string
|
|
if err := rows.Scan(&item.GameID, &item.PlatformCode, &item.Name, &item.IconURL, &item.CoverURL, &item.Category, &item.LaunchMode, &item.Orientation, &item.MinCoin, &gameStatus, &platformStatus, &item.SortOrder); err != nil {
|
|
return nil, err
|
|
}
|
|
item.NameKey = "game." + item.GameID + ".name"
|
|
item.Enabled = gameStatus == gamedomain.StatusActive && platformStatus == gamedomain.StatusActive
|
|
item.Maintenance = gameStatus == gamedomain.StatusMaintenance || platformStatus == gamedomain.StatusMaintenance
|
|
items = append(items, item)
|
|
}
|
|
return items, rows.Err()
|
|
}
|
|
|
|
func (r *Repository) GetLaunchableGame(ctx context.Context, appCode string, gameID string) (gamedomain.LaunchableGame, error) {
|
|
row := r.db.QueryRowContext(ctx,
|
|
`SELECT c.app_code, c.game_id, c.platform_code, c.provider_game_id, c.game_name, c.category,
|
|
c.icon_url, c.cover_url, c.launch_mode, c.orientation, c.min_coin, c.status,
|
|
c.sort_order, COALESCE(CAST(c.tags AS CHAR), '[]'), c.created_at_ms, c.updated_at_ms,
|
|
p.platform_name, p.status, p.api_base_url
|
|
FROM game_catalog c
|
|
JOIN game_platforms p ON p.app_code = c.app_code AND p.platform_code = c.platform_code
|
|
WHERE c.app_code = ? AND c.game_id = ?`,
|
|
appcode.Normalize(appCode), strings.TrimSpace(gameID),
|
|
)
|
|
var item gamedomain.LaunchableGame
|
|
var tagsJSON string
|
|
if err := row.Scan(&item.AppCode, &item.GameID, &item.PlatformCode, &item.ProviderGameID, &item.GameName, &item.Category, &item.IconURL, &item.CoverURL, &item.LaunchMode, &item.Orientation, &item.MinCoin, &item.Status, &item.SortOrder, &tagsJSON, &item.CreatedAtMS, &item.UpdatedAtMS, &item.PlatformName, &item.PlatformStatus, &item.APIBaseURL); err != nil {
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
return gamedomain.LaunchableGame{}, xerr.New(xerr.NotFound, "game not found")
|
|
}
|
|
return gamedomain.LaunchableGame{}, err
|
|
}
|
|
_ = json.Unmarshal([]byte(tagsJSON), &item.Tags)
|
|
return item, nil
|
|
}
|
|
|
|
func (r *Repository) CreateLaunchSession(ctx context.Context, session gamedomain.LaunchSession) error {
|
|
_, err := r.db.ExecContext(ctx,
|
|
`INSERT INTO game_launch_sessions (
|
|
app_code, session_id, user_id, display_user_id, room_id, scene, platform_code, game_id,
|
|
provider_game_id, launch_token_hash, status, expires_at_ms, created_at_ms, updated_at_ms
|
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
appcode.Normalize(session.AppCode), session.SessionID, session.UserID, session.DisplayUserID, session.RoomID, session.Scene,
|
|
session.PlatformCode, session.GameID, session.ProviderGameID, session.LaunchTokenHash, session.Status,
|
|
session.ExpiresAtMS, session.CreatedAtMS, session.UpdatedAtMS,
|
|
)
|
|
return err
|
|
}
|
|
|
|
func (r *Repository) InsertCallbackLog(ctx context.Context, log gamedomain.CallbackLog) error {
|
|
_, err := r.db.ExecContext(ctx,
|
|
`INSERT INTO game_callback_logs (
|
|
app_code, callback_id, platform_code, operation, request_id, provider_request_id,
|
|
signature_valid, request_hash, response_code, response_body_hash, status, created_at_ms
|
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
appcode.Normalize(log.AppCode), log.CallbackID, log.PlatformCode, log.Operation, log.RequestID, log.ProviderRequestID,
|
|
log.SignatureValid, log.RequestHash, log.ResponseCode, log.ResponseBodyHash, log.Status, log.CreatedAtMS,
|
|
)
|
|
return err
|
|
}
|
|
|
|
func (r *Repository) CreateGameOrder(ctx context.Context, order gamedomain.GameOrder) (gamedomain.GameOrder, bool, error) {
|
|
tx, err := r.db.BeginTx(ctx, nil)
|
|
if err != nil {
|
|
return gamedomain.GameOrder{}, false, err
|
|
}
|
|
defer func() { _ = tx.Rollback() }()
|
|
|
|
existing, exists, err := queryOrderForUpdate(ctx, tx, order.AppCode, order.PlatformCode, order.ProviderOrderID)
|
|
if err != nil {
|
|
return gamedomain.GameOrder{}, false, err
|
|
}
|
|
if exists {
|
|
if existing.RequestHash != order.RequestHash {
|
|
_, _ = tx.ExecContext(ctx,
|
|
`UPDATE game_orders SET status = ?, failure_code = ?, failure_message = ?, updated_at_ms = ?
|
|
WHERE app_code = ? AND order_id = ?`,
|
|
gamedomain.OrderStatusConflict, string(xerr.IdempotencyConflict), "provider order payload conflict",
|
|
time.Now().UnixMilli(), appcode.Normalize(order.AppCode), existing.OrderID,
|
|
)
|
|
_ = tx.Commit()
|
|
return gamedomain.GameOrder{}, true, xerr.New(xerr.IdempotencyConflict, "provider order payload conflict")
|
|
}
|
|
if err := tx.Commit(); err != nil {
|
|
return gamedomain.GameOrder{}, false, err
|
|
}
|
|
return existing, true, nil
|
|
}
|
|
nowMs := time.Now().UnixMilli()
|
|
order.AppCode = appcode.Normalize(order.AppCode)
|
|
order.CreatedAtMS = nowMs
|
|
order.UpdatedAtMS = nowMs
|
|
if _, err := tx.ExecContext(ctx,
|
|
`INSERT INTO game_orders (
|
|
app_code, order_id, platform_code, provider_order_id, provider_round_id, game_id, provider_game_id,
|
|
user_id, room_id, op_type, coin_amount, status, request_hash, created_at_ms, updated_at_ms
|
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
order.AppCode, order.OrderID, order.PlatformCode, order.ProviderOrderID, order.ProviderRoundID, order.GameID, order.ProviderGameID,
|
|
order.UserID, order.RoomID, order.OpType, order.CoinAmount, order.Status, order.RequestHash, order.CreatedAtMS, order.UpdatedAtMS,
|
|
); err != nil {
|
|
return gamedomain.GameOrder{}, false, err
|
|
}
|
|
if err := tx.Commit(); err != nil {
|
|
return gamedomain.GameOrder{}, false, err
|
|
}
|
|
return order, false, nil
|
|
}
|
|
|
|
func queryOrderForUpdate(ctx context.Context, tx *sql.Tx, appCode string, platformCode string, providerOrderID string) (gamedomain.GameOrder, bool, error) {
|
|
row := tx.QueryRowContext(ctx,
|
|
`SELECT app_code, order_id, platform_code, provider_order_id, provider_round_id, game_id, provider_game_id,
|
|
user_id, room_id, op_type, coin_amount, status, wallet_transaction_id, wallet_balance_after,
|
|
request_hash, failure_code, failure_message, created_at_ms, updated_at_ms
|
|
FROM game_orders
|
|
WHERE app_code = ? AND platform_code = ? AND provider_order_id = ?
|
|
FOR UPDATE`,
|
|
appcode.Normalize(appCode), strings.TrimSpace(platformCode), strings.TrimSpace(providerOrderID),
|
|
)
|
|
var order gamedomain.GameOrder
|
|
if err := row.Scan(&order.AppCode, &order.OrderID, &order.PlatformCode, &order.ProviderOrderID, &order.ProviderRoundID, &order.GameID, &order.ProviderGameID, &order.UserID, &order.RoomID, &order.OpType, &order.CoinAmount, &order.Status, &order.WalletTransactionID, &order.WalletBalanceAfter, &order.RequestHash, &order.FailureCode, &order.FailureMessage, &order.CreatedAtMS, &order.UpdatedAtMS); err != nil {
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
return gamedomain.GameOrder{}, false, nil
|
|
}
|
|
return gamedomain.GameOrder{}, false, err
|
|
}
|
|
return order, true, nil
|
|
}
|
|
|
|
func (r *Repository) MarkOrderSucceeded(ctx context.Context, appCode string, orderID string, walletTransactionID string, balanceAfter int64, nowMs int64) error {
|
|
_, err := r.db.ExecContext(ctx,
|
|
`UPDATE game_orders
|
|
SET status = ?, wallet_transaction_id = ?, wallet_balance_after = ?, failure_code = '', failure_message = '', updated_at_ms = ?
|
|
WHERE app_code = ? AND order_id = ?`,
|
|
gamedomain.OrderStatusSucceeded, walletTransactionID, balanceAfter, nowMs, appcode.Normalize(appCode), orderID,
|
|
)
|
|
return err
|
|
}
|
|
|
|
func (r *Repository) MarkOrderFailed(ctx context.Context, appCode string, orderID string, status string, code string, message string, nowMs int64) error {
|
|
_, err := r.db.ExecContext(ctx,
|
|
`UPDATE game_orders SET status = ?, failure_code = ?, failure_message = ?, updated_at_ms = ?
|
|
WHERE app_code = ? AND order_id = ?`,
|
|
status, code, truncate(message, 256), nowMs, appcode.Normalize(appCode), orderID,
|
|
)
|
|
return err
|
|
}
|
|
|
|
func (r *Repository) ListPlatforms(ctx context.Context, appCode string, status string) ([]gamedomain.Platform, error) {
|
|
query := `SELECT app_code, platform_code, platform_name, status, api_base_url, sort_order, created_at_ms, updated_at_ms
|
|
FROM game_platforms WHERE app_code = ?`
|
|
args := []any{appcode.Normalize(appCode)}
|
|
if strings.TrimSpace(status) != "" {
|
|
query += " AND status = ?"
|
|
args = append(args, strings.TrimSpace(status))
|
|
}
|
|
query += " ORDER BY sort_order ASC, platform_code ASC"
|
|
rows, err := r.db.QueryContext(ctx, query, args...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var items []gamedomain.Platform
|
|
for rows.Next() {
|
|
var item gamedomain.Platform
|
|
if err := rows.Scan(&item.AppCode, &item.PlatformCode, &item.PlatformName, &item.Status, &item.APIBaseURL, &item.SortOrder, &item.CreatedAtMS, &item.UpdatedAtMS); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, item)
|
|
}
|
|
return items, rows.Err()
|
|
}
|
|
|
|
func (r *Repository) UpsertPlatform(ctx context.Context, platform gamedomain.Platform) (gamedomain.Platform, error) {
|
|
nowMs := time.Now().UnixMilli()
|
|
platform.AppCode = appcode.Normalize(platform.AppCode)
|
|
if platform.CreatedAtMS == 0 {
|
|
platform.CreatedAtMS = nowMs
|
|
}
|
|
platform.UpdatedAtMS = nowMs
|
|
_, err := r.db.ExecContext(ctx,
|
|
`INSERT INTO game_platforms (app_code, platform_code, platform_name, status, api_base_url, sort_order, created_at_ms, updated_at_ms)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
ON DUPLICATE KEY UPDATE platform_name = VALUES(platform_name), status = VALUES(status),
|
|
api_base_url = VALUES(api_base_url), sort_order = VALUES(sort_order), updated_at_ms = VALUES(updated_at_ms)`,
|
|
platform.AppCode, platform.PlatformCode, platform.PlatformName, platform.Status, platform.APIBaseURL, platform.SortOrder, platform.CreatedAtMS, platform.UpdatedAtMS,
|
|
)
|
|
return platform, err
|
|
}
|
|
|
|
func (r *Repository) ListCatalog(ctx context.Context, query gameservice.ListCatalogQuery) ([]gamedomain.CatalogItem, string, error) {
|
|
sqlText := `SELECT app_code, game_id, platform_code, provider_game_id, game_name, category, icon_url, cover_url,
|
|
launch_mode, orientation, min_coin, status, sort_order, COALESCE(CAST(tags AS CHAR), '[]'), created_at_ms, updated_at_ms
|
|
FROM game_catalog WHERE app_code = ?`
|
|
args := []any{appcode.Normalize(query.AppCode)}
|
|
if strings.TrimSpace(query.PlatformCode) != "" {
|
|
sqlText += " AND platform_code = ?"
|
|
args = append(args, strings.TrimSpace(query.PlatformCode))
|
|
}
|
|
if strings.TrimSpace(query.Status) != "" {
|
|
sqlText += " AND status = ?"
|
|
args = append(args, strings.TrimSpace(query.Status))
|
|
}
|
|
sqlText += " ORDER BY sort_order ASC, game_id ASC"
|
|
if query.PageSize > 0 {
|
|
sqlText += " LIMIT ?"
|
|
args = append(args, query.PageSize)
|
|
}
|
|
rows, err := r.db.QueryContext(ctx, sqlText, args...)
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
defer rows.Close()
|
|
var items []gamedomain.CatalogItem
|
|
for rows.Next() {
|
|
item, err := scanCatalog(rows)
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
items = append(items, item)
|
|
}
|
|
return items, "", rows.Err()
|
|
}
|
|
|
|
func (r *Repository) UpsertCatalog(ctx context.Context, item gamedomain.CatalogItem) (gamedomain.CatalogItem, error) {
|
|
nowMs := time.Now().UnixMilli()
|
|
item.AppCode = appcode.Normalize(item.AppCode)
|
|
if item.CreatedAtMS == 0 {
|
|
item.CreatedAtMS = nowMs
|
|
}
|
|
item.UpdatedAtMS = nowMs
|
|
tagsJSON, _ := json.Marshal(item.Tags)
|
|
_, err := r.db.ExecContext(ctx,
|
|
`INSERT INTO game_catalog (
|
|
app_code, game_id, platform_code, provider_game_id, game_name, category, icon_url, cover_url,
|
|
launch_mode, orientation, min_coin, status, sort_order, tags, created_at_ms, updated_at_ms
|
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
ON DUPLICATE KEY UPDATE platform_code = VALUES(platform_code), provider_game_id = VALUES(provider_game_id),
|
|
game_name = VALUES(game_name), category = VALUES(category), icon_url = VALUES(icon_url), cover_url = VALUES(cover_url),
|
|
launch_mode = VALUES(launch_mode), orientation = VALUES(orientation), min_coin = VALUES(min_coin),
|
|
status = VALUES(status), sort_order = VALUES(sort_order), tags = VALUES(tags), updated_at_ms = VALUES(updated_at_ms)`,
|
|
item.AppCode, item.GameID, item.PlatformCode, item.ProviderGameID, item.GameName, item.Category, item.IconURL, item.CoverURL,
|
|
item.LaunchMode, item.Orientation, item.MinCoin, item.Status, item.SortOrder, string(tagsJSON), item.CreatedAtMS, item.UpdatedAtMS,
|
|
)
|
|
return item, err
|
|
}
|
|
|
|
func (r *Repository) SetGameStatus(ctx context.Context, appCode string, gameID string, status string, nowMs int64) (gamedomain.CatalogItem, error) {
|
|
if _, err := r.db.ExecContext(ctx,
|
|
`UPDATE game_catalog SET status = ?, updated_at_ms = ? WHERE app_code = ? AND game_id = ?`,
|
|
status, nowMs, appcode.Normalize(appCode), strings.TrimSpace(gameID),
|
|
); err != nil {
|
|
return gamedomain.CatalogItem{}, err
|
|
}
|
|
return r.getCatalog(ctx, appCode, gameID)
|
|
}
|
|
|
|
func (r *Repository) getCatalog(ctx context.Context, appCode string, gameID string) (gamedomain.CatalogItem, error) {
|
|
row := r.db.QueryRowContext(ctx,
|
|
`SELECT app_code, game_id, platform_code, provider_game_id, game_name, category, icon_url, cover_url,
|
|
launch_mode, orientation, min_coin, status, sort_order, COALESCE(CAST(tags AS CHAR), '[]'), created_at_ms, updated_at_ms
|
|
FROM game_catalog WHERE app_code = ? AND game_id = ?`,
|
|
appcode.Normalize(appCode), strings.TrimSpace(gameID),
|
|
)
|
|
item, err := scanCatalog(row)
|
|
if err != nil {
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
return gamedomain.CatalogItem{}, xerr.New(xerr.NotFound, "game not found")
|
|
}
|
|
return gamedomain.CatalogItem{}, err
|
|
}
|
|
return item, nil
|
|
}
|
|
|
|
type catalogScanner interface {
|
|
Scan(dest ...any) error
|
|
}
|
|
|
|
func scanCatalog(scanner catalogScanner) (gamedomain.CatalogItem, error) {
|
|
var item gamedomain.CatalogItem
|
|
var tagsJSON string
|
|
if err := scanner.Scan(&item.AppCode, &item.GameID, &item.PlatformCode, &item.ProviderGameID, &item.GameName, &item.Category, &item.IconURL, &item.CoverURL, &item.LaunchMode, &item.Orientation, &item.MinCoin, &item.Status, &item.SortOrder, &tagsJSON, &item.CreatedAtMS, &item.UpdatedAtMS); err != nil {
|
|
return gamedomain.CatalogItem{}, err
|
|
}
|
|
_ = json.Unmarshal([]byte(tagsJSON), &item.Tags)
|
|
return item, nil
|
|
}
|
|
|
|
func truncate(value string, max int) string {
|
|
value = strings.TrimSpace(value)
|
|
if len(value) <= max {
|
|
return value
|
|
}
|
|
return value[:max]
|
|
}
|