194 lines
6.1 KiB
Go
194 lines
6.1 KiB
Go
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 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", "用量费用处理失败")
|
|
}
|
|
}
|