2026-07-17 16:55:22 +08:00

291 lines
12 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 实现 room-service 的 MySQL 持久化边界。
package mysql
import (
"context"
"database/sql"
"errors"
"strings"
"time"
"hyapp/pkg/appcode"
roomservice "hyapp/services/room-service/internal/room/service"
)
// SaveRoomMeta 插入房间元数据。
func (r *Repository) SaveRoomMeta(ctx context.Context, meta roomservice.RoomMeta) error {
// rooms 表只保存恢复初始状态需要的元数据,实时态以 snapshot/command log 为准。
appCode := normalizedRecordAppCode(ctx, meta.AppCode)
nowMS := time.Now().UTC().UnixMilli()
_, err := r.db.ExecContext(ctx,
`INSERT INTO rooms (app_code, room_id, room_short_id, owner_user_id, seat_count, mode, status, room_password_hash, visible_region_id, created_at_ms, updated_at_ms)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
appCode,
meta.RoomID,
meta.RoomShortID,
meta.OwnerUserID,
meta.SeatCount,
meta.Mode,
meta.Status,
meta.RoomPasswordHash,
meta.VisibleRegionID,
nowMS,
nowMS,
)
return mapRoomMetaDuplicateError(err)
}
// GetRoomMeta 查询房间元数据。
func (r *Repository) GetRoomMeta(ctx context.Context, roomID string) (roomservice.RoomMeta, bool, error) {
// meta 是无内存 Cell 恢复的入口;不存在时上层返回 room not found。
row := r.db.QueryRowContext(ctx,
`SELECT app_code, room_id, room_short_id, owner_user_id, seat_count, mode, status, room_password_hash, visible_region_id
FROM rooms
WHERE app_code = ? AND room_id = ?`,
appcode.FromContext(ctx),
roomID,
)
var meta roomservice.RoomMeta
if err := row.Scan(&meta.AppCode, &meta.RoomID, &meta.RoomShortID, &meta.OwnerUserID, &meta.SeatCount, &meta.Mode, &meta.Status, &meta.RoomPasswordHash, &meta.VisibleRegionID); err != nil {
if errors.Is(err, sql.ErrNoRows) {
// 未创建房间不是数据库错误,用 exists=false 表达。
return roomservice.RoomMeta{}, false, nil
}
return roomservice.RoomMeta{}, false, err
}
return meta, true, nil
}
// GetRoomMetaByGlobalID 从全局 room_id 解析房间所属 App。
// 该查询只允许可信内部回调使用uk_rooms_room_id 同时保证查询命中索引并阻止跨 App 歧义。
func (r *Repository) GetRoomMetaByGlobalID(ctx context.Context, roomID string) (roomservice.RoomMeta, bool, error) {
row := r.db.QueryRowContext(ctx,
`SELECT app_code, room_id, room_short_id, owner_user_id, seat_count, mode, status, room_password_hash, visible_region_id
FROM rooms
WHERE room_id = ?
LIMIT 1`,
strings.TrimSpace(roomID),
)
var meta roomservice.RoomMeta
if err := row.Scan(&meta.AppCode, &meta.RoomID, &meta.RoomShortID, &meta.OwnerUserID, &meta.SeatCount, &meta.Mode, &meta.Status, &meta.RoomPasswordHash, &meta.VisibleRegionID); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return roomservice.RoomMeta{}, false, nil
}
return roomservice.RoomMeta{}, false, err
}
return meta, true, nil
}
// GetRoomMetaByShortID 通过展示短房号解析内部房间 ID。
func (r *Repository) GetRoomMetaByShortID(ctx context.Context, roomShortID string) (roomservice.RoomMeta, bool, error) {
// rooms.uk_rooms_short_id 保证同 app 下短房号唯一;这里只做解析,不改变 GetRoomMeta 的精确 room_id 语义。
row := r.db.QueryRowContext(ctx,
`SELECT app_code, room_id, room_short_id, owner_user_id, seat_count, mode, status, room_password_hash, visible_region_id
FROM rooms
WHERE app_code = ? AND room_short_id = ?
LIMIT 1`,
appcode.FromContext(ctx),
strings.TrimSpace(roomShortID),
)
var meta roomservice.RoomMeta
if err := row.Scan(&meta.AppCode, &meta.RoomID, &meta.RoomShortID, &meta.OwnerUserID, &meta.SeatCount, &meta.Mode, &meta.Status, &meta.RoomPasswordHash, &meta.VisibleRegionID); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return roomservice.RoomMeta{}, false, nil
}
return roomservice.RoomMeta{}, false, err
}
return meta, true, nil
}
// GetRoomMetaByOwner 查询用户已经创建的房间。
func (r *Repository) GetRoomMetaByOwner(ctx context.Context, ownerUserID int64) (roomservice.RoomMeta, bool, error) {
// rooms.owner_user_id 有唯一约束,查询最多返回一条;该读路径用于 CreateRoom 前置失败。
row := r.db.QueryRowContext(ctx,
`SELECT app_code, room_id, room_short_id, owner_user_id, seat_count, mode, status, room_password_hash, visible_region_id
FROM rooms
WHERE app_code = ? AND owner_user_id = ?
LIMIT 1`,
appcode.FromContext(ctx),
ownerUserID,
)
var meta roomservice.RoomMeta
if err := row.Scan(&meta.AppCode, &meta.RoomID, &meta.RoomShortID, &meta.OwnerUserID, &meta.SeatCount, &meta.Mode, &meta.Status, &meta.RoomPasswordHash, &meta.VisibleRegionID); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return roomservice.RoomMeta{}, false, nil
}
return roomservice.RoomMeta{}, false, err
}
return meta, true, nil
}
// SaveRoomBackground 保存房间背景图素材。command_id 是素材保存幂等键URL 唯一键只负责去重,
// 不能把相同 command_id 的另一份媒体悄悄折叠成旧记录。
func (r *Repository) SaveRoomBackground(ctx context.Context, background roomservice.RoomBackgroundImage) (roomservice.RoomBackgroundImage, error) {
appCode := normalizedRecordAppCode(ctx, background.AppCode)
roomID := strings.TrimSpace(background.RoomID)
imageURL := strings.TrimSpace(background.ImageURL)
nowMS := background.CreatedAtMS
if nowMS <= 0 {
nowMS = time.Now().UTC().UnixMilli()
}
if existing, exists, err := r.getRoomBackgroundByCommand(ctx, appCode, roomID, background.CommandID); err != nil {
return roomservice.RoomBackgroundImage{}, err
} else if exists {
if !sameRoomBackgroundMedia(existing, background) {
return roomservice.RoomBackgroundImage{}, roomservice.ErrRoomBackgroundCommandConflict
}
return existing, nil
}
if strings.TrimSpace(background.Media.ObjectKey) == "" {
// legacy_timed 以 URL 作为历史幂等键;迁移前的行没有 command_id/media重复保存必须继续返回原素材。
if existing, exists, err := r.getRoomBackgroundByURL(ctx, appCode, roomID, imageURL); err != nil {
return roomservice.RoomBackgroundImage{}, err
} else if exists {
return existing, nil
}
}
_, err := r.db.ExecContext(ctx, `
INSERT INTO room_background_images (
app_code, room_id, image_url, command_id, object_key, content_type, media_format,
size_bytes, width, height, animated, frame_count, duration_ms, poster_url,
thumbnail_url, sha256, media_status, created_by_user_id, created_at_ms, updated_at_ms
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON DUPLICATE KEY UPDATE background_id = LAST_INSERT_ID(background_id)
`, appCode, roomID, imageURL, strings.TrimSpace(background.CommandID), background.Media.ObjectKey,
background.Media.ContentType, background.Media.Format, background.Media.SizeBytes, background.Media.Width,
background.Media.Height, background.Media.Animated, background.Media.FrameCount, background.Media.DurationMS,
background.Media.PosterURL, background.Media.ThumbnailURL, background.Media.SHA256, background.Media.Status,
background.CreatedByUserID, nowMS, nowMS)
if err != nil {
return roomservice.RoomBackgroundImage{}, err
}
saved, exists, err := r.getRoomBackgroundByCommand(ctx, appCode, roomID, background.CommandID)
if err != nil {
return roomservice.RoomBackgroundImage{}, err
}
if !exists || !sameRoomBackgroundMedia(saved, background) {
return roomservice.RoomBackgroundImage{}, roomservice.ErrRoomBackgroundCommandConflict
}
return saved, nil
}
// GetRoomBackground 精确读取一个房间背景图素材。
func (r *Repository) GetRoomBackground(ctx context.Context, roomID string, backgroundID int64) (roomservice.RoomBackgroundImage, bool, error) {
row := r.db.QueryRowContext(ctx, `
SELECT `+roomBackgroundSelectColumns+`
FROM room_background_images
WHERE app_code = ? AND room_id = ? AND background_id = ?
LIMIT 1
`, appcode.FromContext(ctx), strings.TrimSpace(roomID), backgroundID)
background, err := scanRoomBackground(row)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return roomservice.RoomBackgroundImage{}, false, nil
}
return roomservice.RoomBackgroundImage{}, false, err
}
return background, true, nil
}
// GetRoomBackgroundByCommand 供 service 在任何可变权限/房态检查之前重放已保存素材。
// command_id 唯一索引保证最多一行app_code 继续从已验签 RequestMeta context 读取,避免跨租户重放。
func (r *Repository) GetRoomBackgroundByCommand(ctx context.Context, roomID string, commandID string) (roomservice.RoomBackgroundImage, bool, error) {
return r.getRoomBackgroundByCommand(ctx, appcode.FromContext(ctx), strings.TrimSpace(roomID), strings.TrimSpace(commandID))
}
// ListRoomBackgrounds 返回某个房间保存过的背景图素材,按最近保存优先。
func (r *Repository) ListRoomBackgrounds(ctx context.Context, roomID string) ([]roomservice.RoomBackgroundImage, error) {
rows, err := r.db.QueryContext(ctx, `
SELECT `+roomBackgroundSelectColumns+`
FROM room_background_images
WHERE app_code = ? AND room_id = ?
ORDER BY created_at_ms DESC, background_id DESC
`, appcode.FromContext(ctx), strings.TrimSpace(roomID))
if err != nil {
return nil, err
}
defer rows.Close()
backgrounds := []roomservice.RoomBackgroundImage{}
for rows.Next() {
background, err := scanRoomBackground(rows)
if err != nil {
return nil, err
}
backgrounds = append(backgrounds, background)
}
return backgrounds, rows.Err()
}
func scanRoomBackground(scanner interface{ Scan(dest ...any) error }) (roomservice.RoomBackgroundImage, error) {
var background roomservice.RoomBackgroundImage
err := scanner.Scan(
&background.AppCode,
&background.BackgroundID,
&background.RoomID,
&background.ImageURL,
&background.CommandID,
&background.Media.ObjectKey,
&background.Media.ContentType,
&background.Media.Format,
&background.Media.SizeBytes,
&background.Media.Width,
&background.Media.Height,
&background.Media.Animated,
&background.Media.FrameCount,
&background.Media.DurationMS,
&background.Media.PosterURL,
&background.Media.ThumbnailURL,
&background.Media.SHA256,
&background.Media.Status,
&background.CreatedByUserID,
&background.CreatedAtMS,
&background.UpdatedAtMS,
)
return background, err
}
const roomBackgroundSelectColumns = `app_code, background_id, room_id, image_url, COALESCE(command_id, ''),
object_key, content_type, media_format, size_bytes, width, height, animated, frame_count,
duration_ms, poster_url, thumbnail_url, sha256, media_status, created_by_user_id, created_at_ms, updated_at_ms`
func (r *Repository) getRoomBackgroundByCommand(ctx context.Context, appCode string, roomID string, commandID string) (roomservice.RoomBackgroundImage, bool, error) {
row := r.db.QueryRowContext(ctx, `SELECT `+roomBackgroundSelectColumns+`
FROM room_background_images WHERE app_code = ? AND room_id = ? AND command_id = ? LIMIT 1`, appCode, roomID, strings.TrimSpace(commandID))
background, err := scanRoomBackground(row)
if errors.Is(err, sql.ErrNoRows) {
return roomservice.RoomBackgroundImage{}, false, nil
}
return background, err == nil, err
}
func (r *Repository) getRoomBackgroundByURL(ctx context.Context, appCode string, roomID string, imageURL string) (roomservice.RoomBackgroundImage, bool, error) {
row := r.db.QueryRowContext(ctx, `SELECT `+roomBackgroundSelectColumns+`
FROM room_background_images WHERE app_code = ? AND room_id = ? AND image_url = ? LIMIT 1`, appCode, roomID, strings.TrimSpace(imageURL))
background, err := scanRoomBackground(row)
if errors.Is(err, sql.ErrNoRows) {
return roomservice.RoomBackgroundImage{}, false, nil
}
return background, err == nil, err
}
func sameRoomBackgroundMedia(left roomservice.RoomBackgroundImage, right roomservice.RoomBackgroundImage) bool {
return strings.TrimSpace(left.ImageURL) == strings.TrimSpace(right.ImageURL) &&
left.CreatedByUserID == right.CreatedByUserID && left.Media == right.Media
}