完善 API Key 能力范围可视化和维护
This commit is contained in:
@@ -19,6 +19,14 @@ type walletBalanceRequest struct {
|
||||
Metadata map[string]any `json:"metadata"`
|
||||
}
|
||||
|
||||
type walletRechargeRequest struct {
|
||||
Currency string `json:"currency" example:"resource"`
|
||||
Amount float64 `json:"amount" example:"100"`
|
||||
Reason string `json:"reason" example:"manual recharge"`
|
||||
IdempotencyKey string `json:"idempotencyKey" example:"wallet-recharge-20260514-001"`
|
||||
Metadata map[string]any `json:"metadata"`
|
||||
}
|
||||
|
||||
// setUserWalletBalance godoc
|
||||
// @Summary 设置用户钱包余额
|
||||
// @Description 管理端把指定用户钱包余额调整到目标值,并记录审计日志;balance 不允许为负数。
|
||||
@@ -95,6 +103,80 @@ func (s *Server) setUserWalletBalance(w http.ResponseWriter, r *http.Request) {
|
||||
})
|
||||
}
|
||||
|
||||
// rechargeUserWalletBalance godoc
|
||||
// @Summary 充值用户钱包余额
|
||||
// @Description 管理端给指定用户钱包追加充值金额,并记录审计日志;amount 必须大于 0。
|
||||
// @Tags billing
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param userID path string true "用户 ID"
|
||||
// @Param input body walletRechargeRequest true "钱包充值请求"
|
||||
// @Success 200 {object} WalletAdjustmentResponse
|
||||
// @Failure 400 {object} ErrorEnvelope
|
||||
// @Failure 401 {object} ErrorEnvelope
|
||||
// @Failure 403 {object} ErrorEnvelope
|
||||
// @Failure 404 {object} ErrorEnvelope
|
||||
// @Failure 500 {object} ErrorEnvelope
|
||||
// @Router /api/admin/users/{userID}/wallet/recharge [post]
|
||||
func (s *Server) rechargeUserWalletBalance(w http.ResponseWriter, r *http.Request) {
|
||||
actor, _ := auth.UserFromContext(r.Context())
|
||||
var input walletRechargeRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid json body")
|
||||
return
|
||||
}
|
||||
if input.Amount <= 0 {
|
||||
writeError(w, http.StatusBadRequest, "wallet recharge amount must be positive")
|
||||
return
|
||||
}
|
||||
gatewayUserID := strings.TrimSpace(r.PathValue("userID"))
|
||||
reason := strings.TrimSpace(input.Reason)
|
||||
if reason == "" {
|
||||
writeError(w, http.StatusBadRequest, "reason is required")
|
||||
return
|
||||
}
|
||||
|
||||
var result store.WalletAdjustmentResult
|
||||
var auditLog store.AuditLog
|
||||
err := s.store.InTx(r.Context(), func(tx store.Tx) error {
|
||||
next, err := s.store.RechargeUserWalletBalanceTx(r.Context(), tx, store.WalletRechargeInput{
|
||||
GatewayUserID: gatewayUserID,
|
||||
Currency: input.Currency,
|
||||
Amount: input.Amount,
|
||||
Reason: reason,
|
||||
IdempotencyKey: input.IdempotencyKey,
|
||||
Metadata: input.Metadata,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
result = next
|
||||
record, err := s.store.RecordAuditLogTx(r.Context(), tx, walletRechargeAuditInput(r, actor, reason, result))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
auditLog = record
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
switch {
|
||||
case store.IsNotFound(err):
|
||||
writeError(w, http.StatusNotFound, "user not found")
|
||||
default:
|
||||
s.logger.Error("recharge user wallet balance failed", "error", err)
|
||||
writeError(w, http.StatusInternalServerError, "recharge user wallet balance failed")
|
||||
}
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]any{
|
||||
"account": result.Account,
|
||||
"before": result.Before,
|
||||
"transaction": result.Transaction,
|
||||
"auditLog": auditLog,
|
||||
})
|
||||
}
|
||||
|
||||
// listAuditLogs godoc
|
||||
// @Summary 列出审计日志
|
||||
// @Description 管理端按分类、动作、目标类型和目标 ID 查询审计日志。
|
||||
@@ -178,6 +260,12 @@ func walletAdjustmentAuditInput(r *http.Request, actor *auth.User, reason string
|
||||
}
|
||||
}
|
||||
|
||||
func walletRechargeAuditInput(r *http.Request, actor *auth.User, reason string, result store.WalletAdjustmentResult) store.AuditLogInput {
|
||||
input := walletAdjustmentAuditInput(r, actor, reason, result)
|
||||
input.Action = "wallet.balance.recharge"
|
||||
return input
|
||||
}
|
||||
|
||||
func requestIP(r *http.Request) string {
|
||||
if forwarded := strings.TrimSpace(r.Header.Get("X-Forwarded-For")); forwarded != "" {
|
||||
parts := strings.Split(forwarded, ",")
|
||||
|
||||
Reference in New Issue
Block a user