feat(gateway): GET /me/usage —— 用户自己租户的用量明细(余额+趋势+最近消耗)
面向用户口径(非 admin,受插件按请求 ctx 租户自动隔离):返回积分余额 + credit_enforce + 按天消耗趋势 + 区间合计 + 最近 10 条消耗明细。复用 UsageTrend/TenantBalance,新增 RecentUsage(按 tenant 取近 N 条 usage_event)。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -550,6 +550,50 @@ func (h *Handler) TenantCurrent(c *gin.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
// MyUsage: GET /api/v1/me/usage?days= —— 当前用户自己租户的用量明细(余额 + 按天趋势 + 最近消耗)。
|
||||
// 面向用户口径(非 admin):只看自己租户,受插件按请求 ctx 租户自动隔离。
|
||||
func (h *Handler) MyUsage(c *gin.Context) {
|
||||
tid := tenantID(c)
|
||||
if tid == "" {
|
||||
c.JSON(http.StatusOK, gin.H{"tenant": nil})
|
||||
return
|
||||
}
|
||||
ctx := c.Request.Context()
|
||||
days := 30
|
||||
if v := c.Query("days"); v != "" {
|
||||
if n, err := strconv.Atoi(v); err == nil && n > 0 && n <= 90 {
|
||||
days = n
|
||||
}
|
||||
}
|
||||
now := time.Now()
|
||||
from := now.AddDate(0, 0, -(days - 1)).Format("20060102")
|
||||
to := now.Format("20060102")
|
||||
|
||||
trend := h.db.UsageTrend(ctx, tid, from, to)
|
||||
var totTok, totCredits, totCost, totTasks int64
|
||||
for _, d := range trend {
|
||||
totTok += d.TotalTok
|
||||
totCredits += d.CreditsMicro
|
||||
totCost += d.CostMicros
|
||||
totTasks += d.TaskCount
|
||||
}
|
||||
recent := make([]gin.H, 0, 10)
|
||||
for _, u := range h.db.RecentUsage(ctx, tid, 10) {
|
||||
recent = append(recent, gin.H{
|
||||
"task_id": u.TaskID, "model": u.Model, "total_tok": u.TotalTok,
|
||||
"credits_micro": u.CreditsMicro, "cost_micros": u.CostMicros, "currency": u.Currency, "ts": u.TS,
|
||||
})
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"from": from, "to": to,
|
||||
"balance_micro": h.db.TenantBalance(ctx, tid),
|
||||
"credit_enforce": h.db.CreditEnforceEnabled(ctx),
|
||||
"trend": trend,
|
||||
"totals": gin.H{"total_tok": totTok, "credits_micro": totCredits, "cost_micros": totCost, "task_count": totTasks},
|
||||
"recent": recent,
|
||||
})
|
||||
}
|
||||
|
||||
// sessionID 从请求取会话标识(真实场景应由会话中间件注入)。
|
||||
func sessionID(c *gin.Context) string {
|
||||
if s := c.GetHeader("X-Session-ID"); s != "" {
|
||||
|
||||
@@ -57,6 +57,7 @@ func New(db *store.Postgres, cache *store.Redis, bus *nats.Bus, blobStore *blob.
|
||||
p.GET("/tasks/:id", h.TaskStatus) // 任务生命周期状态(UI 轮询 submitted/running/done/failed/timeout/waiting/rejected)
|
||||
p.POST("/tasks/:id/approve", middleware.Audit(db), h.ApproveTask) // HITL 人工审批决定(批准/拒绝,审计)
|
||||
p.GET("/tenants/current", h.TenantCurrent) // 当前租户上下文 + 角色(多租户)
|
||||
p.GET("/me/usage", h.MyUsage) // 我的用量明细(余额 + 趋势 + 最近消耗)
|
||||
p.GET("/tasks/:id/eval", h.TaskEval) // 自动化评测结果(综合/质量/忠实度/分级)
|
||||
p.PUT("/memory", h.SetMemory) // 偏好记忆登记(→ mcp-go memory_upsert)
|
||||
p.GET("/memory", h.ListMemory) // 列出当前用户偏好(记忆面板)
|
||||
|
||||
@@ -114,6 +114,20 @@ func (p *Postgres) SaveUsageEvent(ctx context.Context, ev *contract.UsageEvent)
|
||||
})
|
||||
}
|
||||
|
||||
// RecentUsage 返回某租户最近 n 条用量明细(供用户看"最近消耗")。受租户表→用户 ctx 自动过滤,
|
||||
// 这里也显式带 tenant_id 双保险。
|
||||
func (p *Postgres) RecentUsage(ctx context.Context, tenantID string, n int) []UsageEvent {
|
||||
if p.db == nil || tenantID == "" {
|
||||
return nil
|
||||
}
|
||||
if n <= 0 {
|
||||
n = 10
|
||||
}
|
||||
var out []UsageEvent
|
||||
p.db.WithContext(ctx).Where("tenant_id = ?", tenantID).Order("ts desc").Limit(n).Find(&out)
|
||||
return out
|
||||
}
|
||||
|
||||
// pricingForModelName 按模型名查计价(join model 表,pricing 以 model_id 关联)。查不到返回 nil。
|
||||
func (p *Postgres) pricingForModelName(ctx context.Context, name string) *Pricing {
|
||||
if p.db == nil || name == "" {
|
||||
|
||||
Reference in New Issue
Block a user