722 lines
25 KiB
Go
722 lines
25 KiB
Go
package mysql
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"errors"
|
|
"fmt"
|
|
"regexp"
|
|
"strings"
|
|
"time"
|
|
|
|
_ "github.com/go-sql-driver/mysql"
|
|
"hyapp/pkg/appcode"
|
|
"hyapp/pkg/xerr"
|
|
robotdomain "hyapp/services/robot-service/internal/domain/robot"
|
|
)
|
|
|
|
const (
|
|
tableGameRobots = "robot_game_robots"
|
|
tableRoomRobots = "robot_room_robots"
|
|
tableServiceMigrations = "robot_service_migrations"
|
|
migrationGameRobotsV1 = "game_robots_from_game_self_game_robots_v1"
|
|
legacyGameRobotTable = "game_self_game_robots"
|
|
defaultLegacyGameID = robotdomain.DefaultGameID
|
|
)
|
|
|
|
var mysqlIdentifierPattern = regexp.MustCompile(`^[A-Za-z0-9_]+$`)
|
|
|
|
// Repository 是 robot-service 的持久化入口;新逻辑只读写 hyapp_robot 下的 robot_* 表,旧 game_self_game_robots 只作为一次性迁移来源。
|
|
type Repository struct {
|
|
db *sql.DB
|
|
}
|
|
|
|
func Open(ctx context.Context, dsn string) (*Repository, error) {
|
|
db, err := sql.Open("mysql", strings.TrimSpace(dsn))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
db.SetMaxOpenConns(20)
|
|
db.SetMaxIdleConns(10)
|
|
db.SetConnMaxLifetime(30 * time.Minute)
|
|
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, "robot repository is not configured")
|
|
}
|
|
return r.db.PingContext(ctx)
|
|
}
|
|
|
|
func (r *Repository) Migrate(ctx context.Context) error {
|
|
statements := []string{
|
|
`CREATE TABLE IF NOT EXISTS robot_game_robots (
|
|
app_code VARCHAR(32) NOT NULL,
|
|
game_id VARCHAR(96) NOT NULL,
|
|
user_id BIGINT NOT NULL,
|
|
status VARCHAR(32) NOT NULL DEFAULT 'active',
|
|
created_by_admin_id BIGINT NOT NULL DEFAULT 0,
|
|
created_at_ms BIGINT NOT NULL,
|
|
updated_at_ms BIGINT NOT NULL,
|
|
last_used_at_ms BIGINT NOT NULL DEFAULT 0,
|
|
used_count BIGINT NOT NULL DEFAULT 0,
|
|
PRIMARY KEY(app_code, game_id, user_id),
|
|
KEY idx_robot_game_status(app_code, game_id, status, updated_at_ms, user_id),
|
|
KEY idx_robot_game_random_pool(app_code, status, user_id)
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`,
|
|
`CREATE TABLE IF NOT EXISTS robot_room_robots (
|
|
app_code VARCHAR(32) NOT NULL,
|
|
room_scene VARCHAR(64) NOT NULL,
|
|
user_id BIGINT NOT NULL,
|
|
status VARCHAR(32) NOT NULL DEFAULT 'active',
|
|
created_by_admin_id BIGINT NOT NULL DEFAULT 0,
|
|
created_at_ms BIGINT NOT NULL,
|
|
updated_at_ms BIGINT NOT NULL,
|
|
last_used_at_ms BIGINT NOT NULL DEFAULT 0,
|
|
used_count BIGINT NOT NULL DEFAULT 0,
|
|
PRIMARY KEY(app_code, room_scene, user_id),
|
|
KEY idx_robot_room_status(app_code, room_scene, status, updated_at_ms, user_id),
|
|
KEY idx_robot_room_random_pool(app_code, status, user_id)
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`,
|
|
`CREATE TABLE IF NOT EXISTS robot_service_migrations (
|
|
migration_key VARCHAR(128) NOT NULL,
|
|
source_database VARCHAR(64) NOT NULL DEFAULT '',
|
|
source_table VARCHAR(64) NOT NULL DEFAULT '',
|
|
migrated_rows BIGINT NOT NULL DEFAULT 0,
|
|
status VARCHAR(32) NOT NULL,
|
|
created_at_ms BIGINT NOT NULL,
|
|
updated_at_ms BIGINT NOT NULL,
|
|
PRIMARY KEY(migration_key)
|
|
) 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 nil
|
|
}
|
|
|
|
type MigrationResult struct {
|
|
Applied bool
|
|
AlreadyCompleted bool
|
|
SourceFound bool
|
|
LegacyDatabase string
|
|
MigratedRows int64
|
|
}
|
|
|
|
func (r *Repository) MigrateLegacyGameRobots(ctx context.Context, legacyDatabase string) (MigrationResult, error) {
|
|
legacyDatabase = strings.TrimSpace(legacyDatabase)
|
|
result := MigrationResult{LegacyDatabase: legacyDatabase}
|
|
if legacyDatabase == "" {
|
|
return result, nil
|
|
}
|
|
if !mysqlIdentifierPattern.MatchString(legacyDatabase) {
|
|
return result, xerr.New(xerr.InvalidArgument, "legacy game database name is invalid")
|
|
}
|
|
completed, err := r.migrationCompleted(ctx, migrationGameRobotsV1)
|
|
if err != nil {
|
|
return result, err
|
|
}
|
|
if completed {
|
|
result.AlreadyCompleted = true
|
|
return result, nil
|
|
}
|
|
sourceFound, err := r.tableExistsInSchema(ctx, legacyDatabase, legacyGameRobotTable)
|
|
if err != nil {
|
|
return result, err
|
|
}
|
|
result.SourceFound = sourceFound
|
|
if !sourceFound {
|
|
return result, nil
|
|
}
|
|
|
|
tx, err := r.db.BeginTx(ctx, nil)
|
|
if err != nil {
|
|
return result, err
|
|
}
|
|
defer func() { _ = tx.Rollback() }()
|
|
|
|
// 用迁移表行锁串行化首次复制,避免多实例启动时重复读旧表;复制只插入/补齐新表,绝不删除旧 game-service 数据。
|
|
var lockedKey string
|
|
lockErr := tx.QueryRowContext(ctx, `SELECT migration_key FROM robot_service_migrations WHERE migration_key = ? FOR UPDATE`, migrationGameRobotsV1).Scan(&lockedKey)
|
|
if lockErr != nil && !errors.Is(lockErr, sql.ErrNoRows) {
|
|
return result, lockErr
|
|
}
|
|
if lockedKey != "" {
|
|
if err := tx.Commit(); err != nil {
|
|
return result, err
|
|
}
|
|
result.AlreadyCompleted = true
|
|
return result, nil
|
|
}
|
|
|
|
sourceTable := quoteQualifiedIdentifier(legacyDatabase, legacyGameRobotTable)
|
|
var sourceRows int64
|
|
countQuery := fmt.Sprintf(`SELECT COUNT(*) FROM (SELECT app_code, user_id FROM %s GROUP BY app_code, user_id) migrated`, sourceTable)
|
|
if err := tx.QueryRowContext(ctx, countQuery).Scan(&sourceRows); err != nil {
|
|
return result, err
|
|
}
|
|
copyQuery := fmt.Sprintf(
|
|
`INSERT INTO robot_game_robots (
|
|
app_code, game_id, user_id, status, created_by_admin_id, created_at_ms, updated_at_ms, last_used_at_ms, used_count
|
|
)
|
|
SELECT app_code, ? AS game_id, user_id,
|
|
IF(SUM(status = ?) > 0, ?, ?) AS status,
|
|
MIN(created_by_admin_id) AS created_by_admin_id,
|
|
MIN(created_at_ms) AS created_at_ms,
|
|
MAX(updated_at_ms) AS updated_at_ms,
|
|
MAX(last_used_at_ms) AS last_used_at_ms,
|
|
MAX(used_count) AS used_count
|
|
FROM %s
|
|
GROUP BY app_code, user_id
|
|
ON DUPLICATE KEY UPDATE
|
|
status = IF(VALUES(status) = ?, ?, robot_game_robots.status),
|
|
updated_at_ms = GREATEST(robot_game_robots.updated_at_ms, VALUES(updated_at_ms)),
|
|
last_used_at_ms = GREATEST(robot_game_robots.last_used_at_ms, VALUES(last_used_at_ms)),
|
|
used_count = GREATEST(robot_game_robots.used_count, VALUES(used_count))`,
|
|
sourceTable,
|
|
)
|
|
if _, err := tx.ExecContext(ctx, copyQuery,
|
|
defaultLegacyGameID,
|
|
robotdomain.StatusActive,
|
|
robotdomain.StatusActive,
|
|
robotdomain.StatusDisabled,
|
|
robotdomain.StatusActive,
|
|
robotdomain.StatusActive,
|
|
); err != nil {
|
|
return result, err
|
|
}
|
|
nowMS := time.Now().UnixMilli()
|
|
if _, err := tx.ExecContext(ctx,
|
|
`INSERT INTO robot_service_migrations (
|
|
migration_key, source_database, source_table, migrated_rows, status, created_at_ms, updated_at_ms
|
|
) VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
ON DUPLICATE KEY UPDATE updated_at_ms = VALUES(updated_at_ms)`,
|
|
migrationGameRobotsV1, legacyDatabase, legacyGameRobotTable, sourceRows, "completed", nowMS, nowMS,
|
|
); err != nil {
|
|
return result, err
|
|
}
|
|
if err := tx.Commit(); err != nil {
|
|
return result, err
|
|
}
|
|
result.Applied = true
|
|
result.MigratedRows = sourceRows
|
|
return result, nil
|
|
}
|
|
|
|
func (r *Repository) PickGameRobot(ctx context.Context, appCode string, gameID string, excludeUserIDs []int64, nowMS int64) (robotdomain.Robot, error) {
|
|
app := appcode.Normalize(appCode)
|
|
gameID = robotdomain.NormalizeGameID(gameID)
|
|
tx, err := r.db.BeginTx(ctx, nil)
|
|
if err != nil {
|
|
return robotdomain.Robot{}, err
|
|
}
|
|
defer func() { _ = tx.Rollback() }()
|
|
|
|
var item robotdomain.Robot
|
|
err = tx.QueryRowContext(ctx,
|
|
gameRobotPickQuery(excludeUserIDs),
|
|
append([]any{
|
|
gameID, robotdomain.StatusActive, app, robotdomain.StatusActive,
|
|
}, int64SliceToAny(uniquePositiveInt64(excludeUserIDs))...)...,
|
|
).Scan(
|
|
&item.AppCode, &item.GameID, &item.UserID, &item.Status, &item.CreatedByAdminID,
|
|
&item.CreatedAtMS, &item.UpdatedAtMS, &item.LastUsedAtMS, &item.UsedCount,
|
|
)
|
|
if err != nil {
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
return robotdomain.Robot{}, xerr.New(xerr.NotFound, "game robot not found")
|
|
}
|
|
return robotdomain.Robot{}, err
|
|
}
|
|
res, err := tx.ExecContext(ctx,
|
|
`UPDATE robot_game_robots
|
|
SET last_used_at_ms = ?, used_count = used_count + 1
|
|
WHERE app_code = ? AND user_id = ? AND status = ?`,
|
|
nowMS, app, item.UserID, robotdomain.StatusActive,
|
|
)
|
|
if err != nil {
|
|
return robotdomain.Robot{}, err
|
|
}
|
|
affected, _ := res.RowsAffected()
|
|
if affected == 0 {
|
|
return robotdomain.Robot{}, xerr.New(xerr.NotFound, "game robot not found")
|
|
}
|
|
if err := tx.Commit(); err != nil {
|
|
return robotdomain.Robot{}, err
|
|
}
|
|
item.Scope = robotdomain.ScopeGame
|
|
item.LastUsedAtMS = nowMS
|
|
item.UsedCount++
|
|
return item, nil
|
|
}
|
|
|
|
func gameRobotPickQuery(excludeUserIDs []int64) string {
|
|
query := `SELECT r.app_code, ? AS game_id, r.user_id, ? AS status,
|
|
r.created_by_admin_id, r.created_at_ms, r.updated_at_ms,
|
|
r.last_used_at_ms, r.used_count
|
|
FROM robot_game_robots r
|
|
WHERE r.app_code = ? AND r.status = ?`
|
|
if ids := uniquePositiveInt64(excludeUserIDs); len(ids) > 0 {
|
|
query += ` AND r.user_id NOT IN (` + strings.TrimRight(strings.Repeat("?,", len(ids)), ",") + `)`
|
|
}
|
|
query += ` ORDER BY RAND() LIMIT 1 FOR UPDATE SKIP LOCKED`
|
|
return query
|
|
}
|
|
|
|
func (r *Repository) ListGameRobots(ctx context.Context, appCode string, gameID string, status string, pageSize int32, cursor string) ([]robotdomain.Robot, string, error) {
|
|
app := appcode.Normalize(appCode)
|
|
gameID = robotdomain.NormalizeGameID(gameID)
|
|
if pageSize <= 0 || pageSize > 200 {
|
|
pageSize = 50
|
|
}
|
|
status = strings.TrimSpace(status)
|
|
cursorID := int64(0)
|
|
if strings.TrimSpace(cursor) != "" {
|
|
_, _ = fmt.Sscan(strings.TrimSpace(cursor), &cursorID)
|
|
}
|
|
args := []any{gameID, robotdomain.StatusActive, robotdomain.StatusActive, robotdomain.StatusDisabled, app}
|
|
query := `SELECT app_code, game_id, user_id, status, created_by_admin_id, created_at_ms, updated_at_ms, last_used_at_ms, used_count
|
|
FROM (
|
|
SELECT app_code, ? AS game_id, user_id,
|
|
IF(SUM(status = ?) > 0, ?, ?) AS status,
|
|
MIN(created_by_admin_id) AS created_by_admin_id,
|
|
MIN(created_at_ms) AS created_at_ms,
|
|
MAX(updated_at_ms) AS updated_at_ms,
|
|
MAX(last_used_at_ms) AS last_used_at_ms,
|
|
MAX(used_count) AS used_count
|
|
FROM robot_game_robots
|
|
WHERE app_code = ?`
|
|
if cursorID > 0 {
|
|
query += ` AND user_id > ?`
|
|
args = append(args, cursorID)
|
|
}
|
|
query += ` GROUP BY app_code, user_id
|
|
) robots
|
|
WHERE (? = '' OR status = ?)
|
|
ORDER BY user_id ASC LIMIT ?`
|
|
args = append(args, status, status, int(pageSize)+1)
|
|
return scanRobotList(ctx, r.db, robotdomain.ScopeGame, query, args, pageSize)
|
|
}
|
|
|
|
func (r *Repository) RegisterGameRobots(ctx context.Context, appCode string, gameID string, userIDs []int64, adminID int64, nowMS int64) ([]robotdomain.Robot, error) {
|
|
app := appcode.Normalize(appCode)
|
|
gameID = robotdomain.DefaultGameID
|
|
tx, err := r.db.BeginTx(ctx, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer func() { _ = tx.Rollback() }()
|
|
for _, userID := range uniquePositiveInt64(userIDs) {
|
|
if _, err := tx.ExecContext(ctx,
|
|
`INSERT INTO robot_game_robots (
|
|
app_code, game_id, user_id, status, created_by_admin_id, created_at_ms, updated_at_ms,
|
|
last_used_at_ms, used_count
|
|
) VALUES (?, ?, ?, ?, ?, ?, ?, 0, 0)
|
|
ON DUPLICATE KEY UPDATE status = VALUES(status), updated_at_ms = VALUES(updated_at_ms)`,
|
|
app, gameID, userID, robotdomain.StatusActive, adminID, nowMS, nowMS,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
if err := tx.Commit(); err != nil {
|
|
return nil, err
|
|
}
|
|
items, _, err := r.ListGameRobots(ctx, app, "", "", 200, "")
|
|
return items, err
|
|
}
|
|
|
|
func (r *Repository) SetGameRobotStatus(ctx context.Context, appCode string, gameID string, userID int64, status string, nowMS int64) (robotdomain.Robot, error) {
|
|
app := appcode.Normalize(appCode)
|
|
gameID = robotdomain.NormalizeGameID(gameID)
|
|
status = robotdomain.NormalizeStatus(status)
|
|
if status != robotdomain.StatusActive && status != robotdomain.StatusDisabled {
|
|
return robotdomain.Robot{}, xerr.New(xerr.InvalidArgument, "game robot status is invalid")
|
|
}
|
|
res, err := r.db.ExecContext(ctx,
|
|
`UPDATE robot_game_robots
|
|
SET status = ?, updated_at_ms = ?
|
|
WHERE app_code = ? AND user_id = ?`,
|
|
status, nowMS, app, userID,
|
|
)
|
|
if err != nil {
|
|
return robotdomain.Robot{}, err
|
|
}
|
|
affected, _ := res.RowsAffected()
|
|
if affected == 0 {
|
|
return robotdomain.Robot{}, xerr.New(xerr.NotFound, "game robot not found")
|
|
}
|
|
return r.getUniversalGameRobot(ctx, app, gameID, userID)
|
|
}
|
|
|
|
func (r *Repository) DeleteGameRobot(ctx context.Context, appCode string, userID int64) error {
|
|
app := appcode.Normalize(appCode)
|
|
res, err := r.db.ExecContext(ctx, `DELETE FROM robot_game_robots WHERE app_code = ? AND user_id = ?`, app, userID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
affected, _ := res.RowsAffected()
|
|
if affected == 0 {
|
|
return xerr.New(xerr.NotFound, "game robot not found")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (r *Repository) getUniversalGameRobot(ctx context.Context, appCode string, gameID string, userID int64) (robotdomain.Robot, error) {
|
|
var item robotdomain.Robot
|
|
err := r.db.QueryRowContext(ctx,
|
|
`SELECT app_code, ? AS game_id, user_id,
|
|
IF(SUM(status = ?) > 0, ?, ?) AS status,
|
|
MIN(created_by_admin_id), MIN(created_at_ms), MAX(updated_at_ms),
|
|
MAX(last_used_at_ms), MAX(used_count)
|
|
FROM robot_game_robots
|
|
WHERE app_code = ? AND user_id = ?
|
|
GROUP BY app_code, user_id`,
|
|
gameID, robotdomain.StatusActive, robotdomain.StatusActive, robotdomain.StatusDisabled, appCode, userID,
|
|
).Scan(
|
|
&item.AppCode, &item.GameID, &item.UserID, &item.Status, &item.CreatedByAdminID,
|
|
&item.CreatedAtMS, &item.UpdatedAtMS, &item.LastUsedAtMS, &item.UsedCount,
|
|
)
|
|
if err != nil {
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
return robotdomain.Robot{}, xerr.New(xerr.NotFound, "game robot not found")
|
|
}
|
|
return robotdomain.Robot{}, err
|
|
}
|
|
item.Scope = robotdomain.ScopeGame
|
|
return item, nil
|
|
}
|
|
|
|
func (r *Repository) PickRoomRobot(ctx context.Context, appCode string, roomScene string, roomID string, nowMS int64) (robotdomain.Robot, error) {
|
|
app := appcode.Normalize(appCode)
|
|
roomScene = robotdomain.NormalizeRoomScene(roomScene)
|
|
tx, err := r.db.BeginTx(ctx, nil)
|
|
if err != nil {
|
|
return robotdomain.Robot{}, err
|
|
}
|
|
defer func() { _ = tx.Rollback() }()
|
|
var item robotdomain.Robot
|
|
err = tx.QueryRowContext(ctx,
|
|
`SELECT app_code, room_scene, user_id, status, created_by_admin_id, created_at_ms, updated_at_ms, last_used_at_ms, used_count
|
|
FROM robot_room_robots
|
|
WHERE app_code = ? AND room_scene = ? AND status = ?
|
|
ORDER BY RAND()
|
|
LIMIT 1
|
|
FOR UPDATE SKIP LOCKED`,
|
|
app, roomScene, robotdomain.StatusActive,
|
|
).Scan(&item.AppCode, &item.RoomScene, &item.UserID, &item.Status, &item.CreatedByAdminID, &item.CreatedAtMS, &item.UpdatedAtMS, &item.LastUsedAtMS, &item.UsedCount)
|
|
if err != nil {
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
return robotdomain.Robot{}, xerr.New(xerr.NotFound, "room robot not found")
|
|
}
|
|
return robotdomain.Robot{}, err
|
|
}
|
|
res, err := tx.ExecContext(ctx,
|
|
`UPDATE robot_room_robots SET last_used_at_ms = ?, used_count = used_count + 1 WHERE app_code = ? AND room_scene = ? AND user_id = ? AND status = ?`,
|
|
nowMS, app, roomScene, item.UserID, robotdomain.StatusActive,
|
|
)
|
|
if err != nil {
|
|
return robotdomain.Robot{}, err
|
|
}
|
|
affected, _ := res.RowsAffected()
|
|
if affected == 0 {
|
|
return robotdomain.Robot{}, xerr.New(xerr.NotFound, "room robot not found")
|
|
}
|
|
if err := tx.Commit(); err != nil {
|
|
return robotdomain.Robot{}, err
|
|
}
|
|
item.Scope = robotdomain.ScopeRoom
|
|
item.LastUsedAtMS = nowMS
|
|
item.UsedCount++
|
|
return item, nil
|
|
}
|
|
|
|
func (r *Repository) ListRoomRobots(ctx context.Context, appCode string, roomScene string, status string, pageSize int32, cursor string) ([]robotdomain.Robot, string, error) {
|
|
app := appcode.Normalize(appCode)
|
|
roomScene = robotdomain.NormalizeRoomScene(roomScene)
|
|
if pageSize <= 0 || pageSize > 200 {
|
|
pageSize = 50
|
|
}
|
|
status = strings.TrimSpace(status)
|
|
cursorID := int64(0)
|
|
if strings.TrimSpace(cursor) != "" {
|
|
_, _ = fmt.Sscan(strings.TrimSpace(cursor), &cursorID)
|
|
}
|
|
args := []any{app, roomScene}
|
|
query := `SELECT app_code, room_scene, user_id, status, created_by_admin_id, created_at_ms, updated_at_ms, last_used_at_ms, used_count
|
|
FROM robot_room_robots
|
|
WHERE app_code = ? AND room_scene = ?`
|
|
if cursorID > 0 {
|
|
query += ` AND user_id > ?`
|
|
args = append(args, cursorID)
|
|
}
|
|
query += ` AND (? = '' OR status = ?) ORDER BY user_id ASC LIMIT ?`
|
|
args = append(args, status, status, int(pageSize)+1)
|
|
return scanRobotList(ctx, r.db, robotdomain.ScopeRoom, query, args, pageSize)
|
|
}
|
|
|
|
func (r *Repository) RegisterRoomRobots(ctx context.Context, appCode string, roomScene string, userIDs []int64, adminID int64, nowMS int64) ([]robotdomain.Robot, error) {
|
|
app := appcode.Normalize(appCode)
|
|
roomScene = robotdomain.NormalizeRoomScene(roomScene)
|
|
tx, err := r.db.BeginTx(ctx, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer func() { _ = tx.Rollback() }()
|
|
for _, userID := range uniquePositiveInt64(userIDs) {
|
|
if _, err := tx.ExecContext(ctx,
|
|
`INSERT INTO robot_room_robots (
|
|
app_code, room_scene, user_id, status, created_by_admin_id, created_at_ms, updated_at_ms, last_used_at_ms, used_count
|
|
) VALUES (?, ?, ?, ?, ?, ?, ?, 0, 0)
|
|
ON DUPLICATE KEY UPDATE status = VALUES(status), updated_at_ms = VALUES(updated_at_ms)`,
|
|
app, roomScene, userID, robotdomain.StatusActive, adminID, nowMS, nowMS,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
if err := tx.Commit(); err != nil {
|
|
return nil, err
|
|
}
|
|
items, _, err := r.ListRoomRobots(ctx, app, roomScene, "", 200, "")
|
|
return items, err
|
|
}
|
|
|
|
func (r *Repository) SetRoomRobotStatus(ctx context.Context, appCode string, roomScene string, userID int64, status string, nowMS int64) (robotdomain.Robot, error) {
|
|
app := appcode.Normalize(appCode)
|
|
roomScene = robotdomain.NormalizeRoomScene(roomScene)
|
|
status = robotdomain.NormalizeStatus(status)
|
|
if status != robotdomain.StatusActive && status != robotdomain.StatusDisabled {
|
|
return robotdomain.Robot{}, xerr.New(xerr.InvalidArgument, "room robot status is invalid")
|
|
}
|
|
res, err := r.db.ExecContext(ctx,
|
|
`UPDATE robot_room_robots SET status = ?, updated_at_ms = ? WHERE app_code = ? AND room_scene = ? AND user_id = ?`,
|
|
status, nowMS, app, roomScene, userID,
|
|
)
|
|
if err != nil {
|
|
return robotdomain.Robot{}, err
|
|
}
|
|
affected, _ := res.RowsAffected()
|
|
if affected == 0 {
|
|
return robotdomain.Robot{}, xerr.New(xerr.NotFound, "room robot not found")
|
|
}
|
|
return r.getRoomRobot(ctx, app, roomScene, userID)
|
|
}
|
|
|
|
func (r *Repository) DeleteRoomRobot(ctx context.Context, appCode string, roomScene string, userID int64) error {
|
|
app := appcode.Normalize(appCode)
|
|
roomScene = robotdomain.NormalizeRoomScene(roomScene)
|
|
res, err := r.db.ExecContext(ctx, `DELETE FROM robot_room_robots WHERE app_code = ? AND room_scene = ? AND user_id = ?`, app, roomScene, userID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
affected, _ := res.RowsAffected()
|
|
if affected == 0 {
|
|
return xerr.New(xerr.NotFound, "room robot not found")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (r *Repository) getRoomRobot(ctx context.Context, appCode string, roomScene string, userID int64) (robotdomain.Robot, error) {
|
|
var item robotdomain.Robot
|
|
err := r.db.QueryRowContext(ctx,
|
|
`SELECT app_code, room_scene, user_id, status, created_by_admin_id, created_at_ms, updated_at_ms, last_used_at_ms, used_count
|
|
FROM robot_room_robots WHERE app_code = ? AND room_scene = ? AND user_id = ?`,
|
|
appCode, roomScene, userID,
|
|
).Scan(&item.AppCode, &item.RoomScene, &item.UserID, &item.Status, &item.CreatedByAdminID, &item.CreatedAtMS, &item.UpdatedAtMS, &item.LastUsedAtMS, &item.UsedCount)
|
|
if err != nil {
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
return robotdomain.Robot{}, xerr.New(xerr.NotFound, "room robot not found")
|
|
}
|
|
return robotdomain.Robot{}, err
|
|
}
|
|
item.Scope = robotdomain.ScopeRoom
|
|
return item, nil
|
|
}
|
|
|
|
func (r *Repository) dropIndexIfExists(ctx context.Context, tableName string, indexName string) error {
|
|
var count int
|
|
if err := r.db.QueryRowContext(ctx,
|
|
`SELECT COUNT(*)
|
|
FROM information_schema.STATISTICS
|
|
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? AND INDEX_NAME = ?`,
|
|
tableName, indexName,
|
|
).Scan(&count); err != nil {
|
|
return err
|
|
}
|
|
if count == 0 {
|
|
return nil
|
|
}
|
|
_, err := r.db.ExecContext(ctx, `ALTER TABLE `+tableName+` DROP INDEX `+indexName)
|
|
return err
|
|
}
|
|
|
|
func (r *Repository) ensureColumn(ctx context.Context, tableName string, columnName string, columnDDL string) error {
|
|
var count int
|
|
if err := r.db.QueryRowContext(ctx,
|
|
`SELECT COUNT(*)
|
|
FROM information_schema.COLUMNS
|
|
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? AND COLUMN_NAME = ?`,
|
|
tableName, columnName,
|
|
).Scan(&count); err != nil {
|
|
return err
|
|
}
|
|
if count > 0 {
|
|
return nil
|
|
}
|
|
_, err := r.db.ExecContext(ctx, `ALTER TABLE `+tableName+` ADD COLUMN `+columnDDL)
|
|
return err
|
|
}
|
|
|
|
func (r *Repository) ensureIndexDefinition(ctx context.Context, tableName string, indexName string, columns []string, createDDL string) error {
|
|
rows, err := r.db.QueryContext(ctx,
|
|
`SELECT COLUMN_NAME
|
|
FROM information_schema.STATISTICS
|
|
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? AND INDEX_NAME = ?
|
|
ORDER BY SEQ_IN_INDEX`,
|
|
tableName, indexName,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer rows.Close()
|
|
existing := make([]string, 0, len(columns))
|
|
for rows.Next() {
|
|
var column string
|
|
if err := rows.Scan(&column); err != nil {
|
|
return err
|
|
}
|
|
existing = append(existing, column)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return err
|
|
}
|
|
if equalStringSlices(existing, columns) {
|
|
return nil
|
|
}
|
|
if len(existing) > 0 {
|
|
if _, err := r.db.ExecContext(ctx, `ALTER TABLE `+tableName+` DROP INDEX `+indexName); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
_, err = r.db.ExecContext(ctx, createDDL)
|
|
return err
|
|
}
|
|
|
|
func equalStringSlices(left []string, right []string) bool {
|
|
if len(left) != len(right) {
|
|
return false
|
|
}
|
|
for i := range left {
|
|
if left[i] != right[i] {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func (r *Repository) migrationCompleted(ctx context.Context, key string) (bool, error) {
|
|
var count int
|
|
if err := r.db.QueryRowContext(ctx,
|
|
`SELECT COUNT(*) FROM robot_service_migrations WHERE migration_key = ? AND status = ?`,
|
|
key, "completed",
|
|
).Scan(&count); err != nil {
|
|
return false, err
|
|
}
|
|
return count > 0, nil
|
|
}
|
|
|
|
func (r *Repository) tableExistsInSchema(ctx context.Context, schemaName string, tableName string) (bool, error) {
|
|
var count int
|
|
if err := r.db.QueryRowContext(ctx,
|
|
`SELECT COUNT(*)
|
|
FROM information_schema.TABLES
|
|
WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ?`,
|
|
schemaName, tableName,
|
|
).Scan(&count); err != nil {
|
|
return false, err
|
|
}
|
|
return count > 0, nil
|
|
}
|
|
|
|
func quoteQualifiedIdentifier(schemaName string, tableName string) string {
|
|
return quoteIdentifier(schemaName) + "." + quoteIdentifier(tableName)
|
|
}
|
|
|
|
func quoteIdentifier(identifier string) string {
|
|
return "`" + strings.ReplaceAll(identifier, "`", "``") + "`"
|
|
}
|
|
|
|
type queryer interface {
|
|
QueryContext(context.Context, string, ...any) (*sql.Rows, error)
|
|
}
|
|
|
|
func scanRobotList(ctx context.Context, db queryer, scope string, query string, args []any, pageSize int32) ([]robotdomain.Robot, string, error) {
|
|
rows, err := db.QueryContext(ctx, query, args...)
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
defer rows.Close()
|
|
items := []robotdomain.Robot{}
|
|
for rows.Next() {
|
|
var item robotdomain.Robot
|
|
if scope == robotdomain.ScopeRoom {
|
|
if err := rows.Scan(&item.AppCode, &item.RoomScene, &item.UserID, &item.Status, &item.CreatedByAdminID, &item.CreatedAtMS, &item.UpdatedAtMS, &item.LastUsedAtMS, &item.UsedCount); err != nil {
|
|
return nil, "", err
|
|
}
|
|
} else {
|
|
if err := rows.Scan(&item.AppCode, &item.GameID, &item.UserID, &item.Status, &item.CreatedByAdminID, &item.CreatedAtMS, &item.UpdatedAtMS, &item.LastUsedAtMS, &item.UsedCount); err != nil {
|
|
return nil, "", err
|
|
}
|
|
}
|
|
item.Scope = scope
|
|
items = append(items, item)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, "", err
|
|
}
|
|
nextCursor := ""
|
|
if len(items) > int(pageSize) {
|
|
nextCursor = fmt.Sprintf("%d", items[pageSize-1].UserID)
|
|
items = items[:pageSize]
|
|
}
|
|
return items, nextCursor, nil
|
|
}
|
|
|
|
func uniquePositiveInt64(values []int64) []int64 {
|
|
seen := map[int64]struct{}{}
|
|
out := make([]int64, 0, len(values))
|
|
for _, value := range values {
|
|
if value <= 0 {
|
|
continue
|
|
}
|
|
if _, ok := seen[value]; ok {
|
|
continue
|
|
}
|
|
seen[value] = struct{}{}
|
|
out = append(out, value)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func int64SliceToAny(values []int64) []any {
|
|
out := make([]any, 0, len(values))
|
|
for _, value := range values {
|
|
out = append(out, value)
|
|
}
|
|
return out
|
|
}
|