Files
easyai-ai-gateway/apps/api/internal/httpapi/pricing_handlers.go
T
easyai 3056cf8fca
ci / verify (pull_request) Successful in 15m34s
feat(gateway): 补齐桌面端高级媒体直连接口
新增图片矢量化、视频超分、每日用量、计价与任务隔离能力,并通过环境变量解析平台凭据。

已通过 Go 全量门禁、迁移检查、镜像构建以及 Vectorizer 五格式和 Topaz 3 秒视频真实 DEV 验收。
2026-07-22 14:02:53 +08:00

192 lines
6.7 KiB
Go

package httpapi
import (
"encoding/json"
"errors"
"net/http"
"strings"
"time"
"github.com/easyai/easyai-ai-gateway/apps/api/internal/store"
)
// listPricingRuleSets godoc
// @Summary 列出定价规则集
// @Description 管理端返回可分配给平台、模型、租户或用户组的定价规则集。
// @Tags pricing
// @Produce json
// @Security BearerAuth
// @Success 200 {object} PricingRuleSetListResponse
// @Failure 401 {object} ErrorEnvelope
// @Failure 403 {object} ErrorEnvelope
// @Failure 500 {object} ErrorEnvelope
// @Router /api/admin/pricing/rule-sets [get]
func (s *Server) listPricingRuleSets(w http.ResponseWriter, r *http.Request) {
items, err := s.store.ListPricingRuleSets(r.Context())
if err != nil {
s.logger.Error("list pricing rule sets failed", "error", err)
writeError(w, http.StatusInternalServerError, "list pricing rule sets failed")
return
}
writeJSON(w, http.StatusOK, map[string]any{"items": items})
}
// createPricingRuleSet godoc
// @Summary 创建定价规则集
// @Description 管理端创建定价规则集,ruleSetKey、name 和至少一条 rule 必填。
// @Tags pricing
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param input body store.PricingRuleSetInput true "定价规则集请求"
// @Success 201 {object} store.PricingRuleSet
// @Failure 400 {object} ErrorEnvelope
// @Failure 401 {object} ErrorEnvelope
// @Failure 403 {object} ErrorEnvelope
// @Failure 409 {object} ErrorEnvelope
// @Failure 500 {object} ErrorEnvelope
// @Router /api/admin/pricing/rule-sets [post]
func (s *Server) createPricingRuleSet(w http.ResponseWriter, r *http.Request) {
var input store.PricingRuleSetInput
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
writeError(w, http.StatusBadRequest, "invalid json body")
return
}
if !validPricingRuleSetInput(input) {
writeError(w, http.StatusBadRequest, "ruleSetKey, name and at least one rule are required")
return
}
item, err := s.store.CreatePricingRuleSet(r.Context(), input)
if err != nil {
if store.IsUniqueViolation(err) {
writeError(w, http.StatusConflict, "pricing rule set key already exists")
return
}
s.logger.Error("create pricing rule set failed", "error", err)
writeError(w, http.StatusInternalServerError, "create pricing rule set failed")
return
}
writeJSON(w, http.StatusCreated, item)
}
// updatePricingRuleSet godoc
// @Summary 更新定价规则集
// @Description 管理端更新定价规则集及其规则列表。
// @Tags pricing
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param ruleSetID path string true "定价规则集 ID"
// @Param input body store.PricingRuleSetInput true "定价规则集请求"
// @Success 200 {object} store.PricingRuleSet
// @Failure 400 {object} ErrorEnvelope
// @Failure 401 {object} ErrorEnvelope
// @Failure 403 {object} ErrorEnvelope
// @Failure 404 {object} ErrorEnvelope
// @Failure 409 {object} ErrorEnvelope
// @Failure 500 {object} ErrorEnvelope
// @Router /api/admin/pricing/rule-sets/{ruleSetID} [patch]
func (s *Server) updatePricingRuleSet(w http.ResponseWriter, r *http.Request) {
var input store.PricingRuleSetInput
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
writeError(w, http.StatusBadRequest, "invalid json body")
return
}
if !validPricingRuleSetInput(input) {
writeError(w, http.StatusBadRequest, "ruleSetKey, name and at least one rule are required")
return
}
item, err := s.store.UpdatePricingRuleSet(r.Context(), r.PathValue("ruleSetID"), input)
if err != nil {
if store.IsNotFound(err) {
writeError(w, http.StatusNotFound, "pricing rule set not found")
return
}
if store.IsUniqueViolation(err) {
writeError(w, http.StatusConflict, "pricing rule set key already exists")
return
}
s.logger.Error("update pricing rule set failed", "error", err)
writeError(w, http.StatusInternalServerError, "update pricing rule set failed")
return
}
writeJSON(w, http.StatusOK, item)
}
// deletePricingRuleSet godoc
// @Summary 删除定价规则集
// @Description 管理端删除非默认定价规则集;默认规则集受保护。
// @Tags pricing
// @Produce json
// @Security BearerAuth
// @Param ruleSetID path string true "定价规则集 ID"
// @Success 204 "No Content"
// @Failure 401 {object} ErrorEnvelope
// @Failure 403 {object} ErrorEnvelope
// @Failure 404 {object} ErrorEnvelope
// @Failure 500 {object} ErrorEnvelope
// @Router /api/admin/pricing/rule-sets/{ruleSetID} [delete]
func (s *Server) deletePricingRuleSet(w http.ResponseWriter, r *http.Request) {
if err := s.store.DeletePricingRuleSet(r.Context(), r.PathValue("ruleSetID")); err != nil {
if store.IsNotFound(err) {
writeError(w, http.StatusNotFound, "pricing rule set not found")
return
}
if errors.Is(err, store.ErrProtectedDefault) {
writeError(w, http.StatusForbidden, "default pricing rule set cannot be deleted")
return
}
s.logger.Error("delete pricing rule set failed", "error", err)
writeError(w, http.StatusInternalServerError, "delete pricing rule set failed")
return
}
w.WriteHeader(http.StatusNoContent)
}
func validPricingRuleSetInput(input store.PricingRuleSetInput) bool {
if strings.TrimSpace(input.RuleSetKey) == "" || strings.TrimSpace(input.Name) == "" || len(input.Rules) == 0 {
return false
}
if currency := strings.TrimSpace(input.Currency); currency != "" && currency != "resource" {
return false
}
for _, rule := range input.Rules {
if strings.TrimSpace(rule.ResourceType) == "" || strings.TrimSpace(rule.Unit) == "" {
return false
}
if rule.BasePrice < 0 || (rule.BasePrice == 0 && !rule.IsFree) {
return false
}
if currency := strings.TrimSpace(rule.Currency); currency != "" && currency != "resource" {
return false
}
switch calculator := strings.TrimSpace(rule.CalculatorType); calculator {
case "", "token_usage", "unit_weight", "duration_weight", "transition_matrix":
default:
return false
}
calculator := strings.TrimSpace(rule.CalculatorType)
if calculator == "" {
calculator = store.DefaultEffectivePricingCalculator(rule.ResourceType)
}
if store.ValidateEffectivePricingRuleShape(strings.TrimSpace(rule.ResourceType), store.NormalizeEffectivePricingRuleUnit(rule.ResourceType, rule.Unit), calculator) != nil {
return false
}
effectiveFrom, fromOK := pricingEffectiveTime(rule.EffectiveFrom)
effectiveTo, toOK := pricingEffectiveTime(rule.EffectiveTo)
if !fromOK || !toOK || (!effectiveFrom.IsZero() && !effectiveTo.IsZero() && !effectiveFrom.Before(effectiveTo)) {
return false
}
}
return true
}
func pricingEffectiveTime(value string) (time.Time, bool) {
value = strings.TrimSpace(value)
if value == "" {
return time.Time{}, true
}
parsed, err := time.Parse(time.RFC3339, value)
return parsed, err == nil
}