package httpapi import ( "errors" "net/http" "strings" "time" "data-terminal-cloud/internal/deviceauth" "data-terminal-cloud/internal/media" ) func GrantRecordingAccessHandler(service *media.Service) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { principal, ok := resourcePrincipal(w, r, "id") if !ok { return } var input struct { GranteeUserID string `json:"grantee_user_id"` Permission string `json:"permission"` Purpose string `json:"purpose"` ExpiresAt time.Time `json:"expires_at"` } if !decodeWebJSON(w, r, &input, "录音访问授权参数不正确") { return } if !validUUID(input.GranteeUserID) || strings.TrimSpace(input.Purpose) == "" || input.ExpiresAt.IsZero() { writeValidation(w, r, "录音访问授权参数不正确", FieldError{Field: "grant", Message: "被授权用户、权限、用途和到期时间均为必填"}) return } item, err := service.GrantRecordingAccess(r.Context(), principal, r.PathValue("id"), input.GranteeUserID, input.Permission, input.Purpose, RequestID(r.Context()), input.ExpiresAt) if err != nil { writeMediaError(w, r, err) return } WriteJSON(w, r, http.StatusCreated, item) }) } func ListRecordingAccessGrantsHandler(service *media.Service) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { principal, ok := resourcePrincipal(w, r, "id") if !ok { return } items, err := service.ListRecordingAccessGrants(r.Context(), principal, r.PathValue("id")) if err != nil { writeMediaError(w, r, err) return } w.Header().Set("Cache-Control", "no-store") WriteJSON(w, r, http.StatusOK, map[string]any{"items": items}) }) } func RevokeRecordingAccessHandler(service *media.Service) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { principal, ok := resourcePrincipal(w, r, "id") if !ok { return } item, err := service.RevokeRecordingAccess(r.Context(), principal, r.PathValue("id"), RequestID(r.Context())) if err != nil { writeMediaError(w, r, err) return } WriteJSON(w, r, http.StatusOK, item) }) } func GetConversationMediaHandler(service *media.Service) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { principal, ok := resourcePrincipal(w, r, "id") if !ok { return } item, err := service.GetConversationMedia(r.Context(), principal, r.PathValue("id")) if err != nil { writeMediaError(w, r, err) return } w.Header().Set("Cache-Control", "no-store") WriteJSON(w, r, http.StatusOK, item) }) } func CreateMediaAccessHandler(service *media.Service) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { principal, ok := resourcePrincipal(w, r, "id") if !ok { return } var input struct { Action string `json:"action"` Purpose string `json:"purpose"` } if !decodeWebJSON(w, r, &input, "录音访问参数不正确") { return } item, err := service.CreateMediaAccess(r.Context(), principal, r.PathValue("id"), input.Action, input.Purpose, RequestID(r.Context())) if err != nil { writeMediaError(w, r, err) return } w.Header().Set("Cache-Control", "no-store") WriteJSON(w, r, http.StatusOK, item) }) } func PrepareMediaUploadBatchHandler(service *media.Service) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { identity, ok := deviceauth.FromContext(r.Context()) if !ok { WriteError(w, r, http.StatusUnauthorized, "DEVICE_AUTHENTICATION_FAILED", "设备身份验证失败") return } idempotencyKey := strings.TrimSpace(r.Header.Get("Idempotency-Key")) if len(idempotencyKey) < 16 || len(idempotencyKey) > 128 { writeValidation(w, r, "录音上传批次参数不正确", FieldError{Field: "Idempotency-Key", Message: "长度必须为16至128个字符"}) return } var input struct { Segments []media.SegmentDeclaration `json:"segments"` } if !decodeWebJSON(w, r, &input, "录音上传批次参数不正确") { return } item, err := service.PrepareUploadBatch(r.Context(), identity, r.PathValue("id"), idempotencyKey, RequestID(r.Context()), input.Segments) if err != nil { writeMediaError(w, r, err) return } w.Header().Set("Cache-Control", "no-store") WriteJSON(w, r, http.StatusCreated, item) }) } func CompleteMediaUploadBatchHandler(service *media.Service) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { identity, ok := deviceauth.FromContext(r.Context()) if !ok { WriteError(w, r, http.StatusUnauthorized, "DEVICE_AUTHENTICATION_FAILED", "设备身份验证失败") return } item, err := service.CompleteUploadBatch(r.Context(), identity, r.PathValue("id"), RequestID(r.Context())) if err != nil { writeMediaError(w, r, err) return } w.Header().Set("Cache-Control", "no-store") WriteJSON(w, r, http.StatusAccepted, item) }) } func GetDeviceMediaUploadBatchHandler(service *media.Service) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { identity, ok := deviceauth.FromContext(r.Context()) if !ok { WriteError(w, r, http.StatusUnauthorized, "DEVICE_AUTHENTICATION_FAILED", "设备身份验证失败") return } item, err := service.GetUploadBatch(r.Context(), identity, r.PathValue("id"), false) if err != nil { writeMediaError(w, r, err) return } w.Header().Set("Cache-Control", "no-store") WriteJSON(w, r, http.StatusOK, item) }) } func AcknowledgeLocalMediaDeleteHandler(service *media.Service) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { identity, ok := deviceauth.FromContext(r.Context()) if !ok { WriteError(w, r, http.StatusUnauthorized, "DEVICE_AUTHENTICATION_FAILED", "设备身份验证失败") return } var input struct { Status string `json:"status"` ErrorCode string `json:"error_code"` } if !decodeWebJSON(w, r, &input, "本地录音删除回执参数不正确") { return } item, err := service.AcknowledgeLocalDelete(r.Context(), identity, r.PathValue("id"), input.Status, input.ErrorCode, RequestID(r.Context())) if err != nil { writeMediaError(w, r, err) return } w.Header().Set("Cache-Control", "no-store") WriteJSON(w, r, http.StatusOK, item) }) } func writeMediaError(w http.ResponseWriter, r *http.Request, err error) { switch { case errors.Is(err, media.ErrRecordingAccessDenied): WriteError(w, r, http.StatusForbidden, "RECORDING_ACCESS_DENIED", "当前账号没有该录音的播放或下载授权") case errors.Is(err, media.ErrNotFound): WriteError(w, r, http.StatusNotFound, "RESOURCE_NOT_FOUND", "录音上传批次不存在") case errors.Is(err, media.ErrAccessDenied): WriteError(w, r, http.StatusForbidden, "UPLOAD_NOT_ALLOWED", "当前授权、通话状态或设备归属不允许上传录音") case errors.Is(err, media.ErrInvalidDeclaration): writeValidation(w, r, "录音或授权参数不正确", FieldError{Field: "request", Message: "轨道、序号、编码、时长、大小、SHA-256、权限或用途不符合要求"}) case errors.Is(err, media.ErrExpired): WriteError(w, r, http.StatusGone, "UPLOAD_BATCH_EXPIRED", "录音上传批次已经过期") case errors.Is(err, media.ErrConflict): WriteError(w, r, http.StatusConflict, "RESOURCE_STATE_CONFLICT", "录音分片清单与已有数据冲突") case errors.Is(err, media.ErrLocalDeleteTooEarly): WriteError(w, r, http.StatusConflict, "LOCAL_DELETE_NOT_DUE", "尚未到达设备本地录音的允许删除时间") default: WriteError(w, r, http.StatusInternalServerError, "INTERNAL_ERROR", "录音上传处理失败") } }