121 lines
6.7 KiB
Go
121 lines
6.7 KiB
Go
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(
|
||
®istration.AppCode, ®istration.UserID, ®istration.UploadID, ®istration.ClientCommandID,
|
||
®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.CreatedAtMS, ®istration.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)
|
||
}
|