353 lines
10 KiB
Go
353 lines
10 KiB
Go
package httpapi
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"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 !allow(w, r, http.MethodGet) {
|
|
return
|
|
}
|
|
if _, ok := h.authorize(w, r, PlatformSuperAdmin); !ok {
|
|
return
|
|
}
|
|
rules, err := h.service.ListPrices(r.Context())
|
|
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})
|
|
}
|