fi:修复公共模块问题

This commit is contained in:
qian committed 2026-06-03 08:48:26 +08:00
1 parent 1565416899
commit 573e9ad21b
23 files changed
+376 -231

No files matched your search

-4
View File
@@ -4,7 +4,6 @@ import (
"bufio"
"context"
"encoding/json"
"io"
"regexp"
"strings"
@@ -314,6 +313,3 @@ func extractClaudeNonStreamText(respBody string) string {
b, _ := json.Marshal(result)
return string(b)
}
// init 确保 bufio 和 bytes 被 import 使用
var _ = io.EOF
+6 -1
View File
@@ -124,7 +124,12 @@ func (bw *BatchWriter) autoFlush() {
_ = bw.Flush(gctx.New())
// Reset timer
if bw.closed {
return
}
// Stop before Reset per Go docs recommendation
bw.flushTimer.Stop()
bw.flushTimer.Reset(5 * time.Second)
}
+12 -5
View File
@@ -60,11 +60,18 @@ func (s *ConfigService) GetOption(ctx context.Context, key string) string {
return gconv.String(val)
}
var option Option
var option *Option
err := dao.SysOptions.Ctx(ctx).
Where("key", key).
Scan(&option)
if err != nil || option.Key == "" {
if err != nil {
g.Log().Warningf(ctx, "[Config] DB error reading option %s: %v", key, err)
if def := GetSettingDef(key); def != nil {
return def.Default
}
return ""
}
if option == nil {
if def := GetSettingDef(key); def != nil {
return def.Default
}
@@ -220,7 +227,7 @@ func (s *ConfigService) SetOption(ctx context.Context, key, value string) error
s.cache.Delete(ctx, key)
s.cache.Delete(ctx, "public_options")
s.cache.Delete(ctx, "category:"+getCategoryForKey(key))
s.publishChange(key)
s.publishChange(ctx, key)
return nil
}
@@ -425,8 +432,8 @@ func getCategoryForKey(key string) string {
}
// publishChange notifies other instances via Redis Pub/Sub to invalidate their local cache.
func (s *ConfigService) publishChange(key string) {
_, _ = g.Redis().Publish(context.Background(), "settings:changed", key)
func (s *ConfigService) publishChange(ctx context.Context, key string) {
_, _ = g.Redis().Publish(ctx, "settings:changed", key)
}
// TypedValue converts a string value to its proper Go type based on SettingType.
+6 -4
View File
@@ -25,19 +25,21 @@ func IsFeatureEnabled(ctx context.Context, tenantID int64, featureKey string) bo
// 2. 查租户当前套餐
if tenantID > 0 {
var planID int64
var planRow *struct {
PlanID int64
}
err := dao.PlnTenantPlans.Ctx(ctx).
Where("tenant_id", tenantID).
Where("status", "active").
Fields("plan_id").
Limit(1).
Scan(&planID)
if err == nil && planID > 0 {
Scan(&planRow)
if err == nil && planRow != nil && planRow.PlanID > 0 {
var flag *struct {
Enabled bool `json:"enabled"`
}
err = dao.PlnFeatureFlags.Ctx(ctx).
Where("plan_id", planID).
Where("plan_id", planRow.PlanID).
Where("feature_key", featureKey).
Scan(&flag)
if err == nil && flag != nil && flag.Enabled {
+2 -2
View File
@@ -37,7 +37,7 @@ var jwtSecret []byte
func InitJWTSecret(ctx context.Context) {
secret := g.Cfg().MustGet(ctx, "jwt.secret").String()
if secret == "" {
secret = "team-api-default-jwt-secret-change-me-in-production"
panic("jwt.secret is not configured — refusing to start with weak key")
}
jwtSecret = []byte(secret)
}
@@ -45,7 +45,7 @@ func InitJWTSecret(ctx context.Context) {
// GetJWTSecret returns the cached JWT signing secret.
func GetJWTSecret() []byte {
if jwtSecret == nil {
jwtSecret = []byte("team-api-default-jwt-secret-change-me-in-production")
panic("JWT secret not initialized — call InitJWTSecret first")
}
return jwtSecret
}
+23 -4
View File
@@ -12,18 +12,37 @@ import (
)
var (
proxyClientOnce sync.Once
proxyClientMu sync.RWMutex
proxyClientValue *http.Client
)
// GetHTTPClient returns an HTTP client configured with proxy if channel_proxy_url is set in system settings.
// Rebuilds on each call so runtime config changes take effect.
func GetHTTPClient() *http.Client {
proxyClientOnce.Do(func() {
proxyClientValue = buildProxyClient()
})
proxyClientMu.RLock()
client := proxyClientValue
proxyClientMu.RUnlock()
if client != nil {
return client
}
proxyClientMu.Lock()
defer proxyClientMu.Unlock()
// Double-check after acquiring write lock
if proxyClientValue != nil {
return proxyClientValue
}
proxyClientValue = buildProxyClient()
return proxyClientValue
}
// ResetHTTPClient forces rebuilding the HTTP client on next GetHTTPClient call.
func ResetHTTPClient() {
proxyClientMu.Lock()
defer proxyClientMu.Unlock()
proxyClientValue = nil
}
func buildProxyClient() *http.Client {
transport := http.DefaultTransport.(*http.Transport).Clone()
+3 -2
View File
@@ -10,6 +10,7 @@ import (
"time"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gctx"
)
const (
@@ -163,7 +164,7 @@ func GeminiExchangeCode(sessionID, code string) (*OAuthKeyData, error) {
if oauthType == "code_assist" {
projectID, err = geminiLoadCodeAssist(tokenResp.AccessToken)
if err != nil {
g.Log().Warningf(nil, "Gemini loadCodeAssist 失败: %v", err)
g.Log().Warningf(gctx.New(), "Gemini loadCodeAssist 失败: %v", err)
// 不阻断流程,projectID 留空
}
}
@@ -171,7 +172,7 @@ func GeminiExchangeCode(sessionID, code string) (*OAuthKeyData, error) {
// 获取用户邮箱
email, err := geminiGetUserInfo(tokenResp.AccessToken)
if err != nil {
g.Log().Warningf(nil, "Gemini 获取用户信息失败: %v", err)
g.Log().Warningf(gctx.New(), "Gemini 获取用户信息失败: %v", err)
}
// 计算过期时间,带安全窗口
+5 -4
View File
@@ -12,6 +12,7 @@ import (
"time"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gctx"
)
const (
@@ -128,7 +129,7 @@ func OpenAIExchangeCode(sessionID, code, state string) (*OAuthKeyData, error) {
}
if resp.StatusCode != http.StatusOK {
g.Log().Warningf(nil, "OpenAI token 交换失败, status=%d, body=%s", resp.StatusCode, string(body))
g.Log().Warningf(gctx.New(), "OpenAI token 交换失败, status=%d, body=%s", resp.StatusCode, string(body))
return nil, fmt.Errorf("token 交换失败 (HTTP %d): %s", resp.StatusCode, string(body))
}
@@ -140,7 +141,7 @@ func OpenAIExchangeCode(sessionID, code, state string) (*OAuthKeyData, error) {
// 解析 ID token JWT payload 提取用户信息
claims, err := parseOpenAIIDToken(tokenResp.IDToken)
if err != nil {
g.Log().Warningf(nil, "解析 OpenAI ID token 失败: %v", err)
g.Log().Warningf(gctx.New(), "解析 OpenAI ID token 失败: %v", err)
// ID token 解析失败不阻断流程,继续返回基本 token 数据
}
@@ -191,7 +192,7 @@ func OpenAIRefreshToken(refreshToken string) (*OAuthKeyData, error) {
}
if resp.StatusCode != http.StatusOK {
g.Log().Warningf(nil, "OpenAI token 刷新失败, status=%d, body=%s", resp.StatusCode, string(body))
g.Log().Warningf(gctx.New(), "OpenAI token 刷新失败, status=%d, body=%s", resp.StatusCode, string(body))
return nil, fmt.Errorf("token 刷新失败 (HTTP %d): %s", resp.StatusCode, string(body))
}
@@ -203,7 +204,7 @@ func OpenAIRefreshToken(refreshToken string) (*OAuthKeyData, error) {
// 刷新响应中可能包含新的 ID token
claims, err := parseOpenAIIDToken(tokenResp.IDToken)
if err != nil {
g.Log().Warningf(nil, "解析 OpenAI ID token (refresh) 失败: %v", err)
g.Log().Warningf(gctx.New(), "解析 OpenAI ID token (refresh) 失败: %v", err)
}
keyData := &OAuthKeyData{
+3
View File
@@ -68,6 +68,9 @@ func EnsurePartitions(ctx context.Context) error {
continue
}
// DDL statements cannot use parameterized placeholders (?/) — partition/table names
// and DATE literals in FOR VALUES are not bindable. Names are validated by safeNameRe
// and dates are generated by time.Format (fixed format), so no injection risk.
sql := fmt.Sprintf(
`CREATE TABLE IF NOT EXISTS %s PARTITION OF %s FOR VALUES FROM ('%s') TO ('%s')`,
partitionName, pt.table, from, to,
+2 -2
View File
@@ -95,7 +95,7 @@ func FetchLiteLLMPricing(ctx context.Context) (map[string]*LiteLLMModelEntry, er
}
if err := litellmCache.Set(ctx, litellmCacheKey, raw, litellmCacheTTL); err != nil {
g.Log().Warning(ctx, "[PricingSource] failed to cache pricing data: %v", err)
g.Log().Warningf(ctx, "[PricingSource] failed to cache pricing data: %v", err)
}
g.Log().Infof(ctx, "[PricingSource] loaded %d models from LiteLLM", len(raw))
@@ -242,7 +242,7 @@ func FetchModelsDevPricing(ctx context.Context) (map[string]*ModelsDevModelEntry
}
if err := modelsDevCache.Set(ctx, modelsDevCacheKey, result, modelsDevCacheTTL); err != nil {
g.Log().Warning(ctx, "[PricingSource] failed to cache models.dev data: %v", err)
g.Log().Warningf(ctx, "[PricingSource] failed to cache models.dev data: %v", err)
}
g.Log().Infof(ctx, "[PricingSource] loaded %d models from models.dev", len(result))
+22 -6
View File
@@ -9,6 +9,7 @@ import (
"strings"
"time"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/frame/g"
"github.com/golang-jwt/jwt/v5"
@@ -188,10 +189,16 @@ func Enable2FA(ctx context.Context, userType string, userID int64, secret, code,
// Hash backup codes for storage
hashedCodes := make([]string, len(plainCodes))
for i, code := range plainCodes {
hash, _ := crypto.HashPassword(code)
hash, herr := crypto.HashPassword(code)
if herr != nil {
return nil, gerror.Wrapf(herr, "hash backup code failed")
}
hashedCodes[i] = hash
}
codesJSON, _ := json.Marshal(hashedCodes)
codesJSON, jerr := json.Marshal(hashedCodes)
if jerr != nil {
return nil, gerror.Wrapf(jerr, "marshal backup codes failed")
}
// Encrypt TOTP secret
encKey := getEncryptionKey(ctx)
@@ -341,10 +348,16 @@ func RegenerateBackupCodes(ctx context.Context, userType string, userID int64, c
hashedCodes := make([]string, len(plainCodes))
for i, c := range plainCodes {
hash, _ := crypto.HashPassword(c)
hash, herr := crypto.HashPassword(c)
if herr != nil {
return nil, gerror.Wrapf(herr, "hash backup code failed")
}
hashedCodes[i] = hash
}
codesJSON, _ := json.Marshal(hashedCodes)
codesJSON, jerr := json.Marshal(hashedCodes)
if jerr != nil {
return nil, gerror.Wrapf(jerr, "marshal backup codes failed")
}
if userType == "admin" {
_, err = dao.SysAdminUsers.Ctx(ctx).Where("id", userID).Data(do.SysAdminUsers{
@@ -458,7 +471,10 @@ func consumeBackupCode(ctx context.Context, userType string, userID int64, index
newCodes = append(newCodes, c)
}
}
codesJSON, _ := json.Marshal(newCodes)
codesJSON, merr := json.Marshal(newCodes)
if merr != nil {
return gerror.Wrapf(merr, "marshal backup codes failed")
}
var err error
if userType == "admin" {
@@ -476,7 +492,7 @@ func consumeBackupCode(ctx context.Context, userType string, userID int64, index
func getEncryptionKey(ctx context.Context) []byte {
hexKey := g.Cfg().MustGet(ctx, "crypto.encryptionKey").String()
if hexKey == "" {
hexKey = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
panic("crypto.encryptionKey is not configured — refusing to start with weak key")
}
return crypto.MustGetEncryptionKey(hexKey)
}
+10 -4
View File
@@ -111,11 +111,14 @@ func (s *FileService) Upload(ctx context.Context, upload *FileUpload) (*FileReco
// GetDownloadURL generates a presigned URL for downloading a file.
func (s *FileService) GetDownloadURL(ctx context.Context, fileID int64) (string, error) {
var record FileRecord
var record *FileRecord
err := dao.FilFiles.Ctx(ctx).
Where("id", fileID).
Scan(&record)
if err != nil || record.ID == 0 {
if err != nil {
return "", gerror.Wrapf(err, "query file %d", fileID)
}
if record == nil {
return "", gerror.Newf("file not found: %d", fileID)
}
@@ -124,11 +127,14 @@ func (s *FileService) GetDownloadURL(ctx context.Context, fileID int64) (string,
// Delete deletes a file from storage and marks it as deleted.
func (s *FileService) Delete(ctx context.Context, fileID int64) error {
var record FileRecord
var record *FileRecord
err := dao.FilFiles.Ctx(ctx).
Where("id", fileID).
Scan(&record)
if err != nil || record.ID == 0 {
if err != nil {
return gerror.Wrapf(err, "query file %d", fileID)
}
if record == nil {
return gerror.Newf("file not found: %d", fileID)
}
+10 -9
View File
@@ -3,7 +3,6 @@ package common
import (
"context"
"crypto/rand"
"database/sql"
"fmt"
"math/big"
"strings"
@@ -51,9 +50,9 @@ func SendVerifyCode(ctx context.Context, email string, purpose VerifyPurpose) er
if err == nil && hourlyCount.Int() > 5 {
return NewBusinessError(consts.CodeRateLimitExceeded, "每小时最多发送5次验证码")
}
if hourlyCount.Int() == 1 {
// Set expiry on first increment
_, _ = g.Redis().Do(ctx, "EXPIRE", hourlyKey, 3600)
// Always ensure TTL is set (idempotent: EXPIRE resets existing TTL or sets missing one)
if _, err := g.Redis().Do(ctx, "EXPIRE", hourlyKey, 3600); err != nil {
g.Log().Warningf(ctx, "failed to set hourly rate limit TTL: %v", err)
}
// Generate 6-digit code
@@ -74,6 +73,11 @@ func SendVerifyCode(ctx context.Context, email string, purpose VerifyPurpose) er
}
// Send email (fire and forget, log error but don't fail)
// Set cooldown before async send (prevents duplicate sends within 60s)
if _, err := g.Redis().Do(ctx, "SETEX", cooldownKey, 60, "1"); err != nil {
g.Log().Warningf(ctx, "failed to set verify code cooldown: %v", err)
}
go func() {
bgCtx := context.Background()
emailCfg, err := EmailConfigFromOptions(bgCtx)
@@ -100,9 +104,6 @@ func SendVerifyCode(ctx context.Context, email string, purpose VerifyPurpose) er
}
}()
// Set cooldown
_, _ = g.Redis().Do(ctx, "SETEX", cooldownKey, 60, "1")
return nil
}
@@ -129,11 +130,11 @@ func VerifyCode(ctx context.Context, email, code, purpose string) error {
OrderDesc("created_at").
Limit(1).
Scan(&record)
if err != nil && err != sql.ErrNoRows {
if err != nil {
return gerror.Wrapf(err, "query verify code")
}
if record.ID == 0 {
if record == nil {
return NewBusinessError(consts.CodeVerifyCodeInvalid, "验证码错误")
}
+9 -15
View File
@@ -3,7 +3,6 @@ package tenant
import (
"context"
"encoding/json"
"fmt"
"strings"
"github.com/gogf/gf/v2/frame/g"
@@ -105,17 +104,14 @@ func (s *sTenant) AuditLogs(ctx context.Context, req *v1.TenantAuditLogsReq) (*v
page, pageSize := common.NormalizePagination(req.Page, req.PageSize)
// 使用原生 SQL 查询,绕过 GoFrame ScanAndCount 对 map[string]any 的 bug
dataSQL := fmt.Sprintf(
`SELECT id, tenant_id, user_id, user_type, action, resource_type, resource_id, detail, changes_json, ip_address, created_at
FROM aud_operation_logs WHERE tenant_id = ? ORDER BY created_at DESC LIMIT %d OFFSET %d`,
pageSize, (page-1)*pageSize,
)
result, err := g.DB().Ctx(ctx).Query(ctx, dataSQL, tenantID)
dataSQL := `SELECT id, tenant_id, user_id, user_type, action, resource_type, resource_id, detail, changes_json, ip_address, created_at
FROM aud_operation_logs WHERE tenant_id = ? ORDER BY created_at DESC LIMIT ? OFFSET ?`
result, err := g.DB().Query(ctx, dataSQL, tenantID, pageSize, (page-1)*pageSize)
if err != nil {
return nil, err
}
countResult, err := g.DB().Ctx(ctx).Query(ctx, "SELECT COUNT(*) AS total FROM aud_operation_logs WHERE tenant_id = ?", tenantID)
countResult, err := g.DB().Query(ctx, "SELECT COUNT(*) AS total FROM aud_operation_logs WHERE tenant_id = ?", tenantID)
if err != nil {
return nil, err
}
@@ -207,7 +203,7 @@ func (s *sTenant) TenantRequestAuditLogs(ctx context.Context, req *v1.TenantRequ
fromClause := "aud_request_logs a LEFT JOIN tnt_users t ON a.user_id = t.id AND a.tenant_id = t.tenant_id LEFT JOIN tnt_projects p ON a.project_id = p.id"
countSQL := "SELECT COUNT(*) AS total FROM " + fromClause + " WHERE " + where
countResult, err := g.DB().Ctx(ctx).Query(ctx, countSQL, args...)
countResult, err := g.DB().Query(ctx, countSQL, args...)
if err != nil {
return nil, err
}
@@ -216,12 +212,10 @@ func (s *sTenant) TenantRequestAuditLogs(ctx context.Context, req *v1.TenantRequ
total = countResult[0]["total"].Int()
}
dataSQL := fmt.Sprintf(
`SELECT a.id, a.request_id, COALESCE(t.username, '') AS username, a.project_id, COALESCE(p.name, '') AS project_name, a.user_id, a.method, a.path, a.query_params, a.status_code, a.client_ip, a.user_agent, a.latency_ms, a.first_token_ms, a.tenant_audit_level AS audit_level, a.task_id, a.task_status, a.task_completed_at, a.created_at
FROM %s WHERE %s ORDER BY a.created_at DESC LIMIT %d OFFSET %d`,
fromClause, where, pageSize, (page-1)*pageSize,
)
result, err := g.DB().Ctx(ctx).Query(ctx, dataSQL, args...)
dataSQL := `SELECT a.id, a.request_id, COALESCE(t.username, '') AS username, a.project_id, COALESCE(p.name, '') AS project_name, a.user_id, a.method, a.path, a.query_params, a.status_code, a.client_ip, a.user_agent, a.latency_ms, a.first_token_ms, a.tenant_audit_level AS audit_level, a.task_id, a.task_status, a.task_completed_at, a.created_at
FROM ` + fromClause + ` WHERE ` + where + ` ORDER BY a.created_at DESC LIMIT ? OFFSET ?`
dataArgs := append(args, pageSize, (page-1)*pageSize)
result, err := g.DB().Query(ctx, dataSQL, dataArgs...)
if err != nil {
return nil, err
}
+54 -26
View File
@@ -2,17 +2,23 @@ package tenant
import (
"context"
"github.com/qianfree/team-api/internal/dao"
"github.com/qianfree/team-api/internal/logic/common"
"math"
"time"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gtime"
v1 "github.com/qianfree/team-api/api/tenant/v1"
"github.com/qianfree/team-api/internal/dao"
"github.com/qianfree/team-api/internal/logic/common"
"github.com/qianfree/team-api/internal/middleware"
)
// roundUSD 精确到小数点后 6 位,符合 CLAUDE.md 资金精度规范
func roundUSD(v float64) float64 {
return math.Round(v*1000000) / 1000000
}
// Dashboard returns the tenant dashboard statistics.
func (s *sTenant) Dashboard(ctx context.Context, req *v1.TenantDashboardReq) (*v1.TenantDashboardRes, error) {
role := middleware.GetUserRole(ctx)
@@ -20,8 +26,9 @@ func (s *sTenant) Dashboard(ctx context.Context, req *v1.TenantDashboardReq) (*v
return nil, common.NewForbiddenError("需要 owner 或 admin 权限")
}
tenantID := middleware.GetTenantID(ctx)
today := time.Now().Format("2006-01-02")
monthStart := time.Now().Format("2006-01") + "-01"
now := gtime.Now()
today := now.Format("Y-m-d")
monthStart := now.Format("Y-m") + "-01"
// 今日统计
type dayStats struct {
@@ -31,19 +38,25 @@ func (s *sTenant) Dashboard(ctx context.Context, req *v1.TenantDashboardReq) (*v
TotalCost float64 `json:"total_cost"`
}
var todayRow dayStats
dao.BilUsageLogs.Ctx(ctx).
err := dao.BilUsageLogs.Ctx(ctx).
Where("tenant_id", tenantID).
Where("created_at >= ?", today+" 00:00:00").
Fields("COUNT(*) as requests, COALESCE(SUM(input_tokens), 0) as input_tokens, COALESCE(SUM(output_tokens), 0) as output_tokens, COALESCE(SUM(total_cost), 0) as total_cost").
Scan(&todayRow)
if err != nil {
return nil, err
}
// 本月统计
var monthRow dayStats
dao.BilUsageLogs.Ctx(ctx).
err = dao.BilUsageLogs.Ctx(ctx).
Where("tenant_id", tenantID).
Where("created_at >= ?", monthStart+" 00:00:00").
Fields("COUNT(*) as requests, COALESCE(SUM(input_tokens), 0) as input_tokens, COALESCE(SUM(output_tokens), 0) as output_tokens, COALESCE(SUM(total_cost), 0) as total_cost").
Scan(&monthRow)
if err != nil {
return nil, err
}
// 钱包余额
var wallet *struct {
@@ -51,10 +64,13 @@ func (s *sTenant) Dashboard(ctx context.Context, req *v1.TenantDashboardReq) (*v
FrozenBalance float64 `json:"frozen_balance"`
WarningThreshold float64 `json:"warning_threshold"`
}
dao.BilWallets.Ctx(ctx).
err = dao.BilWallets.Ctx(ctx).
Where("tenant_id", tenantID).
Fields("balance, frozen_balance, warning_threshold").
Scan(&wallet)
if err != nil {
return nil, err
}
if wallet == nil {
wallet = &struct {
@@ -65,35 +81,41 @@ func (s *sTenant) Dashboard(ctx context.Context, req *v1.TenantDashboardReq) (*v
}
// 活跃Key数
activeKeys, _ := dao.ApiKeys.Ctx(ctx).
activeKeys, err := dao.ApiKeys.Ctx(ctx).
Where("tenant_id", tenantID).
Where("status", "active").
Count()
if err != nil {
return nil, err
}
// 成员数
memberCount, _ := dao.TntUsers.Ctx(ctx).
memberCount, err := dao.TntUsers.Ctx(ctx).
Where("tenant_id", tenantID).
Where("status", "active").
Count()
if err != nil {
return nil, err
}
return &v1.TenantDashboardRes{
Today: map[string]any{
"requests": todayRow.Requests,
"input_tokens": todayRow.InputTokens,
"output_tokens": todayRow.OutputTokens,
"total_cost": todayRow.TotalCost,
"total_cost": roundUSD(todayRow.TotalCost),
},
Month: map[string]any{
"requests": monthRow.Requests,
"input_tokens": monthRow.InputTokens,
"output_tokens": monthRow.OutputTokens,
"total_cost": monthRow.TotalCost,
"total_cost": roundUSD(monthRow.TotalCost),
},
Wallet: map[string]any{
"balance": wallet.Balance,
"frozen_balance": wallet.FrozenBalance,
"available": wallet.Balance - wallet.FrozenBalance,
"warning_threshold": wallet.WarningThreshold,
"balance": roundUSD(wallet.Balance),
"frozen_balance": roundUSD(wallet.FrozenBalance),
"available": roundUSD(wallet.Balance - wallet.FrozenBalance),
"warning_threshold": roundUSD(wallet.WarningThreshold),
},
ActiveKeys: activeKeys,
MemberCount: memberCount,
@@ -112,7 +134,7 @@ func (s *sTenant) TokenTrends(ctx context.Context, req *v1.TenantTokenTrendsReq)
days = 30
}
startDate := time.Now().AddDate(0, 0, -days).Format("2006-01-02")
startDate := gtime.Now().AddDate(0, 0, -days).Format("Y-m-d")
type tokenTrendRow struct {
Date string `json:"date"`
@@ -149,7 +171,7 @@ func (s *sTenant) TokenTrends(ctx context.Context, req *v1.TenantTokenTrendsReq)
"input_tokens": r.InputTokens,
"output_tokens": r.OutputTokens,
"requests": r.Requests,
"total_cost": r.TotalCost,
"total_cost": roundUSD(r.TotalCost),
})
}
@@ -170,7 +192,7 @@ func (s *sTenant) ModelDistribution(ctx context.Context, req *v1.TenantModelDist
days = 30
}
startDate := time.Now().AddDate(0, 0, -days).Format("2006-01-02")
startDate := gtime.Now().AddDate(0, 0, -days).Format("Y-m-d")
type modelDistRow struct {
ModelName string `json:"model_name"`
@@ -208,7 +230,7 @@ func (s *sTenant) ModelDistribution(ctx context.Context, req *v1.TenantModelDist
"requests": r.Requests,
"input_tokens": r.InputTokens,
"output_tokens": r.OutputTokens,
"total_cost": r.TotalCost,
"total_cost": roundUSD(r.TotalCost),
})
}
@@ -225,27 +247,33 @@ func (s *sTenant) BalancePrediction(ctx context.Context, req *v1.TenantBalancePr
}
tenantID := middleware.GetTenantID(ctx)
sevenDaysAgo := time.Now().AddDate(0, 0, -7).Format("2006-01-02")
sevenDaysAgo := gtime.Now().AddDate(0, 0, -7).Format("Y-m-d")
var stats struct {
TotalCost float64 `json:"total_cost"`
}
dao.BilUsageLogs.Ctx(ctx).
err := dao.BilUsageLogs.Ctx(ctx).
Where("tenant_id", tenantID).
Where("created_at >= ?", sevenDaysAgo+" 00:00:00").
Fields("COALESCE(SUM(total_cost), 0) as total_cost").
Scan(&stats)
if err != nil {
return nil, err
}
dailyAvg := stats.TotalCost / 7.0
dailyAvg := roundUSD(stats.TotalCost / 7.0)
var wallet *struct {
Balance float64 `json:"balance"`
FrozenBalance float64 `json:"frozen_balance"`
}
dao.BilWallets.Ctx(ctx).
err = dao.BilWallets.Ctx(ctx).
Where("tenant_id", tenantID).
Fields("balance, frozen_balance").
Scan(&wallet)
if err != nil {
return nil, err
}
if wallet == nil {
wallet = &struct {
Balance float64 `json:"balance"`
@@ -253,7 +281,7 @@ func (s *sTenant) BalancePrediction(ctx context.Context, req *v1.TenantBalancePr
}{Balance: 0, FrozenBalance: 0}
}
available := wallet.Balance - wallet.FrozenBalance
available := roundUSD(wallet.Balance - wallet.FrozenBalance)
res := &v1.TenantBalancePredictionRes{
DailyAvgCost: dailyAvg,
AvailableBalance: available,
@@ -308,7 +336,7 @@ func (s *sTenant) GetMemberUsageRanking(ctx context.Context, req *v1.TenantMembe
limit = 10
}
startDate := time.Now().AddDate(0, 0, -days).Format("2006-01-02")
startDate := gtime.Now().AddDate(0, 0, -days).Format("Y-m-d")
type memberUsageRow struct {
UserId int64 `json:"user_id"`
@@ -353,7 +381,7 @@ func (s *sTenant) GetMemberUsageRanking(ctx context.Context, req *v1.TenantMembe
"requests": r.Requests,
"input_tokens": r.InputTokens,
"output_tokens": r.OutputTokens,
"total_cost": r.TotalCost,
"total_cost": roundUSD(r.TotalCost),
})
}
+1 -1
View File
@@ -129,7 +129,7 @@ func (s *sTenant) ChangeEmail(ctx context.Context, req *v1.TenantChangeEmailReq)
// Get old email for notification
var user *entity.TntUsers
err = dao.TntUsers.Ctx(ctx).
Where("id", userID).Scan(&user)
Where("id", userID).Where("tenant_id", tenantID).Scan(&user)
if err = common.IgnoreScanNoRows(err); err != nil {
return nil, err
}
+5 -9
View File
@@ -44,7 +44,7 @@ func (s *sTenant) CreateFeedback(ctx context.Context, req *v1.FeedbackCreateReq)
func (s *sTenant) ListFeedbacks(ctx context.Context, req *v1.FeedbackListReq) (*v1.FeedbackListRes, error) {
tenantID := middleware.GetTenantID(ctx)
userID := middleware.GetUserID(ctx)
page, pageSize := normalizePagination(req.Page, req.PageSize)
page, pageSize := common.NormalizePagination(req.Page, req.PageSize)
query := dao.SptFeedbacks.Ctx(ctx).
Where("tenant_id", tenantID).
@@ -78,22 +78,18 @@ func (s *sTenant) GetFeedback(ctx context.Context, req *v1.FeedbackGetReq) (*v1.
tenantID := middleware.GetTenantID(ctx)
userID := middleware.GetUserID(ctx)
var row v1.FeedbackGetRes
var row *v1.FeedbackGetRes
err := dao.SptFeedbacks.Ctx(ctx).
Where("id", req.Id).
Where("tenant_id", tenantID).
Where("user_id", userID).
Scan(&row)
if err != nil {
if err = common.IgnoreScanNoRows(err); err != nil {
return nil, err
}
if row.Id == 0 {
if row == nil {
return nil, common.NewBusinessError(10063, "反馈不存在")
}
return &row, nil
}
func normalizePagination(page, pageSize int) (int, int) {
return common.NormalizePagination(page, pageSize)
return row, nil
}
+104 -64
View File
@@ -19,6 +19,78 @@ type pricingTierRow struct {
OutputPrice float64 `json:"output_price"`
}
// memberModelScopeNoAccess 表示成员无权访问任何模型的哨兵值
const memberModelScopeNoAccess = -1
// tenantModelPriceRow 显式分配模型的价格查询结果
type tenantModelPriceRow struct {
ModelDBID int64 `json:"model_db_id"`
ID int64 `json:"id"`
BillingMode *string `json:"billing_mode"`
PerRequestPrice *float64 `json:"per_request_price"`
DiscountRatio *float64 `json:"discount_ratio"`
MaxConcurrency *int `json:"max_concurrency"`
BaseInputPrice float64 `json:"base_input_price"`
BaseOutputPrice float64 `json:"base_output_price"`
BaseCacheReadPrice float64 `json:"base_cache_read_price"`
BaseCacheCreationPrice float64 `json:"base_cache_creation_price"`
BaseBillingMode string `json:"base_billing_mode"`
CustomInputPrice *float64 `json:"custom_input_price"`
CustomOutputPrice *float64 `json:"custom_output_price"`
CustomCacheReadPrice *float64 `json:"custom_cache_read_price"`
CustomCacheCreationPrice *float64 `json:"custom_cache_creation_price"`
CustomPricingTiers string `json:"custom_pricing_tiers"`
}
// groupPriceRow 分组模型的 base 价格查询结果
type groupPriceRow struct {
ModelID int64 `json:"model_id"`
BaseBillingMode string `json:"base_billing_mode"`
BaseInputPrice float64 `json:"base_input_price"`
BaseOutputPrice float64 `json:"base_output_price"`
}
// baseTierRow 阶梯定价查询结果
type baseTierRow struct {
ModelId int64 `json:"model_id"`
MinTokens int64 `json:"min_tokens"`
MaxTokens *int64 `json:"max_tokens"`
InputPrice float64 `json:"input_price"`
OutputPrice float64 `json:"output_price"`
}
// memberScopeRow 成员模型范围查询结果
type memberScopeRow struct {
ModelID int64 `json:"model_id"`
ModelName string `json:"model_name"`
}
// priceInfo 显式模型的完整价格信息
type priceInfo struct {
ID int64
BillingMode *string
PerRequestPrice *float64
DiscountRatio *float64
MaxConcurrency *int
BaseInputPrice float64
BaseOutputPrice float64
BaseCacheReadPrice float64
BaseCacheCreationPrice float64
BaseBillingMode string
CustomInputPrice *float64
CustomOutputPrice *float64
CustomCacheReadPrice *float64
CustomCacheCreationPrice *float64
CustomPricingTiers string
}
// groupPriceInfo 分组模型的价格信息
type groupPriceInfo struct {
BaseBillingMode string
BaseInputPrice float64
BaseOutputPrice float64
}
// ListAvailableModels 获取租户可用的模型列表
func (s *sTenant) ListAvailableModels(ctx context.Context, req *v1.TenantAvailableModelsReq) (*v1.TenantAvailableModelsRes, error) {
tenantID := middleware.GetTenantID(ctx)
@@ -33,24 +105,7 @@ func (s *sTenant) ListAvailableModels(ctx context.Context, req *v1.TenantAvailab
}
// 查询显式分配模型的完整价格信息
var priceResults []struct {
ModelDBID int64 `json:"model_db_id"`
ID int64 `json:"id"`
BillingMode *string `json:"billing_mode"`
PerRequestPrice *float64 `json:"per_request_price"`
DiscountRatio *float64 `json:"discount_ratio"`
MaxConcurrency *int `json:"max_concurrency"`
BaseInputPrice float64 `json:"base_input_price"`
BaseOutputPrice float64 `json:"base_output_price"`
BaseCacheReadPrice float64 `json:"base_cache_read_price"`
BaseCacheCreationPrice float64 `json:"base_cache_creation_price"`
BaseBillingMode string `json:"base_billing_mode"`
CustomInputPrice *float64 `json:"custom_input_price"`
CustomOutputPrice *float64 `json:"custom_output_price"`
CustomCacheReadPrice *float64 `json:"custom_cache_read_price"`
CustomCacheCreationPrice *float64 `json:"custom_cache_creation_price"`
CustomPricingTiers string `json:"custom_pricing_tiers"`
}
var priceResults []tenantModelPriceRow
explicitDBIDs := make([]int64, 0)
for _, m := range models {
@@ -60,32 +115,18 @@ func (s *sTenant) ListAvailableModels(ctx context.Context, req *v1.TenantAvailab
}
if len(explicitDBIDs) > 0 {
_ = dao.MdlTenantModels.Ctx(ctx).
err = dao.MdlTenantModels.Ctx(ctx).
LeftJoin("mdl_pricing p ON p.model_id = mdl_tenant_models.model_id AND p.min_tokens = 0").
Where("mdl_tenant_models.tenant_id", tenantID).
WhereIn("mdl_tenant_models.model_id", explicitDBIDs).
Fields("mdl_tenant_models.model_id AS model_db_id, mdl_tenant_models.id, mdl_tenant_models.billing_mode, mdl_tenant_models.per_request_price, mdl_tenant_models.discount_ratio, mdl_tenant_models.max_concurrency, p.input_price AS base_input_price, p.output_price AS base_output_price, p.cache_read_price AS base_cache_read_price, p.cache_creation_price AS base_cache_creation_price, p.billing_mode AS base_billing_mode, mdl_tenant_models.custom_input_price, mdl_tenant_models.custom_output_price, mdl_tenant_models.custom_cache_read_price, mdl_tenant_models.custom_cache_creation_price, mdl_tenant_models.custom_pricing_tiers").
Scan(&priceResults)
if err != nil {
return nil, err
}
}
// 构建显式模型价格映射
type priceInfo struct {
ID int64
BillingMode *string
PerRequestPrice *float64
DiscountRatio *float64
MaxConcurrency *int
BaseInputPrice float64
BaseOutputPrice float64
BaseCacheReadPrice float64
BaseCacheCreationPrice float64
BaseBillingMode string
CustomInputPrice *float64
CustomOutputPrice *float64
CustomCacheReadPrice *float64
CustomCacheCreationPrice *float64
CustomPricingTiers string
}
priceMap := make(map[int64]*priceInfo, len(priceResults))
for _, r := range priceResults {
priceMap[r.ModelDBID] = &priceInfo{
@@ -115,24 +156,17 @@ func (s *sTenant) ListAvailableModels(ctx context.Context, req *v1.TenantAvailab
}
}
type groupPriceInfo struct {
BaseBillingMode string `json:"base_billing_mode"`
BaseInputPrice float64 `json:"base_input_price"`
BaseOutputPrice float64 `json:"base_output_price"`
}
groupPriceMap := make(map[int64]*groupPriceInfo)
if len(groupDBIDs) > 0 {
var groupPrices []struct {
ModelID int64 `json:"model_id"`
BaseBillingMode string `json:"base_billing_mode"`
BaseInputPrice float64 `json:"base_input_price"`
BaseOutputPrice float64 `json:"base_output_price"`
}
_ = dao.MdlPricing.Ctx(ctx).
var groupPrices []groupPriceRow
err = dao.MdlPricing.Ctx(ctx).
WhereIn("model_id", groupDBIDs).
Where("min_tokens", 0).
Fields("model_id, billing_mode AS base_billing_mode, input_price AS base_input_price, output_price AS base_output_price").
Scan(&groupPrices)
if err != nil {
return nil, err
}
for _, gp := range groupPrices {
groupPriceMap[gp.ModelID] = &groupPriceInfo{
@@ -158,14 +192,8 @@ func (s *sTenant) ListAvailableModels(ctx context.Context, req *v1.TenantAvailab
baseTiersMap := make(map[int64][]v1.PricingTierItem)
if len(tieredModelDBIDs) > 0 {
var baseTiers []struct {
ModelId int64 `json:"model_id"`
MinTokens int64 `json:"min_tokens"`
MaxTokens *int64 `json:"max_tokens"`
InputPrice float64 `json:"input_price"`
OutputPrice float64 `json:"output_price"`
}
_ = dao.MdlPricing.Ctx(ctx).
var baseTiers []baseTierRow
err = dao.MdlPricing.Ctx(ctx).
WhereIn("model_id", tieredModelDBIDs).
Where("billing_mode", "tiered").
Where("min_tokens > 0").
@@ -173,6 +201,9 @@ func (s *sTenant) ListAvailableModels(ctx context.Context, req *v1.TenantAvailab
OrderAsc("model_id").
OrderAsc("min_tokens").
Scan(&baseTiers)
if err != nil {
return nil, err
}
for _, t := range baseTiers {
baseTiersMap[t.ModelId] = append(baseTiersMap[t.ModelId], v1.PricingTierItem{
@@ -288,23 +319,32 @@ func (s *sTenant) ListAvailableModels(ctx context.Context, req *v1.TenantAvailab
// 按成员模型范围过滤
userID := middleware.GetUserID(ctx)
if userID > 0 {
var memberScopes []struct {
ModelID int64 `json:"model_id"`
ModelName string `json:"model_name"`
}
_ = g.DB().Model("tnt_member_model_scopes ms").Ctx(ctx).
var memberScopes []memberScopeRow
err = g.DB().Model("tnt_member_model_scopes ms").Ctx(ctx).
LeftJoin("mdl_models m ON ms.model_id = m.id").
Where("ms.tenant_id", tenantID).
Where("ms.user_id", userID).
Fields("ms.model_id, m.model_id as model_name").
Scan(&memberScopes)
if err != nil {
return nil, err
}
if len(memberScopes) > 0 {
// 检查是否存在"无权访问任何模型"的哨兵值
hasNoAccess := false
for _, s := range memberScopes {
if s.ModelID == memberModelScopeNoAccess {
hasNoAccess = true
break
}
}
if hasNoAccess {
return &v1.TenantAvailableModelsRes{List: nil}, nil
}
allowed := make(map[string]bool, len(memberScopes))
for _, s := range memberScopes {
if s.ModelID == -1 {
return &v1.TenantAvailableModelsRes{List: nil}, nil
}
if s.ModelName != "" {
allowed[s.ModelName] = true
}
+46 -38
View File
@@ -4,12 +4,14 @@ import (
"context"
"fmt"
"math"
"sort"
"strings"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gtime"
v1 "github.com/qianfree/team-api/api/tenant/v1"
"github.com/qianfree/team-api/internal/logic/common"
"github.com/qianfree/team-api/internal/middleware"
)
@@ -25,35 +27,33 @@ func (s *sTenant) ModelComparison(ctx context.Context, req *v1.ModelComparisonRe
models[i] = strings.TrimSpace(m)
}
if len(models) < 2 || len(models) > 4 {
return nil, fmt.Errorf("请选择 2-4 个模型进行对比")
return nil, common.NewBadRequestError("请选择 2-4 个模型进行对比")
}
since := gtime.Now().AddDate(0, 0, -req.Days).Format("Y-m-d")
// Build model placeholders
placeholders := make([]string, len(models))
// Build args for parameterized query
args := []any{tenantID, since}
for i, m := range models {
placeholders[i] = "?"
for _, m := range models {
args = append(args, m)
}
modelIn := strings.Join(placeholders, ",")
// Aggregate per-model stats
aggQuery := fmt.Sprintf(`
SELECT
model_name,
COUNT(*) as requests,
COUNT(*) FILTER (WHERE status = 'success') as success_count,
COALESCE(AVG(latency_ms) FILTER (WHERE status = 'success'), 0) as avg_latency,
COALESCE(PERCENTILE_CONT(0.95) WITHIN GROUP (ORDER BY latency_ms) FILTER (WHERE status = 'success'), 0) as p95_latency,
COALESCE(SUM(total_cost), 0) as total_cost,
COALESCE(SUM(input_tokens), 0) as input_tokens,
COALESCE(SUM(output_tokens), 0) as output_tokens
FROM bil_usage_logs
WHERE tenant_id = ? AND created_at >= ? AND model_name IN (%s)
GROUP BY model_name
`, modelIn)
// Aggregate per-model stats (parameterized IN clause, no fmt.Sprintf)
inClause := strings.Repeat("?,", len(models)-1) + "?"
aggQuery := `
SELECT
model_name,
COUNT(*) as requests,
COUNT(*) FILTER (WHERE status = 'success') as success_count,
COALESCE(AVG(latency_ms) FILTER (WHERE status = 'success'), 0) as avg_latency,
COALESCE(PERCENTILE_CONT(0.95) WITHIN GROUP (ORDER BY latency_ms) FILTER (WHERE status = 'success'), 0) as p95_latency,
COALESCE(SUM(total_cost), 0) as total_cost,
COALESCE(SUM(input_tokens), 0) as input_tokens,
COALESCE(SUM(output_tokens), 0) as output_tokens
FROM bil_usage_logs
WHERE tenant_id = ? AND created_at >= ? AND model_name IN (` + inClause + `)
GROUP BY model_name
`
records, err := g.DB().Raw(aggQuery, args...).All()
if err != nil {
@@ -133,7 +133,7 @@ func (s *sTenant) ModelComparison(ctx context.Context, req *v1.ModelComparisonRe
}
// Daily trends
trends := fetchTrends(ctx, tenantID, since, models, modelIn, args)
trends := fetchTrends(ctx, tenantID, since, models, args)
return &v1.ModelComparisonRes{
Summary: v1.ModelComparisonSummary{
@@ -209,19 +209,20 @@ func recommendBest(items []v1.ModelComparisonItem) (string, string) {
return best, fmt.Sprintf("综合评分 %.1f(费用40%% + 延迟30%% + 成功率30%%)", bestScore)
}
func fetchTrends(ctx context.Context, tenantID int64, since string, models []string, modelIn string, baseArgs []any) []v1.ModelTrendDay {
trendQuery := fmt.Sprintf(`
SELECT
DATE(created_at) as day,
model_name,
COUNT(*) as requests,
COALESCE(SUM(total_cost), 0) as cost,
COALESCE(AVG(latency_ms) FILTER (WHERE status = 'success'), 0) as latency
FROM bil_usage_logs
WHERE tenant_id = ? AND created_at >= ? AND model_name IN (%s)
GROUP BY DATE(created_at), model_name
ORDER BY day
`, modelIn)
func fetchTrends(ctx context.Context, tenantID int64, since string, models []string, baseArgs []any) []v1.ModelTrendDay {
inClause := strings.Repeat("?,", len(models)-1) + "?"
trendQuery := `
SELECT
DATE(created_at) as day,
model_name,
COUNT(*) as requests,
COALESCE(SUM(total_cost), 0) as cost,
COALESCE(AVG(latency_ms) FILTER (WHERE status = 'success'), 0) as latency
FROM bil_usage_logs
WHERE tenant_id = ? AND created_at >= ? AND model_name IN (` + inClause + `)
GROUP BY DATE(created_at), model_name
ORDER BY day
`
records, err := g.DB().Raw(trendQuery, baseArgs...).All()
if err != nil {
@@ -240,11 +241,18 @@ func fetchTrends(ctx context.Context, tenantID int64, since string, models []str
})
}
var trends []v1.ModelTrendDay
for day, details := range dayMap {
// Sort days for deterministic output
days := make([]string, 0, len(dayMap))
for day := range dayMap {
days = append(days, day)
}
sort.Strings(days)
trends := make([]v1.ModelTrendDay, 0, len(days))
for _, day := range days {
trends = append(trends, v1.ModelTrendDay{
Date: day,
Details: details,
Details: dayMap[day],
})
}
return trends
+42 -20
View File
@@ -6,7 +6,6 @@ import (
"github.com/qianfree/team-api/internal/dao"
do "github.com/qianfree/team-api/internal/model/do"
"strings"
"time"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/os/gtime"
@@ -44,17 +43,36 @@ func (s *sTenant) Notifications(ctx context.Context, req *v1.TenantNotifications
return nil, err
}
// Enrich items with read status for broadcasts
for i, item := range items {
// Enrich items with read status for broadcasts (batch query to avoid N+1)
broadcastIDs := make([]int64, 0)
for _, item := range items {
if item.IsBroadcast == 1 {
readCount, _ := dao.NtfReadStatus.Ctx(ctx).
Where("message_id", item.Id).
Where("user_id", userID).
Count()
if readCount > 0 {
items[i].IsRead = 1
} else {
items[i].IsRead = 0
broadcastIDs = append(broadcastIDs, item.Id)
}
}
if len(broadcastIDs) > 0 {
readStatuses := make([]struct {
MessageId int64 `json:"message_id"`
}, 0)
err = dao.NtfReadStatus.Ctx(ctx).
Where("user_id", userID).
WhereIn("message_id", broadcastIDs).
Fields("message_id").
Scan(&readStatuses)
if err != nil {
return nil, err
}
readMap := make(map[int64]bool, len(readStatuses))
for _, rs := range readStatuses {
readMap[rs.MessageId] = true
}
for i, item := range items {
if item.IsBroadcast == 1 {
if readMap[item.Id] {
items[i].IsRead = 1
} else {
items[i].IsRead = 0
}
}
}
}
@@ -96,7 +114,7 @@ func (s *sTenant) UnreadCount(ctx context.Context, req *v1.TenantUnreadCountReq)
).
Count()
if err != nil {
broadcastUnread = 0
return nil, err
}
totalUnread := personalUnread + broadcastUnread
@@ -213,13 +231,12 @@ func (s *sTenant) MarkAllRead(ctx context.Context, req *v1.TenantMarkAllReadReq)
// Batch insert read_status records
if len(broadcastIDs) > 0 {
now := time.Now()
batch := make([]do.NtfReadStatus, 0, len(broadcastIDs))
for _, b := range broadcastIDs {
batch = append(batch, do.NtfReadStatus{
MessageId: b.ID,
UserId: userID,
ReadAt: gtime.NewFromTime(now),
ReadAt: gtime.Now(),
})
}
_, err = dao.NtfReadStatus.Ctx(ctx).Batch(len(batch)).Data(batch).Insert()
@@ -320,7 +337,10 @@ func (s *sTenant) NotificationPreferencesUpdate(ctx context.Context, req *v1.Ten
} else {
query = query.Where("user_id IS NULL")
}
query.Fields("id").Scan(&existing)
err = query.Fields("id").Scan(&existing)
if err != nil {
return nil, err
}
if existing != nil && existing.ID > 0 {
// Update existing
@@ -377,7 +397,7 @@ func loadPreferences(ctx context.Context, tenantID, userID int64, scope string)
type prefRow struct {
Preferences string `json:"preferences"`
}
var row prefRow
var row *prefRow
query := dao.NtfPreferences.Ctx(ctx).
Where("tenant_id", tenantID).
Where("scope", scope)
@@ -386,9 +406,11 @@ func loadPreferences(ctx context.Context, tenantID, userID int64, scope string)
} else {
query = query.Where("user_id IS NULL")
}
query.Fields("preferences").Scan(&row)
if err := query.Fields("preferences").Scan(&row); err != nil {
return getDefaultPreferences()
}
if row.Preferences == "" || row.Preferences == "{}" {
if row == nil || row.Preferences == "" || row.Preferences == "{}" {
return getDefaultPreferences()
}
@@ -438,8 +460,8 @@ func mergePreferences(org, user map[string]any) map[string]any {
if orgMap, ok := orgVal.(map[string]any); ok {
// Merge the nested maps
combined := make(map[string]any)
for ok, ov := range orgMap {
combined[ok] = ov
for orgKey, orgValItem := range orgMap {
combined[orgKey] = orgValItem
}
for uk, uv := range userMap {
combined[uk] = uv
+5 -6
View File
@@ -6,6 +6,7 @@ import (
"time"
"github.com/gogf/gf/v2/frame/g"
"github.com/qianfree/team-api/internal/dao"
v1 "github.com/qianfree/team-api/api/tenant/v1"
"github.com/qianfree/team-api/internal/middleware"
@@ -164,12 +165,10 @@ func (s *sTenant) PersonalDashboard(ctx context.Context, req *v1.PersonalDashboa
QuotaResetAt string `json:"quota_reset_at"`
}
var qRow *quotaRow
err = g.DB().Ctx(ctx).Raw(`
SELECT quota_type, COALESCE(quota_limit, 0) as quota_limit,
COALESCE(quota_used, 0) as quota_used, quota_period,
COALESCE(TO_CHAR(quota_reset_at, 'YYYY-MM-DD HH24:MI:SS'), '') as quota_reset_at
FROM tnt_users WHERE id = ? AND tenant_id = ?
`, userID, tenantID).Scan(&qRow)
err = dao.TntUsers.Ctx(ctx).
Where("id", userID).Where("tenant_id", tenantID).
Fields("quota_type, COALESCE(quota_limit, 0) as quota_limit, COALESCE(quota_used, 0) as quota_used, quota_period, COALESCE(TO_CHAR(quota_reset_at, 'YYYY-MM-DD HH24:MI:SS'), '') as quota_reset_at").
Scan(&qRow)
if err == nil && qRow != nil && qRow.QuotaType != "" && qRow.QuotaType != "none" {
q := &v1.PersonalQuotaStatus{
QuotaType: qRow.QuotaType,
+4 -4
View File
@@ -54,8 +54,8 @@ func findActiveApiKey(ctx context.Context) (*playgroundApiKey, error) {
func buildPlaygroundRelayContext(ctx context.Context, key *playgroundApiKey, recorder http.ResponseWriter) *handler.RelayContext {
requestID := ""
if v := ctx.Value("requestId"); v != nil {
requestID = v.(string)
if r := g.RequestFromCtx(ctx); r != nil {
requestID = r.GetCtxVar("RequestId").String()
}
return &handler.RelayContext{
TenantID: middleware.GetTenantID(ctx),
@@ -106,8 +106,8 @@ func convertRelayError(err error) error {
}
func getRequestID(ctx context.Context) string {
if v := ctx.Value("requestId"); v != nil {
return v.(string)
if r := g.RequestFromCtx(ctx); r != nil {
return r.GetCtxVar("RequestId").String()
}
return ""
}
+2 -1
View File
@@ -57,7 +57,8 @@ func (s *sTenant) TenantTaskList(ctx context.Context, req *v1.TenantTaskListReq)
}
var total int
err := m.Page(req.Page, req.PageSize).ScanAndCount(&tasks, &total, false)
page, pageSize := common.NormalizePagination(req.Page, req.PageSize)
err := m.Page(page, pageSize).ScanAndCount(&tasks, &total, false)
if err != nil {
return nil, err
}