package httpapi import ( "encoding/json" "errors" "io" "math" "net/http" "strconv" "strings" "git.nianxx.cn/wangxuming/NianAIGC/backend/internal/billing" "git.nianxx.cn/wangxuming/NianAIGC/backend/internal/identity" "git.nianxx.cn/wangxuming/NianAIGC/backend/internal/providers" ) type BillingHTTPService = billing.HTTPService type BillingAccountStore = billing.AccountConfigStore type billingHandler struct { authorizer *PlatformAuthorizer service BillingHTTPService accounts BillingAccountStore builder ProviderCommandBuilder } func NewBillingHandler(authorizer *PlatformAuthorizer, service BillingHTTPService, accounts BillingAccountStore) http.Handler { return &billingHandler{authorizer: authorizer, service: service, accounts: accounts} } func NewBillingHandlerWithBuilder(authorizer *PlatformAuthorizer, service BillingHTTPService, accounts BillingAccountStore, builder ProviderCommandBuilder) http.Handler { return &billingHandler{authorizer: authorizer, service: service, accounts: accounts, builder: builder} } func (h *billingHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { switch { case r.URL.Path == "/api/billing": h.billing(w, r) case r.URL.Path == "/api/billing/quote": h.quote(w, r) case r.URL.Path == "/api/admin/billing": h.adminOverview(w, r) case r.URL.Path == "/api/admin/billing/account": h.account(w, r) case r.URL.Path == "/api/admin/billing/adjustments": h.adjust(w, r) case r.URL.Path == "/api/admin/billing/prices": h.prices(w, r) case strings.HasPrefix(r.URL.Path, "/api/admin/billing/prices/"): h.price(w, r, strings.TrimPrefix(r.URL.Path, "/api/admin/billing/prices/")) default: http.NotFound(w, r) } } func (h *billingHandler) billing(w http.ResponseWriter, r *http.Request) { if !allow(w, r, http.MethodGet) { return } session, ok := h.authorize(w, r, PlatformApp) if !ok { return } if session.User.OrganizationID == "" { writeAPIError(w, 422, "当前账号未绑定组织。") return } if h.service == nil { writeAPIError(w, 500, "服务器内部错误。") return } overview, err := h.service.Overview(r.Context(), session.User.OrganizationID, session.User.ID) if err != nil { writeDomainError(w, err) return } if h.accounts != nil { config, err := h.accounts.Load(r.Context()) if err != nil { writeDomainError(w, err) return } overview.BillingAccount = config } writeJSON(w, 200, struct { Organization any `json:"organization"` billing.Overview }{Organization: map[string]string{"id": session.User.OrganizationID, "name": first(session.User.OrganizationName, session.User.OrganizationID)}, Overview: overview}) } func (h *billingHandler) quote(w http.ResponseWriter, r *http.Request) { if !allow(w, r, http.MethodPost) { return } if h.service == nil { writeAPIError(w, 500, "服务器内部错误。") return } session, ok := h.authorize(w, r, PlatformApp) if !ok { return } var body map[string]any decodeJSON(r, &body) capability := billingString(body["capability"]) if capability == "" { if billingString(body["kind"]) == "video" { capability = "video.generate" } else { capability = "image.generate" } } provider, reqKey := billingString(body["provider"]), billingString(body["reqKey"]) parameters := billing.Parameters{} if h.builder != nil { prepared, err := h.builder.Build(r.Context(), session.User.ID, "", capability, "", body) if err != nil { writeJobError(w, err, false) return } provider, reqKey = prepared.Job.Provider, prepared.Job.ReqKey var request providers.Request if json.Unmarshal(prepared.Job.RequestPayload, &request) != nil { writeAPIError(w, 500, "服务器内部错误。") return } for key, value := range request.Settings { parameters[key] = value } parameters["referenceImageCount"] = float64(len(request.InputURLs)) } command := billing.QuoteCommand{AccountID: session.User.ID, OrganizationID: session.User.OrganizationID, OrganizationName: session.User.OrganizationName, Role: session.User.Role, Provider: provider, Capability: capability, ReqKey: reqKey, Payload: body, Parameters: parameters} if parameters, ok := body["parameters"].(map[string]any); ok { for key, value := range parameters { command.Parameters[key] = value } } quote, err := h.service.Quote(r.Context(), command) if err != nil { writeDomainError(w, err) return } writeJSON(w, 200, map[string]any{"quote": quote}) } func (h *billingHandler) adminOverview(w http.ResponseWriter, r *http.Request) { if !allow(w, r, http.MethodGet) { return } if _, ok := h.authorize(w, r, PlatformSuperAdmin); !ok { return } overview, err := h.service.AdminOverview(r.Context()) if err != nil { writeDomainError(w, err) return } if h.accounts != nil { overview.BillingAccount, err = h.accounts.Load(r.Context()) if err != nil { writeDomainError(w, err) return } } writeJSON(w, 200, overview) } func (h *billingHandler) account(w http.ResponseWriter, r *http.Request) { if !allow(w, r, http.MethodPatch) { return } if _, ok := h.authorize(w, r, PlatformSuperAdmin); !ok { return } if h.accounts == nil { writeAPIError(w, 500, "服务器内部错误。") return } var body map[string]any decodeJSON(r, &body) config := billing.AccountConfig{AccountName: billingString(body["accountName"]), BankName: billingString(body["bankName"]), AccountNumber: billingString(body["accountNumber"]), Contact: billingString(body["contact"])} if err := h.accounts.Save(r.Context(), config); err != nil { writeDomainError(w, err) return } config, err := h.accounts.Load(r.Context()) if err != nil { writeDomainError(w, err) return } writeJSON(w, 200, map[string]any{"billingAccount": config}) } func (h *billingHandler) adjust(w http.ResponseWriter, r *http.Request) { if !allow(w, r, http.MethodPost) { return } session, ok := h.authorize(w, r, PlatformSuperAdmin) if !ok { return } var body map[string]any decodeJSON(r, &body) organizationID, note, direction := billingString(body["organizationId"]), billingString(body["note"]), billingString(body["direction"]) amount := numberValue(body["amountFen"]) if amount <= 0 { amount = numberValue(body["amountYuan"]) * 100 } amountFen := int64(math.Round(amount)) if organizationID == "" { writeAPIError(w, 400, "组织不能为空。") return } if amountFen <= 0 { writeAPIError(w, 400, "请输入大于 0 的金额。") return } if direction != "credit" && direction != "debit" { writeAPIError(w, 400, "余额变动方向无效。") return } if note == "" { writeAPIError(w, 400, "备注不能为空。") return } delta := amountFen if direction == "debit" { delta = -amountFen } result, err := h.service.Adjust(r.Context(), billing.AdjustmentCommand{OrganizationID: organizationID, OperatorID: session.User.ID, Direction: direction, Note: note, AmountFen: amountFen, DeltaFen: delta}) if err != nil { writeDomainError(w, err) return } writeJSON(w, 200, result) } func (h *billingHandler) prices(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet && r.Method != http.MethodPatch { w.Header().Set("Allow", "GET, PATCH") writeAPIError(w, http.StatusMethodNotAllowed, "方法不允许。") return } if _, ok := h.authorize(w, r, PlatformSuperAdmin); !ok { return } if r.Method == http.MethodPatch { h.updatePrices(w, r) return } rules, err := h.service.ListPrices(r.Context()) if err != nil { writeDomainError(w, err) return } writeJSON(w, 200, map[string]any{"priceRules": rules}) } // Called only after the collection route has authorized a platform super admin. func (h *billingHandler) updatePrices(w http.ResponseWriter, r *http.Request) { var body struct { RuleIDs []string `json:"ruleIds"` MarkupMultiplier any `json:"markupMultiplier"` } decoder := json.NewDecoder(r.Body) decoder.DisallowUnknownFields() if err := decoder.Decode(&body); err != nil { writeAPIError(w, 400, "请提供规则列表和上浮倍率,不能修改标准价格或参数。") return } if err := decoder.Decode(&struct{}{}); err != io.EOF { writeAPIError(w, 400, "请求必须包含一个有效的 JSON 对象。") return } rules, err := h.service.UpdatePrices(r.Context(), billing.BulkPricePatch{ RuleIDs: body.RuleIDs, MarkupMultiplier: numberValue(body.MarkupMultiplier), }) if err != nil { writeDomainError(w, err) return } writeJSON(w, 200, map[string]any{"priceRules": rules}) } func (h *billingHandler) price(w http.ResponseWriter, r *http.Request, id string) { if !allow(w, r, http.MethodPatch) { return } if _, ok := h.authorize(w, r, PlatformSuperAdmin); !ok { return } if id == "" || strings.Contains(id, "/") { http.NotFound(w, r) return } var body map[string]any decodeJSON(r, &body) for key := range body { if key != "markupMultiplier" && key != "dimensionKey" && key != "tierValue" { writeAPIError(w, 400, "平台标准价格与参数由系统维护,超管仅可调整上浮倍率。") return } } patch := billing.PricePatch{MarkupMultiplier: numberValue(body["markupMultiplier"]), DimensionKey: billingString(body["dimensionKey"]), TierValue: billingString(body["tierValue"])} rule, err := h.service.GetPrice(r.Context(), id) if err != nil { writeDomainError(w, err) return } if rule == nil { writeAPIError(w, 404, "计费规则不存在") return } if err := billing.ValidatePricePatch(rule, patch); err != nil { writeAPIError(w, 400, err.Error()) return } patch.MarkupMultiplier = math.Round(patch.MarkupMultiplier*10000) / 10000 rule, err = h.service.UpdatePrice(r.Context(), id, patch) if err != nil { writeDomainError(w, err) return } if rule == nil { writeAPIError(w, 404, "计费规则不存在") return } writeJSON(w, 200, map[string]any{"rule": rule}) } func (h *billingHandler) authorize(w http.ResponseWriter, r *http.Request, requirement PlatformRequirement) (identity.Session, bool) { session, err := h.authorizer.Authorize(r, requirement) if err != nil { writeAuthError(w, err) return identity.Session{}, false } return session, true } func decodeJSON(r *http.Request, value any) { _ = json.NewDecoder(r.Body).Decode(value) } func billingString(value any) string { text, _ := value.(string); return strings.TrimSpace(text) } func numberValue(value any) float64 { switch number := value.(type) { case float64: return number case string: parsed, err := strconv.ParseFloat(strings.TrimSpace(number), 64) if err == nil && !math.IsNaN(parsed) && !math.IsInf(parsed, 0) { return parsed } } return 0 } func first(value, fallback string) string { if value != "" { return value } return fallback } func allow(w http.ResponseWriter, r *http.Request, method string) bool { if r.Method == method { return true } w.Header().Set("Allow", method) writeAPIError(w, 405, http.StatusText(405)) return false } func writeAuthError(w http.ResponseWriter, err error) { var auth *PlatformAuthError if errors.As(err, &auth) { writeAPIError(w, auth.Status, auth.Message) return } writeAPIError(w, 500, "服务器内部错误。") } func writeDomainError(w http.ResponseWriter, err error) { status := billing.HTTPStatus(err) if status == http.StatusServiceUnavailable { writeAPIError(w, status, "计费服务暂不可用,请联系管理员。") return } if status >= 500 { writeAPIError(w, 500, "服务器内部错误。") return } writeAPIError(w, status, err.Error()) } func writeAPIError(w http.ResponseWriter, status int, message string) { writeJSON(w, status, map[string]string{"error": message}) }