170 lines
4.5 KiB
Go
170 lines
4.5 KiB
Go
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
|
||
}
|