169 lines
8.4 KiB
Go
169 lines
8.4 KiB
Go
package mysql
|
||
|
||
import (
|
||
"context"
|
||
"database/sql"
|
||
"errors"
|
||
"strings"
|
||
"time"
|
||
|
||
"hyapp/pkg/appcode"
|
||
roomservice "hyapp/services/room-service/internal/room/service"
|
||
)
|
||
|
||
// GetRoomMediaUploadRegistration 按上传 command_id 的主键读取首次授权事实。
|
||
// 该查询只命中 PRIMARY KEY,不扫描媒体表,可安全放在每次专用上传的权限检查之前。
|
||
func (r *Repository) GetRoomMediaUploadRegistration(ctx context.Context, roomID string, commandID string) (roomservice.RoomMediaUploadRegistration, bool, error) {
|
||
row := r.db.QueryRowContext(ctx, `
|
||
SELECT app_code, room_id, command_id, actor_user_id, purpose, media_payload_sha256,
|
||
expected_object_key, object_url, status, content_type, media_format, size_bytes,
|
||
width, height, animated, frame_count, duration_ms, sha256,
|
||
authorized_room_version, created_at_ms, updated_at_ms
|
||
FROM room_media_upload_registrations
|
||
WHERE app_code = ? AND room_id = ? AND command_id = ?
|
||
LIMIT 1`, appcode.FromContext(ctx), strings.TrimSpace(roomID), strings.TrimSpace(commandID))
|
||
registration, err := scanRoomMediaUploadRegistration(row)
|
||
if errors.Is(err, sql.ErrNoRows) {
|
||
return roomservice.RoomMediaUploadRegistration{}, false, nil
|
||
}
|
||
return registration, err == nil, err
|
||
}
|
||
|
||
// GetActiveRoomMediaUploadByObjectKey 是 Save/Send 接受媒体事实前的权威反查。
|
||
// expected_object_key 有唯一索引,查询不会按 URL 或 JSON 字段扫描。
|
||
func (r *Repository) GetActiveRoomMediaUploadByObjectKey(ctx context.Context, objectKey string) (roomservice.RoomMediaUploadRegistration, bool, error) {
|
||
row := r.db.QueryRowContext(ctx, `
|
||
SELECT app_code, room_id, command_id, actor_user_id, purpose, media_payload_sha256,
|
||
expected_object_key, object_url, status, content_type, media_format, size_bytes,
|
||
width, height, animated, frame_count, duration_ms, sha256,
|
||
authorized_room_version, created_at_ms, updated_at_ms
|
||
FROM room_media_upload_registrations
|
||
WHERE app_code = ? AND expected_object_key = ? AND status = 'active'
|
||
LIMIT 1`, appcode.FromContext(ctx), strings.TrimLeft(strings.TrimSpace(objectKey), "/"))
|
||
registration, err := scanRoomMediaUploadRegistration(row)
|
||
if errors.Is(err, sql.ErrNoRows) {
|
||
return roomservice.RoomMediaUploadRegistration{}, false, nil
|
||
}
|
||
return registration, err == nil, err
|
||
}
|
||
|
||
// RegisterRoomMediaUpload 在首次授权通过后原子占用 command_id。
|
||
// 并发重复通过唯一主键收敛到首行,再逐字段比较;不同文件、用户或用途不能被 ON DUPLICATE 静默吞掉。
|
||
func (r *Repository) RegisterRoomMediaUpload(ctx context.Context, registration roomservice.RoomMediaUploadRegistration) (roomservice.RoomMediaUploadRegistration, error) {
|
||
registration.AppCode = appcode.FromContext(ctx)
|
||
registration.RoomID = strings.TrimSpace(registration.RoomID)
|
||
registration.CommandID = strings.TrimSpace(registration.CommandID)
|
||
registration.Purpose = strings.TrimSpace(registration.Purpose)
|
||
registration.MediaPayloadSHA256 = strings.ToLower(strings.TrimSpace(registration.MediaPayloadSHA256))
|
||
registration.ExpectedObjectKey = strings.TrimLeft(strings.TrimSpace(registration.ExpectedObjectKey), "/")
|
||
registration.ObjectURL = ""
|
||
registration.Status = "authorized"
|
||
if registration.CreatedAtMS <= 0 {
|
||
registration.CreatedAtMS = time.Now().UTC().UnixMilli()
|
||
}
|
||
if registration.UpdatedAtMS <= 0 {
|
||
registration.UpdatedAtMS = registration.CreatedAtMS
|
||
}
|
||
_, err := r.db.ExecContext(ctx, `
|
||
INSERT INTO room_media_upload_registrations (
|
||
app_code, room_id, command_id, actor_user_id, purpose, media_payload_sha256,
|
||
expected_object_key, object_url, status, content_type, media_format, size_bytes,
|
||
width, height, animated, frame_count, duration_ms, sha256,
|
||
authorized_room_version, created_at_ms, updated_at_ms
|
||
) VALUES (?, ?, ?, ?, ?, ?, ?, '', 'authorized', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||
ON DUPLICATE KEY UPDATE command_id = room_media_upload_registrations.command_id`,
|
||
registration.AppCode, registration.RoomID, registration.CommandID, registration.ActorUserID,
|
||
registration.Purpose, registration.MediaPayloadSHA256, registration.ExpectedObjectKey,
|
||
registration.Media.ContentType, registration.Media.Format, registration.Media.SizeBytes,
|
||
registration.Media.Width, registration.Media.Height, registration.Media.Animated,
|
||
registration.Media.FrameCount, registration.Media.DurationMS, registration.Media.SHA256, registration.AuthorizedVersion,
|
||
registration.CreatedAtMS, registration.UpdatedAtMS)
|
||
if err != nil {
|
||
return roomservice.RoomMediaUploadRegistration{}, err
|
||
}
|
||
saved, exists, err := r.GetRoomMediaUploadRegistration(ctx, registration.RoomID, registration.CommandID)
|
||
if err != nil {
|
||
return roomservice.RoomMediaUploadRegistration{}, err
|
||
}
|
||
if !exists || !sameRoomMediaUploadRegistration(saved, registration) {
|
||
return roomservice.RoomMediaUploadRegistration{}, roomservice.ErrRoomMediaUploadCommandConflict
|
||
}
|
||
return saved, nil
|
||
}
|
||
|
||
// CompleteRoomMediaUpload 只把首次授权的 expected_object_key 推进为 active。
|
||
// URL 和完整媒体事实必须先由 service 与授权记录逐字段匹配;并发 complete 仍以首个 URL 为准。
|
||
func (r *Repository) CompleteRoomMediaUpload(ctx context.Context, registration roomservice.RoomMediaUploadRegistration) (roomservice.RoomMediaUploadRegistration, error) {
|
||
registration.AppCode = appcode.FromContext(ctx)
|
||
registration.RoomID = strings.TrimSpace(registration.RoomID)
|
||
registration.CommandID = strings.TrimSpace(registration.CommandID)
|
||
registration.ExpectedObjectKey = strings.TrimLeft(strings.TrimSpace(registration.ExpectedObjectKey), "/")
|
||
registration.ObjectURL = strings.TrimSpace(registration.ObjectURL)
|
||
registration.Status = "active"
|
||
if registration.UpdatedAtMS <= 0 {
|
||
registration.UpdatedAtMS = time.Now().UTC().UnixMilli()
|
||
}
|
||
_, err := r.db.ExecContext(ctx, `
|
||
UPDATE room_media_upload_registrations
|
||
SET object_url = ?, status = 'active', updated_at_ms = ?
|
||
WHERE app_code = ? AND room_id = ? AND command_id = ? AND status = 'authorized'`,
|
||
registration.ObjectURL, registration.UpdatedAtMS, registration.AppCode, registration.RoomID, registration.CommandID)
|
||
if err != nil {
|
||
return roomservice.RoomMediaUploadRegistration{}, err
|
||
}
|
||
saved, exists, err := r.GetRoomMediaUploadRegistration(ctx, registration.RoomID, registration.CommandID)
|
||
if err != nil {
|
||
return roomservice.RoomMediaUploadRegistration{}, err
|
||
}
|
||
if !exists || !sameRoomMediaUploadRegistration(saved, registration) ||
|
||
saved.Status != "active" || strings.TrimSpace(saved.ObjectURL) != registration.ObjectURL {
|
||
return roomservice.RoomMediaUploadRegistration{}, roomservice.ErrRoomMediaUploadCommandConflict
|
||
}
|
||
return saved, nil
|
||
}
|
||
|
||
func scanRoomMediaUploadRegistration(scanner interface{ Scan(dest ...any) error }) (roomservice.RoomMediaUploadRegistration, error) {
|
||
var registration roomservice.RoomMediaUploadRegistration
|
||
err := scanner.Scan(
|
||
®istration.AppCode,
|
||
®istration.RoomID,
|
||
®istration.CommandID,
|
||
®istration.ActorUserID,
|
||
®istration.Purpose,
|
||
®istration.MediaPayloadSHA256,
|
||
®istration.ExpectedObjectKey,
|
||
®istration.ObjectURL,
|
||
®istration.Status,
|
||
®istration.Media.ContentType,
|
||
®istration.Media.Format,
|
||
®istration.Media.SizeBytes,
|
||
®istration.Media.Width,
|
||
®istration.Media.Height,
|
||
®istration.Media.Animated,
|
||
®istration.Media.FrameCount,
|
||
®istration.Media.DurationMS,
|
||
®istration.Media.SHA256,
|
||
®istration.AuthorizedVersion,
|
||
®istration.CreatedAtMS,
|
||
®istration.UpdatedAtMS,
|
||
)
|
||
if err == nil {
|
||
registration.Media.ObjectKey = registration.ExpectedObjectKey
|
||
registration.Media.URL = registration.ObjectURL
|
||
if registration.Status == "active" {
|
||
registration.Media.Status = "active"
|
||
}
|
||
}
|
||
return registration, err
|
||
}
|
||
|
||
func sameRoomMediaUploadRegistration(left roomservice.RoomMediaUploadRegistration, right roomservice.RoomMediaUploadRegistration) bool {
|
||
return appcode.Normalize(left.AppCode) == appcode.Normalize(right.AppCode) &&
|
||
strings.TrimSpace(left.RoomID) == strings.TrimSpace(right.RoomID) &&
|
||
strings.TrimSpace(left.CommandID) == strings.TrimSpace(right.CommandID) &&
|
||
left.ActorUserID == right.ActorUserID &&
|
||
strings.TrimSpace(left.Purpose) == strings.TrimSpace(right.Purpose) &&
|
||
strings.EqualFold(strings.TrimSpace(left.MediaPayloadSHA256), strings.TrimSpace(right.MediaPayloadSHA256)) &&
|
||
strings.TrimLeft(strings.TrimSpace(left.ExpectedObjectKey), "/") == strings.TrimLeft(strings.TrimSpace(right.ExpectedObjectKey), "/")
|
||
}
|