package mysql import ( "context" "database/sql" "errors" "fmt" "strings" _ "github.com/go-sql-driver/mysql" "hyapp/pkg/appcode" "hyapp/pkg/xerr" activitydomain "hyapp/services/activity-service/internal/domain/activity" ) // Repository 是 activity-service 的 MySQL 存储入口。 type Repository struct { db *sql.DB } // Open 创建 MySQL 连接池并做启动 ping。 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 } return &Repository{db: db}, nil } // Migrate performs narrow, idempotent local schema fixes for activity-service. func (r *Repository) Migrate(ctx context.Context) error { if r == nil || r.db == nil { return xerr.New(xerr.Unavailable, "mysql repository is not configured") } if err := r.ensureLuckyDrawPoolID(ctx); err != nil { return err } return nil } // Close 释放 MySQL 连接池。 func (r *Repository) Close() error { if r == nil || r.db == nil { return nil } return r.db.Close() } // Ping 验证 MySQL 当前可用。 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) } func (r *Repository) ensureLuckyDrawPoolID(ctx context.Context) error { const table = "lucky_draw_records" hasPoolID, err := r.columnExists(ctx, table, "pool_id") if err != nil { return err } if !hasPoolID { if _, err := r.db.ExecContext(ctx, ` ALTER TABLE lucky_draw_records ADD COLUMN pool_id VARCHAR(96) NOT NULL DEFAULT '' COMMENT '幸运礼物奖池 ID' AFTER anchor_id`); err != nil { return err } } if _, err := r.db.ExecContext(ctx, `UPDATE lucky_draw_records SET pool_id = gift_id WHERE pool_id = ''`); err != nil { return err } hasPoolIndex, err := r.indexExists(ctx, table, "idx_lucky_draw_pool") if err != nil { return err } if !hasPoolIndex { if _, err := r.db.ExecContext(ctx, `CREATE INDEX idx_lucky_draw_pool ON lucky_draw_records (app_code, pool_id, created_at_ms)`); err != nil { return err } } return nil } func (r *Repository) columnExists(ctx context.Context, table string, column string) (bool, error) { database, err := r.currentDatabase(ctx) if err != nil { return false, err } var count int if err := r.db.QueryRowContext(ctx, ` SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ? AND COLUMN_NAME = ?`, database, table, column, ).Scan(&count); err != nil { return false, err } return count > 0, nil } func (r *Repository) indexExists(ctx context.Context, table string, index string) (bool, error) { database, err := r.currentDatabase(ctx) if err != nil { return false, err } var count int if err := r.db.QueryRowContext(ctx, ` SELECT COUNT(*) FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ? AND INDEX_NAME = ?`, database, table, index, ).Scan(&count); err != nil { return false, err } return count > 0, nil } func (r *Repository) currentDatabase(ctx context.Context) (string, error) { var database sql.NullString if err := r.db.QueryRowContext(ctx, `SELECT DATABASE()`).Scan(&database); err != nil { return "", err } if !database.Valid || strings.TrimSpace(database.String) == "" { return "", fmt.Errorf("mysql database is not selected") } return database.String, nil } // GetActivity 从 MySQL 活动配置表读取同步查询投影。 func (r *Repository) GetActivity(ctx context.Context, activityID string) (activitydomain.Activity, error) { if r == nil || r.db == nil { return activitydomain.Activity{}, xerr.New(xerr.Unavailable, "mysql repository is not configured") } row := r.db.QueryRowContext(ctx, `SELECT app_code, activity_id, status, updated_at_ms FROM activities WHERE app_code = ? AND activity_id = ?`, appcode.FromContext(ctx), activityID, ) var activity activitydomain.Activity var status string if err := row.Scan(&activity.AppCode, &activity.ActivityID, &status, &activity.UpdatedAtMs); err != nil { if errors.Is(err, sql.ErrNoRows) { // 未配置活动时返回 NotFound,上层可决定是否降级为 inactive。 return activitydomain.Activity{}, xerr.New(xerr.NotFound, "activity not found") } return activitydomain.Activity{}, err } activity.Status = activitydomain.Status(status) return activity, nil }