Files

307 lines
8.3 KiB
Go
Raw Permalink Normal View History

2026-08-15 17:04:47 +08:00
package service
import (
"strings"
"time"
"gorm.io/gorm"
"nl-pms-api/internal/commonservice"
"nl-pms-api/internal/model"
)
// AdminService 运营后台:统计、用户、团队。
type AdminService struct {
DB *gorm.DB
}
type StatsPoint struct {
Date string `json:"date"`
Count int64 `json:"count"`
}
type TokenPoint struct {
Date string `json:"date"`
PromptTokens int64 `json:"promptTokens"`
CompletionTokens int64 `json:"completionTokens"`
Calls int64 `json:"calls"`
}
type OverviewStats struct {
UserCount int64 `json:"userCount"`
TeamCount int64 `json:"teamCount"`
DAUToday int64 `json:"dauToday"`
DAUSeries []StatsPoint `json:"dauSeries"`
TokenToday TokenPoint `json:"tokenToday"`
TokenSeries []TokenPoint `json:"tokenSeries"`
}
// Overview 概览统计。
func (s *AdminService) Overview(days int) (*OverviewStats, error) {
if days <= 0 || days > 90 {
days = 14
}
out := &OverviewStats{
DAUSeries: []StatsPoint{},
TokenSeries: []TokenPoint{},
}
s.DB.Model(&model.User{}).Count(&out.UserCount)
s.DB.Model(&model.Team{}).Count(&out.TeamCount)
today := time.Now().In(time.Local).Format("2006-01-02")
s.DB.Model(&model.UserDailyActive{}).Where("active_date = ?", today).Count(&out.DAUToday)
start := time.Now().In(time.Local).AddDate(0, 0, -(days - 1)).Format("2006-01-02")
type dauRow struct {
ActiveDate string
Cnt int64
}
var dauRows []dauRow
s.DB.Model(&model.UserDailyActive{}).
Select("active_date, COUNT(*) AS cnt").
Where("active_date >= ?", start).
Group("active_date").
Order("active_date").
Scan(&dauRows)
dauMap := map[string]int64{}
for _, r := range dauRows {
dauMap[r.ActiveDate] = r.Cnt
}
type tokRow struct {
UsageDate string
PromptTokens int64
CompletionTokens int64
Calls int64
}
var tokRows []tokRow
s.DB.Model(&model.AIUsageDaily{}).
Select("usage_date, SUM(prompt_tokens) AS prompt_tokens, SUM(completion_tokens) AS completion_tokens, SUM(calls) AS calls").
Where("usage_date >= ?", start).
Group("usage_date").
Order("usage_date").
Scan(&tokRows)
tokMap := map[string]tokRow{}
for _, r := range tokRows {
tokMap[r.UsageDate] = r
}
for i := 0; i < days; i++ {
d := time.Now().In(time.Local).AddDate(0, 0, -(days - 1 - i)).Format("2006-01-02")
out.DAUSeries = append(out.DAUSeries, StatsPoint{Date: d, Count: dauMap[d]})
t := tokMap[d]
pt := TokenPoint{
Date: d,
PromptTokens: t.PromptTokens,
CompletionTokens: t.CompletionTokens,
Calls: t.Calls,
}
out.TokenSeries = append(out.TokenSeries, pt)
if d == today {
out.TokenToday = pt
}
}
return out, nil
}
type AdminUserItem struct {
ID int64 `json:"id"`
Username string `json:"username"`
Nickname string `json:"nickname"`
AIBanned int `json:"aiBanned"`
Disabled int `json:"disabled"`
LastLoginIP string `json:"lastLoginIp"`
LastSeenAt string `json:"lastSeenAt"`
CreatedAt string `json:"createdAt"`
}
// ListUsers 用户列表。
func (s *AdminService) ListUsers() ([]AdminUserItem, error) {
var users []model.User
if err := s.DB.Order("id ASC").Find(&users).Error; err != nil {
return nil, commonservice.Internal("QUERY_FAILED")
}
ids := make([]int64, 0, len(users))
for _, u := range users {
ids = append(ids, u.ID)
}
nick := map[int64]string{}
if len(ids) > 0 {
var profiles []model.UserProfile
s.DB.Where("user_id IN ?", ids).Find(&profiles)
for _, p := range profiles {
nick[p.UserID] = p.Nickname
}
}
out := make([]AdminUserItem, 0, len(users))
for _, u := range users {
out = append(out, AdminUserItem{
ID: u.ID,
Username: u.Username,
Nickname: nick[u.ID],
AIBanned: u.AIBanned,
Disabled: u.Disabled,
LastLoginIP: u.LastLoginIP,
LastSeenAt: u.LastSeenAt,
CreatedAt: u.CreatedAt,
})
}
return out, nil
}
// PatchUser 更新用户 ai_banned / disabled不可禁用自己
func (s *AdminService) PatchUser(actorID, targetID int64, aiBanned, disabled *int) error {
if actorID != commonservice.AdminUserID {
return commonservice.Forbidden("FORBIDDEN")
}
if targetID <= 0 {
return commonservice.BadRequest("BAD_REQUEST")
}
if targetID == commonservice.AdminUserID && disabled != nil && *disabled != 0 {
return commonservice.BadRequest("CANNOT_DISABLE_ADMIN")
}
updates := map[string]any{}
if aiBanned != nil {
if *aiBanned != 0 {
updates["ai_banned"] = 1
} else {
updates["ai_banned"] = 0
}
}
if disabled != nil {
if *disabled != 0 {
updates["disabled"] = 1
} else {
updates["disabled"] = 0
}
}
if len(updates) == 0 {
return commonservice.BadRequest("BAD_REQUEST")
}
res := s.DB.Model(&model.User{}).Where("id = ?", targetID).Updates(updates)
if res.Error != nil {
return commonservice.Internal("SAVE_FAILED")
}
if res.RowsAffected == 0 {
return commonservice.NotFound("NOT_FOUND")
}
return nil
}
type AdminTeamItem struct {
ID int64 `json:"id"`
Name string `json:"name"`
OwnerID int64 `json:"ownerId"`
OwnerName string `json:"ownerName"`
AIBanned int `json:"aiBanned"`
Members int64 `json:"members"`
CreatedAt string `json:"createdAt"`
}
// ListTeams 团队列表。
func (s *AdminService) ListTeams() ([]AdminTeamItem, error) {
var teams []model.Team
if err := s.DB.Order("id ASC").Find(&teams).Error; err != nil {
return nil, commonservice.Internal("QUERY_FAILED")
}
out := make([]AdminTeamItem, 0, len(teams))
for _, t := range teams {
item := AdminTeamItem{
ID: t.ID, Name: t.Name, OwnerID: t.OwnerID,
AIBanned: t.AIBanned, CreatedAt: t.CreatedAt,
}
var u model.User
if s.DB.Select("username").First(&u, t.OwnerID).Error == nil {
item.OwnerName = u.Username
}
s.DB.Model(&model.TeamMember{}).Where("team_id = ?", t.ID).Count(&item.Members)
out = append(out, item)
}
return out, nil
}
// PatchTeam 更新团队 ai_banned。
func (s *AdminService) PatchTeam(actorID, teamID int64, aiBanned *int) error {
if actorID != commonservice.AdminUserID {
return commonservice.Forbidden("FORBIDDEN")
}
if teamID <= 0 || aiBanned == nil {
return commonservice.BadRequest("BAD_REQUEST")
}
v := 0
if *aiBanned != 0 {
v = 1
}
res := s.DB.Model(&model.Team{}).Where("id = ?", teamID).Update("ai_banned", v)
if res.Error != nil {
return commonservice.Internal("SAVE_FAILED")
}
if res.RowsAffected == 0 {
return commonservice.NotFound("NOT_FOUND")
}
return nil
}
// AIPolicy 当前用户是否允许使用 AI。
type AIPolicy struct {
Allowed bool `json:"allowed"`
Reason string `json:"reason,omitempty"`
}
// GetAIPolicy 综合用户禁用/禁 AI 与所属团队禁 AI。
func (s *AdminService) GetAIPolicy(userID int64) (*AIPolicy, error) {
var u model.User
if err := s.DB.Select("id", "disabled", "ai_banned").First(&u, userID).Error; err != nil {
return nil, commonservice.Unauthorized("UNAUTHORIZED")
}
if u.Disabled != 0 {
return &AIPolicy{Allowed: false, Reason: "ACCOUNT_DISABLED"}, nil
}
if u.AIBanned != 0 {
return &AIPolicy{Allowed: false, Reason: "USER_AI_BANNED"}, nil
}
var n int64
s.DB.Table("team_members").
Joins("JOIN teams ON teams.id = team_members.team_id").
Where("team_members.user_id = ? AND teams.ai_banned = 1", userID).
Count(&n)
if n > 0 {
return &AIPolicy{Allowed: false, Reason: "TEAM_AI_BANNED"}, nil
}
return &AIPolicy{Allowed: true}, nil
}
// ReportAIUsage 累加当日 token 用量。
func (s *AdminService) ReportAIUsage(userID, teamID int64, provider string, prompt, completion int64, estimated bool) error {
if userID <= 0 {
return commonservice.Unauthorized("UNAUTHORIZED")
}
provider = strings.TrimSpace(provider)
if provider == "" {
provider = "unknown"
}
if prompt < 0 {
prompt = 0
}
if completion < 0 {
completion = 0
}
today := time.Now().In(time.Local).Format("2006-01-02")
est := 0
if estimated {
est = 1
}
q := `INSERT INTO ai_usage_daily(user_id,team_id,usage_date,provider,prompt_tokens,completion_tokens,calls,estimated)
VALUES(?,?,?,?,?,?,1,?)
ON DUPLICATE KEY UPDATE
prompt_tokens=prompt_tokens+VALUES(prompt_tokens),
completion_tokens=completion_tokens+VALUES(completion_tokens),
calls=calls+1,
estimated=IF(VALUES(estimated)=1,1,estimated)`
if err := s.DB.Exec(q, userID, teamID, today, provider, prompt, completion, est).Error; err != nil {
return commonservice.Internal("SAVE_FAILED")
}
return nil
}