package httpapi import ( "errors" "net/http" "time" "data-terminal-cloud/internal/billing" "data-terminal-cloud/internal/concurrency" "data-terminal-cloud/internal/idempotency" ) func CreateOpsPriceBookHandler(service *billing.Service) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { principal, ok := webPrincipal(w, r) if !ok { return } tenantID, key, ok := opsCreateHeaders(w, r) if !ok { return } var body billing.CreatePriceBookInput if !decodeWebJSON(w, r, &body, "费用单价版本参数不正确") { return } body.TenantID = tenantID item, created, err := service.CreatePriceBook(r.Context(), principal, key, RequestID(r.Context()), body) if err != nil { writeBillingError(w, r, err) return } status := http.StatusCreated if !created { status = http.StatusOK w.Header().Set("Idempotency-Replayed", "true") } w.Header().Set("ETag", concurrency.FormatETag(item.RowVersion)) w.Header().Set("Cache-Control", "no-store") WriteJSON(w, r, status, item) }) } func ListOpsPriceBooksHandler(service *billing.Service) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { principal, ok := webPrincipal(w, r) if !ok { return } tenantID, ok := opsTenantHeader(w, r) if !ok { return } items, err := service.ListPriceBooks(r.Context(), principal, tenantID) if err != nil { writeBillingError(w, r, err) return } w.Header().Set("Cache-Control", "no-store") WriteJSON(w, r, http.StatusOK, map[string]any{"items": items}) }) } func PublishOpsPriceBookHandler(service *billing.Service) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { principal, ok := resourcePrincipal(w, r, "id") if !ok { return } tenantID, ok := opsTenantHeader(w, r) if !ok { return } expected, err := concurrency.ParseIfMatch(r) if err != nil { writeETagError(w, r, err) return } var body struct { Reason string `json:"reason"` } if !decodeWebJSON(w, r, &body, "费用版本发布参数不正确") { return } item, err := service.PublishPriceBook(r.Context(), principal, tenantID, r.PathValue("id"), RequestID(r.Context()), body.Reason, expected) if err != nil { writeBillingError(w, r, err) return } w.Header().Set("ETag", concurrency.FormatETag(item.RowVersion)) w.Header().Set("Cache-Control", "no-store") WriteJSON(w, r, http.StatusOK, item) }) } func CreateOpsQuotaPolicyHandler(service *billing.Service) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { principal, ok := webPrincipal(w, r) if !ok { return } tenantID, key, ok := opsCreateHeaders(w, r) if !ok { return } var body billing.QuotaInput if !decodeWebJSON(w, r, &body, "用量额度策略参数不正确") { return } item, created, err := service.CreateQuotaPolicy(r.Context(), principal, tenantID, key, RequestID(r.Context()), body) if err != nil { writeBillingError(w, r, err) return } status := http.StatusCreated if !created { status = http.StatusOK w.Header().Set("Idempotency-Replayed", "true") } w.Header().Set("ETag", concurrency.FormatETag(item.RowVersion)) w.Header().Set("Cache-Control", "no-store") WriteJSON(w, r, status, item) }) } func ListOpsQuotaPoliciesHandler(service *billing.Service) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { principal, ok := webPrincipal(w, r) if !ok { return } tenantID, ok := opsTenantHeader(w, r) if !ok { return } items, err := service.ListQuotaPolicies(r.Context(), principal, tenantID) if err != nil { writeBillingError(w, r, err) return } w.Header().Set("Cache-Control", "no-store") WriteJSON(w, r, http.StatusOK, map[string]any{"items": items}) }) } func GetOpsCostSummaryHandler(service *billing.Service) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { principal, ok := webPrincipal(w, r) if !ok { return } tenantID, ok := opsTenantHeader(w, r) if !ok { return } from, fromErr := time.Parse(time.RFC3339, r.URL.Query().Get("from")) to, toErr := time.Parse(time.RFC3339, r.URL.Query().Get("to")) if fromErr != nil || toErr != nil { writeValidation(w, r, "费用汇总时间范围不正确", FieldError{Field: "from/to", Message: "必须是RFC3339时间且范围不超过366天"}) return } item, err := service.CostSummary(r.Context(), principal, tenantID, from, to) if err != nil { writeBillingError(w, r, err) return } w.Header().Set("Cache-Control", "no-store") WriteJSON(w, r, http.StatusOK, item) }) } func GetFamilyUsageOverviewHandler(service *billing.Service) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { principal, ok := webPrincipal(w, r) if !ok { return } from, to, timezone, ok := usageOverviewQuery(w, r) if !ok { return } item, err := service.FamilyUsageOverview( r.Context(), principal, r.PathValue("id"), r.URL.Query().Get("device_id"), timezone, from, to, ) if err != nil { writeBillingError(w, r, err) return } w.Header().Set("Cache-Control", "no-store") WriteJSON(w, r, http.StatusOK, item) }) } func GetOpsUsageOverviewHandler(service *billing.Service) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { principal, ok := webPrincipal(w, r) if !ok { return } tenantID, ok := opsTenantHeader(w, r) if !ok { return } from, to, timezone, ok := usageOverviewQuery(w, r) if !ok { return } item, err := service.OpsUsageOverview( r.Context(), principal, tenantID, r.URL.Query().Get("household_id"), r.URL.Query().Get("device_id"), timezone, from, to, ) if err != nil { writeBillingError(w, r, err) return } w.Header().Set("Cache-Control", "no-store") WriteJSON(w, r, http.StatusOK, item) }) } func usageOverviewQuery(w http.ResponseWriter, r *http.Request) (time.Time, time.Time, string, bool) { from, fromErr := time.Parse(time.RFC3339, r.URL.Query().Get("from")) to, toErr := time.Parse(time.RFC3339, r.URL.Query().Get("to")) timezone := r.URL.Query().Get("timezone") if timezone == "" { timezone = "Asia/Shanghai" } if fromErr != nil || toErr != nil { writeValidation(w, r, "通话用量时间范围不正确", FieldError{Field: "from/to", Message: "必须是RFC3339时间且范围不超过366天"}) return time.Time{}, time.Time{}, "", false } if _, err := time.LoadLocation(timezone); err != nil { writeValidation(w, r, "通话用量时区不正确", FieldError{Field: "timezone", Message: "必须是有效的IANA时区"}) return time.Time{}, time.Time{}, "", false } return from, to, timezone, true } func writeBillingError(w http.ResponseWriter, r *http.Request, err error) { switch { case errors.Is(err, billing.ErrInvalid): writeValidation(w, r, "用量、费用或额度参数不正确", FieldError{Field: "billing", Message: "字段、单位、时区、金额或范围不符合约束"}) case errors.Is(err, billing.ErrAccessDenied): WriteError(w, r, http.StatusForbidden, "OPS_ACCESS_DENIED", "当前账号没有所需的内部运营权限") case errors.Is(err, billing.ErrNotFound): WriteError(w, r, http.StatusNotFound, "RESOURCE_NOT_FOUND", "费用或额度资源不存在") case errors.Is(err, billing.ErrVersionConflict): WriteError(w, r, http.StatusPreconditionFailed, "VERSION_CONFLICT", "资源版本已变化,请重新读取后再操作") case errors.Is(err, billing.ErrSelfApproval): WriteError(w, r, http.StatusConflict, "SELF_APPROVAL_FORBIDDEN", "费用版本创建人不能审批自己的变更") case errors.Is(err, billing.ErrStateConflict): WriteError(w, r, http.StatusConflict, "BILLING_STATE_CONFLICT", "当前状态不允许执行该操作") case errors.Is(err, idempotency.ErrKeyConflict): WriteError(w, r, http.StatusConflict, "IDEMPOTENCY_KEY_CONFLICT", "重复请求识别码已被不同请求使用") case errors.Is(err, idempotency.ErrInProgress): WriteError(w, r, http.StatusConflict, "IDEMPOTENCY_IN_PROGRESS", "相同请求正在处理中") default: WriteError(w, r, http.StatusInternalServerError, "INTERNAL_ERROR", "用量费用处理失败") } }