package httpapi import ( "encoding/hex" "encoding/json" "errors" "fmt" "net/http" "strconv" "strings" "time" "data-terminal-cloud/internal/concurrency" "data-terminal-cloud/internal/identity" "data-terminal-cloud/internal/pagination" "data-terminal-cloud/internal/saas" "github.com/google/uuid" ) func ListHouseholdsHandler(service *saas.Service) http.Handler { return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { principal, ok := webPrincipal(writer, request) if !ok { return } tenantID := strings.TrimSpace(request.Header.Get("X-Tenant-Id")) if tenantID != "" && !validUUID(tenantID) { writeValidation(writer, request, "客户组织编号不正确", FieldError{Field: "X-Tenant-Id", Message: "必须是UUID"}) return } items, err := service.ListHouseholds(request.Context(), principal, tenantID) if err != nil { writeSaaSError(writer, request, err) return } WriteJSON(writer, request, http.StatusOK, map[string]any{"items": items}) }) } func CreateHouseholdHandler(service *saas.Service) http.Handler { return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { principal, ok := webPrincipal(writer, request) if !ok { return } tenantID := strings.TrimSpace(request.Header.Get("X-Tenant-Id")) if !validUUID(tenantID) { writeValidation(writer, request, "创建家庭参数不正确", FieldError{Field: "X-Tenant-Id", Message: "必须提供有效的客户组织UUID"}) return } var input struct { Name string `json:"name"` } if !decodeWebJSON(writer, request, &input, "创建家庭参数不正确") { return } input.Name = strings.TrimSpace(input.Name) if input.Name == "" || len([]rune(input.Name)) > 200 { writeValidation(writer, request, "创建家庭参数不正确", FieldError{Field: "name", Message: "不能为空且最长200字符"}) return } result, err := service.CreateHousehold(request.Context(), principal, tenantID, input.Name, RequestID(request.Context())) if err != nil { writeSaaSError(writer, request, err) return } writer.Header().Set("ETag", concurrency.FormatETag(result.Version)) WriteJSON(writer, request, http.StatusCreated, result) }) } func GetHouseholdHandler(service *saas.Service) http.Handler { return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { principal, ok := resourcePrincipal(writer, request, "id") if !ok { return } result, err := service.GetHousehold(request.Context(), principal, request.PathValue("id")) if err != nil { writeSaaSError(writer, request, err) return } writer.Header().Set("ETag", concurrency.FormatETag(result.Version)) WriteJSON(writer, request, http.StatusOK, result) }) } func PatchHouseholdHandler(service *saas.Service) http.Handler { return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { principal, ok := resourcePrincipal(writer, request, "id") if !ok { return } expected, err := concurrency.ParseIfMatch(request) if err != nil { writeETagError(writer, request, err) return } var input struct { Name string `json:"name"` } if !decodeWebJSON(writer, request, &input, "修改家庭参数不正确") { return } input.Name = strings.TrimSpace(input.Name) if input.Name == "" || len([]rune(input.Name)) > 200 { writeValidation(writer, request, "修改家庭参数不正确", FieldError{Field: "name", Message: "不能为空且最长200字符"}) return } result, err := service.PatchHousehold(request.Context(), principal, request.PathValue("id"), input.Name, expected, RequestID(request.Context())) if err != nil { writeSaaSError(writer, request, err) return } writer.Header().Set("ETag", concurrency.FormatETag(result.Version)) WriteJSON(writer, request, http.StatusOK, result) }) } func ListHouseholdMembersHandler(service *saas.Service) http.Handler { return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { principal, ok := resourcePrincipal(writer, request, "id") if !ok { return } items, err := service.ListHouseholdMembers(request.Context(), principal, request.PathValue("id")) if err != nil { writeSaaSError(writer, request, err) return } WriteJSON(writer, request, http.StatusOK, map[string]any{"items": items}) }) } func ListMemberInvitationsHandler(service *saas.Service) http.Handler { return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { principal, ok := resourcePrincipal(writer, request, "id") if !ok { return } items, err := service.ListMemberInvitations(request.Context(), principal, request.PathValue("id")) if err != nil { writeSaaSError(writer, request, err) return } WriteJSON(writer, request, http.StatusOK, map[string]any{"items": items}) }) } func CreateMemberInvitationHandler(service *saas.Service) http.Handler { return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { principal, ok := resourcePrincipal(writer, request, "id") if !ok { return } var input struct { InviteeHint string `json:"invitee_hint"` Role identity.Role `json:"role"` ExpiresInHours int `json:"expires_in_hours"` } if !decodeWebJSON(writer, request, &input, "成员邀请参数不正确") { return } input.InviteeHint = strings.TrimSpace(input.InviteeHint) if input.ExpiresInHours == 0 { input.ExpiresInHours = 168 } fieldErrors := make([]FieldError, 0) if len([]rune(input.InviteeHint)) > 255 { fieldErrors = append(fieldErrors, FieldError{Field: "invitee_hint", Message: "最长255字符"}) } if !oneOf(string(input.Role), "admin", "editor", "viewer") { fieldErrors = append(fieldErrors, FieldError{Field: "role", Message: "只允许admin、editor或viewer"}) } if input.ExpiresInHours < 1 || input.ExpiresInHours > 720 { fieldErrors = append(fieldErrors, FieldError{Field: "expires_in_hours", Message: "必须为1至720小时"}) } if len(fieldErrors) > 0 { WriteAPIError(writer, request, http.StatusBadRequest, Error{Code: "VALIDATION_FAILED", Message: "成员邀请参数不正确", FieldErrors: fieldErrors}) return } result, err := service.CreateMemberInvitation(request.Context(), principal, request.PathValue("id"), input.InviteeHint, input.Role, time.Now().UTC().Add(time.Duration(input.ExpiresInHours)*time.Hour), RequestID(request.Context())) if err != nil { writeSaaSError(writer, request, err) return } writer.Header().Set("Cache-Control", "no-store") WriteJSON(writer, request, http.StatusCreated, result) }) } func RevokeMemberInvitationHandler(service *saas.Service) http.Handler { return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { principal, ok := resourcePrincipal(writer, request, "id") if !ok { return } if !validUUID(request.PathValue("invitation_id")) { writeValidation(writer, request, "邀请编号不正确", FieldError{Field: "invitation_id", Message: "必须是UUID"}) return } result, err := service.RevokeMemberInvitation(request.Context(), principal, request.PathValue("id"), request.PathValue("invitation_id"), RequestID(request.Context())) if err != nil { writeSaaSError(writer, request, err) return } WriteJSON(writer, request, http.StatusOK, result) }) } func PreviewMemberInvitationHandler(service *saas.Service) http.Handler { return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { if _, ok := webPrincipal(writer, request); !ok { return } var input struct { Token string `json:"token"` } if !decodeWebJSON(writer, request, &input, "邀请预览参数不正确") { return } input.Token = strings.TrimSpace(input.Token) if len(input.Token) < 32 || len(input.Token) > 128 { writeValidation(writer, request, "邀请预览参数不正确", FieldError{Field: "token", Message: "邀请凭据长度不正确"}) return } result, err := service.PreviewMemberInvitation(request.Context(), input.Token) if err != nil { writeSaaSError(writer, request, err) return } writer.Header().Set("Cache-Control", "no-store") WriteJSON(writer, request, http.StatusOK, result) }) } func AcceptMemberInvitationHandler(service *saas.Service) http.Handler { return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { principal, ok := webPrincipal(writer, request) if !ok { return } var input struct { Token string `json:"token"` } if !decodeWebJSON(writer, request, &input, "接受邀请参数不正确") { return } input.Token = strings.TrimSpace(input.Token) if len(input.Token) < 32 || len(input.Token) > 128 { writeValidation(writer, request, "接受邀请参数不正确", FieldError{Field: "token", Message: "邀请凭据长度不正确"}) return } result, replayed, err := service.AcceptMemberInvitation(request.Context(), principal, input.Token, RequestID(request.Context())) if err != nil { writeSaaSError(writer, request, err) return } if replayed { writer.Header().Set("Idempotency-Replayed", "true") } writer.Header().Set("Cache-Control", "no-store") WriteJSON(writer, request, http.StatusCreated, result) }) } func PatchHouseholdMemberHandler(service *saas.Service) http.Handler { return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { principal, ok := resourcePrincipal(writer, request, "id") if !ok { return } if !validUUID(request.PathValue("member_id")) { writeValidation(writer, request, "成员编号不正确", FieldError{Field: "member_id", Message: "必须是UUID"}) return } expected, err := concurrency.ParseIfMatch(request) if err != nil { writeETagError(writer, request, err) return } var input struct { Role identity.Role `json:"role"` } if !decodeWebJSON(writer, request, &input, "成员角色参数不正确") { return } if !oneOf(string(input.Role), "admin", "editor", "viewer") { writeValidation(writer, request, "成员角色参数不正确", FieldError{Field: "role", Message: "只允许admin、editor或viewer;所有权必须使用转移接口"}) return } result, err := service.PatchHouseholdMemberRole(request.Context(), principal, request.PathValue("id"), request.PathValue("member_id"), input.Role, expected, RequestID(request.Context())) if err != nil { writeSaaSError(writer, request, err) return } writer.Header().Set("ETag", concurrency.FormatETag(result.Version)) WriteJSON(writer, request, http.StatusOK, result) }) } func RemoveHouseholdMemberHandler(service *saas.Service) http.Handler { return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { principal, ok := resourcePrincipal(writer, request, "id") if !ok { return } if !validUUID(request.PathValue("member_id")) { writeValidation(writer, request, "成员编号不正确", FieldError{Field: "member_id", Message: "必须是UUID"}) return } expected, err := concurrency.ParseIfMatch(request) if err != nil { writeETagError(writer, request, err) return } result, err := service.RemoveHouseholdMember(request.Context(), principal, request.PathValue("id"), request.PathValue("member_id"), expected, RequestID(request.Context())) if err != nil { writeSaaSError(writer, request, err) return } writer.Header().Set("ETag", concurrency.FormatETag(result.Version)) WriteJSON(writer, request, http.StatusOK, result) }) } func TransferHouseholdOwnershipHandler(service *saas.Service) http.Handler { return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { principal, ok := resourcePrincipal(writer, request, "id") if !ok { return } expected, err := concurrency.ParseIfMatch(request) if err != nil { writeETagError(writer, request, err) return } var input struct { MemberID string `json:"member_id"` } if !decodeWebJSON(writer, request, &input, "转移所有权参数不正确") { return } if !validUUID(input.MemberID) { writeValidation(writer, request, "转移所有权参数不正确", FieldError{Field: "member_id", Message: "必须是有效成员UUID"}) return } result, err := service.TransferHouseholdOwnership(request.Context(), principal, request.PathValue("id"), input.MemberID, expected, RequestID(request.Context())) if err != nil { writeSaaSError(writer, request, err) return } writer.Header().Set("ETag", concurrency.FormatETag(result.Version)) WriteJSON(writer, request, http.StatusOK, result) }) } func ListSubjectsHandler(service *saas.Service) http.Handler { return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { principal, ok := resourcePrincipal(writer, request, "id") if !ok { return } items, err := service.ListSubjects(request.Context(), principal, request.PathValue("id")) if err != nil { writeSaaSError(writer, request, err) return } WriteJSON(writer, request, http.StatusOK, map[string]any{"items": items}) }) } func CreateSubjectHandler(service *saas.Service) http.Handler { return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { principal, ok := resourcePrincipal(writer, request, "id") if !ok { return } var input saas.CreateSubjectInput if !decodeWebJSON(writer, request, &input, "创建长者参数不正确") { return } input.PreferredName = strings.TrimSpace(input.PreferredName) input.Language = strings.TrimSpace(input.Language) input.Timezone = strings.TrimSpace(input.Timezone) if input.ProfileData == nil { input.ProfileData = map[string]any{} } if input.ConversationPreferences == nil { input.ConversationPreferences = map[string]any{} } if fieldErrors := validateSubject(input.PreferredName, input.Language, input.Timezone, input.ProfileData, input.ConversationPreferences); len(fieldErrors) > 0 { WriteAPIError(writer, request, http.StatusBadRequest, Error{Code: "VALIDATION_FAILED", Message: "创建长者参数不正确", FieldErrors: fieldErrors}) return } result, err := service.CreateSubject(request.Context(), principal, request.PathValue("id"), RequestID(request.Context()), input) if err != nil { writeSaaSError(writer, request, err) return } writer.Header().Set("ETag", concurrency.FormatETag(result.Version)) WriteJSON(writer, request, http.StatusCreated, result) }) } func GetSubjectHandler(service *saas.Service) http.Handler { return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { principal, ok := resourcePrincipal(writer, request, "id") if !ok { return } result, err := service.GetSubject(request.Context(), principal, request.PathValue("id")) if err != nil { writeSaaSError(writer, request, err) return } writer.Header().Set("ETag", concurrency.FormatETag(result.Version)) WriteJSON(writer, request, http.StatusOK, result) }) } func GetConversationReadinessHandler(service *saas.Service) http.Handler { return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { principal, ok := resourcePrincipal(writer, request, "id") if !ok { return } result, err := service.GetConversationReadiness(request.Context(), principal, request.PathValue("id")) if err != nil { writeSaaSError(writer, request, err) return } writer.Header().Set("Cache-Control", "no-store") WriteJSON(writer, request, http.StatusOK, result) }) } func PatchSubjectHandler(service *saas.Service) http.Handler { return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { principal, ok := resourcePrincipal(writer, request, "id") if !ok { return } expected, err := concurrency.ParseIfMatch(request) if err != nil { writeETagError(writer, request, err) return } var input saas.PatchSubjectInput if !decodeWebJSON(writer, request, &input, "修改长者参数不正确") { return } if input.PreferredName == nil && input.Language == nil && input.Timezone == nil && input.ProfileData == nil && input.ConversationPreferences == nil && input.Status == nil { writeValidation(writer, request, "修改长者参数不正确", FieldError{Field: "body", Message: "至少提供一个待修改字段"}) return } fieldErrors := make([]FieldError, 0) if input.PreferredName != nil { value := strings.TrimSpace(*input.PreferredName) input.PreferredName = &value if value == "" || len([]rune(value)) > 120 { fieldErrors = append(fieldErrors, FieldError{Field: "preferred_name", Message: "不能为空且最长120字符"}) } } if input.Language != nil { value := strings.TrimSpace(*input.Language) input.Language = &value if !validLanguage(value) { fieldErrors = append(fieldErrors, FieldError{Field: "language", Message: "必须是有效的语言标签,最长32字符"}) } } if input.Timezone != nil { value := strings.TrimSpace(*input.Timezone) input.Timezone = &value if !validTimezone(value) { fieldErrors = append(fieldErrors, FieldError{Field: "timezone", Message: "必须是有效的IANA时区"}) } } if input.ProfileData != nil { fieldErrors = append(fieldErrors, validateSubjectContext(*input.ProfileData, nil)...) } if input.ConversationPreferences != nil { fieldErrors = append(fieldErrors, validateSubjectContext(nil, *input.ConversationPreferences)...) } if input.Status != nil && *input.Status != "active" && *input.Status != "suspended" { fieldErrors = append(fieldErrors, FieldError{Field: "status", Message: "只允许active或suspended"}) } if len(fieldErrors) > 0 { WriteAPIError(writer, request, http.StatusBadRequest, Error{Code: "VALIDATION_FAILED", Message: "修改长者参数不正确", FieldErrors: fieldErrors}) return } result, err := service.PatchSubject(request.Context(), principal, request.PathValue("id"), RequestID(request.Context()), expected, input) if err != nil { writeSaaSError(writer, request, err) return } writer.Header().Set("ETag", concurrency.FormatETag(result.Version)) WriteJSON(writer, request, http.StatusOK, result) }) } func ListConsentsHandler(service *saas.Service) http.Handler { return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { principal, ok := resourcePrincipal(writer, request, "id") if !ok { return } items, err := service.ListConsents(request.Context(), principal, request.PathValue("id")) if err != nil { writeSaaSError(writer, request, err) return } WriteJSON(writer, request, http.StatusOK, map[string]any{"items": items}) }) } func ListSubjectFactsHandler(service *saas.Service) http.Handler { return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { principal, ok := resourcePrincipal(writer, request, "id") if !ok { return } items, err := service.ListSubjectFacts(request.Context(), principal, request.PathValue("id")) if err != nil { writeSaaSError(writer, request, err) return } WriteJSON(writer, request, http.StatusOK, map[string]any{"items": items}) }) } func CreateSubjectFactHandler(service *saas.Service) http.Handler { return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { principal, ok := resourcePrincipal(writer, request, "id") if !ok { return } var input saas.CreateFactInput if !decodeWebJSON(writer, request, &input, "创建稳定事实参数不正确") { return } normalizeCreateFact(&input) if fieldErrors := validateFact(input.FactType, input.Content, input.SourceType, input.SourceRef, input.ConfirmationStatus, input.Sensitivity, input.ValidFrom, input.ValidTo); len(fieldErrors) > 0 { WriteAPIError(writer, request, http.StatusBadRequest, Error{Code: "VALIDATION_FAILED", Message: "创建稳定事实参数不正确", FieldErrors: fieldErrors}) return } result, err := service.CreateSubjectFact(request.Context(), principal, request.PathValue("id"), RequestID(request.Context()), input) if err != nil { writeSaaSError(writer, request, err) return } writer.Header().Set("ETag", concurrency.FormatETag(result.Version)) WriteJSON(writer, request, http.StatusCreated, result) }) } func PatchSubjectFactHandler(service *saas.Service) http.Handler { return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { principal, ok := resourcePrincipal(writer, request, "id") if !ok { return } if !validUUID(request.PathValue("fact_id")) { writeValidation(writer, request, "事实编号不正确", FieldError{Field: "fact_id", Message: "必须是UUID"}) return } expected, err := concurrency.ParseIfMatch(request) if err != nil { writeETagError(writer, request, err) return } var input saas.PatchFactInput if !decodeWebJSON(writer, request, &input, "修改稳定事实参数不正确") { return } if input.FactType == nil && input.Content == nil && input.SourceRef == nil && input.ConfirmationStatus == nil && input.Sensitivity == nil && input.ValidFrom == nil && input.ValidTo == nil { writeValidation(writer, request, "修改稳定事实参数不正确", FieldError{Field: "body", Message: "至少提供一个待修改字段"}) return } fieldErrors := make([]FieldError, 0) if input.FactType != nil { value := strings.TrimSpace(*input.FactType) input.FactType = &value if value == "" || len(value) > 64 { fieldErrors = append(fieldErrors, FieldError{Field: "fact_type", Message: "不能为空且最长64字符"}) } } if input.Content != nil { value := strings.TrimSpace(*input.Content) input.Content = &value if value == "" || len([]rune(value)) > 4000 { fieldErrors = append(fieldErrors, FieldError{Field: "content", Message: "不能为空且最长4000字符"}) } } if input.SourceRef != nil { value := strings.TrimSpace(*input.SourceRef) input.SourceRef = &value if len(value) > 300 { fieldErrors = append(fieldErrors, FieldError{Field: "source_ref", Message: "最长300字符"}) } } if input.ConfirmationStatus != nil && !oneOf(*input.ConfirmationStatus, "pending", "confirmed", "rejected", "superseded") { fieldErrors = append(fieldErrors, FieldError{Field: "confirmation_status", Message: "状态不正确"}) } if input.Sensitivity != nil && !oneOf(*input.Sensitivity, "normal", "sensitive", "high") { fieldErrors = append(fieldErrors, FieldError{Field: "sensitivity", Message: "敏感等级不正确"}) } if input.ValidFrom != nil && input.ValidTo != nil && input.ValidTo.Before(*input.ValidFrom) { fieldErrors = append(fieldErrors, FieldError{Field: "valid_to", Message: "不能早于valid_from"}) } if len(fieldErrors) > 0 { WriteAPIError(writer, request, http.StatusBadRequest, Error{Code: "VALIDATION_FAILED", Message: "修改稳定事实参数不正确", FieldErrors: fieldErrors}) return } result, err := service.PatchSubjectFact(request.Context(), principal, request.PathValue("id"), request.PathValue("fact_id"), RequestID(request.Context()), expected, input) if err != nil { writeSaaSError(writer, request, err) return } writer.Header().Set("ETag", concurrency.FormatETag(result.Version)) WriteJSON(writer, request, http.StatusOK, result) }) } func GrantConsentHandler(service *saas.Service) http.Handler { return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { principal, ok := resourcePrincipal(writer, request, "id") if !ok { return } var input saas.GrantConsentInput if !decodeWebJSON(writer, request, &input, "授权参数不正确") { return } input.Type = strings.TrimSpace(input.Type) input.PolicyVersion = strings.TrimSpace(input.PolicyVersion) input.NoticeDigest = strings.ToLower(strings.TrimSpace(input.NoticeDigest)) input.EvidenceRef = strings.TrimSpace(input.EvidenceRef) input.GrantedAt = time.Now().UTC() if input.Scope == nil { input.Scope = map[string]any{} } fieldErrors := make([]FieldError, 0) if !saas.IsConsentType(input.Type) { fieldErrors = append(fieldErrors, FieldError{Field: "type", Message: "不是支持的分项授权类型"}) } if input.PolicyVersion == "" || len(input.PolicyVersion) > 64 { fieldErrors = append(fieldErrors, FieldError{Field: "policy_version", Message: "不能为空且最长64字符"}) } if len(input.NoticeDigest) != 64 { fieldErrors = append(fieldErrors, FieldError{Field: "notice_digest", Message: "必须是64位SHA-256十六进制摘要"}) } else if _, err := hex.DecodeString(input.NoticeDigest); err != nil { fieldErrors = append(fieldErrors, FieldError{Field: "notice_digest", Message: "必须是SHA-256十六进制摘要"}) } if len(input.EvidenceRef) > 300 { fieldErrors = append(fieldErrors, FieldError{Field: "evidence_ref", Message: "最长300字符"}) } if input.ExpiresAt != nil && !input.ExpiresAt.After(input.GrantedAt) { fieldErrors = append(fieldErrors, FieldError{Field: "expires_at", Message: "必须晚于授权时间"}) } if len(fieldErrors) > 0 { WriteAPIError(writer, request, http.StatusBadRequest, Error{Code: "VALIDATION_FAILED", Message: "授权参数不正确", FieldErrors: fieldErrors}) return } result, err := service.GrantConsent(request.Context(), principal, request.PathValue("id"), RequestID(request.Context()), input) if err != nil { writeSaaSError(writer, request, err) return } WriteJSON(writer, request, http.StatusCreated, result) }) } func WithdrawConsentHandler(service *saas.Service) http.Handler { return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { principal, ok := resourcePrincipal(writer, request, "id") if !ok { return } if !validUUID(request.PathValue("consent_id")) { writeValidation(writer, request, "授权编号不正确", FieldError{Field: "consent_id", Message: "必须是UUID"}) return } var input struct{} if request.Body != nil && request.ContentLength != 0 && !decodeWebJSON(writer, request, &input, "撤回授权参数不正确") { return } result, err := service.WithdrawConsent(request.Context(), principal, request.PathValue("id"), request.PathValue("consent_id"), RequestID(request.Context())) if err != nil { writeSaaSError(writer, request, err) return } WriteJSON(writer, request, http.StatusOK, result) }) } func ListManagedDevicesHandler(service *saas.Service) http.Handler { return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { principal, ok := webPrincipal(writer, request) if !ok { return } items, err := service.ListDevices(request.Context(), principal) if err != nil { writeSaaSError(writer, request, err) return } WriteJSON(writer, request, http.StatusOK, map[string]any{"items": items}) }) } func ClaimDeviceHandler(service *saas.Service) http.Handler { return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { principal, ok := webPrincipal(writer, request) if !ok { return } var input struct { ClaimCode string `json:"claim_code"` HouseholdID string `json:"household_id"` SubjectID string `json:"subject_id"` } if !decodeWebJSON(writer, request, &input, "设备绑定参数不正确") { return } input.ClaimCode = strings.TrimSpace(input.ClaimCode) fieldErrors := make([]FieldError, 0) if len(input.ClaimCode) < 16 || len(input.ClaimCode) > 128 { fieldErrors = append(fieldErrors, FieldError{Field: "claim_code", Message: "长度必须为16至128字符"}) } if validUUID(input.ClaimCode) { fieldErrors = append(fieldErrors, FieldError{Field: "claim_code", Message: "不能填写设备编号,请输入设备屏幕或维护端生成的一次性绑定码"}) } if !validUUID(input.HouseholdID) { fieldErrors = append(fieldErrors, FieldError{Field: "household_id", Message: "必须是UUID"}) } if !validUUID(input.SubjectID) { fieldErrors = append(fieldErrors, FieldError{Field: "subject_id", Message: "必须是UUID"}) } if len(fieldErrors) > 0 { WriteAPIError(writer, request, http.StatusBadRequest, Error{Code: "VALIDATION_FAILED", Message: "设备绑定参数不正确", FieldErrors: fieldErrors}) return } result, replayed, err := service.ClaimDevice(request.Context(), principal, input.ClaimCode, input.HouseholdID, input.SubjectID, RequestID(request.Context())) if err != nil { writeSaaSError(writer, request, err) return } if replayed { writer.Header().Set("Idempotency-Replayed", "true") } WriteJSON(writer, request, http.StatusCreated, result) }) } func GetManagedDeviceHandler(service *saas.Service) http.Handler { return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { principal, ok := resourcePrincipal(writer, request, "id") if !ok { return } result, err := service.GetDevice(request.Context(), principal, request.PathValue("id")) if err != nil { writeSaaSError(writer, request, err) return } WriteJSON(writer, request, http.StatusOK, result) }) } func GetDeviceBindingImpactHandler(service *saas.Service) http.Handler { return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { principal, ok := resourcePrincipal(writer, request, "id") if !ok { return } action := strings.TrimSpace(request.URL.Query().Get("action")) targetSubjectID := strings.TrimSpace(request.URL.Query().Get("target_subject_id")) fieldErrors := make([]FieldError, 0) if action != saas.DeviceBindingActionUnbind && action != saas.DeviceBindingActionTransfer { fieldErrors = append(fieldErrors, FieldError{Field: "action", Message: "必须是unbind或transfer"}) } if action == saas.DeviceBindingActionTransfer && !validUUID(targetSubjectID) { fieldErrors = append(fieldErrors, FieldError{Field: "target_subject_id", Message: "转移时必须提供目标长者UUID"}) } if len(fieldErrors) > 0 { WriteAPIError(writer, request, http.StatusBadRequest, Error{Code: "VALIDATION_FAILED", Message: "设备绑定影响检查参数不正确", FieldErrors: fieldErrors}) return } result, err := service.GetDeviceBindingImpact(request.Context(), principal, request.PathValue("id"), action, targetSubjectID) if err != nil { writeSaaSError(writer, request, err) return } writer.Header().Set("Cache-Control", "no-store") WriteJSON(writer, request, http.StatusOK, result) }) } func ChangeDeviceBindingHandler(service *saas.Service) http.Handler { return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { principal, ok := resourcePrincipal(writer, request, "id") if !ok { return } var input struct { Action string `json:"action"` ExpectedBindingID string `json:"expected_binding_id"` TargetSubjectID string `json:"target_subject_id"` Reason string `json:"reason"` Acknowledged bool `json:"acknowledged"` } if !decodeWebJSON(writer, request, &input, "设备绑定变更参数不正确") { return } input.Action = strings.TrimSpace(input.Action) input.ExpectedBindingID = strings.TrimSpace(input.ExpectedBindingID) input.TargetSubjectID = strings.TrimSpace(input.TargetSubjectID) input.Reason = strings.TrimSpace(input.Reason) fieldErrors := make([]FieldError, 0) if input.Action != saas.DeviceBindingActionUnbind && input.Action != saas.DeviceBindingActionTransfer { fieldErrors = append(fieldErrors, FieldError{Field: "action", Message: "必须是unbind或transfer"}) } if !validUUID(input.ExpectedBindingID) { fieldErrors = append(fieldErrors, FieldError{Field: "expected_binding_id", Message: "必须是影响检查返回的当前绑定UUID"}) } if input.Action == saas.DeviceBindingActionTransfer && !validUUID(input.TargetSubjectID) { fieldErrors = append(fieldErrors, FieldError{Field: "target_subject_id", Message: "转移时必须提供目标长者UUID"}) } if len(input.Reason) < 3 || len(input.Reason) > 300 { fieldErrors = append(fieldErrors, FieldError{Field: "reason", Message: "长度必须为3至300字符"}) } if !input.Acknowledged { fieldErrors = append(fieldErrors, FieldError{Field: "acknowledged", Message: "必须确认已核对影响范围"}) } if len(fieldErrors) > 0 { WriteAPIError(writer, request, http.StatusBadRequest, Error{Code: "VALIDATION_FAILED", Message: "设备绑定变更参数不正确", FieldErrors: fieldErrors}) return } result, err := service.ChangeDeviceBinding(request.Context(), principal, request.PathValue("id"), RequestID(request.Context()), saas.ChangeDeviceBindingInput{ Action: input.Action, ExpectedBindingID: input.ExpectedBindingID, TargetSubjectID: input.TargetSubjectID, Reason: input.Reason, Acknowledged: input.Acknowledged, }) if err != nil { writeSaaSError(writer, request, err) return } WriteJSON(writer, request, http.StatusOK, result) }) } func GetDevicePresenceHandler(service *saas.Service) http.Handler { return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { principal, ok := resourcePrincipal(writer, request, "id") if !ok { return } result, err := service.GetDevicePresence(request.Context(), principal, request.PathValue("id")) if err != nil { writeSaaSError(writer, request, err) return } writer.Header().Set("Cache-Control", "no-store") WriteJSON(writer, request, http.StatusOK, result) }) } func GetDeviceTwinHandler(service *saas.Service) http.Handler { return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { principal, ok := resourcePrincipal(writer, request, "id") if !ok { return } result, err := service.GetDeviceTwin(request.Context(), principal, request.PathValue("id")) if err != nil { writeSaaSError(writer, request, err) return } writer.Header().Set("ETag", concurrency.FormatETag(result.DesiredRevision)) WriteJSON(writer, request, http.StatusOK, result) }) } func PatchDeviceTwinHandler(service *saas.Service) http.Handler { return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { principal, ok := resourcePrincipal(writer, request, "id") if !ok { return } expected, err := concurrency.ParseIfMatch(request) if err != nil { writeETagError(writer, request, err) return } var input struct { DesiredConfig map[string]any `json:"desired_config"` } if !decodeWebJSON(writer, request, &input, "设备配置参数不正确") { return } if input.DesiredConfig == nil { writeValidation(writer, request, "设备配置参数不正确", FieldError{Field: "desired_config", Message: "必须是JSON对象"}) return } if field, ok := forbiddenDeviceConfigField(input.DesiredConfig, "desired_config"); ok { writeValidation(writer, request, "设备配置包含禁止字段", FieldError{Field: field, Message: "不得下发密钥、令牌、口令或凭据"}) return } allowed := map[string]bool{"volume": true, "interaction": true, "upload": true, "rtc": true, "policy_refs": true} for key := range input.DesiredConfig { if !allowed[key] { writeValidation(writer, request, "设备配置参数不正确", FieldError{Field: "desired_config." + key, Message: "不是允许下发的非敏感配置分类"}) return } } if fieldErrors := validateDesiredDeviceConfig(input.DesiredConfig); len(fieldErrors) > 0 { writeValidation(writer, request, "设备配置参数不正确", fieldErrors...) return } result, err := service.PatchDeviceTwin(request.Context(), principal, request.PathValue("id"), RequestID(request.Context()), expected, input.DesiredConfig) if err != nil { writeSaaSError(writer, request, err) return } writer.Header().Set("ETag", concurrency.FormatETag(result.DesiredRevision)) WriteJSON(writer, request, http.StatusOK, result) }) } func ListSubjectConversationsHandler(service *saas.Service) http.Handler { return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { principal, ok := resourcePrincipal(writer, request, "id") if !ok { return } query := request.URL.Query() limit, err := pagination.ParseLimit(query.Get("limit")) if err != nil { writeValidation(writer, request, "分页参数不正确", FieldError{Field: "limit", Message: "必须是1至100的整数"}) return } filter := saas.ConversationFilter{Status: strings.TrimSpace(query.Get("status")), EngineMode: strings.TrimSpace(query.Get("engine_mode")), Limit: limit, Cursor: strings.TrimSpace(query.Get("cursor"))} fieldErrors := make([]FieldError, 0) if filter.Status != "" && !oneOf(filter.Status, "PREPARING", "ROOM_ISSUED", "STARTING_AGENT", "ACTIVE", "STOPPING", "COMPLETED", "FAILED", "EXPIRED") { fieldErrors = append(fieldErrors, FieldError{Field: "status", Message: "通话状态不正确"}) } if filter.EngineMode != "" && !oneOf(filter.EngineMode, "s2s_pure", "s2s_hybrid", "cascade") { fieldErrors = append(fieldErrors, FieldError{Field: "engine_mode", Message: "语音处理方式不正确"}) } if value := strings.TrimSpace(query.Get("from")); value != "" { parsed, parseErr := time.Parse(time.RFC3339, value) if parseErr != nil { fieldErrors = append(fieldErrors, FieldError{Field: "from", Message: "必须是RFC3339时间"}) } else { filter.From = &parsed } } if value := strings.TrimSpace(query.Get("to")); value != "" { parsed, parseErr := time.Parse(time.RFC3339, value) if parseErr != nil { fieldErrors = append(fieldErrors, FieldError{Field: "to", Message: "必须是RFC3339时间"}) } else { filter.To = &parsed } } if filter.From != nil && filter.To != nil && !filter.From.Before(*filter.To) { fieldErrors = append(fieldErrors, FieldError{Field: "to", Message: "必须晚于from"}) } if len(fieldErrors) > 0 { WriteAPIError(writer, request, http.StatusBadRequest, Error{Code: "VALIDATION_FAILED", Message: "通话查询参数不正确", FieldErrors: fieldErrors}) return } result, err := service.ListSubjectConversations(request.Context(), principal, request.PathValue("id"), filter) if err != nil { writeSaaSError(writer, request, err) return } WriteJSON(writer, request, http.StatusOK, result) }) } func GetConversationHandler(service *saas.Service) http.Handler { return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { principal, ok := resourcePrincipal(writer, request, "id") if !ok { return } result, err := service.GetConversation(request.Context(), principal, request.PathValue("id")) if err != nil { writeSaaSError(writer, request, err) return } WriteJSON(writer, request, http.StatusOK, result) }) } func GetConversationUsageHandler(service *saas.Service) http.Handler { return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { principal, ok := resourcePrincipal(writer, request, "id") if !ok { return } result, err := service.GetConversationUsage(request.Context(), principal, request.PathValue("id")) if err != nil { writeSaaSError(writer, request, err) return } WriteJSON(writer, request, http.StatusOK, result) }) } func ListConversationTurnsHandler(service *saas.Service) http.Handler { return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { principal, ok := resourcePrincipal(writer, request, "id") if !ok { return } limit, err := pagination.ParseLimit(request.URL.Query().Get("limit")) if err != nil { writeValidation(writer, request, "分页参数不正确", FieldError{Field: "limit", Message: "必须是1至100的整数"}) return } result, err := service.ListConversationTurns(request.Context(), principal, request.PathValue("id"), limit, strings.TrimSpace(request.URL.Query().Get("cursor"))) if err != nil { writeSaaSError(writer, request, err) return } WriteJSON(writer, request, http.StatusOK, result) }) } func ListConversationSummariesHandler(service *saas.Service) http.Handler { return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { principal, ok := resourcePrincipal(writer, request, "id") if !ok { return } items, err := service.ListConversationContentSummaries(request.Context(), principal, request.PathValue("id")) if err != nil { writeSaaSError(writer, request, err) return } WriteJSON(writer, request, http.StatusOK, map[string]any{"items": items}) }) } func RegenerateConversationSummaryHandler(service *saas.Service) http.Handler { return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { principal, ok := resourcePrincipal(writer, request, "id") if !ok { return } version, err := service.RegenerateConversationSummary(request.Context(), principal, request.PathValue("id"), RequestID(request.Context())) if err != nil { writeSaaSError(writer, request, err) return } WriteJSON(writer, request, http.StatusAccepted, map[string]any{"generation_version": version, "status": "queued"}) }) } func ListSubjectStoriesHandler(service *saas.Service) http.Handler { return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { principal, ok := resourcePrincipal(writer, request, "id") if !ok { return } status := strings.TrimSpace(request.URL.Query().Get("status")) if status != "" && !oneOf(status, "CANDIDATE", "IN_REVIEW", "CONFIRMED", "REJECTED", "SUPERSEDED") { writeValidation(writer, request, "故事查询参数不正确", FieldError{Field: "status", Message: "故事状态不正确"}) return } items, err := service.ListSubjectStories(request.Context(), principal, request.PathValue("id"), status) if err != nil { writeSaaSError(writer, request, err) return } WriteJSON(writer, request, http.StatusOK, map[string]any{"items": items}) }) } func GetStoryHandler(service *saas.Service) http.Handler { return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { principal, ok := resourcePrincipal(writer, request, "id") if !ok { return } result, err := service.GetStory(request.Context(), principal, request.PathValue("id")) if err != nil { writeSaaSError(writer, request, err) return } writer.Header().Set("ETag", concurrency.FormatETag(result.Version)) WriteJSON(writer, request, http.StatusOK, result) }) } func PatchStoryHandler(service *saas.Service) http.Handler { return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { principal, ok := resourcePrincipal(writer, request, "id") if !ok { return } expected, err := concurrency.ParseIfMatch(request) if err != nil { writeETagError(writer, request, err) return } var input struct { Content string `json:"content"` Reason string `json:"reason"` ErrorType string `json:"error_type"` } if !decodeWebJSON(writer, request, &input, "故事修订参数不正确") { return } input.Content = strings.TrimSpace(input.Content) input.Reason = strings.TrimSpace(input.Reason) input.ErrorType = strings.TrimSpace(input.ErrorType) if input.Content == "" || len([]rune(input.Content)) > 50000 || input.Reason == "" || len([]rune(input.Reason)) > 500 { writeValidation(writer, request, "故事修订参数不正确", FieldError{Field: "content", Message: "不能为空且最长50000字符"}, FieldError{Field: "reason", Message: "不能为空且最长500字符"}) return } if input.ErrorType != "" && !saas.IsContentErrorType(input.ErrorType) { writeValidation(writer, request, "故事修订参数不正确", FieldError{Field: "error_type", Message: "质量问题类型不正确"}) return } result, err := service.ReviseStory(request.Context(), principal, request.PathValue("id"), input.Content, input.Reason, RequestID(request.Context()), expected, input.ErrorType) if err != nil { writeSaaSError(writer, request, err) return } writer.Header().Set("ETag", concurrency.FormatETag(result.Version)) WriteJSON(writer, request, http.StatusOK, result) }) } func UpdateFactCandidateHandler(service *saas.Service) http.Handler { return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { principal, ok := resourcePrincipal(writer, request, "id") if !ok { return } if !validUUID(request.PathValue("fact_id")) { writeValidation(writer, request, "事实候选编号不正确", FieldError{Field: "fact_id", Message: "必须是UUID"}) return } expected, err := concurrency.ParseIfMatch(request) if err != nil { writeETagError(writer, request, err) return } var input struct { FactType *string `json:"fact_type"` Content *string `json:"content"` Sensitivity *string `json:"sensitivity"` Status *string `json:"status"` ErrorType *string `json:"error_type"` Reason string `json:"reason"` } if !decodeWebJSON(writer, request, &input, "事实候选审核参数不正确") { return } input.Reason = strings.TrimSpace(input.Reason) fieldErrors := make([]FieldError, 0) if input.Reason == "" || len([]rune(input.Reason)) > 500 { fieldErrors = append(fieldErrors, FieldError{Field: "reason", Message: "不能为空且最长500字符"}) } if input.FactType != nil { value := strings.TrimSpace(*input.FactType) input.FactType = &value if value == "" || len(value) > 64 { fieldErrors = append(fieldErrors, FieldError{Field: "fact_type", Message: "不能为空且最长64字符"}) } } if input.Content != nil { value := strings.TrimSpace(*input.Content) input.Content = &value if value == "" || len([]rune(value)) > 4000 { fieldErrors = append(fieldErrors, FieldError{Field: "content", Message: "不能为空且最长4000字符"}) } } if input.Sensitivity != nil && *input.Sensitivity != "normal" && *input.Sensitivity != "sensitive" && *input.Sensitivity != "high" { fieldErrors = append(fieldErrors, FieldError{Field: "sensitivity", Message: "必须为normal、sensitive或high"}) } if input.Status != nil && *input.Status != "CANDIDATE" && *input.Status != "CONFIRMED" && *input.Status != "REJECTED" && *input.Status != "SUPERSEDED" { fieldErrors = append(fieldErrors, FieldError{Field: "status", Message: "事实候选状态不正确"}) } if input.ErrorType != nil { value := strings.TrimSpace(*input.ErrorType) input.ErrorType = &value if value == "" || !saas.IsContentErrorType(value) { fieldErrors = append(fieldErrors, FieldError{Field: "error_type", Message: "质量问题类型不正确"}) } } if input.FactType == nil && input.Content == nil && input.Sensitivity == nil && input.Status == nil { fieldErrors = append(fieldErrors, FieldError{Field: "body", Message: "至少提供一个需要修改或审核的字段"}) } if len(fieldErrors) > 0 { WriteAPIError(writer, request, http.StatusBadRequest, Error{Code: "VALIDATION_FAILED", Message: "事实候选审核参数不正确", FieldErrors: fieldErrors}) return } result, err := service.UpdateFactCandidate(request.Context(), principal, request.PathValue("id"), request.PathValue("fact_id"), RequestID(request.Context()), expected, saas.UpdateFactCandidateInput{ FactType: input.FactType, Content: input.Content, Sensitivity: input.Sensitivity, Status: input.Status, ErrorType: input.ErrorType, Reason: input.Reason, }) if err != nil { writeSaaSError(writer, request, err) return } writer.Header().Set("ETag", concurrency.FormatETag(result.Version)) WriteJSON(writer, request, http.StatusOK, result) }) } func ConfirmStoryHandler(service *saas.Service) http.Handler { return storyDecisionHandler(service, true) } func RejectStoryHandler(service *saas.Service) http.Handler { return storyDecisionHandler(service, false) } func storyDecisionHandler(service *saas.Service, confirm bool) http.Handler { return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { principal, ok := resourcePrincipal(writer, request, "id") if !ok { return } expected, err := concurrency.ParseIfMatch(request) if err != nil { writeETagError(writer, request, err) return } var input struct { Reason string `json:"reason"` ErrorType string `json:"error_type"` } if !decodeWebJSON(writer, request, &input, "故事审核参数不正确") { return } input.Reason = strings.TrimSpace(input.Reason) input.ErrorType = strings.TrimSpace(input.ErrorType) if input.Reason == "" || len([]rune(input.Reason)) > 500 { writeValidation(writer, request, "故事审核参数不正确", FieldError{Field: "reason", Message: "不能为空且最长500字符"}) return } if input.ErrorType != "" && !saas.IsContentErrorType(input.ErrorType) { writeValidation(writer, request, "故事审核参数不正确", FieldError{Field: "error_type", Message: "质量问题类型不正确"}) return } if confirm { story, memory, err := service.ConfirmStory(request.Context(), principal, request.PathValue("id"), input.Reason, RequestID(request.Context()), expected) if err != nil { writeSaaSError(writer, request, err) return } writer.Header().Set("ETag", concurrency.FormatETag(story.Version)) WriteJSON(writer, request, http.StatusOK, map[string]any{"story": story, "memory": memory}) return } story, err := service.RejectStory(request.Context(), principal, request.PathValue("id"), input.Reason, RequestID(request.Context()), expected, input.ErrorType) if err != nil { writeSaaSError(writer, request, err) return } writer.Header().Set("ETag", concurrency.FormatETag(story.Version)) WriteJSON(writer, request, http.StatusOK, story) }) } func ListSubjectMemoriesHandler(service *saas.Service) http.Handler { return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { principal, ok := resourcePrincipal(writer, request, "id") if !ok { return } query := strings.TrimSpace(request.URL.Query().Get("q")) if len([]rune(query)) > 200 { writeValidation(writer, request, "记忆查询参数不正确", FieldError{Field: "q", Message: "最长200字符"}) return } items, err := service.ListSubjectMemories(request.Context(), principal, request.PathValue("id"), query) if err != nil { writeSaaSError(writer, request, err) return } WriteJSON(writer, request, http.StatusOK, map[string]any{"items": items}) }) } func GetMemoryHandler(service *saas.Service) http.Handler { return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { principal, ok := resourcePrincipal(writer, request, "id") if !ok { return } result, err := service.GetMemory(request.Context(), principal, request.PathValue("id")) if err != nil { writeSaaSError(writer, request, err) return } writer.Header().Set("ETag", concurrency.FormatETag(result.Version)) WriteJSON(writer, request, http.StatusOK, result) }) } func PatchMemoryHandler(service *saas.Service) http.Handler { return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { principal, ok := resourcePrincipal(writer, request, "id") if !ok { return } expected, err := concurrency.ParseIfMatch(request) if err != nil { writeETagError(writer, request, err) return } var input struct { Content *string `json:"content"` Status *string `json:"status"` ErrorType *string `json:"error_type"` Reason string `json:"reason"` } if !decodeWebJSON(writer, request, &input, "记忆修改参数不正确") { return } input.Reason = strings.TrimSpace(input.Reason) fieldErrors := make([]FieldError, 0) if input.Content == nil && input.Status == nil { fieldErrors = append(fieldErrors, FieldError{Field: "body", Message: "必须提供content或status"}) } if input.Content != nil { trimmed := strings.TrimSpace(*input.Content) input.Content = &trimmed if trimmed == "" || len([]rune(trimmed)) > 50000 { fieldErrors = append(fieldErrors, FieldError{Field: "content", Message: "不能为空且最长50000字符"}) } } if input.Status != nil && !oneOf(*input.Status, "ACTIVE", "DISABLED") { fieldErrors = append(fieldErrors, FieldError{Field: "status", Message: "只允许ACTIVE或DISABLED"}) } if input.ErrorType != nil { value := strings.TrimSpace(*input.ErrorType) input.ErrorType = &value if value == "" || !saas.IsContentErrorType(value) { fieldErrors = append(fieldErrors, FieldError{Field: "error_type", Message: "质量问题类型不正确"}) } } if input.Reason == "" || len([]rune(input.Reason)) > 500 { fieldErrors = append(fieldErrors, FieldError{Field: "reason", Message: "不能为空且最长500字符"}) } if len(fieldErrors) > 0 { WriteAPIError(writer, request, http.StatusBadRequest, Error{Code: "VALIDATION_FAILED", Message: "记忆修改参数不正确", FieldErrors: fieldErrors}) return } result, err := service.UpdateMemory(request.Context(), principal, request.PathValue("id"), RequestID(request.Context()), expected, saas.UpdateMemoryInput{Content: input.Content, Status: input.Status, ErrorType: input.ErrorType, Reason: input.Reason}) if err != nil { writeSaaSError(writer, request, err) return } writer.Header().Set("ETag", concurrency.FormatETag(result.Version)) WriteJSON(writer, request, http.StatusOK, result) }) } func DeleteMemoryHandler(service *saas.Service) http.Handler { return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { principal, ok := resourcePrincipal(writer, request, "id") if !ok { return } expected, err := concurrency.ParseIfMatch(request) if err != nil { writeETagError(writer, request, err) return } var input struct { Reason string `json:"reason"` } if !decodeWebJSON(writer, request, &input, "记忆删除参数不正确") { return } input.Reason = strings.TrimSpace(input.Reason) if input.Reason == "" || len([]rune(input.Reason)) > 500 { writeValidation(writer, request, "记忆删除参数不正确", FieldError{Field: "reason", Message: "不能为空且最长500字符"}) return } result, err := service.RequestMemoryDeletion(request.Context(), principal, request.PathValue("id"), input.Reason, RequestID(request.Context()), expected) if err != nil { writeSaaSError(writer, request, err) return } writer.Header().Set("ETag", concurrency.FormatETag(result.Version)) WriteJSON(writer, request, http.StatusAccepted, result) }) } func ListPrivacyRequestsHandler(service *saas.Service) http.Handler { return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { principal, ok := resourcePrincipal(writer, request, "id") if !ok { return } items, err := service.ListPrivacyRequests(request.Context(), principal, request.PathValue("id")) if err != nil { writeSaaSError(writer, request, err) return } WriteJSON(writer, request, http.StatusOK, map[string]any{"items": items}) }) } func CreatePrivacyRequestHandler(service *saas.Service) http.Handler { return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { principal, ok := resourcePrincipal(writer, request, "id") if !ok { return } idempotencyKey := strings.TrimSpace(request.Header.Get("Idempotency-Key")) if len(idempotencyKey) < 16 || len(idempotencyKey) > 128 { writeValidation(writer, request, "缺少有效的重复请求识别码", FieldError{Field: "Idempotency-Key", Message: "长度必须为16至128字符"}) return } var input struct { RequestType string `json:"request_type"` Reason string `json:"reason"` Confirmation string `json:"confirmation"` Scope struct { Categories []string `json:"categories"` ConsentTypes []string `json:"consent_types"` Corrections struct { PreferredName *string `json:"preferred_name"` Language *string `json:"language"` Timezone *string `json:"timezone"` ProfileData *map[string]any `json:"profile_data"` ConversationPreferences *map[string]any `json:"conversation_preferences"` } `json:"corrections"` } `json:"scope"` } if !decodeWebJSON(writer, request, &input, "隐私请求参数不正确") { return } input.RequestType = strings.ToUpper(strings.TrimSpace(input.RequestType)) if input.RequestType != "ACCESS" && input.RequestType != "CORRECTION" && input.RequestType != "CONSENT_WITHDRAWAL" && input.RequestType != "DELETE" { writeValidation(writer, request, "隐私请求参数不正确", FieldError{Field: "request_type", Message: "必须为ACCESS、CORRECTION、CONSENT_WITHDRAWAL或DELETE"}) return } if input.RequestType == "DELETE" { input.Reason = strings.TrimSpace(input.Reason) input.Confirmation = strings.TrimSpace(input.Confirmation) if input.Reason == "" || len([]rune(input.Reason)) > 500 { writeValidation(writer, request, "删除申请参数不正确", FieldError{Field: "reason", Message: "不能为空且最长500字符"}) return } if input.Confirmation == "" { writeValidation(writer, request, "删除申请参数不正确", FieldError{Field: "confirmation", Message: "必须输入主体称呼进行确认"}) return } } allowed := map[string]bool{"profile": true, "consents": true, "device_bindings": true, "conversations": true, "transcripts": true, "summaries": true, "stories": true, "memories": true} seen := map[string]bool{} categories := make([]string, 0, len(input.Scope.Categories)) for index, raw := range input.Scope.Categories { category := strings.TrimSpace(raw) if !allowed[category] { writeValidation(writer, request, "隐私请求参数不正确", FieldError{Field: fmt.Sprintf("scope.categories[%d]", index), Message: "数据分类不正确"}) return } if !seen[category] { seen[category] = true categories = append(categories, category) } } scope := map[string]any{} if len(categories) > 0 { scope["categories"] = categories } if input.RequestType == "CORRECTION" { corrections := map[string]any{} if input.Scope.Corrections.PreferredName != nil { value := strings.TrimSpace(*input.Scope.Corrections.PreferredName) if len([]rune(value)) < 1 || len([]rune(value)) > 120 { writeValidation(writer, request, "更正申请参数不正确", FieldError{Field: "scope.corrections.preferred_name", Message: "长度必须为1至120字符"}) return } corrections["preferred_name"] = value } if input.Scope.Corrections.Language != nil { value := strings.TrimSpace(*input.Scope.Corrections.Language) if len(value) < 2 || len(value) > 20 { writeValidation(writer, request, "更正申请参数不正确", FieldError{Field: "scope.corrections.language", Message: "长度必须为2至20字符"}) return } corrections["language"] = value } if input.Scope.Corrections.Timezone != nil { value := strings.TrimSpace(*input.Scope.Corrections.Timezone) if _, err := time.LoadLocation(value); err != nil { writeValidation(writer, request, "更正申请参数不正确", FieldError{Field: "scope.corrections.timezone", Message: "必须是有效IANA时区"}) return } corrections["timezone"] = value } if input.Scope.Corrections.ProfileData != nil { corrections["profile_data"] = *input.Scope.Corrections.ProfileData } if input.Scope.Corrections.ConversationPreferences != nil { corrections["conversation_preferences"] = *input.Scope.Corrections.ConversationPreferences } if len(corrections) == 0 { writeValidation(writer, request, "更正申请参数不正确", FieldError{Field: "scope.corrections", Message: "至少提供一个需要更正的字段"}) return } scope = map[string]any{"corrections": corrections} } if input.RequestType == "CONSENT_WITHDRAWAL" { allowedConsentTypes := map[string]bool{ saas.ConsentTranscriptStorage: true, saas.ConsentRawAudioStorage: true, saas.ConsentStoryExtraction: true, saas.ConsentPersonalMemory: true, saas.ConsentFamilySharing: true, } seenConsentTypes := map[string]bool{} consentTypes := make([]string, 0, len(input.Scope.ConsentTypes)) for index, raw := range input.Scope.ConsentTypes { consentType := strings.TrimSpace(raw) if !allowedConsentTypes[consentType] { writeValidation(writer, request, "授权撤回申请参数不正确", FieldError{Field: fmt.Sprintf("scope.consent_types[%d]", index), Message: "授权类型不正确"}) return } if !seenConsentTypes[consentType] { seenConsentTypes[consentType] = true consentTypes = append(consentTypes, consentType) } } if len(consentTypes) == 0 { writeValidation(writer, request, "授权撤回申请参数不正确", FieldError{Field: "scope.consent_types", Message: "至少选择一种授权"}) return } scope = map[string]any{"consent_types": consentTypes} } result, created, err := service.CreatePrivacyRequest(request.Context(), principal, request.PathValue("id"), idempotencyKey, RequestID(request.Context()), saas.CreatePrivacyRequestInput{RequestType: input.RequestType, Scope: scope, Reason: input.Reason, Confirmation: input.Confirmation}) if err != nil { writeSaaSError(writer, request, err) return } status := http.StatusAccepted if !created { writer.Header().Set("Idempotency-Replayed", "true") status = http.StatusOK } WriteJSON(writer, request, status, result) }) } func GetPrivacyRequestHandler(service *saas.Service) http.Handler { return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { principal, ok := resourcePrincipal(writer, request, "id") if !ok { return } result, err := service.GetPrivacyRequest(request.Context(), principal, request.PathValue("id")) if err != nil { writeSaaSError(writer, request, err) return } writer.Header().Set("ETag", concurrency.FormatETag(result.Version)) writer.Header().Set("Cache-Control", "no-store") WriteJSON(writer, request, http.StatusOK, result) }) } func ListExportTasksHandler(service *saas.Service) http.Handler { return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { principal, ok := resourcePrincipal(writer, request, "id") if !ok { return } items, err := service.ListExportTasks(request.Context(), principal, request.PathValue("id")) if err != nil { writeSaaSError(writer, request, err) return } writer.Header().Set("Cache-Control", "no-store") WriteJSON(writer, request, http.StatusOK, map[string]any{"items": items}) }) } func CreateExportTaskHandler(service *saas.Service) http.Handler { return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { principal, ok := resourcePrincipal(writer, request, "id") if !ok { return } idempotencyKey := strings.TrimSpace(request.Header.Get("Idempotency-Key")) if len(idempotencyKey) < 16 || len(idempotencyKey) > 128 { writeValidation(writer, request, "缺少有效的重复请求识别码", FieldError{Field: "Idempotency-Key", Message: "长度必须为16至128字符"}) return } var input struct { Scope struct { Categories []string `json:"categories"` } `json:"scope"` } if !decodeWebJSON(writer, request, &input, "数据导出参数不正确") { return } categories, err := saas.NormalizeExportCategories(input.Scope.Categories) if err != nil { writeValidation(writer, request, "数据导出参数不正确", FieldError{Field: "scope.categories", Message: "包含不支持的数据分类"}) return } scope := map[string]any{} if len(categories) > 0 { scope["categories"] = categories } result, created, err := service.CreateExportTask(request.Context(), principal, request.PathValue("id"), idempotencyKey, RequestID(request.Context()), scope) if err != nil { writeSaaSError(writer, request, err) return } status := http.StatusAccepted if !created { writer.Header().Set("Idempotency-Replayed", "true") status = http.StatusOK } writer.Header().Set("Cache-Control", "no-store") WriteJSON(writer, request, status, result) }) } func GetExportTaskHandler(service *saas.Service) http.Handler { return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { principal, ok := resourcePrincipal(writer, request, "id") if !ok { return } result, err := service.GetExportTask(request.Context(), principal, request.PathValue("id")) if err != nil { writeSaaSError(writer, request, err) return } writer.Header().Set("Cache-Control", "no-store") WriteJSON(writer, request, http.StatusOK, result) }) } func webPrincipal(writer http.ResponseWriter, request *http.Request) (identity.Principal, bool) { principal, ok := identity.PrincipalFromContext(request.Context()) if !ok { WriteError(writer, request, http.StatusUnauthorized, "AUTHENTICATION_REQUIRED", "请先登录后再访问") } return principal, ok } func resourcePrincipal(writer http.ResponseWriter, request *http.Request, pathName string) (identity.Principal, bool) { principal, ok := webPrincipal(writer, request) if !ok { return identity.Principal{}, false } if !validUUID(request.PathValue(pathName)) { writeValidation(writer, request, "资源编号不正确", FieldError{Field: pathName, Message: "必须是UUID"}) return identity.Principal{}, false } return principal, true } func decodeWebJSON(writer http.ResponseWriter, request *http.Request, target any, message string) bool { decoder := json.NewDecoder(request.Body) decoder.DisallowUnknownFields() if err := decoder.Decode(target); err != nil || ensureJSONEOF(decoder) != nil { writeValidation(writer, request, message, FieldError{Field: "body", Message: "必须是符合契约的单个JSON对象"}) return false } return true } func validateSubject(name, language, timezone string, profile, preferences map[string]any) []FieldError { result := make([]FieldError, 0) if name == "" || len([]rune(name)) > 120 { result = append(result, FieldError{Field: "preferred_name", Message: "不能为空且最长120字符"}) } if !validLanguage(language) { result = append(result, FieldError{Field: "language", Message: "必须是有效的语言标签,最长32字符"}) } if !validTimezone(timezone) { result = append(result, FieldError{Field: "timezone", Message: "必须是有效的IANA时区"}) } if profile == nil { result = append(result, FieldError{Field: "profile_data", Message: "必须是JSON对象"}) } if preferences == nil { result = append(result, FieldError{Field: "conversation_preferences", Message: "必须是JSON对象"}) } result = append(result, validateSubjectContext(profile, preferences)...) return result } func validateSubjectContext(profile, preferences map[string]any) []FieldError { result := make([]FieldError, 0) result = append(result, validateOptionalContextString(profile, "background", "profile_data.background", 500)...) result = append(result, validateOptionalContextList(profile, "interests", "profile_data.interests", 8, 50)...) result = append(result, validateOptionalContextString(preferences, "conversation_goal", "conversation_preferences.conversation_goal", 200)...) result = append(result, validateOptionalContextList(preferences, "preferred_topics", "conversation_preferences.preferred_topics", 8, 50)...) if preferences != nil { if raw, exists := preferences["pace"]; exists { pace, ok := raw.(string) if !ok || (pace != "slow" && pace != "normal" && pace != "lively") { result = append(result, FieldError{Field: "conversation_preferences.pace", Message: "只允许slow、normal或lively"}) } } } return result } func validateOptionalContextString(values map[string]any, key, field string, limit int) []FieldError { if values == nil { return nil } raw, exists := values[key] if !exists { return nil } value, ok := raw.(string) if !ok || len([]rune(strings.TrimSpace(value))) > limit { return []FieldError{{Field: field, Message: fmt.Sprintf("必须是最长%d字符的文本", limit)}} } return nil } func validateOptionalContextList(values map[string]any, key, field string, maxItems, itemLimit int) []FieldError { if values == nil { return nil } raw, exists := values[key] if !exists { return nil } items, ok := raw.([]any) if !ok || len(items) > maxItems { return []FieldError{{Field: field, Message: fmt.Sprintf("必须是最多%d项的文本数组", maxItems)}} } for _, item := range items { value, ok := item.(string) if !ok || strings.TrimSpace(value) == "" || len([]rune(strings.TrimSpace(value))) > itemLimit { return []FieldError{{Field: field, Message: fmt.Sprintf("每项必须是1至%d字符的文本", itemLimit)}} } } return nil } func validLanguage(value string) bool { if value == "" || len(value) > 32 { return false } for _, character := range value { if (character >= 'a' && character <= 'z') || (character >= 'A' && character <= 'Z') || (character >= '0' && character <= '9') || character == '-' { continue } return false } return true } func validTimezone(value string) bool { if value == "" || len(value) > 64 { return false } _, err := time.LoadLocation(value) return err == nil } func validUUID(value string) bool { _, err := uuid.Parse(value); return err == nil } func normalizeCreateFact(input *saas.CreateFactInput) { input.FactType = strings.TrimSpace(input.FactType) input.Content = strings.TrimSpace(input.Content) input.SourceType = strings.TrimSpace(input.SourceType) input.SourceRef = strings.TrimSpace(input.SourceRef) input.ConfirmationStatus = strings.TrimSpace(input.ConfirmationStatus) input.Sensitivity = strings.TrimSpace(input.Sensitivity) } func validateFact(factType, content, sourceType, sourceRef, status, sensitivity string, validFrom, validTo *time.Time) []FieldError { result := make([]FieldError, 0) if factType == "" || len(factType) > 64 { result = append(result, FieldError{Field: "fact_type", Message: "不能为空且最长64字符"}) } if content == "" || len([]rune(content)) > 4000 { result = append(result, FieldError{Field: "content", Message: "不能为空且最长4000字符"}) } if !oneOf(sourceType, "subject_statement", "family_entry", "model_candidate", "imported") { result = append(result, FieldError{Field: "source_type", Message: "来源类型不正确"}) } if len(sourceRef) > 300 { result = append(result, FieldError{Field: "source_ref", Message: "最长300字符"}) } if !oneOf(status, "pending", "confirmed", "rejected", "superseded") { result = append(result, FieldError{Field: "confirmation_status", Message: "状态不正确"}) } if status == "confirmed" && sourceRef == "" { result = append(result, FieldError{Field: "source_ref", Message: "确认事实必须保留来源引用"}) } if !oneOf(sensitivity, "normal", "sensitive", "high") { result = append(result, FieldError{Field: "sensitivity", Message: "敏感等级不正确"}) } if validFrom != nil && validTo != nil && validTo.Before(*validFrom) { result = append(result, FieldError{Field: "valid_to", Message: "不能早于valid_from"}) } return result } func oneOf(value string, allowed ...string) bool { for _, candidate := range allowed { if value == candidate { return true } } return false } func forbiddenDeviceConfigField(value any, path string) (string, bool) { forbidden := []string{"secret", "token", "password", "credential", "private_key", "api_key"} switch typed := value.(type) { case map[string]any: for key, child := range typed { lower := strings.ToLower(key) for _, fragment := range forbidden { if strings.Contains(lower, fragment) { return path + "." + key, true } } if found, ok := forbiddenDeviceConfigField(child, path+"."+key); ok { return found, true } } case []any: for index, child := range typed { if found, ok := forbiddenDeviceConfigField(child, path+"["+strconv.Itoa(index)+"]"); ok { return found, true } } } return "", false } func validateDesiredDeviceConfig(config map[string]any) []FieldError { volume, exists := config["volume"] if !exists { return nil } volumeObject, ok := volume.(map[string]any) if !ok { return []FieldError{{Field: "desired_config.volume", Message: "必须是JSON对象"}} } speaker, exists := volumeObject["speaker"] if !exists { return nil } speakerNumber, ok := speaker.(float64) if !ok || speakerNumber < 0 || speakerNumber > 100 || speakerNumber != float64(int(speakerNumber)) { return []FieldError{{Field: "desired_config.volume.speaker", Message: "必须是0至100的整数"}} } return nil } func writeValidation(writer http.ResponseWriter, request *http.Request, message string, fieldErrors ...FieldError) { WriteAPIError(writer, request, http.StatusBadRequest, Error{Code: "VALIDATION_FAILED", Message: message, FieldErrors: fieldErrors}) } func writeETagError(writer http.ResponseWriter, request *http.Request, err error) { if errors.Is(err, concurrency.ErrPreconditionRequired) { WriteError(writer, request, http.StatusPreconditionRequired, "PRECONDITION_REQUIRED", "修改资源必须提供If-Match") return } if errors.Is(err, concurrency.ErrVersionConflict) { WriteError(writer, request, http.StatusPreconditionFailed, "VERSION_CONFLICT", "资源已被其他操作修改,请刷新后重试") return } writeValidation(writer, request, "If-Match格式不正确", FieldError{Field: "If-Match", Message: "必须使用服务端返回的强ETag"}) } func writeSaaSError(writer http.ResponseWriter, request *http.Request, err error) { switch { case errors.Is(err, saas.ErrAccessDenied): WriteError(writer, request, http.StatusForbidden, "RESOURCE_ACCESS_DENIED", "当前账号无权访问该资源") case errors.Is(err, saas.ErrNotFound): WriteError(writer, request, http.StatusNotFound, "RESOURCE_NOT_FOUND", "请求的资源不存在") case errors.Is(err, saas.ErrVersionConflict): WriteError(writer, request, http.StatusPreconditionFailed, "VERSION_CONFLICT", "资源已被其他操作修改,请刷新后重试") case errors.Is(err, saas.ErrEffectiveConsentExist): WriteError(writer, request, http.StatusConflict, "EFFECTIVE_CONSENT_EXISTS", "该类型已有当前有效的授权") case errors.Is(err, saas.ErrPolicyVersionMismatch): WriteError(writer, request, http.StatusConflict, "POLICY_VERSION_MISMATCH", "授权政策版本已经更新,请重新阅读并确认") case errors.Is(err, saas.ErrDeviceClaimInvalid): WriteError(writer, request, http.StatusConflict, "DEVICE_CLAIM_INVALID", "设备绑定码无效、已过期或已撤销") case errors.Is(err, saas.ErrDeviceClaimUsed): WriteError(writer, request, http.StatusConflict, "DEVICE_CLAIM_ALREADY_USED", "设备绑定码已经用于其他绑定") case errors.Is(err, saas.ErrDeviceAlreadyBound): WriteError(writer, request, http.StatusConflict, "DEVICE_ALREADY_BOUND", "设备已经存在有效绑定") case errors.Is(err, saas.ErrDeviceBindingBlocked): WriteError(writer, request, http.StatusConflict, "DEVICE_BINDING_BLOCKED", "设备仍有活动通话、未完成命令或固件任务,请先处理阻断项") case errors.Is(err, saas.ErrDeviceBindingStale): WriteError(writer, request, http.StatusConflict, "DEVICE_BINDING_STALE", "设备绑定已发生变化,请重新执行影响检查") case errors.Is(err, saas.ErrIdempotencyConflict): WriteError(writer, request, http.StatusConflict, "IDEMPOTENCY_KEY_CONFLICT", "重复请求识别码已用于不同的请求内容") case errors.Is(err, saas.ErrSubjectDeleteConflict): WriteError(writer, request, http.StatusConflict, "SUBJECT_ACTIVE_CONVERSATION", "主体仍有未结束的通话,请先正常结束通话") case errors.Is(err, saas.ErrDeleteConfirmation): WriteError(writer, request, http.StatusConflict, "DELETE_CONFIRMATION_MISMATCH", "删除确认与主体称呼不一致") case errors.Is(err, saas.ErrExportUnavailable): WriteAPIError(writer, request, http.StatusServiceUnavailable, Error{Code: "EXPORT_DOWNLOAD_UNAVAILABLE", Message: "导出文件下载地址暂时不可用", Retryable: true}) case errors.Is(err, saas.ErrFirmwareStateConflict): WriteError(writer, request, http.StatusConflict, "FIRMWARE_STATE_CONFLICT", "固件版本或灰度批次状态不允许当前操作") case errors.Is(err, saas.ErrFirmwareSigningKeyConflict): WriteError(writer, request, http.StatusConflict, "FIRMWARE_SIGNING_KEY_CONFLICT", "固件签名密钥编号已绑定其他公钥、已被吊销或不存在") case errors.Is(err, saas.ErrFirmwareSigningKeyInUse): WriteError(writer, request, http.StatusConflict, "FIRMWARE_SIGNING_KEY_IN_USE", "固件签名密钥仍被上传中版本或运行中灰度批次引用") case errors.Is(err, saas.ErrFirmwareSigningKeyInactive): WriteError(writer, request, http.StatusConflict, "FIRMWARE_SIGNING_KEY_INACTIVE", "固件签名密钥不存在或已被吊销") case errors.Is(err, saas.ErrFirmwareReleaseInUse): WriteError(writer, request, http.StatusConflict, "FIRMWARE_RELEASE_IN_USE", "固件版本仍有运行中灰度批次,请先暂停版本后再退役") case errors.Is(err, saas.ErrInvitationInvalid): WriteError(writer, request, http.StatusConflict, "MEMBER_INVITATION_INVALID", "成员邀请无效、已过期或已撤销") case errors.Is(err, saas.ErrMemberExists): WriteError(writer, request, http.StatusConflict, "HOUSEHOLD_MEMBER_EXISTS", "账号已是家庭成员或已经是目标所有者") case errors.Is(err, saas.ErrOwnerProtected): WriteError(writer, request, http.StatusConflict, "HOUSEHOLD_OWNER_PROTECTED", "不能直接修改或移除家庭所有者,请先转移所有权") case errors.Is(err, saas.ErrFactSourceRequired): WriteError(writer, request, http.StatusConflict, "FACT_SOURCE_REQUIRED", "确认事实必须保留可追溯来源") case errors.Is(err, saas.ErrStorySourceRequired): WriteError(writer, request, http.StatusConflict, "STORY_SOURCE_REQUIRED", "确认故事必须保留可追溯的原始话轮") case errors.Is(err, saas.ErrPersonalizedMemoryConsentNeeded): WriteError(writer, request, http.StatusConflict, "PERSONALIZED_MEMORY_CONSENT_REQUIRED", "生成个性化记忆前必须取得当前有效授权") case errors.Is(err, saas.ErrReviewStateConflict): WriteError(writer, request, http.StatusConflict, "STORY_REVIEW_STATE_CONFLICT", "当前故事审核状态不允许此操作") case errors.Is(err, saas.ErrFactReviewStateConflict): WriteError(writer, request, http.StatusConflict, "FACT_REVIEW_STATE_CONFLICT", "当前事实候选状态不允许此操作") case errors.Is(err, saas.ErrConversationSummaryNotReady): WriteError(writer, request, http.StatusConflict, "CONVERSATION_SUMMARY_NOT_READY", "聊天尚未结束或没有可用于总结的文字内容") case errors.Is(err, pagination.ErrInvalidCursor): writeValidation(writer, request, "分页游标无效", FieldError{Field: "cursor", Message: "游标无效、已损坏或不是本服务签发"}) default: WriteAPIError(writer, request, http.StatusInternalServerError, Error{Code: "INTERNAL_ERROR", Message: "服务暂时不可用", Retryable: true}) } }