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

169 lines
8.4 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
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(
&registration.AppCode,
&registration.RoomID,
&registration.CommandID,
&registration.ActorUserID,
&registration.Purpose,
&registration.MediaPayloadSHA256,
&registration.ExpectedObjectKey,
&registration.ObjectURL,
&registration.Status,
&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 {
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), "/")
}