121 lines
6.7 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 user
import (
"context"
"database/sql"
"errors"
"strings"
"time"
"hyapp/pkg/appcode"
userdomain "hyapp/services/user-service/internal/domain/user"
)
// GetAvatarUpload 按完整租户主键读取上传凭证PRIMARY KEY 保证资料更新和上传重试不会扫描媒体表。
func (r *Repository) GetAvatarUpload(ctx context.Context, userID int64, uploadID string) (userdomain.AvatarUploadRegistration, bool, error) {
row := r.db.QueryRowContext(ctx, `
SELECT app_code, user_id, avatar_upload_id, client_command_id, media_payload_sha256,
expected_object_key, object_url, status, content_type, media_format, size_bytes,
width, height, animated, frame_count, duration_ms, sha256, created_at_ms, updated_at_ms
FROM user_avatar_upload_registrations
WHERE app_code = ? AND user_id = ? AND avatar_upload_id = ?
LIMIT 1`, appcode.FromContext(ctx), userID, strings.TrimSpace(uploadID))
registration, err := scanAvatarUpload(row)
if errors.Is(err, sql.ErrNoRows) {
return userdomain.AvatarUploadRegistration{}, false, nil
}
return registration, err == nil, err
}
// RegisterAvatarUpload 原子占用客户端 command_id 派生的凭证;并发重试只允许完全相同的媒体载荷。
func (r *Repository) RegisterAvatarUpload(ctx context.Context, registration userdomain.AvatarUploadRegistration) (userdomain.AvatarUploadRegistration, error) {
registration.AppCode = appcode.FromContext(ctx)
registration.UploadID = strings.TrimSpace(registration.UploadID)
registration.ClientCommandID = strings.TrimSpace(registration.ClientCommandID)
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 user_avatar_upload_registrations (
app_code, user_id, avatar_upload_id, client_command_id, media_payload_sha256,
expected_object_key, object_url, status, content_type, media_format, size_bytes,
width, height, animated, frame_count, duration_ms, sha256, created_at_ms, updated_at_ms
) VALUES (?, ?, ?, ?, ?, ?, '', 'authorized', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON DUPLICATE KEY UPDATE avatar_upload_id = user_avatar_upload_registrations.avatar_upload_id`,
registration.AppCode, registration.UserID, registration.UploadID, registration.ClientCommandID,
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.CreatedAtMS, registration.UpdatedAtMS)
if err != nil {
return userdomain.AvatarUploadRegistration{}, err
}
saved, exists, err := r.GetAvatarUpload(ctx, registration.UserID, registration.UploadID)
if err != nil {
return userdomain.AvatarUploadRegistration{}, err
}
if !exists || !sameAvatarUploadRecord(saved, registration) {
return userdomain.AvatarUploadRegistration{}, userdomain.ErrAvatarUploadCommandConflict
}
return saved, nil
}
// CompleteAvatarUpload 只允许 authorized -> active并发完成以首次受信 COS URL 为准。
func (r *Repository) CompleteAvatarUpload(ctx context.Context, registration userdomain.AvatarUploadRegistration) (userdomain.AvatarUploadRegistration, error) {
registration.AppCode = appcode.FromContext(ctx)
registration.ObjectURL = strings.TrimSpace(registration.ObjectURL)
registration.UpdatedAtMS = time.Now().UTC().UnixMilli()
_, err := r.db.ExecContext(ctx, `
UPDATE user_avatar_upload_registrations
SET object_url = ?, status = 'active', updated_at_ms = ?
WHERE app_code = ? AND user_id = ? AND avatar_upload_id = ? AND status = 'authorized'`,
registration.ObjectURL, registration.UpdatedAtMS, registration.AppCode, registration.UserID, registration.UploadID)
if err != nil {
return userdomain.AvatarUploadRegistration{}, err
}
saved, exists, err := r.GetAvatarUpload(ctx, registration.UserID, registration.UploadID)
if err != nil {
return userdomain.AvatarUploadRegistration{}, err
}
if !exists || !sameAvatarUploadRecord(saved, registration) || saved.Status != "active" || saved.ObjectURL != registration.ObjectURL {
return userdomain.AvatarUploadRegistration{}, userdomain.ErrAvatarUploadCommandConflict
}
return saved, nil
}
func scanAvatarUpload(scanner interface{ Scan(dest ...any) error }) (userdomain.AvatarUploadRegistration, error) {
var registration userdomain.AvatarUploadRegistration
err := scanner.Scan(
&registration.AppCode, &registration.UserID, &registration.UploadID, &registration.ClientCommandID,
&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.CreatedAtMS, &registration.UpdatedAtMS,
)
if err != nil {
return userdomain.AvatarUploadRegistration{}, err
}
registration.Media.URL, registration.Media.ObjectKey = registration.ObjectURL, registration.ExpectedObjectKey
registration.Media.Status = registration.Status
return registration, nil
}
func sameAvatarUploadRecord(left userdomain.AvatarUploadRegistration, right userdomain.AvatarUploadRegistration) bool {
return appcode.Normalize(left.AppCode) == appcode.Normalize(right.AppCode) && left.UserID == right.UserID &&
strings.TrimSpace(left.UploadID) == strings.TrimSpace(right.UploadID) &&
strings.TrimSpace(left.ClientCommandID) == strings.TrimSpace(right.ClientCommandID) &&
strings.EqualFold(strings.TrimSpace(left.MediaPayloadSHA256), strings.TrimSpace(right.MediaPayloadSHA256)) &&
strings.TrimLeft(strings.TrimSpace(left.ExpectedObjectKey), "/") == strings.TrimLeft(strings.TrimSpace(right.ExpectedObjectKey), "/") &&
strings.EqualFold(left.Media.ContentType, right.Media.ContentType) && strings.EqualFold(left.Media.Format, right.Media.Format) &&
left.Media.SizeBytes == right.Media.SizeBytes && left.Media.Width == right.Media.Width && left.Media.Height == right.Media.Height &&
left.Media.Animated == right.Media.Animated && left.Media.FrameCount == right.Media.FrameCount &&
left.Media.DurationMS == right.Media.DurationMS && strings.EqualFold(left.Media.SHA256, right.Media.SHA256)
}