diff --git a/internal/logic/admin/admin.go b/internal/logic/admin/admin.go index 3d4e1e3..8e000b3 100644 --- a/internal/logic/admin/admin.go +++ b/internal/logic/admin/admin.go @@ -1,8 +1,6 @@ package admin import ( - "context" - "github.com/qianfree/team-api/internal/service" ) @@ -17,16 +15,3 @@ func New() *sAdmin { func init() { service.RegisterAdmin(New()) } - -// getCtxUserID extracts the admin user ID from context. -// The key "userId" is set by admin_auth middleware. -func getCtxUserID(ctx context.Context) int64 { - val := ctx.Value("userId") - if val == nil { - return 0 - } - if id, ok := val.(int64); ok { - return id - } - return 0 -} diff --git a/internal/logic/admin/admin_user.go b/internal/logic/admin/admin_user.go index 648854b..558df5a 100644 --- a/internal/logic/admin/admin_user.go +++ b/internal/logic/admin/admin_user.go @@ -6,7 +6,6 @@ import ( "strings" "github.com/gogf/gf/v2/database/gdb" - "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gtime" v1 "github.com/qianfree/team-api/api/admin/v1" @@ -191,7 +190,7 @@ func (s *sAdmin) UpdateUser(ctx context.Context, req *v1.AdminUserUpdateReq) (*v // DeleteUser deletes an admin user. func (s *sAdmin) DeleteUser(ctx context.Context, req *v1.AdminUserDeleteReq) (*v1.AdminUserDeleteRes, error) { - currentUserID := ctxUserID(ctx) + currentUserID := common.GetCtxUserID(ctx) if req.Id == currentUserID { return nil, common.NewBadRequestError("不能删除当前登录的用户") @@ -227,7 +226,7 @@ func (s *sAdmin) DeleteUser(ctx context.Context, req *v1.AdminUserDeleteReq) (*v // UpdateUserStatus enables or disables an admin user. func (s *sAdmin) UpdateUserStatus(ctx context.Context, req *v1.AdminUserUpdateStatusReq) (*v1.AdminUserUpdateStatusRes, error) { - currentUserID := ctxUserID(ctx) + currentUserID := common.GetCtxUserID(ctx) if req.Id == currentUserID { return nil, common.NewBadRequestError("不能修改当前登录用户的状态") @@ -309,67 +308,23 @@ func buildUserFilters(m *gdb.Model, keyword, role, status string) *gdb.Model { // ExportUsers exports admin users to CSV or Excel. func (s *sAdmin) ExportUsers(ctx context.Context, req *v1.AdminUserExportReq) (*v1.AdminUserExportRes, error) { - r := g.RequestFromCtx(ctx) - format := req.Format - if format == "" { - format = "csv" - } - if format == "excel" { - format = "xlsx" - } - - columns := []export.Column{ - {Field: "id", Header: "ID"}, - {Field: "username", Header: "用户名"}, - {Field: "email", Header: "邮箱"}, - {Field: "display_name", Header: "显示名称"}, - {Field: "role", Header: "角色"}, - {Field: "status", Header: "状态"}, - {Field: "last_login_at", Header: "最后登录时间"}, - {Field: "last_login_ip", Header: "最后登录IP"}, - {Field: "created_at", Header: "创建时间"}, - } - config := export.Config{ - Format: format, + Format: req.Format, Filename: "管理员_" + gtime.Now().Format("Ymd_His"), - Columns: columns, + Columns: []export.Column{ + {Field: "id", Header: "ID"}, + {Field: "username", Header: "用户名"}, + {Field: "email", Header: "邮箱"}, + {Field: "display_name", Header: "显示名称"}, + {Field: "role", Header: "角色"}, + {Field: "status", Header: "状态"}, + {Field: "last_login_at", Header: "最后登录时间"}, + {Field: "last_login_ip", Header: "最后登录IP"}, + {Field: "created_at", Header: "创建时间"}, + }, } - if format == "xlsx" { - m := buildUserFilters(dao.SysAdminUsers.Ctx(ctx), req.Keyword, req.Role, req.Status) - var users []struct { - Id int64 `json:"id"` - Username string `json:"username"` - Email string `json:"email"` - DisplayName string `json:"display_name"` - Role string `json:"role"` - Status string `json:"status"` - LastLoginAt *gtime.Time `json:"last_login_at"` - LastLoginIp string `json:"last_login_ip"` - CreatedAt *gtime.Time `json:"created_at"` - } - if err := m.OrderDesc("id").Scan(&users); err != nil { - return nil, err - } - data := make([]map[string]any, len(users)) - for i, u := range users { - data[i] = map[string]any{ - "id": u.Id, - "username": u.Username, - "email": u.Email, - "display_name": u.DisplayName, - "role": u.Role, - "status": u.Status, - "last_login_at": u.LastLoginAt.String(), - "last_login_ip": u.LastLoginIp, - "created_at": u.CreatedAt.String(), - } - } - return nil, export.WriteExcel(r, config, data) - } - - return nil, export.StreamCSV(r, config, func(yield func(map[string]any) bool) { + return nil, export.GenericExport(ctx, config, func(yield func(map[string]any) bool) { offset := 0 for { m := buildUserFilters(dao.SysAdminUsers.Ctx(ctx), req.Keyword, req.Role, req.Status) diff --git a/internal/logic/admin/audit.go b/internal/logic/admin/audit.go index 7a009be..e48c811 100644 --- a/internal/logic/admin/audit.go +++ b/internal/logic/admin/audit.go @@ -340,33 +340,6 @@ func (s *sAdmin) GetRequestAuditLogDetail(ctx context.Context, req *v1.RequestAu // ExportOperationLogs exports operation logs to CSV or Excel. func (s *sAdmin) ExportOperationLogs(ctx context.Context, req *v1.OperationLogExportReq) (*v1.OperationLogExportRes, error) { - r := g.RequestFromCtx(ctx) - format := req.Format - if format == "" { - format = "csv" - } - if format == "excel" { - format = "xlsx" - } - - columns := []export.Column{ - {Field: "id", Header: "ID"}, - {Field: "user_id", Header: "用户ID"}, - {Field: "user_type", Header: "用户类型"}, - {Field: "action", Header: "操作"}, - {Field: "resource_type", Header: "目标类型"}, - {Field: "resource_id", Header: "目标ID"}, - {Field: "ip_address", Header: "IP地址"}, - {Field: "detail", Header: "详情"}, - {Field: "created_at", Header: "创建时间"}, - } - - config := export.Config{ - Format: format, - Filename: "操作日志_" + gtime.Now().Format("Ymd_His"), - Columns: columns, - } - buildOpLogWhere := func() (string, []any) { var conditions []string var args []any @@ -399,35 +372,23 @@ func (s *sAdmin) ExportOperationLogs(ctx context.Context, req *v1.OperationLogEx selectFields := "id, user_id, user_type, action, resource_type, resource_id, ip_address, detail, created_at" - if format == "xlsx" { - where, args := buildOpLogWhere() - items, _, err := queryPage(ctx, "aud_operation_logs", selectFields, where, "created_at DESC", 1, 100000, args...) - if err != nil { - return nil, err - } - // Ensure time fields are formatted as strings - data := make([]map[string]any, len(items)) - for i, item := range items { - createdAt := "" - if v, ok := item["created_at"]; ok { - createdAt = fmt.Sprintf("%v", v) - } - data[i] = map[string]any{ - "id": item["id"], - "user_id": item["user_id"], - "user_type": item["user_type"], - "action": item["action"], - "resource_type": item["resource_type"], - "resource_id": item["resource_id"], - "ip_address": item["ip_address"], - "detail": item["detail"], - "created_at": createdAt, - } - } - return nil, export.WriteExcel(r, config, data) + config := export.Config{ + Format: req.Format, + Filename: "操作日志_" + gtime.Now().Format("Ymd_His"), + Columns: []export.Column{ + {Field: "id", Header: "ID"}, + {Field: "user_id", Header: "用户ID"}, + {Field: "user_type", Header: "用户类型"}, + {Field: "action", Header: "操作"}, + {Field: "resource_type", Header: "目标类型"}, + {Field: "resource_id", Header: "目标ID"}, + {Field: "ip_address", Header: "IP地址"}, + {Field: "detail", Header: "详情"}, + {Field: "created_at", Header: "创建时间"}, + }, } - return nil, export.StreamCSV(r, config, func(yield func(map[string]any) bool) { + return nil, export.GenericExport(ctx, config, func(yield func(map[string]any) bool) { offset := 0 for { where, args := buildOpLogWhere() diff --git a/internal/logic/admin/auth.go b/internal/logic/admin/auth.go index cd5883b..05e7ced 100644 --- a/internal/logic/admin/auth.go +++ b/internal/logic/admin/auth.go @@ -19,29 +19,8 @@ import ( "github.com/qianfree/team-api/internal/utility/crypto" ) -// ctxUserID extracts user ID from context. -func ctxUserID(ctx context.Context) int64 { - val := ctx.Value("userId") - if val == nil { - return 0 - } - if id, ok := val.(int64); ok { - return id - } - return 0 -} - -// ctxSessionID extracts session ID from context. -func ctxSessionID(ctx context.Context) int64 { - val := ctx.Value("sessionId") - if val == nil { - return 0 - } - if id, ok := val.(int64); ok { - return id - } - return 0 -} +// NOTE: ctxUserID and ctxSessionID have been removed. +// Use common.GetCtxUserID(ctx) and common.GetCtxSessionID(ctx) instead. // Login handles admin login. func (s *sAdmin) Login(ctx context.Context, req *v1.AdminLoginReq) (*v1.AdminLoginRes, error) { @@ -159,7 +138,7 @@ func (s *sAdmin) Login(ctx context.Context, req *v1.AdminLoginReq) (*v1.AdminLog // Logout handles admin logout. func (s *sAdmin) Logout(ctx context.Context, _ *v1.AdminLogoutReq) (*v1.AdminLogoutRes, error) { - sessionID := ctxSessionID(ctx) + sessionID := common.GetCtxSessionID(ctx) // Mark session as revoked in Redis for instant effect common.MarkSessionRevoked(ctx, sessionID) @@ -235,7 +214,7 @@ func (s *sAdmin) Refresh(ctx context.Context, req *v1.AdminRefreshReq) (*v1.Admi // ListSessions returns active sessions for the current admin user. func (s *sAdmin) ListSessions(ctx context.Context, req *v1.AdminSessionListReq) (*v1.AdminSessionListRes, error) { - currentSessionID := ctxSessionID(ctx) + currentSessionID := common.GetCtxSessionID(ctx) page := req.Page pageSize := req.PageSize if page < 1 { @@ -346,7 +325,7 @@ func (s *sAdmin) ForceLogout(ctx context.Context, req *v1.AdminForceLogoutReq) ( // ChangePassword handles admin password change. func (s *sAdmin) ChangePassword(ctx context.Context, req *v1.AdminChangePasswordReq) (*v1.AdminChangePasswordRes, error) { - userID := ctxUserID(ctx) + userID := common.GetCtxUserID(ctx) var user *entity.SysAdminUsers err := dao.SysAdminUsers.Ctx(ctx). diff --git a/internal/logic/admin/changelog.go b/internal/logic/admin/changelog.go index f945965..6dec465 100644 --- a/internal/logic/admin/changelog.go +++ b/internal/logic/admin/changelog.go @@ -18,7 +18,7 @@ func (s *sAdmin) CreateChangelog(ctx context.Context, req *v1.ChangelogCreateReq "content": req.Content, "type": req.Type, "status": "draft", - "created_by": getCtxUserID(ctx), + "created_by": common.GetCtxUserID(ctx), }).Insert() if err != nil { return nil, err diff --git a/internal/logic/admin/channel.go b/internal/logic/admin/channel.go index 8d24f2f..4fadae5 100644 --- a/internal/logic/admin/channel.go +++ b/internal/logic/admin/channel.go @@ -618,84 +618,24 @@ func (s *sAdmin) GetChannelHealthTrend(ctx context.Context, req *v1.ChannelHealt // ExportChannels exports channel list to CSV or Excel. func (s *sAdmin) ExportChannels(ctx context.Context, req *v1.ChannelExportReq) (*v1.ChannelExportRes, error) { - r := g.RequestFromCtx(ctx) - format := req.Format - if format == "" { - format = "csv" - } - if format == "excel" { - format = "xlsx" - } - - columns := []export.Column{ - {Field: "id", Header: "ID"}, - {Field: "name", Header: "名称"}, - {Field: "type_name", Header: "供应商类型"}, - {Field: "status", Header: "状态"}, - {Field: "priority", Header: "优先级"}, - {Field: "weight", Header: "权重"}, - {Field: "health_score", Header: "健康分数"}, - {Field: "created_at", Header: "创建时间"}, - } - - config := export.Config{ - Format: format, - Filename: "渠道_" + gtime.Now().Format("Ymd_His"), - Columns: columns, - } - channelFields := "chn_channels.id, chn_channels.name, chn_channels.type, chn_channels.status, chn_channels.priority, chn_channels.weight, chn_channels.created_at, h.health_score" - if format == "xlsx" { - query := dao.ChnChannels.Ctx(ctx). - LeftJoin("chn_health_scores h ON chn_channels.id = h.channel_id") - if req.Type > 0 { - query = query.Where("chn_channels.type", req.Type) - } - if req.Status != "" { - query = query.Where("chn_channels.status", req.Status) - } - if req.Search != "" { - query = query.Where("chn_channels.name LIKE ? OR chn_channels.remark LIKE ?", "%"+req.Search+"%", "%"+req.Search+"%") - } - var channels []struct { - ID int64 `json:"id"` - Name string `json:"name"` - Type int `json:"type"` - Status string `json:"status"` - Priority int `json:"priority"` - Weight int `json:"weight"` - CreatedAt *gtime.Time `json:"created_at"` - HealthScore *float64 `json:"health_score"` - } - if err := query.Fields(channelFields).OrderDesc("chn_channels.priority").Scan(&channels); err != nil { - return nil, err - } - data := make([]map[string]any, len(channels)) - for i, ch := range channels { - typeName := providerTypeNames[ch.Type] - if typeName == "" { - typeName = fmt.Sprintf("Unknown(%d)", ch.Type) - } - healthScore := "" - if ch.HealthScore != nil { - healthScore = fmt.Sprintf("%.2f", *ch.HealthScore) - } - data[i] = map[string]any{ - "id": ch.ID, - "name": ch.Name, - "type_name": typeName, - "status": ch.Status, - "priority": ch.Priority, - "weight": ch.Weight, - "health_score": healthScore, - "created_at": ch.CreatedAt.String(), - } - } - return nil, export.WriteExcel(r, config, data) + config := export.Config{ + Format: req.Format, + Filename: "渠道_" + gtime.Now().Format("Ymd_His"), + Columns: []export.Column{ + {Field: "id", Header: "ID"}, + {Field: "name", Header: "名称"}, + {Field: "type_name", Header: "供应商类型"}, + {Field: "status", Header: "状态"}, + {Field: "priority", Header: "优先级"}, + {Field: "weight", Header: "权重"}, + {Field: "health_score", Header: "健康分数"}, + {Field: "created_at", Header: "创建时间"}, + }, } - return nil, export.StreamCSV(r, config, func(yield func(map[string]any) bool) { + return nil, export.GenericExport(ctx, config, func(yield func(map[string]any) bool) { offset := 0 for { query := dao.ChnChannels.Ctx(ctx). diff --git a/internal/logic/admin/dashboard.go b/internal/logic/admin/dashboard.go index 9c1211c..caff290 100644 --- a/internal/logic/admin/dashboard.go +++ b/internal/logic/admin/dashboard.go @@ -549,15 +549,6 @@ func (s *sAdmin) GetDashboardRecentAlerts(ctx context.Context, req *v1.AdminDash // ExportUsageLogs exports usage logs to CSV or Excel. func (s *sAdmin) ExportUsageLogs(ctx context.Context, req *v1.AdminUsageLogExportReq) (*v1.AdminUsageLogExportRes, error) { - r := g.RequestFromCtx(ctx) - format := req.Format - if format == "" { - format = "csv" - } - if format == "excel" { - format = "xlsx" - } - columns := []export.Column{ {Field: "id", Header: "ID"}, {Field: "tenant_name", Header: "租户名称"}, @@ -572,7 +563,7 @@ func (s *sAdmin) ExportUsageLogs(ctx context.Context, req *v1.AdminUsageLogExpor } config := export.Config{ - Format: format, + Format: req.Format, Filename: "用量日志_" + gtime.Now().Format("Ymd_His"), Columns: columns, } @@ -618,36 +609,7 @@ func (s *sAdmin) ExportUsageLogs(ctx context.Context, req *v1.AdminUsageLogExpor fromClause := "bil_usage_logs u LEFT JOIN tnt_users t ON u.user_id = t.id AND u.tenant_id = t.tenant_id LEFT JOIN tnt_tenants tn ON u.tenant_id = tn.id" selectFields := "u.id, COALESCE(tn.name, '') AS tenant_name, COALESCE(t.username, '') AS username, u.model_name, u.request_type, u.input_tokens, u.output_tokens, u.total_cost, u.status, u.created_at" - if format == "xlsx" { - where, args := buildUsageWhere() - sql := fmt.Sprintf("SELECT %s FROM %s%s ORDER BY u.created_at DESC", selectFields, fromClause, where) - result, err := g.DB().Ctx(ctx).Query(ctx, sql, args...) - if err != nil { - return nil, err - } - data := make([]map[string]any, len(result)) - for i, row := range result { - createdAt := "" - if t, ok := row["created_at"]; ok { - createdAt = fmt.Sprintf("%v", t.Val()) - } - data[i] = map[string]any{ - "id": row["id"].Val(), - "tenant_name": row["tenant_name"].Val(), - "username": row["username"].Val(), - "model_name": row["model_name"].Val(), - "request_type": row["request_type"].Val(), - "input_tokens": row["input_tokens"].Val(), - "output_tokens": row["output_tokens"].Val(), - "total_cost": row["total_cost"].Val(), - "status": row["status"].Val(), - "created_at": createdAt, - } - } - return nil, export.WriteExcel(r, config, data) - } - - return nil, export.StreamCSV(r, config, func(yield func(map[string]any) bool) { + return nil, export.GenericExport(ctx, config, func(yield func(map[string]any) bool) { offset := 0 for { where, args := buildUsageWhere() @@ -686,15 +648,6 @@ func (s *sAdmin) ExportUsageLogs(ctx context.Context, req *v1.AdminUsageLogExpor // ExportBillingRecords exports billing records to CSV or Excel. func (s *sAdmin) ExportBillingRecords(ctx context.Context, req *v1.AdminBillingRecordExportReq) (*v1.AdminBillingRecordExportRes, error) { - r := g.RequestFromCtx(ctx) - format := req.Format - if format == "" { - format = "csv" - } - if format == "excel" { - format = "xlsx" - } - columns := []export.Column{ {Field: "id", Header: "ID"}, {Field: "tenant_name", Header: "租户名称"}, @@ -709,7 +662,7 @@ func (s *sAdmin) ExportBillingRecords(ctx context.Context, req *v1.AdminBillingR } config := export.Config{ - Format: format, + Format: req.Format, Filename: "计费记录_" + gtime.Now().Format("Ymd_His"), Columns: columns, } @@ -801,15 +754,7 @@ func (s *sAdmin) ExportBillingRecords(ctx context.Context, req *v1.AdminBillingR return data, nil } - if format == "xlsx" { - data, err := fetchRecords(0, 100000) - if err != nil { - return nil, err - } - return nil, export.WriteExcel(r, config, data) - } - - return nil, export.StreamCSV(r, config, func(yield func(map[string]any) bool) { + return nil, export.GenericExport(ctx, config, func(yield func(map[string]any) bool) { offset := 0 for { batch, err := fetchRecords(offset, 1000) diff --git a/internal/logic/admin/data_governance.go b/internal/logic/admin/data_governance.go index 654d602..74f335e 100644 --- a/internal/logic/admin/data_governance.go +++ b/internal/logic/admin/data_governance.go @@ -59,7 +59,7 @@ func (s *sAdmin) DataGovernanceExport(ctx context.Context, req *v1.DataGovernanc payload, _ := json.Marshal(map[string]any{ "tenant_id": req.TenantID, "scopes": req.Scopes, - "requested_by": getCtxUserID(ctx), + "requested_by": common.GetCtxUserID(ctx), }) result, err := dao.TskTasks.Ctx(ctx).Data(do.TskTasks{ Name: fmt.Sprintf("数据导出 [租户%d]", req.TenantID), @@ -93,7 +93,7 @@ func (s *sAdmin) DataGovernanceDeletion(ctx context.Context, req *v1.DataGoverna payload, _ := json.Marshal(map[string]any{ "tenant_id": req.TenantID, "reason": req.Reason, - "requested_by": getCtxUserID(ctx), + "requested_by": common.GetCtxUserID(ctx), }) result, err := dao.TskTasks.Ctx(ctx).Data(do.TskTasks{ Name: fmt.Sprintf("数据删除 [租户%d]", req.TenantID), diff --git a/internal/logic/admin/error_log.go b/internal/logic/admin/error_log.go index 2e9068e..73c18c7 100644 --- a/internal/logic/admin/error_log.go +++ b/internal/logic/admin/error_log.go @@ -10,6 +10,7 @@ import ( v1 "github.com/qianfree/team-api/api/admin/v1" "github.com/qianfree/team-api/internal/dao" + "github.com/qianfree/team-api/internal/logic/common" ) // ErrorLogList returns a paginated list of system error logs. @@ -83,7 +84,7 @@ func (s *sAdmin) ErrorLogDetail(ctx context.Context, req *v1.ErrorLogDetailReq) // ErrorLogResolve marks an error log as resolved. func (s *sAdmin) ErrorLogResolve(ctx context.Context, req *v1.ErrorLogResolveReq) (*v1.ErrorLogResolveRes, error) { - userID := getCtxUserID(ctx) + userID := common.GetCtxUserID(ctx) _, err := dao.SysErrorLogs.Ctx(ctx). Where("id", req.Id). Data(g.Map{ @@ -99,7 +100,7 @@ func (s *sAdmin) ErrorLogResolve(ctx context.Context, req *v1.ErrorLogResolveReq // ErrorLogBatchResolve marks multiple error logs as resolved. func (s *sAdmin) ErrorLogBatchResolve(ctx context.Context, req *v1.ErrorLogBatchResolveReq) (*v1.ErrorLogBatchResolveRes, error) { - userID := getCtxUserID(ctx) + userID := common.GetCtxUserID(ctx) _, err := dao.SysErrorLogs.Ctx(ctx). WhereIn("id", req.Ids). Data(g.Map{ diff --git a/internal/logic/admin/feedback.go b/internal/logic/admin/feedback.go index ec5e345..30182cf 100644 --- a/internal/logic/admin/feedback.go +++ b/internal/logic/admin/feedback.go @@ -104,7 +104,7 @@ func (s *sAdmin) ReplyToFeedback(ctx context.Context, req *v1.FeedbackReplyReq) updateData := g.Map{ "admin_reply": req.Reply, - "admin_reply_by": getCtxUserID(ctx), + "admin_reply_by": common.GetCtxUserID(ctx), "admin_reply_at": gtime.Now(), } if req.Status != "" { diff --git a/internal/logic/admin/help_center.go b/internal/logic/admin/help_center.go index ed9cda3..b0ba5f7 100644 --- a/internal/logic/admin/help_center.go +++ b/internal/logic/admin/help_center.go @@ -197,7 +197,7 @@ func (s *sAdmin) CreateHelpArticle(ctx context.Context, req *v1.HelpArticleCreat "content": req.Content, "summary": req.Summary, "status": req.Status, - "author_id": getCtxUserID(ctx), + "author_id": common.GetCtxUserID(ctx), "sort_order": req.SortOrder, "keywords": req.Keywords, } diff --git a/internal/logic/admin/member.go b/internal/logic/admin/member.go index e2786db..f47fffd 100644 --- a/internal/logic/admin/member.go +++ b/internal/logic/admin/member.go @@ -8,7 +8,6 @@ import ( "github.com/gogf/gf/v2/database/gdb" "github.com/gogf/gf/v2/errors/gerror" - "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gtime" v1 "github.com/qianfree/team-api/api/admin/v1" @@ -351,76 +350,25 @@ func buildMemberFilters(m *gdb.Model, keyword, status, role string, tenantID int // ExportMembers exports member list to CSV or Excel. func (s *sAdmin) ExportMembers(ctx context.Context, req *v1.AdminMemberExportReq) (*v1.AdminMemberExportRes, error) { - r := g.RequestFromCtx(ctx) - format := req.Format - if format == "" { - format = "csv" - } - if format == "excel" { - format = "xlsx" - } - - columns := []export.Column{ - {Field: "id", Header: "ID"}, - {Field: "tenant_name", Header: "租户名称"}, - {Field: "username", Header: "用户名"}, - {Field: "email", Header: "邮箱"}, - {Field: "display_name", Header: "显示名称"}, - {Field: "role", Header: "角色"}, - {Field: "status", Header: "状态"}, - {Field: "last_login_at", Header: "最后登录时间"}, - {Field: "created_at", Header: "创建时间"}, - } - - config := export.Config{ - Format: format, - Filename: "成员_" + gtime.Now().Format("Ymd_His"), - Columns: columns, - } - memberFields := "tnt_users.id, tnt_users.tenant_id, t.name as tenant_name, t.code as tenant_code, tnt_users.username, tnt_users.email, tnt_users.display_name, tnt_users.role, tnt_users.status, tnt_users.last_login_at, tnt_users.last_login_ip, tnt_users.failed_attempts, tnt_users.created_at" - if format == "xlsx" { - m := buildMemberFilters( - dao.TntUsers.Ctx(ctx).LeftJoin("tnt_tenants t ON tnt_users.tenant_id = t.id"), - req.Keyword, req.Status, req.Role, req.TenantID, - ) - var members []struct { - Id int64 `json:"id"` - TenantId int64 `json:"tenant_id"` - TenantName string `json:"tenant_name"` - TenantCode string `json:"tenant_code"` - Username string `json:"username"` - Email string `json:"email"` - DisplayName string `json:"display_name"` - Role string `json:"role"` - Status string `json:"status"` - LastLoginAt *gtime.Time `json:"last_login_at"` - LastLoginIp string `json:"last_login_ip"` - FailedAttempts int `json:"failed_attempts"` - CreatedAt *gtime.Time `json:"created_at"` - } - if err := m.Fields(memberFields).OrderDesc("tnt_users.id").Scan(&members); err != nil { - return nil, err - } - data := make([]map[string]any, len(members)) - for i, m := range members { - data[i] = map[string]any{ - "id": m.Id, - "tenant_name": m.TenantName, - "username": m.Username, - "email": m.Email, - "display_name": m.DisplayName, - "role": m.Role, - "status": m.Status, - "last_login_at": m.LastLoginAt.String(), - "created_at": m.CreatedAt.String(), - } - } - return nil, export.WriteExcel(r, config, data) + config := export.Config{ + Format: req.Format, + Filename: "成员_" + gtime.Now().Format("Ymd_His"), + Columns: []export.Column{ + {Field: "id", Header: "ID"}, + {Field: "tenant_name", Header: "租户名称"}, + {Field: "username", Header: "用户名"}, + {Field: "email", Header: "邮箱"}, + {Field: "display_name", Header: "显示名称"}, + {Field: "role", Header: "角色"}, + {Field: "status", Header: "状态"}, + {Field: "last_login_at", Header: "最后登录时间"}, + {Field: "created_at", Header: "创建时间"}, + }, } - return nil, export.StreamCSV(r, config, func(yield func(map[string]any) bool) { + return nil, export.GenericExport(ctx, config, func(yield func(map[string]any) bool) { offset := 0 for { m := buildMemberFilters( @@ -428,19 +376,15 @@ func (s *sAdmin) ExportMembers(ctx context.Context, req *v1.AdminMemberExportReq req.Keyword, req.Status, req.Role, req.TenantID, ) var batch []struct { - Id int64 `json:"id"` - TenantId int64 `json:"tenant_id"` - TenantName string `json:"tenant_name"` - TenantCode string `json:"tenant_code"` - Username string `json:"username"` - Email string `json:"email"` - DisplayName string `json:"display_name"` - Role string `json:"role"` - Status string `json:"status"` - LastLoginAt *gtime.Time `json:"last_login_at"` - LastLoginIp string `json:"last_login_ip"` - FailedAttempts int `json:"failed_attempts"` - CreatedAt *gtime.Time `json:"created_at"` + Id int64 `json:"id"` + TenantName string `json:"tenant_name"` + Username string `json:"username"` + Email string `json:"email"` + DisplayName string `json:"display_name"` + Role string `json:"role"` + Status string `json:"status"` + LastLoginAt *gtime.Time `json:"last_login_at"` + CreatedAt *gtime.Time `json:"created_at"` } if err := m.Fields(memberFields).OrderDesc("tnt_users.id").Limit(1000).Offset(offset).Scan(&batch); err != nil { return diff --git a/internal/logic/admin/model.go b/internal/logic/admin/model.go index c6da3f6..9c0cf4a 100644 --- a/internal/logic/admin/model.go +++ b/internal/logic/admin/model.go @@ -627,75 +627,24 @@ func (s *sAdmin) FetchOfficialModelInfo(ctx context.Context, req *v1.ModelFetchO // ExportModels exports model list to CSV or Excel. func (s *sAdmin) ExportModels(ctx context.Context, req *v1.ModelExportReq) (*v1.ModelExportRes, error) { - r := g.RequestFromCtx(ctx) - format := req.Format - if format == "" { - format = "csv" - } - if format == "excel" { - format = "xlsx" - } - - columns := []export.Column{ - {Field: "id", Header: "ID"}, - {Field: "model_id", Header: "模型标识"}, - {Field: "model_name", Header: "显示名称"}, - {Field: "category", Header: "分类"}, - {Field: "status", Header: "状态"}, - {Field: "max_context_tokens", Header: "最大上下文"}, - {Field: "max_output_tokens", Header: "最大输出"}, - {Field: "created_at", Header: "创建时间"}, - } - - config := export.Config{ - Format: format, - Filename: "模型_" + gtime.Now().Format("Ymd_His"), - Columns: columns, - } - modelFields := "id, model_id, model_name, category, status, max_context_tokens, max_output_tokens, created_at" - if format == "xlsx" { - query := dao.MdlModels.Ctx(ctx) - if req.Category != "" { - query = query.Where("category", req.Category) - } - if req.Status != "" { - query = query.Where("status", req.Status) - } - if req.Search != "" { - query = query.Where("model_id LIKE ? OR model_name LIKE ?", "%"+req.Search+"%", "%"+req.Search+"%") - } - var models []struct { - ID int64 `json:"id"` - ModelId string `json:"model_id"` - ModelName string `json:"model_name"` - Category string `json:"category"` - Status string `json:"status"` - MaxContextTokens int `json:"max_context_tokens"` - MaxOutputTokens int `json:"max_output_tokens"` - CreatedAt *gtime.Time `json:"created_at"` - } - if err := query.Fields(modelFields).OrderDesc("id").Scan(&models); err != nil { - return nil, err - } - data := make([]map[string]any, len(models)) - for i, m := range models { - data[i] = map[string]any{ - "id": m.ID, - "model_id": m.ModelId, - "model_name": m.ModelName, - "category": m.Category, - "status": m.Status, - "max_context_tokens": m.MaxContextTokens, - "max_output_tokens": m.MaxOutputTokens, - "created_at": m.CreatedAt.String(), - } - } - return nil, export.WriteExcel(r, config, data) + config := export.Config{ + Format: req.Format, + Filename: "模型_" + gtime.Now().Format("Ymd_His"), + Columns: []export.Column{ + {Field: "id", Header: "ID"}, + {Field: "model_id", Header: "模型标识"}, + {Field: "model_name", Header: "显示名称"}, + {Field: "category", Header: "分类"}, + {Field: "status", Header: "状态"}, + {Field: "max_context_tokens", Header: "最大上下文"}, + {Field: "max_output_tokens", Header: "最大输出"}, + {Field: "created_at", Header: "创建时间"}, + }, } - return nil, export.StreamCSV(r, config, func(yield func(map[string]any) bool) { + return nil, export.GenericExport(ctx, config, func(yield func(map[string]any) bool) { offset := 0 for { query := dao.MdlModels.Ctx(ctx) diff --git a/internal/logic/admin/notification.go b/internal/logic/admin/notification.go index 4e4fa28..e81d42d 100644 --- a/internal/logic/admin/notification.go +++ b/internal/logic/admin/notification.go @@ -255,7 +255,7 @@ func (s *sAdmin) CreateAnnouncement(ctx context.Context, req *v1.AnnouncementCre } // Get admin user ID from context - adminUserID := getCtxUserID(ctx) + adminUserID := common.GetCtxUserID(ctx) data := do.NtfAnnouncements{ Title: req.Title, diff --git a/internal/logic/admin/order.go b/internal/logic/admin/order.go index 7627a07..97b3614 100644 --- a/internal/logic/admin/order.go +++ b/internal/logic/admin/order.go @@ -4,7 +4,6 @@ import ( "context" "fmt" - "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gtime" v1 "github.com/qianfree/team-api/api/admin/v1" "github.com/qianfree/team-api/internal/dao" @@ -61,7 +60,7 @@ func (s *sAdmin) GetOrder(ctx context.Context, req *v1.OrderDetailReq) (*v1.Orde // RefundOrder 发起退款 func (s *sAdmin) RefundOrder(ctx context.Context, req *v1.OrderRefundReq) (*v1.OrderRefundRes, error) { - adminUserID := ctxUserID(ctx) + adminUserID := common.GetCtxUserID(ctx) var order struct { TenantID int64 `json:"tenant_id"` @@ -107,7 +106,7 @@ func (s *sAdmin) RefundOrder(ctx context.Context, req *v1.OrderRefundReq) (*v1.O // OrderComplete 手动完成订单 func (s *sAdmin) OrderComplete(ctx context.Context, req *v1.OrderCompleteReq) (*v1.OrderCompleteRes, error) { - adminUserID := ctxUserID(ctx) + adminUserID := common.GetCtxUserID(ctx) orderNo, err := getOrderForComplete(ctx, req.Id) if err != nil { @@ -208,15 +207,6 @@ func markOrderPaidByAdmin(ctx context.Context, orderID int64, adminUserID int64) // ExportOrders exports order list to CSV or Excel. func (s *sAdmin) ExportOrders(ctx context.Context, req *v1.OrderExportReq) (*v1.OrderExportRes, error) { - r := g.RequestFromCtx(ctx) - format := req.Format - if format == "" { - format = "csv" - } - if format == "excel" { - format = "xlsx" - } - columns := []export.Column{ {Field: "id", Header: "ID"}, {Field: "order_no", Header: "订单号"}, @@ -229,51 +219,14 @@ func (s *sAdmin) ExportOrders(ctx context.Context, req *v1.OrderExportReq) (*v1. } config := export.Config{ - Format: format, + Format: req.Format, Filename: "订单_" + gtime.Now().Format("Ymd_His"), Columns: columns, } orderFields := "id, order_no, tenant_id, order_type, final_amount, payment_channel, status, created_at" - if format == "xlsx" { - query := dao.OrdOrders.Ctx(ctx) - if req.Status != "" { - query = query.Where("status", req.Status) - } - if req.TenantID != "" { - query = query.Where("tenant_id", req.TenantID) - } - var orders []struct { - Id int64 `json:"id"` - OrderNo string `json:"order_no"` - TenantId int64 `json:"tenant_id"` - OrderType string `json:"order_type"` - FinalAmount float64 `json:"final_amount"` - PaymentChannel string `json:"payment_channel"` - Status string `json:"status"` - CreatedAt *gtime.Time `json:"created_at"` - } - if err := query.Fields(orderFields).OrderDesc("created_at").Scan(&orders); err != nil { - return nil, err - } - data := make([]map[string]any, len(orders)) - for i, o := range orders { - data[i] = map[string]any{ - "id": o.Id, - "order_no": o.OrderNo, - "tenant_id": o.TenantId, - "order_type": o.OrderType, - "final_amount": o.FinalAmount, - "payment_channel": o.PaymentChannel, - "status": o.Status, - "created_at": o.CreatedAt.String(), - } - } - return nil, export.WriteExcel(r, config, data) - } - - return nil, export.StreamCSV(r, config, func(yield func(map[string]any) bool) { + return nil, export.GenericExport(ctx, config, func(yield func(map[string]any) bool) { offset := 0 for { query := dao.OrdOrders.Ctx(ctx) diff --git a/internal/logic/admin/plan.go b/internal/logic/admin/plan.go index c1b160b..1864b32 100644 --- a/internal/logic/admin/plan.go +++ b/internal/logic/admin/plan.go @@ -5,7 +5,6 @@ import ( "github.com/qianfree/team-api/internal/dao" do "github.com/qianfree/team-api/internal/model/do" - "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gtime" v1 "github.com/qianfree/team-api/api/admin/v1" @@ -136,15 +135,6 @@ func (s *sAdmin) ToggleRecommend(ctx context.Context, req *v1.PlanToggleRecommen // ExportPlans exports plan list to CSV or Excel. func (s *sAdmin) ExportPlans(ctx context.Context, req *v1.PlanExportReq) (*v1.PlanExportRes, error) { - r := g.RequestFromCtx(ctx) - format := req.Format - if format == "" { - format = "csv" - } - if format == "excel" { - format = "xlsx" - } - columns := []export.Column{ {Field: "id", Header: "ID"}, {Field: "name", Header: "名称"}, @@ -158,54 +148,14 @@ func (s *sAdmin) ExportPlans(ctx context.Context, req *v1.PlanExportReq) (*v1.Pl } config := export.Config{ - Format: format, + Format: req.Format, Filename: "套餐_" + gtime.Now().Format("Ymd_His"), Columns: columns, } planFields := "id, name, identifier, monthly_price, yearly_price, status, monthly_quota_tokens, is_recommended, created_at" - if format == "xlsx" { - query := dao.PlnPlans.Ctx(ctx) - if req.Status != "" { - query = query.Where("status", req.Status) - } - var plans []struct { - Id int64 `json:"id"` - Name string `json:"name"` - Identifier string `json:"identifier"` - MonthlyPrice float64 `json:"monthly_price"` - YearlyPrice float64 `json:"yearly_price"` - Status string `json:"status"` - MonthlyQuotaTokens int64 `json:"monthly_quota_tokens"` - IsRecommended bool `json:"is_recommended"` - CreatedAt *gtime.Time `json:"created_at"` - } - if err := query.Fields(planFields).OrderAsc("sort_order").Scan(&plans); err != nil { - return nil, err - } - data := make([]map[string]any, len(plans)) - for i, p := range plans { - isRec := "否" - if p.IsRecommended { - isRec = "是" - } - data[i] = map[string]any{ - "id": p.Id, - "name": p.Name, - "identifier": p.Identifier, - "monthly_price": p.MonthlyPrice, - "yearly_price": p.YearlyPrice, - "status": p.Status, - "monthly_quota_tokens": p.MonthlyQuotaTokens, - "is_recommended": isRec, - "created_at": p.CreatedAt.String(), - } - } - return nil, export.WriteExcel(r, config, data) - } - - return nil, export.StreamCSV(r, config, func(yield func(map[string]any) bool) { + return nil, export.GenericExport(ctx, config, func(yield func(map[string]any) bool) { offset := 0 for { query := dao.PlnPlans.Ctx(ctx) diff --git a/internal/logic/admin/promo_code.go b/internal/logic/admin/promo_code.go index b392d6f..cef1a63 100644 --- a/internal/logic/admin/promo_code.go +++ b/internal/logic/admin/promo_code.go @@ -5,7 +5,6 @@ import ( "github.com/qianfree/team-api/internal/dao" "github.com/qianfree/team-api/internal/logic/common" - "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gtime" v1 "github.com/qianfree/team-api/api/admin/v1" @@ -81,15 +80,6 @@ func (s *sAdmin) GetPromoCodeUsages(ctx context.Context, req *v1.PromoCodeUsages // ExportPromoCodes exports promo code list to CSV or Excel. func (s *sAdmin) ExportPromoCodes(ctx context.Context, req *v1.PromoCodeExportReq) (*v1.PromoCodeExportRes, error) { - r := g.RequestFromCtx(ctx) - format := req.Format - if format == "" { - format = "csv" - } - if format == "excel" { - format = "xlsx" - } - columns := []export.Column{ {Field: "id", Header: "ID"}, {Field: "code", Header: "优惠码"}, @@ -104,48 +94,14 @@ func (s *sAdmin) ExportPromoCodes(ctx context.Context, req *v1.PromoCodeExportRe } config := export.Config{ - Format: format, + Format: req.Format, Filename: "优惠码_" + gtime.Now().Format("Ymd_His"), Columns: columns, } promoFields := "id, code, name, type, discount_value, min_amount, total_count, used_count, status, created_at" - if format == "xlsx" { - var items []struct { - Id int64 `json:"id"` - Code string `json:"code"` - Name string `json:"name"` - Type string `json:"type"` - DiscountValue float64 `json:"discount_value"` - MinAmount float64 `json:"min_amount"` - TotalCount int `json:"total_count"` - UsedCount int `json:"used_count"` - Status string `json:"status"` - CreatedAt *gtime.Time `json:"created_at"` - } - if err := dao.OrdPromoCodes.Ctx(ctx).Fields(promoFields).OrderDesc("created_at").Scan(&items); err != nil { - return nil, err - } - data := make([]map[string]any, len(items)) - for i, item := range items { - data[i] = map[string]any{ - "id": item.Id, - "code": item.Code, - "name": item.Name, - "type": item.Type, - "discount_value": item.DiscountValue, - "min_amount": item.MinAmount, - "total_count": item.TotalCount, - "used_count": item.UsedCount, - "status": item.Status, - "created_at": item.CreatedAt.String(), - } - } - return nil, export.WriteExcel(r, config, data) - } - - return nil, export.StreamCSV(r, config, func(yield func(map[string]any) bool) { + return nil, export.GenericExport(ctx, config, func(yield func(map[string]any) bool) { offset := 0 for { var batch []struct { diff --git a/internal/logic/admin/redemption.go b/internal/logic/admin/redemption.go index 6ff5edf..6f52fa6 100644 --- a/internal/logic/admin/redemption.go +++ b/internal/logic/admin/redemption.go @@ -183,15 +183,6 @@ func (s *sAdmin) ListRedemptionUsages(ctx context.Context, req *v1.RedemptionUsa // ExportRedemptions exports redemption list to CSV or Excel. func (s *sAdmin) ExportRedemptions(ctx context.Context, req *v1.RedemptionExportReq) (*v1.RedemptionExportRes, error) { - r := g.RequestFromCtx(ctx) - format := req.Format - if format == "" { - format = "csv" - } - if format == "excel" { - format = "xlsx" - } - columns := []export.Column{ {Field: "id", Header: "ID"}, {Field: "code", Header: "兑换码"}, @@ -204,48 +195,14 @@ func (s *sAdmin) ExportRedemptions(ctx context.Context, req *v1.RedemptionExport } config := export.Config{ - Format: format, + Format: req.Format, Filename: "兑换码_" + gtime.Now().Format("Ymd_His"), Columns: columns, } redemptionFields := "id, code, type, value, used_count, status, batch_no, created_at" - if format == "xlsx" { - query := dao.OrdRedemptions.Ctx(ctx) - if req.Status != "" { - query = query.Where("status", req.Status) - } - var items []struct { - Id int64 `json:"id"` - Code string `json:"code"` - Type string `json:"type"` - Value float64 `json:"value"` - UsedCount int `json:"used_count"` - Status string `json:"status"` - BatchNo string `json:"batch_no"` - CreatedAt *gtime.Time `json:"created_at"` - } - if err := query.Fields(redemptionFields).OrderDesc("created_at").Scan(&items); err != nil { - return nil, err - } - data := make([]map[string]any, len(items)) - for i, item := range items { - data[i] = map[string]any{ - "id": item.Id, - "code": item.Code, - "type": item.Type, - "value": item.Value, - "used_count": item.UsedCount, - "status": item.Status, - "batch_no": item.BatchNo, - "created_at": item.CreatedAt.String(), - } - } - return nil, export.WriteExcel(r, config, data) - } - - return nil, export.StreamCSV(r, config, func(yield func(map[string]any) bool) { + return nil, export.GenericExport(ctx, config, func(yield func(map[string]any) bool) { offset := 0 for { query := dao.OrdRedemptions.Ctx(ctx) diff --git a/internal/logic/admin/security.go b/internal/logic/admin/security.go index 92cb4d7..97be766 100644 --- a/internal/logic/admin/security.go +++ b/internal/logic/admin/security.go @@ -99,7 +99,7 @@ func (s *sAdmin) Verify2FA(ctx context.Context, req *v1.Admin2FAVerifyReq) (*v1. // Setup2FA starts the 2FA setup process for the current admin user. func (s *sAdmin) Setup2FA(ctx context.Context, _ *v1.Admin2FASetupReq) (*v1.Admin2FASetupRes, error) { - userID := ctxUserID(ctx) + userID := common.GetCtxUserID(ctx) secret, uri, err := common.Setup2FA(ctx, "admin", userID) if err != nil { return nil, err @@ -122,7 +122,7 @@ func (s *sAdmin) Setup2FA(ctx context.Context, _ *v1.Admin2FASetupReq) (*v1.Admi // Enable2FA confirms and enables 2FA after verifying the code. func (s *sAdmin) Enable2FA(ctx context.Context, req *v1.Admin2FAEnableReq) (*v1.Admin2FAEnableRes, error) { - userID := ctxUserID(ctx) + userID := common.GetCtxUserID(ctx) // Get pending secret from session/cache (stored temporarily during setup) secret, err := getPendingTOTPSecret(ctx, userID) @@ -143,7 +143,7 @@ func (s *sAdmin) Enable2FA(ctx context.Context, req *v1.Admin2FAEnableReq) (*v1. // Disable2FA disables 2FA for the current admin user. func (s *sAdmin) Disable2FA(ctx context.Context, req *v1.Admin2FADisableReq) (*v1.Admin2FADisableRes, error) { - userID := ctxUserID(ctx) + userID := common.GetCtxUserID(ctx) err := common.Disable2FA(ctx, "admin", userID, req.Code) if err != nil { return nil, err @@ -153,7 +153,7 @@ func (s *sAdmin) Disable2FA(ctx context.Context, req *v1.Admin2FADisableReq) (*v // RegenerateBackupCodes generates new backup codes. func (s *sAdmin) RegenerateBackupCodes(ctx context.Context, req *v1.Admin2FARegenerateBackupCodesReq) (*v1.Admin2FARegenerateBackupCodesRes, error) { - userID := ctxUserID(ctx) + userID := common.GetCtxUserID(ctx) codes, err := common.RegenerateBackupCodes(ctx, "admin", userID, req.Code) if err != nil { return nil, err @@ -163,7 +163,7 @@ func (s *sAdmin) RegenerateBackupCodes(ctx context.Context, req *v1.Admin2FARege // ConfirmHighRisk generates a confirm token for high-risk operations. func (s *sAdmin) ConfirmHighRisk(ctx context.Context, req *v1.Admin2FAConfirmReq) (*v1.Admin2FAConfirmRes, error) { - userID := ctxUserID(ctx) + userID := common.GetCtxUserID(ctx) enabled, err := common.Is2FAEnabled(ctx, "admin", userID) if err != nil { diff --git a/internal/logic/admin/tenant.go b/internal/logic/admin/tenant.go index 1743d63..1baba5b 100644 --- a/internal/logic/admin/tenant.go +++ b/internal/logic/admin/tenant.go @@ -6,7 +6,6 @@ import ( "github.com/gogf/gf/v2/database/gdb" "github.com/gogf/gf/v2/errors/gerror" - "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gtime" v1 "github.com/qianfree/team-api/api/admin/v1" @@ -444,15 +443,6 @@ func (s *sAdmin) UpdateTenantChannelScope(ctx context.Context, req *v1.TenantCha // ExportTenants exports tenant list to CSV or Excel. func (s *sAdmin) ExportTenants(ctx context.Context, req *v1.TenantExportReq) (*v1.TenantExportRes, error) { - r := g.RequestFromCtx(ctx) - format := req.Format - if format == "" { - format = "csv" - } - if format == "excel" { - format = "xlsx" - } - columns := []export.Column{ {Field: "id", Header: "ID"}, {Field: "name", Header: "名称"}, @@ -465,20 +455,18 @@ func (s *sAdmin) ExportTenants(ctx context.Context, req *v1.TenantExportReq) (*v } config := export.Config{ - Format: format, + Format: req.Format, Filename: "租户_" + gtime.Now().Format("Ymd_His"), Columns: columns, } fetchTenantRow := func(t struct { - Id int64 `json:"id"` - Name string `json:"name"` - Code string `json:"code"` - OwnerUserID int64 `json:"owner_user_id"` - Status string `json:"status"` - MaxMembers int `json:"max_members"` - MaxConcurrency int `json:"max_concurrency"` - CreatedAt *gtime.Time `json:"created_at"` + Id int64 `json:"id"` + Name string `json:"name"` + Code string `json:"code"` + OwnerUserID int64 `json:"owner_user_id"` + Status string `json:"status"` + CreatedAt *gtime.Time `json:"created_at"` }) map[string]any { var owner *struct { DisplayName string `json:"display_name"` @@ -513,36 +501,7 @@ func (s *sAdmin) ExportTenants(ctx context.Context, req *v1.TenantExportReq) (*v } } - if format == "xlsx" { - m := dao.TntTenants.Ctx(ctx) - if req.Keyword != "" { - keyword := "%" + strings.TrimSpace(req.Keyword) + "%" - m = m.Where("name LIKE ? OR code LIKE ?", keyword, keyword) - } - if req.Status != "" { - m = m.Where("status", req.Status) - } - var tenants []struct { - Id int64 `json:"id"` - Name string `json:"name"` - Code string `json:"code"` - OwnerUserID int64 `json:"owner_user_id"` - Status string `json:"status"` - MaxMembers int `json:"max_members"` - MaxConcurrency int `json:"max_concurrency"` - CreatedAt *gtime.Time `json:"created_at"` - } - if err := m.OrderDesc("id").Scan(&tenants); err != nil { - return nil, err - } - data := make([]map[string]any, len(tenants)) - for i, t := range tenants { - data[i] = fetchTenantRow(t) - } - return nil, export.WriteExcel(r, config, data) - } - - return nil, export.StreamCSV(r, config, func(yield func(map[string]any) bool) { + return nil, export.GenericExport(ctx, config, func(yield func(map[string]any) bool) { offset := 0 for { m := dao.TntTenants.Ctx(ctx) @@ -554,16 +513,14 @@ func (s *sAdmin) ExportTenants(ctx context.Context, req *v1.TenantExportReq) (*v m = m.Where("status", req.Status) } var batch []struct { - Id int64 `json:"id"` - Name string `json:"name"` - Code string `json:"code"` - OwnerUserID int64 `json:"owner_user_id"` - Status string `json:"status"` - MaxMembers int `json:"max_members"` - MaxConcurrency int `json:"max_concurrency"` - CreatedAt *gtime.Time `json:"created_at"` + Id int64 `json:"id"` + Name string `json:"name"` + Code string `json:"code"` + OwnerUserID int64 `json:"owner_user_id"` + Status string `json:"status"` + CreatedAt *gtime.Time `json:"created_at"` } - if err := m.OrderDesc("id").Limit(1000).Offset(offset).Scan(&batch); err != nil { + if err := m.Fields("id, name, code, owner_user_id, status, created_at").OrderDesc("id").Limit(1000).Offset(offset).Scan(&batch); err != nil { return } for _, t := range batch { diff --git a/internal/logic/admin/ticket.go b/internal/logic/admin/ticket.go index f90d884..8c24a64 100644 --- a/internal/logic/admin/ticket.go +++ b/internal/logic/admin/ticket.go @@ -198,7 +198,7 @@ func (s *sAdmin) ReplyToTicketAdmin(ctx context.Context, req *v1.TicketReplyReq) return nil, common.NewBadRequestError("回复内容不能为空") } - adminID := getCtxUserID(ctx) + adminID := common.GetCtxUserID(ctx) var ticket *struct { Status string `json:"status"` diff --git a/internal/logic/common/session.go b/internal/logic/common/session.go index 9d42740..00f172c 100644 --- a/internal/logic/common/session.go +++ b/internal/logic/common/session.go @@ -232,3 +232,29 @@ func CleanExpiredSessions(ctx context.Context) (int64, error) { } return result.RowsAffected() } + +// GetCtxUserID extracts user ID from context. +// The key "userId" is set by admin_auth or tenant_auth middleware. +func GetCtxUserID(ctx context.Context) int64 { + val := ctx.Value("userId") + if val == nil { + return 0 + } + if id, ok := val.(int64); ok { + return id + } + return 0 +} + +// GetCtxSessionID extracts session ID from context. +// The key "sessionId" is set by admin_auth or tenant_auth middleware. +func GetCtxSessionID(ctx context.Context) int64 { + val := ctx.Value("sessionId") + if val == nil { + return 0 + } + if id, ok := val.(int64); ok { + return id + } + return 0 +} diff --git a/internal/logic/open/open_api.go b/internal/logic/open/open_api.go index 3d3e412..a1507d2 100644 --- a/internal/logic/open/open_api.go +++ b/internal/logic/open/open_api.go @@ -11,6 +11,7 @@ import ( v1 "github.com/qianfree/team-api/api/open/v1" "github.com/qianfree/team-api/internal/dao" "github.com/qianfree/team-api/internal/logic/relay" + "github.com/qianfree/team-api/internal/middleware" do "github.com/qianfree/team-api/internal/model/do" "github.com/qianfree/team-api/internal/service" "github.com/qianfree/team-api/internal/utility/crypto" @@ -26,34 +27,12 @@ func init() { service.RegisterOpen(New()) } -// ctxOpenTenantID extracts tenant_id from context (injected by OpenPlatformAuth middleware). -func ctxOpenTenantID(ctx context.Context) int64 { - val := ctx.Value("openTenantId") - if val != nil { - if id, ok := val.(int64); ok { - return id - } - } - return 0 -} - -// ctxOpenAppID extracts app id from context. -func ctxOpenAppID(ctx context.Context) int64 { - val := ctx.Value("openAppId") - if val != nil { - if id, ok := val.(int64); ok { - return id - } - } - return 0 -} - // ============================================================ // 成员管理 // ============================================================ func (s *sOpen) OpenMemberList(ctx context.Context, req *v1.OpenMemberListReq) (*v1.OpenMemberListRes, error) { - tenantID := ctxOpenTenantID(ctx) + tenantID := middleware.GetOpenTenantID(ctx) if tenantID == 0 { return nil, fmt.Errorf("unauthorized") } @@ -113,7 +92,7 @@ func (s *sOpen) OpenMemberList(ctx context.Context, req *v1.OpenMemberListReq) ( } func (s *sOpen) OpenMemberCreate(ctx context.Context, req *v1.OpenMemberCreateReq) (*v1.OpenMemberCreateRes, error) { - tenantID := ctxOpenTenantID(ctx) + tenantID := middleware.GetOpenTenantID(ctx) if tenantID == 0 { return nil, fmt.Errorf("unauthorized") } @@ -147,7 +126,7 @@ func (s *sOpen) OpenMemberCreate(ctx context.Context, req *v1.OpenMemberCreateRe } func (s *sOpen) OpenMemberUpdate(ctx context.Context, req *v1.OpenMemberUpdateReq) (*v1.OpenMemberUpdateRes, error) { - tenantID := ctxOpenTenantID(ctx) + tenantID := middleware.GetOpenTenantID(ctx) if tenantID == 0 { return nil, fmt.Errorf("unauthorized") } @@ -172,7 +151,7 @@ func (s *sOpen) OpenMemberUpdate(ctx context.Context, req *v1.OpenMemberUpdateRe } func (s *sOpen) OpenMemberDelete(ctx context.Context, req *v1.OpenMemberDeleteReq) (*v1.OpenMemberDeleteRes, error) { - tenantID := ctxOpenTenantID(ctx) + tenantID := middleware.GetOpenTenantID(ctx) if tenantID == 0 { return nil, fmt.Errorf("unauthorized") } @@ -201,7 +180,7 @@ func (s *sOpen) OpenMemberDelete(ctx context.Context, req *v1.OpenMemberDeleteRe // ============================================================ func (s *sOpen) OpenMemberQuota(ctx context.Context, req *v1.OpenMemberQuotaReq) (*v1.OpenMemberQuotaRes, error) { - tenantID := ctxOpenTenantID(ctx) + tenantID := middleware.GetOpenTenantID(ctx) if tenantID == 0 { return nil, fmt.Errorf("unauthorized") } @@ -230,7 +209,7 @@ func (s *sOpen) OpenMemberQuota(ctx context.Context, req *v1.OpenMemberQuotaReq) } func (s *sOpen) OpenMemberQuotaUpdate(ctx context.Context, req *v1.OpenMemberQuotaUpdateReq) (*v1.OpenMemberQuotaUpdateRes, error) { - tenantID := ctxOpenTenantID(ctx) + tenantID := middleware.GetOpenTenantID(ctx) if tenantID == 0 { return nil, fmt.Errorf("unauthorized") } @@ -263,7 +242,7 @@ func (s *sOpen) OpenMemberQuotaUpdate(ctx context.Context, req *v1.OpenMemberQuo } func (s *sOpen) OpenMemberModels(ctx context.Context, req *v1.OpenMemberModelsReq) (*v1.OpenMemberModelsRes, error) { - tenantID := ctxOpenTenantID(ctx) + tenantID := middleware.GetOpenTenantID(ctx) if tenantID == 0 { return nil, fmt.Errorf("unauthorized") } @@ -288,7 +267,7 @@ func (s *sOpen) OpenMemberModels(ctx context.Context, req *v1.OpenMemberModelsRe } func (s *sOpen) OpenMemberModelsUpdate(ctx context.Context, req *v1.OpenMemberModelsUpdateReq) (*v1.OpenMemberModelsUpdateRes, error) { - tenantID := ctxOpenTenantID(ctx) + tenantID := middleware.GetOpenTenantID(ctx) if tenantID == 0 { return nil, fmt.Errorf("unauthorized") } @@ -325,7 +304,7 @@ func (s *sOpen) OpenMemberModelsUpdate(ctx context.Context, req *v1.OpenMemberMo // ============================================================ func (s *sOpen) OpenKeyList(ctx context.Context, req *v1.OpenKeyListReq) (*v1.OpenKeyListRes, error) { - tenantID := ctxOpenTenantID(ctx) + tenantID := middleware.GetOpenTenantID(ctx) if tenantID == 0 { return nil, fmt.Errorf("unauthorized") } @@ -378,7 +357,7 @@ func (s *sOpen) OpenKeyList(ctx context.Context, req *v1.OpenKeyListReq) (*v1.Op } func (s *sOpen) OpenKeyCreate(ctx context.Context, req *v1.OpenKeyCreateReq) (*v1.OpenKeyCreateRes, error) { - tenantID := ctxOpenTenantID(ctx) + tenantID := middleware.GetOpenTenantID(ctx) if tenantID == 0 { return nil, fmt.Errorf("unauthorized") } @@ -424,7 +403,7 @@ func (s *sOpen) OpenKeyCreate(ctx context.Context, req *v1.OpenKeyCreateReq) (*v } func (s *sOpen) OpenKeyDelete(ctx context.Context, req *v1.OpenKeyDeleteReq) (*v1.OpenKeyDeleteRes, error) { - tenantID := ctxOpenTenantID(ctx) + tenantID := middleware.GetOpenTenantID(ctx) if tenantID == 0 { return nil, fmt.Errorf("unauthorized") } @@ -438,7 +417,7 @@ func (s *sOpen) OpenKeyDelete(ctx context.Context, req *v1.OpenKeyDeleteReq) (*v // ============================================================ func (s *sOpen) OpenUsageQuery(ctx context.Context, req *v1.OpenUsageQueryReq) (*v1.OpenUsageQueryRes, error) { - tenantID := ctxOpenTenantID(ctx) + tenantID := middleware.GetOpenTenantID(ctx) if tenantID == 0 { return nil, fmt.Errorf("unauthorized") } @@ -516,7 +495,7 @@ func (s *sOpen) OpenUsageQuery(ctx context.Context, req *v1.OpenUsageQueryReq) ( // ============================================================ func (s *sOpen) OpenBillingQuery(ctx context.Context, req *v1.OpenBillingQueryReq) (*v1.OpenBillingQueryRes, error) { - tenantID := ctxOpenTenantID(ctx) + tenantID := middleware.GetOpenTenantID(ctx) if tenantID == 0 { return nil, fmt.Errorf("unauthorized") } diff --git a/internal/logic/tenant/api_key.go b/internal/logic/tenant/api_key.go index 884f74d..7ab18a6 100644 --- a/internal/logic/tenant/api_key.go +++ b/internal/logic/tenant/api_key.go @@ -12,6 +12,7 @@ import ( "github.com/qianfree/team-api/internal/consts" lcommon "github.com/qianfree/team-api/internal/logic/common" "github.com/qianfree/team-api/internal/logic/relay" + "github.com/qianfree/team-api/internal/middleware" do "github.com/qianfree/team-api/internal/model/do" v1 "github.com/qianfree/team-api/api/tenant/v1" @@ -20,9 +21,9 @@ import ( // ApiKeyList 列出 API Keys,支持按类型过滤 func (s *sTenant) ApiKeyList(ctx context.Context, req *v1.TenantApiKeyListReq) (*v1.TenantApiKeyListRes, error) { - tenantID := ctxTenantID(ctx) - userID := ctxUserID(ctx) - role := ctxUserRole(ctx) + tenantID := middleware.GetTenantID(ctx) + userID := middleware.GetUserID(ctx) + role := middleware.GetUserRole(ctx) page, pageSize := lcommon.NormalizePagination(req.Page, req.PageSize) // member 只能查看个人密钥 @@ -111,9 +112,9 @@ func (s *sTenant) ApiKeyList(ctx context.Context, req *v1.TenantApiKeyListReq) ( // ApiKeyCreate 创建新的 API Key func (s *sTenant) ApiKeyCreate(ctx context.Context, req *v1.TenantApiKeyCreateReq) (*v1.TenantApiKeyCreateRes, error) { - tenantID := ctxTenantID(ctx) - userID := ctxUserID(ctx) - role := ctxUserRole(ctx) + tenantID := middleware.GetTenantID(ctx) + userID := middleware.GetUserID(ctx) + role := middleware.GetUserRole(ctx) keyType := req.KeyType if keyType == "" { @@ -204,9 +205,9 @@ func (s *sTenant) ApiKeyCreate(ctx context.Context, req *v1.TenantApiKeyCreateRe // ApiKeyDelete 禁用 API Key func (s *sTenant) ApiKeyDelete(ctx context.Context, req *v1.TenantApiKeyDeleteReq) (*v1.TenantApiKeyDeleteRes, error) { - tenantID := ctxTenantID(ctx) - userID := ctxUserID(ctx) - role := ctxUserRole(ctx) + tenantID := middleware.GetTenantID(ctx) + userID := middleware.GetUserID(ctx) + role := middleware.GetUserRole(ctx) keyID := req.Id // 先查询密钥信息以判断类型 @@ -262,9 +263,9 @@ func (s *sTenant) ApiKeyDelete(ctx context.Context, req *v1.TenantApiKeyDeleteRe // ApiKeyUpdate 更新 API Key 的可编辑字段 func (s *sTenant) ApiKeyUpdate(ctx context.Context, req *v1.TenantApiKeyUpdateReq) (*v1.TenantApiKeyUpdateRes, error) { - tenantID := ctxTenantID(ctx) - userID := ctxUserID(ctx) - role := ctxUserRole(ctx) + tenantID := middleware.GetTenantID(ctx) + userID := middleware.GetUserID(ctx) + role := middleware.GetUserRole(ctx) keyID := req.Id // 查询密钥信息 @@ -388,9 +389,9 @@ func (s *sTenant) ApiKeyUpdate(ctx context.Context, req *v1.TenantApiKeyUpdateRe // ApiKeyUpdateScopes 更新 API Key 的模型 scope func (s *sTenant) ApiKeyUpdateScopes(ctx context.Context, req *v1.TenantApiKeyUpdateScopesReq) (*v1.TenantApiKeyUpdateScopesRes, error) { - tenantID := ctxTenantID(ctx) - userID := ctxUserID(ctx) - role := ctxUserRole(ctx) + tenantID := middleware.GetTenantID(ctx) + userID := middleware.GetUserID(ctx) + role := middleware.GetUserRole(ctx) keyID := req.Id // 先查询密钥信息以判断类型 @@ -448,7 +449,7 @@ func (s *sTenant) ApiKeyUpdateScopes(ctx context.Context, req *v1.TenantApiKeyUp // ApiKeyModelScopes 查询 API Key 的模型范围 func (s *sTenant) ApiKeyModelScopes(ctx context.Context, req *v1.TenantApiKeyModelScopesReq) (*v1.TenantApiKeyModelScopesRes, error) { - tenantID := ctxTenantID(ctx) + tenantID := middleware.GetTenantID(ctx) // 验证 key 属于该租户 count, err := dao.ApiKeys.Ctx(ctx). @@ -556,14 +557,8 @@ func listProjectApiKeys(ctx context.Context, tenantID, projectID int64, page, pa // ExportApiKeys exports the tenant API key list as CSV or Excel. func (s *sTenant) ExportApiKeys(ctx context.Context, req *v1.TenantApiKeyExportReq) (*v1.TenantApiKeyExportRes, error) { - r := g.RequestFromCtx(ctx) - format := req.Format - if format == "" { - format = "csv" - } - - tenantID := ctxTenantID(ctx) - userID := ctxUserID(ctx) + tenantID := middleware.GetTenantID(ctx) + userID := middleware.GetUserID(ctx) columns := []export.Column{ {Field: "id", Header: "ID"}, @@ -577,7 +572,7 @@ func (s *sTenant) ExportApiKeys(ctx context.Context, req *v1.TenantApiKeyExportR } config := export.Config{ - Format: format, + Format: req.Format, Filename: "API密钥_" + gtime.Now().Format("Ymd_His"), Columns: columns, } @@ -595,44 +590,7 @@ func (s *sTenant) ExportApiKeys(ctx context.Context, req *v1.TenantApiKeyExportR return query } - if format == "xlsx" { - type keyRow struct { - Id int64 `json:"id"` - Name string `json:"name"` - KeyPrefix string `json:"key_prefix"` - KeyType string `json:"key_type"` - Scope string `json:"scope"` - Status string `json:"status"` - ExpiresAt *time.Time `json:"expires_at"` - CreatedAt *time.Time `json:"created_at"` - } - - var keys []keyRow - err := buildQuery(). - Fields("id, name, key_prefix, key_type, scope, status, expires_at, created_at"). - OrderDesc("created_at"). - Scan(&keys) - if err != nil { - return nil, err - } - - data := make([]map[string]any, 0, len(keys)) - for _, k := range keys { - data = append(data, map[string]any{ - "id": k.Id, - "name": k.Name, - "key_prefix": k.KeyPrefix, - "key_type": k.KeyType, - "scope": k.Scope, - "status": k.Status, - "expires_at": k.ExpiresAt, - "created_at": k.CreatedAt, - }) - } - return nil, export.WriteExcel(r, config, data) - } - - return nil, export.StreamCSV(r, config, func(yield func(map[string]any) bool) { + return nil, export.GenericExport(ctx, config, func(yield func(map[string]any) bool) { offset := 0 for { type keyRow struct { diff --git a/internal/logic/tenant/audit_config.go b/internal/logic/tenant/audit_config.go index 1e2b66e..6cfff43 100644 --- a/internal/logic/tenant/audit_config.go +++ b/internal/logic/tenant/audit_config.go @@ -12,6 +12,7 @@ import ( "github.com/qianfree/team-api/internal/model/entity" "github.com/qianfree/team-api/internal/logic/common" + "github.com/qianfree/team-api/internal/middleware" v1 "github.com/qianfree/team-api/api/tenant/v1" ) @@ -48,11 +49,11 @@ func saveTenantSettings(ctx context.Context, tenantID int64, settings map[string // AuditConfigGet returns the tenant's own audit level. // 租户审计级别与全局级别完全独立,未设置时默认 masked。 func (s *sTenant) AuditConfigGet(ctx context.Context, req *v1.TenantAuditConfigGetReq) (*v1.TenantAuditConfigGetRes, error) { - role := ctxUserRole(ctx) + role := middleware.GetUserRole(ctx) if role != "owner" && role != "admin" { return nil, common.NewForbiddenError("需要 owner 或 admin 权限") } - tenantID := ctxTenantID(ctx) + tenantID := middleware.GetTenantID(ctx) tenantLevel := common.GetTenantAuditLevel(ctx, tenantID) if tenantLevel == "" { @@ -70,7 +71,7 @@ func (s *sTenant) AuditConfigUpdate(ctx context.Context, req *v1.TenantAuditConf if err := ownerOnly(ctx); err != nil { return nil, err } - tenantID := ctxTenantID(ctx) + tenantID := middleware.GetTenantID(ctx) level := req.AuditLevel @@ -94,11 +95,11 @@ func (s *sTenant) AuditConfigUpdate(ctx context.Context, req *v1.TenantAuditConf // AuditLogs returns a paginated list of audit logs for the tenant. func (s *sTenant) AuditLogs(ctx context.Context, req *v1.TenantAuditLogsReq) (*v1.TenantAuditLogsRes, error) { - role := ctxUserRole(ctx) + role := middleware.GetUserRole(ctx) if role != "owner" && role != "admin" { return nil, common.NewForbiddenError("需要 owner 或 admin 权限") } - tenantID := ctxTenantID(ctx) + tenantID := middleware.GetTenantID(ctx) page, pageSize := common.NormalizePagination(req.Page, req.PageSize) @@ -152,11 +153,11 @@ func isValidAuditLevel(level string) bool { // TenantRequestAuditLogs 分页查询租户的请求审计日志(不含 body,性能优先) func (s *sTenant) TenantRequestAuditLogs(ctx context.Context, req *v1.TenantRequestAuditLogsReq) (*v1.TenantRequestAuditLogsRes, error) { - role := ctxUserRole(ctx) + role := middleware.GetUserRole(ctx) if role != "owner" && role != "admin" { return nil, common.NewForbiddenError("需要 owner 或 admin 权限") } - tenantID := ctxTenantID(ctx) + tenantID := middleware.GetTenantID(ctx) page, pageSize := common.NormalizePagination(req.Page, req.PageSize) var conditions []string @@ -236,11 +237,11 @@ func (s *sTenant) TenantRequestAuditLogs(ctx context.Context, req *v1.TenantRequ // TenantRequestAuditLogDetail 查询单条请求审计日志详情(含 request_body 和 response_body) func (s *sTenant) TenantRequestAuditLogDetail(ctx context.Context, req *v1.TenantRequestAuditLogDetailReq) (*v1.TenantRequestAuditLogDetailRes, error) { - role := ctxUserRole(ctx) + role := middleware.GetUserRole(ctx) if role != "owner" && role != "admin" { return nil, common.NewForbiddenError("需要 owner 或 admin 权限") } - tenantID := ctxTenantID(ctx) + tenantID := middleware.GetTenantID(ctx) var record *entity.AudRequestLogs err := dao.AudRequestLogs.Ctx(ctx). diff --git a/internal/logic/tenant/auth.go b/internal/logic/tenant/auth.go index 9ef750f..f7c9e2e 100644 --- a/internal/logic/tenant/auth.go +++ b/internal/logic/tenant/auth.go @@ -19,6 +19,7 @@ import ( "github.com/qianfree/team-api/internal/consts" "github.com/qianfree/team-api/internal/dao" "github.com/qianfree/team-api/internal/logic/common" + "github.com/qianfree/team-api/internal/middleware" "github.com/qianfree/team-api/internal/model/entity" "github.com/qianfree/team-api/internal/utility/crypto" ) @@ -388,7 +389,7 @@ func (s *sTenant) Login(ctx context.Context, req *v1.TenantLoginReq) (*v1.Tenant // Logout handles tenant user logout. func (s *sTenant) Logout(ctx context.Context, req *v1.TenantLogoutReq) (*v1.TenantLogoutRes, error) { - sessionID := ctxSessionID(ctx) + sessionID := middleware.GetSessionID(ctx) common.MarkSessionRevoked(ctx, sessionID) err := common.RevokeSession(ctx, sessionID) if err != nil { @@ -452,7 +453,7 @@ func (s *sTenant) Refresh(ctx context.Context, req *v1.TenantRefreshReq) (*v1.Te // ChangePassword handles tenant user password change. func (s *sTenant) ChangePassword(ctx context.Context, req *v1.TenantChangePasswordReq) (*v1.TenantChangePasswordRes, error) { - userID := ctxUserID(ctx) + userID := middleware.GetUserID(ctx) var user *entity.TntUsers err := dao.TntUsers.Ctx(ctx). @@ -492,8 +493,8 @@ func (s *sTenant) ChangePassword(ctx context.Context, req *v1.TenantChangePasswo // ListSessions returns active sessions for the current tenant user. func (s *sTenant) ListSessions(ctx context.Context, req *v1.TenantSessionListReq) (*v1.TenantSessionListRes, error) { - userID := ctxUserID(ctx) - currentSessionID := ctxSessionID(ctx) + userID := middleware.GetUserID(ctx) + currentSessionID := middleware.GetSessionID(ctx) sessions, err := common.ListSessions(ctx, "tenant", userID) if err != nil { diff --git a/internal/logic/tenant/billing.go b/internal/logic/tenant/billing.go index 06368bb..d60bccd 100644 --- a/internal/logic/tenant/billing.go +++ b/internal/logic/tenant/billing.go @@ -14,16 +14,17 @@ import ( "github.com/gogf/gf/v2/os/gtime" v1 "github.com/qianfree/team-api/api/tenant/v1" + "github.com/qianfree/team-api/internal/middleware" "github.com/qianfree/team-api/internal/utility/export" ) // Wallet 获取租户钱包余额 func (s *sTenant) Wallet(ctx context.Context, req *v1.TenantWalletReq) (*v1.TenantWalletRes, error) { - role := ctxUserRole(ctx) + role := middleware.GetUserRole(ctx) if role != "owner" && role != "admin" { return nil, common.NewForbiddenError("需要 owner 或 admin 权限") } - tenantID := ctxTenantID(ctx) + tenantID := middleware.GetTenantID(ctx) type walletRow struct { Balance float64 `json:"balance"` @@ -74,11 +75,11 @@ func (s *sTenant) Wallet(ctx context.Context, req *v1.TenantWalletReq) (*v1.Tena // WalletTransactions 获取租户钱包流水 func (s *sTenant) WalletTransactions(ctx context.Context, req *v1.TenantWalletTransactionsReq) (*v1.TenantWalletTransactionsRes, error) { - role := ctxUserRole(ctx) + role := middleware.GetUserRole(ctx) if role != "owner" && role != "admin" { return nil, common.NewForbiddenError("需要 owner 或 admin 权限") } - tenantID := ctxTenantID(ctx) + tenantID := middleware.GetTenantID(ctx) page, pageSize := common.NormalizePagination(req.Page, req.PageSize) query := dao.BilTransactions.Ctx(ctx). @@ -151,9 +152,9 @@ func (s *sTenant) WalletTransactions(ctx context.Context, req *v1.TenantWalletTr // UsageLogs 获取租户用量日志 func (s *sTenant) UsageLogs(ctx context.Context, req *v1.TenantUsageLogsReq) (*v1.TenantUsageLogsRes, error) { - tenantID := ctxTenantID(ctx) - userID := ctxUserID(ctx) - role := ctxUserRole(ctx) + tenantID := middleware.GetTenantID(ctx) + userID := middleware.GetUserID(ctx) + role := middleware.GetUserRole(ctx) page, pageSize := common.NormalizePagination(req.Page, req.PageSize) var conditions []string @@ -243,15 +244,9 @@ func (s *sTenant) UsageLogs(ctx context.Context, req *v1.TenantUsageLogsReq) (*v // ExportUsageLogs exports the tenant usage logs as CSV or Excel. func (s *sTenant) ExportUsageLogs(ctx context.Context, req *v1.TenantUsageLogsExportReq) (*v1.TenantUsageLogsExportRes, error) { - r := g.RequestFromCtx(ctx) - format := req.Format - if format == "" { - format = "csv" - } - - tenantID := ctxTenantID(ctx) - userID := ctxUserID(ctx) - role := ctxUserRole(ctx) + tenantID := middleware.GetTenantID(ctx) + userID := middleware.GetUserID(ctx) + role := middleware.GetUserRole(ctx) columns := []export.Column{ {Field: "id", Header: "ID"}, @@ -266,7 +261,7 @@ func (s *sTenant) ExportUsageLogs(ctx context.Context, req *v1.TenantUsageLogsEx } config := export.Config{ - Format: format, + Format: req.Format, Filename: "用量日志_" + gtime.Now().Format("Ymd_His"), Columns: columns, } @@ -309,40 +304,7 @@ func (s *sTenant) ExportUsageLogs(ctx context.Context, req *v1.TenantUsageLogsEx where := strings.Join(conditions, " AND ") fromClause := "bil_usage_logs u LEFT JOIN tnt_users t ON u.user_id = t.id AND u.tenant_id = t.tenant_id LEFT JOIN tnt_projects p ON u.project_id = p.id LEFT JOIN api_keys ak ON u.api_key_id = ak.id" - if format == "xlsx" { - dataSQL := fmt.Sprintf( - `SELECT u.id, COALESCE(t.username, '') AS username, u.model_name, u.request_type, - u.input_tokens, u.output_tokens, u.total_cost, u.status, u.created_at - FROM %s WHERE %s ORDER BY u.created_at DESC`, - fromClause, where, - ) - result, err := g.DB().Ctx(ctx).Query(ctx, dataSQL, args...) - if err != nil { - return nil, err - } - - data := make([]map[string]any, 0, len(result)) - for _, row := range result { - m := make(map[string]any, len(row)) - for k, v := range row { - switch raw := v.Val().(type) { - case []byte: - s := string(raw) - if f, err := strconv.ParseFloat(s, 64); err == nil { - m[k] = f - } else { - m[k] = s - } - default: - m[k] = raw - } - } - data = append(data, m) - } - return nil, export.WriteExcel(r, config, data) - } - - return nil, export.StreamCSV(r, config, func(yield func(map[string]any) bool) { + return nil, export.GenericExport(ctx, config, func(yield func(map[string]any) bool) { offset := 0 for { dataSQL := fmt.Sprintf( @@ -384,18 +346,12 @@ func (s *sTenant) ExportUsageLogs(ctx context.Context, req *v1.TenantUsageLogsEx // ExportWalletTransactions exports the tenant wallet transactions as CSV or Excel. func (s *sTenant) ExportWalletTransactions(ctx context.Context, req *v1.TenantWalletTransactionsExportReq) (*v1.TenantWalletTransactionsExportRes, error) { - role := ctxUserRole(ctx) + role := middleware.GetUserRole(ctx) if role != "owner" && role != "admin" { return nil, common.NewForbiddenError("需要 owner 或 admin 权限") } - r := g.RequestFromCtx(ctx) - format := req.Format - if format == "" { - format = "csv" - } - - tenantID := ctxTenantID(ctx) + tenantID := middleware.GetTenantID(ctx) columns := []export.Column{ {Field: "id", Header: "ID"}, @@ -407,46 +363,12 @@ func (s *sTenant) ExportWalletTransactions(ctx context.Context, req *v1.TenantWa } config := export.Config{ - Format: format, + Format: req.Format, Filename: "交易记录_" + gtime.Now().Format("Ymd_His"), Columns: columns, } - if format == "xlsx" { - type transactionRow struct { - Id int64 `json:"id"` - Type string `json:"type"` - Amount float64 `json:"amount"` - BalanceAfter float64 `json:"balance_after"` - Description string `json:"description"` - CreatedAt *gtime.Time `json:"created_at"` - } - - var records []transactionRow - err := dao.BilTransactions.Ctx(ctx). - Where("tenant_id", tenantID). - Fields("id, type, amount, balance_after, description, created_at"). - OrderDesc("created_at"). - Scan(&records) - if err != nil { - return nil, err - } - - data := make([]map[string]any, 0, len(records)) - for _, r := range records { - data = append(data, map[string]any{ - "id": r.Id, - "type": r.Type, - "amount": r.Amount, - "balance_after": r.BalanceAfter, - "description": r.Description, - "created_at": r.CreatedAt, - }) - } - return nil, export.WriteExcel(r, config, data) - } - - return nil, export.StreamCSV(r, config, func(yield func(map[string]any) bool) { + return nil, export.GenericExport(ctx, config, func(yield func(map[string]any) bool) { offset := 0 for { type transactionRow struct { @@ -493,11 +415,11 @@ func (s *sTenant) ExportWalletTransactions(ctx context.Context, req *v1.TenantWa // WalletFrozenItems 获取冻结明细 func (s *sTenant) WalletFrozenItems(ctx context.Context, req *v1.TenantWalletFrozenItemsReq) (*v1.TenantWalletFrozenItemsRes, error) { - role := ctxUserRole(ctx) + role := middleware.GetUserRole(ctx) if role != "owner" && role != "admin" { return nil, common.NewForbiddenError("需要 owner 或 admin 权限") } - tenantID := ctxTenantID(ctx) + tenantID := middleware.GetTenantID(ctx) items, err := billing.GetFrozenItems(ctx, tenantID) if err != nil { diff --git a/internal/logic/tenant/dashboard.go b/internal/logic/tenant/dashboard.go index e240e84..8e8efdb 100644 --- a/internal/logic/tenant/dashboard.go +++ b/internal/logic/tenant/dashboard.go @@ -11,15 +11,16 @@ import ( "github.com/gogf/gf/v2/frame/g" v1 "github.com/qianfree/team-api/api/tenant/v1" + "github.com/qianfree/team-api/internal/middleware" ) // Dashboard returns the tenant dashboard statistics. func (s *sTenant) Dashboard(ctx context.Context, req *v1.TenantDashboardReq) (*v1.TenantDashboardRes, error) { - role := ctxUserRole(ctx) + role := middleware.GetUserRole(ctx) if role != "owner" && role != "admin" { return nil, common.NewForbiddenError("需要 owner 或 admin 权限") } - tenantID := ctxTenantID(ctx) + tenantID := middleware.GetTenantID(ctx) today := time.Now().Format("2006-01-02") monthStart := time.Now().Format("2006-01") + "-01" @@ -102,11 +103,11 @@ func (s *sTenant) Dashboard(ctx context.Context, req *v1.TenantDashboardReq) (*v // TokenTrends returns daily token usage for the past N days. func (s *sTenant) TokenTrends(ctx context.Context, req *v1.TenantTokenTrendsReq) (*v1.TenantTokenTrendsRes, error) { - role := ctxUserRole(ctx) + role := middleware.GetUserRole(ctx) if role != "owner" && role != "admin" { return nil, common.NewForbiddenError("需要 owner 或 admin 权限") } - tenantID := ctxTenantID(ctx) + tenantID := middleware.GetTenantID(ctx) days := req.Days if days <= 0 || days > 90 { days = 30 @@ -160,11 +161,11 @@ func (s *sTenant) TokenTrends(ctx context.Context, req *v1.TenantTokenTrendsReq) // ModelDistribution returns the distribution of model usage. func (s *sTenant) ModelDistribution(ctx context.Context, req *v1.TenantModelDistributionReq) (*v1.TenantModelDistributionRes, error) { - role := ctxUserRole(ctx) + role := middleware.GetUserRole(ctx) if role != "owner" && role != "admin" { return nil, common.NewForbiddenError("需要 owner 或 admin 权限") } - tenantID := ctxTenantID(ctx) + tenantID := middleware.GetTenantID(ctx) days := req.Days if days <= 0 || days > 90 { days = 30 @@ -219,11 +220,11 @@ func (s *sTenant) ModelDistribution(ctx context.Context, req *v1.TenantModelDist // BalancePrediction predicts when the balance will be exhausted. func (s *sTenant) BalancePrediction(ctx context.Context, req *v1.TenantBalancePredictionReq) (*v1.TenantBalancePredictionRes, error) { - role := ctxUserRole(ctx) + role := middleware.GetUserRole(ctx) if role != "owner" && role != "admin" { return nil, common.NewForbiddenError("需要 owner 或 admin 权限") } - tenantID := ctxTenantID(ctx) + tenantID := middleware.GetTenantID(ctx) sevenDaysAgo := time.Now().AddDate(0, 0, -7).Format("2006-01-02") @@ -282,7 +283,7 @@ func (s *sTenant) BalancePrediction(ctx context.Context, req *v1.TenantBalancePr // BudgetAlerts checks member and project budget usage and returns those above 80%. func (s *sTenant) BudgetAlerts(ctx context.Context, req *v1.TenantBudgetAlertsReq) (*v1.TenantBudgetAlertsRes, error) { - role := ctxUserRole(ctx) + role := middleware.GetUserRole(ctx) if role != "owner" && role != "admin" { return nil, common.NewForbiddenError("需要 owner 或 admin 权限") } @@ -294,11 +295,11 @@ func (s *sTenant) BudgetAlerts(ctx context.Context, req *v1.TenantBudgetAlertsRe // GetMemberUsageRanking returns top members by usage cost in a given date range. func (s *sTenant) GetMemberUsageRanking(ctx context.Context, req *v1.TenantMemberUsageRankingReq) (*v1.TenantMemberUsageRankingRes, error) { - role := ctxUserRole(ctx) + role := middleware.GetUserRole(ctx) if role != "owner" && role != "admin" { return nil, common.NewForbiddenError("需要 owner 或 admin 权限") } - tenantID := ctxTenantID(ctx) + tenantID := middleware.GetTenantID(ctx) days := req.Days if days <= 0 || days > 90 { days = 30 diff --git a/internal/logic/tenant/email.go b/internal/logic/tenant/email.go index aad2533..6c7a80f 100644 --- a/internal/logic/tenant/email.go +++ b/internal/logic/tenant/email.go @@ -9,6 +9,7 @@ import ( "github.com/qianfree/team-api/internal/consts" "github.com/qianfree/team-api/internal/dao" "github.com/qianfree/team-api/internal/logic/common" + "github.com/qianfree/team-api/internal/middleware" "github.com/qianfree/team-api/internal/model/entity" "github.com/qianfree/team-api/internal/utility/crypto" ) @@ -102,8 +103,8 @@ func (s *sTenant) ResetPassword(ctx context.Context, req *v1.TenantResetPassword // ChangeEmail handles email change for a tenant user. func (s *sTenant) ChangeEmail(ctx context.Context, req *v1.TenantChangeEmailReq) (*v1.TenantChangeEmailRes, error) { - tenantID := ctxTenantID(ctx) - userID := ctxUserID(ctx) + tenantID := middleware.GetTenantID(ctx) + userID := middleware.GetUserID(ctx) newEmail := strings.TrimSpace(strings.ToLower(req.NewEmail)) // Verify code for new email diff --git a/internal/logic/tenant/feedback.go b/internal/logic/tenant/feedback.go index a9e21ed..4c400f3 100644 --- a/internal/logic/tenant/feedback.go +++ b/internal/logic/tenant/feedback.go @@ -6,14 +6,15 @@ import ( v1 "github.com/qianfree/team-api/api/tenant/v1" "github.com/qianfree/team-api/internal/logic/common" + "github.com/qianfree/team-api/internal/middleware" "github.com/gogf/gf/v2/frame/g" ) // CreateFeedback 提交反馈 func (s *sTenant) CreateFeedback(ctx context.Context, req *v1.FeedbackCreateReq) (*v1.FeedbackCreateRes, error) { - tenantID := ctxTenantID(ctx) - userID := ctxUserID(ctx) + tenantID := middleware.GetTenantID(ctx) + userID := middleware.GetUserID(ctx) metadata := "{}" if req.Metadata != nil { @@ -40,8 +41,8 @@ func (s *sTenant) CreateFeedback(ctx context.Context, req *v1.FeedbackCreateReq) // ListFeedbacks 我的反馈列表 func (s *sTenant) ListFeedbacks(ctx context.Context, req *v1.FeedbackListReq) (*v1.FeedbackListRes, error) { - tenantID := ctxTenantID(ctx) - userID := ctxUserID(ctx) + tenantID := middleware.GetTenantID(ctx) + userID := middleware.GetUserID(ctx) page, pageSize := normalizePagination(req.Page, req.PageSize) query := g.DB().Model("spt_feedbacks").Ctx(ctx). @@ -73,8 +74,8 @@ func (s *sTenant) ListFeedbacks(ctx context.Context, req *v1.FeedbackListReq) (* // GetFeedback 反馈详情 func (s *sTenant) GetFeedback(ctx context.Context, req *v1.FeedbackGetReq) (*v1.FeedbackGetRes, error) { - tenantID := ctxTenantID(ctx) - userID := ctxUserID(ctx) + tenantID := middleware.GetTenantID(ctx) + userID := middleware.GetUserID(ctx) var row v1.FeedbackGetRes err := g.DB().Model("spt_feedbacks").Ctx(ctx). diff --git a/internal/logic/tenant/invitation.go b/internal/logic/tenant/invitation.go index f563ee0..449006b 100644 --- a/internal/logic/tenant/invitation.go +++ b/internal/logic/tenant/invitation.go @@ -7,13 +7,14 @@ import ( 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" "github.com/gogf/gf/v2/os/gtime" ) // InvitationList returns a paginated list of invitation records for the tenant. func (s *sTenant) InvitationList(ctx context.Context, req *v1.TenantInvitationListReq) (*v1.TenantInvitationListRes, error) { - tenantID := ctxTenantID(ctx) + tenantID := middleware.GetTenantID(ctx) page, pageSize := common.NormalizePagination(req.Page, req.PageSize) type row struct { @@ -102,7 +103,7 @@ func (s *sTenant) InvitationList(ctx context.Context, req *v1.TenantInvitationLi // RevokeInvitation revokes a pending invitation by setting used_by_user_id = -1. func (s *sTenant) RevokeInvitation(ctx context.Context, req *v1.TenantInvitationRevokeReq) (*v1.TenantInvitationRevokeRes, error) { - tenantID := ctxTenantID(ctx) + tenantID := middleware.GetTenantID(ctx) var inv struct { ID int64 `json:"id"` diff --git a/internal/logic/tenant/lifecycle.go b/internal/logic/tenant/lifecycle.go index 0900029..8557d4d 100644 --- a/internal/logic/tenant/lifecycle.go +++ b/internal/logic/tenant/lifecycle.go @@ -11,6 +11,7 @@ import ( "github.com/gogf/gf/v2/os/gtime" v1 "github.com/qianfree/team-api/api/tenant/v1" + "github.com/qianfree/team-api/internal/middleware" ) // 合法状态转换映射 @@ -72,7 +73,7 @@ func TransitionTenantStatus(ctx context.Context, tenantID int64, newStatus strin // RequestClosure 申请关户 func (s *sTenant) RequestClosure(ctx context.Context, req *v1.TenantRequestClosureReq) (*v1.TenantRequestClosureRes, error) { - tenantID := ctxTenantID(ctx) + tenantID := middleware.GetTenantID(ctx) if err := TransitionTenantStatus(ctx, tenantID, "closing"); err != nil { return nil, err } @@ -81,7 +82,7 @@ func (s *sTenant) RequestClosure(ctx context.Context, req *v1.TenantRequestClosu // CancelClosure 取消关户 func (s *sTenant) CancelClosure(ctx context.Context, req *v1.TenantCancelClosureReq) (*v1.TenantCancelClosureRes, error) { - tenantID := ctxTenantID(ctx) + tenantID := middleware.GetTenantID(ctx) if err := TransitionTenantStatus(ctx, tenantID, "active"); err != nil { return nil, err } diff --git a/internal/logic/tenant/member.go b/internal/logic/tenant/member.go index 5613ad2..2541aaa 100644 --- a/internal/logic/tenant/member.go +++ b/internal/logic/tenant/member.go @@ -18,13 +18,14 @@ import ( "github.com/qianfree/team-api/internal/consts" "github.com/qianfree/team-api/internal/dao" "github.com/qianfree/team-api/internal/logic/common" + "github.com/qianfree/team-api/internal/middleware" "github.com/qianfree/team-api/internal/utility/crypto" "github.com/qianfree/team-api/internal/utility/export" ) // ListMembers returns a paginated list of tenant members. func (s *sTenant) ListMembers(ctx context.Context, req *v1.TenantMemberListReq) (*v1.TenantMemberListRes, error) { - tenantID := ctxTenantID(ctx) + tenantID := middleware.GetTenantID(ctx) page, pageSize := common.NormalizePagination(req.Page, req.PageSize) @@ -83,8 +84,8 @@ func (s *sTenant) ListMembers(ctx context.Context, req *v1.TenantMemberListReq) // InviteMember generates an invitation link. func (s *sTenant) InviteMember(ctx context.Context, req *v1.TenantMemberInviteReq) (*v1.TenantMemberInviteRes, error) { - tenantID := ctxTenantID(ctx) - creatorID := ctxUserID(ctx) + tenantID := middleware.GetTenantID(ctx) + creatorID := middleware.GetUserID(ctx) // Check member limit memberCount, err := dao.TntUsers.Ctx(ctx). @@ -358,7 +359,7 @@ func (s *sTenant) JoinByInvite(ctx context.Context, req *v1.TenantMemberJoinReq) // CreateMember directly creates a member account within the tenant. func (s *sTenant) CreateMember(ctx context.Context, req *v1.TenantMemberCreateReq) (*v1.TenantMemberCreateRes, error) { - tenantID := ctxTenantID(ctx) + tenantID := middleware.GetTenantID(ctx) // Validate role if err := common.ValidateTenantRole(req.Role); err != nil { @@ -464,8 +465,8 @@ func (s *sTenant) CreateMember(ctx context.Context, req *v1.TenantMemberCreateRe // RemoveMember removes a member from the tenant. // Revokes all API keys, anonymizes personal data, releases member model scopes. func (s *sTenant) RemoveMember(ctx context.Context, req *v1.TenantMemberRemoveReq) (*v1.TenantMemberRemoveRes, error) { - tenantID := ctxTenantID(ctx) - currentUserID := ctxUserID(ctx) + tenantID := middleware.GetTenantID(ctx) + currentUserID := middleware.GetUserID(ctx) memberID := req.Id if memberID == currentUserID { @@ -596,8 +597,8 @@ func (s *sTenant) EnableMember(ctx context.Context, tenantID, userID int64) erro // UpdateMemberRole updates a member's role. func (s *sTenant) UpdateMemberRole(ctx context.Context, req *v1.TenantMemberUpdateRoleReq) (*v1.TenantMemberUpdateRoleRes, error) { - tenantID := ctxTenantID(ctx) - currentUserID := ctxUserID(ctx) + tenantID := middleware.GetTenantID(ctx) + currentUserID := middleware.GetUserID(ctx) memberID := req.Id if memberID == currentUserID { @@ -637,8 +638,8 @@ func (s *sTenant) UpdateMemberRole(ctx context.Context, req *v1.TenantMemberUpda // ResetMemberPassword resets a member's password. Only admins can reset other members' passwords. func (s *sTenant) ResetMemberPassword(ctx context.Context, req *v1.TenantMemberResetPasswordReq) (*v1.TenantMemberResetPasswordRes, error) { - tenantID := ctxTenantID(ctx) - currentUserID := ctxUserID(ctx) + tenantID := middleware.GetTenantID(ctx) + currentUserID := middleware.GetUserID(ctx) memberID := req.Id if memberID == currentUserID { @@ -686,7 +687,7 @@ func (s *sTenant) ResetMemberPassword(ctx context.Context, req *v1.TenantMemberR // GetMember returns a single member's detail. func (s *sTenant) GetMember(ctx context.Context, req *v1.TenantMemberGetReq) (*v1.TenantMemberGetRes, error) { - tenantID := ctxTenantID(ctx) + tenantID := middleware.GetTenantID(ctx) var user struct { Id int64 `json:"id"` @@ -723,7 +724,7 @@ func (s *sTenant) GetMember(ctx context.Context, req *v1.TenantMemberGetReq) (*v // GetMemberUsage returns usage statistics for a single member. func (s *sTenant) GetMemberUsage(ctx context.Context, req *v1.TenantMemberUsageReq) (*v1.TenantMemberUsageRes, error) { - tenantID := ctxTenantID(ctx) + tenantID := middleware.GetTenantID(ctx) // Verify member exists in tenant var user struct { @@ -776,7 +777,7 @@ func (s *sTenant) GetMemberUsage(ctx context.Context, req *v1.TenantMemberUsageR // ListMemberApiKeys returns a paginated list of API keys belonging to a specific member. func (s *sTenant) ListMemberApiKeys(ctx context.Context, req *v1.TenantMemberApiKeysReq) (*v1.TenantMemberApiKeysRes, error) { - tenantID := ctxTenantID(ctx) + tenantID := middleware.GetTenantID(ctx) page, pageSize := common.NormalizePagination(req.Page, req.PageSize) @@ -843,13 +844,7 @@ func (s *sTenant) ListMemberApiKeys(ctx context.Context, req *v1.TenantMemberApi // ExportMembers exports the tenant member list as CSV or Excel. func (s *sTenant) ExportMembers(ctx context.Context, req *v1.TenantMemberExportReq) (*v1.TenantMemberExportRes, error) { - r := g.RequestFromCtx(ctx) - format := req.Format - if format == "" { - format = "csv" - } - - tenantID := ctxTenantID(ctx) + tenantID := middleware.GetTenantID(ctx) columns := []export.Column{ {Field: "id", Header: "ID"}, @@ -862,52 +857,12 @@ func (s *sTenant) ExportMembers(ctx context.Context, req *v1.TenantMemberExportR } config := export.Config{ - Format: format, + Format: req.Format, Filename: "成员列表_" + gtime.Now().Format("Ymd_His"), Columns: columns, } - if format == "xlsx" { - model := dao.TntUsers.Ctx(ctx). - Where("tenant_id", tenantID) - if req.Keyword != "" { - keyword := "%" + strings.TrimSpace(req.Keyword) + "%" - model = model.Where("username LIKE ? OR email LIKE ?", keyword, keyword) - } - if req.Role != "" { - model = model.Where("role", req.Role) - } - - var users []struct { - Id int64 `json:"id"` - Username string `json:"username"` - Email string `json:"email"` - DisplayName string `json:"display_name"` - Role string `json:"role"` - Status string `json:"status"` - CreatedAt string `json:"created_at"` - } - err := model.OrderDesc("id").Scan(&users) - if err != nil { - return nil, err - } - - data := make([]map[string]any, 0, len(users)) - for _, u := range users { - data = append(data, map[string]any{ - "id": u.Id, - "username": u.Username, - "email": u.Email, - "display_name": u.DisplayName, - "role": u.Role, - "status": u.Status, - "created_at": u.CreatedAt, - }) - } - return nil, export.WriteExcel(r, config, data) - } - - return nil, export.StreamCSV(r, config, func(yield func(map[string]any) bool) { + return nil, export.GenericExport(ctx, config, func(yield func(map[string]any) bool) { offset := 0 for { model := dao.TntUsers.Ctx(ctx). diff --git a/internal/logic/tenant/member_import.go b/internal/logic/tenant/member_import.go index 6f8483b..4ce4a9d 100644 --- a/internal/logic/tenant/member_import.go +++ b/internal/logic/tenant/member_import.go @@ -16,6 +16,7 @@ import ( "github.com/gogf/gf/v2/os/gtime" "github.com/qianfree/team-api/internal/logic/common" + "github.com/qianfree/team-api/internal/middleware" "github.com/qianfree/team-api/internal/utility/crypto" v1 "github.com/qianfree/team-api/api/tenant/v1" @@ -31,8 +32,8 @@ type ImportResult struct { // MemberImport parses CSV content, validates, creates an import record. func (s *sTenant) MemberImport(ctx context.Context, req *v1.TenantMemberImportReq) (*v1.TenantMemberImportRes, error) { - tenantID := ctxTenantID(ctx) - creatorID := ctxUserID(ctx) + tenantID := middleware.GetTenantID(ctx) + creatorID := middleware.GetUserID(ctx) // Get CSV from multipart form file file := g.RequestFromCtx(ctx).GetUploadFile("file") @@ -60,7 +61,7 @@ func (s *sTenant) MemberImport(ctx context.Context, req *v1.TenantMemberImportRe // ImportRecords returns a paginated list of import records. func (s *sTenant) ImportRecords(ctx context.Context, req *v1.TenantImportRecordsReq) (*v1.TenantImportRecordsRes, error) { - tenantID := ctxTenantID(ctx) + tenantID := middleware.GetTenantID(ctx) page, pageSize := common.NormalizePagination(req.Page, req.PageSize) @@ -117,7 +118,7 @@ func (s *sTenant) ImportRecords(ctx context.Context, req *v1.TenantImportRecords // ImportRecordGet returns the status of a member import. func (s *sTenant) ImportRecordGet(ctx context.Context, req *v1.TenantImportRecordGetReq) (*v1.TenantImportRecordGetRes, error) { - tenantID := ctxTenantID(ctx) + tenantID := middleware.GetTenantID(ctx) var record struct { ID int64 `json:"id"` diff --git a/internal/logic/tenant/member_model_scope.go b/internal/logic/tenant/member_model_scope.go index af55afe..fe298a1 100644 --- a/internal/logic/tenant/member_model_scope.go +++ b/internal/logic/tenant/member_model_scope.go @@ -9,11 +9,12 @@ import ( do "github.com/qianfree/team-api/internal/model/do" v1 "github.com/qianfree/team-api/api/tenant/v1" + "github.com/qianfree/team-api/internal/middleware" ) // MemberModelScopes returns the model IDs available for a member. func (s *sTenant) MemberModelScopes(ctx context.Context, req *v1.TenantMemberModelScopesReq) (*v1.TenantMemberModelScopesRes, error) { - tenantID := ctxTenantID(ctx) + tenantID := middleware.GetTenantID(ctx) var rows []struct { ModelID int64 `json:"model_id"` @@ -35,7 +36,7 @@ func (s *sTenant) MemberModelScopes(ctx context.Context, req *v1.TenantMemberMod // MemberModelScopesSet sets the available models for a member (full replace). func (s *sTenant) MemberModelScopesSet(ctx context.Context, req *v1.TenantMemberModelScopesSetReq) (*v1.TenantMemberModelScopesSetRes, error) { - tenantID := ctxTenantID(ctx) + tenantID := middleware.GetTenantID(ctx) err := g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error { // Delete existing scopes diff --git a/internal/logic/tenant/member_quota.go b/internal/logic/tenant/member_quota.go index b477778..dbd2441 100644 --- a/internal/logic/tenant/member_quota.go +++ b/internal/logic/tenant/member_quota.go @@ -10,10 +10,11 @@ import ( v1 "github.com/qianfree/team-api/api/tenant/v1" "github.com/qianfree/team-api/internal/dao" + "github.com/qianfree/team-api/internal/middleware" ) func (s *sTenant) MemberQuota(ctx context.Context, req *v1.TenantMemberQuotaReq) (*v1.TenantMemberQuotaRes, error) { - tenantID := ctxTenantID(ctx) + tenantID := middleware.GetTenantID(ctx) var row struct { QuotaType string `json:"quota_type"` @@ -49,7 +50,7 @@ func (s *sTenant) MemberQuota(ctx context.Context, req *v1.TenantMemberQuotaReq) } func (s *sTenant) MemberQuotaSet(ctx context.Context, req *v1.TenantMemberQuotaSetReq) (*v1.TenantMemberQuotaSetRes, error) { - tenantID := ctxTenantID(ctx) + tenantID := middleware.GetTenantID(ctx) if req.QuotaType == "periodic" && req.Period == "" { return nil, fmt.Errorf("周期类型不能为空") diff --git a/internal/logic/tenant/model.go b/internal/logic/tenant/model.go index 57d8439..1b1200f 100644 --- a/internal/logic/tenant/model.go +++ b/internal/logic/tenant/model.go @@ -7,6 +7,7 @@ import ( "github.com/gogf/gf/v2/frame/g" v1 "github.com/qianfree/team-api/api/tenant/v1" "github.com/qianfree/team-api/internal/dao" + "github.com/qianfree/team-api/internal/middleware" ) // pricingTierRow 阶梯定价行(与 billing.pricingTierRow 结构一致) @@ -19,7 +20,7 @@ type pricingTierRow struct { // ListAvailableModels 获取租户可用的模型列表 func (s *sTenant) ListAvailableModels(ctx context.Context, req *v1.TenantAvailableModelsReq) (*v1.TenantAvailableModelsRes, error) { - tenantID := ctxTenantID(ctx) + tenantID := middleware.GetTenantID(ctx) query := dao.MdlTenantModels.Ctx(ctx). LeftJoin("mdl_models m ON mdl_tenant_models.model_id = m.id"). diff --git a/internal/logic/tenant/model_comparison.go b/internal/logic/tenant/model_comparison.go index 7459733..a4a6abf 100644 --- a/internal/logic/tenant/model_comparison.go +++ b/internal/logic/tenant/model_comparison.go @@ -10,10 +10,11 @@ import ( "github.com/gogf/gf/v2/os/gtime" v1 "github.com/qianfree/team-api/api/tenant/v1" + "github.com/qianfree/team-api/internal/middleware" ) func (s *sTenant) ModelComparison(ctx context.Context, req *v1.ModelComparisonReq) (*v1.ModelComparisonRes, error) { - tenantID := ctxTenantID(ctx) + tenantID := middleware.GetTenantID(ctx) if req.Days <= 0 { req.Days = 7 diff --git a/internal/logic/tenant/notification.go b/internal/logic/tenant/notification.go index 541d337..64b0fc5 100644 --- a/internal/logic/tenant/notification.go +++ b/internal/logic/tenant/notification.go @@ -15,14 +15,15 @@ import ( v1 "github.com/qianfree/team-api/api/tenant/v1" "github.com/qianfree/team-api/internal/logic/common" + "github.com/qianfree/team-api/internal/middleware" "github.com/qianfree/team-api/internal/utility/export" ) // Notifications 获取用户的通知列表(个人消息 + 广播消息) func (s *sTenant) Notifications(ctx context.Context, req *v1.TenantNotificationsReq) (*v1.TenantNotificationsRes, error) { - tenantID := ctxTenantID(ctx) - userID := ctxUserID(ctx) - role := ctxUserRole(ctx) + tenantID := middleware.GetTenantID(ctx) + userID := middleware.GetUserID(ctx) + role := middleware.GetUserRole(ctx) page, pageSize := common.NormalizePagination(req.Page, req.PageSize) @@ -69,9 +70,9 @@ func (s *sTenant) Notifications(ctx context.Context, req *v1.TenantNotifications // UnreadCount 获取未读消息数量 func (s *sTenant) UnreadCount(ctx context.Context, req *v1.TenantUnreadCountReq) (*v1.TenantUnreadCountRes, error) { - tenantID := ctxTenantID(ctx) - userID := ctxUserID(ctx) - role := ctxUserRole(ctx) + tenantID := middleware.GetTenantID(ctx) + userID := middleware.GetUserID(ctx) + role := middleware.GetUserRole(ctx) // Count personal unread messages personalUnread, err := dao.NtfMessages.Ctx(ctx). @@ -108,8 +109,8 @@ func (s *sTenant) UnreadCount(ctx context.Context, req *v1.TenantUnreadCountReq) // MarkRead 标记消息为已读 func (s *sTenant) MarkRead(ctx context.Context, req *v1.TenantMarkReadReq) (*v1.TenantMarkReadRes, error) { - tenantID := ctxTenantID(ctx) - userID := ctxUserID(ctx) + tenantID := middleware.GetTenantID(ctx) + userID := middleware.GetUserID(ctx) // Verify the message belongs to this tenant var msg *struct { @@ -132,7 +133,7 @@ func (s *sTenant) MarkRead(ctx context.Context, req *v1.TenantMarkReadReq) (*v1. // Check role access for broadcast messages if msg.IsBroadcast == 1 && msg.TargetRoles != "" { - role := ctxUserRole(ctx) + role := middleware.GetUserRole(ctx) if !containsRole(msg.TargetRoles, role) { return nil, common.NewForbiddenError("无权访问该消息") } @@ -174,9 +175,9 @@ func (s *sTenant) MarkRead(ctx context.Context, req *v1.TenantMarkReadReq) (*v1. // MarkAllRead 标记所有未读消息为已读 func (s *sTenant) MarkAllRead(ctx context.Context, req *v1.TenantMarkAllReadReq) (*v1.TenantMarkAllReadRes, error) { - tenantID := ctxTenantID(ctx) - userID := ctxUserID(ctx) - role := ctxUserRole(ctx) + tenantID := middleware.GetTenantID(ctx) + userID := middleware.GetUserID(ctx) + role := middleware.GetUserRole(ctx) // Mark all personal unread messages as read _, err := dao.NtfMessages.Ctx(ctx). @@ -233,8 +234,8 @@ func (s *sTenant) MarkAllRead(ctx context.Context, req *v1.TenantMarkAllReadReq) // DeleteNotification 删除已读的个人消息(广播消息不允许用户删除) func (s *sTenant) DeleteNotification(ctx context.Context, req *v1.TenantNotificationDeleteReq) (*v1.TenantNotificationDeleteRes, error) { - tenantID := ctxTenantID(ctx) - userID := ctxUserID(ctx) + tenantID := middleware.GetTenantID(ctx) + userID := middleware.GetUserID(ctx) var msg struct { ID int64 `json:"id"` @@ -275,8 +276,8 @@ func (s *sTenant) DeleteNotification(ctx context.Context, req *v1.TenantNotifica // NotificationPreferencesGet 获取合并后的通知偏好(组织 + 用户) func (s *sTenant) NotificationPreferencesGet(ctx context.Context, req *v1.TenantNotificationPreferencesGetReq) (*v1.TenantNotificationPreferencesGetRes, error) { - tenantID := ctxTenantID(ctx) - userID := ctxUserID(ctx) + tenantID := middleware.GetTenantID(ctx) + userID := middleware.GetUserID(ctx) orgPrefs := loadPreferences(ctx, tenantID, 0, "org") userPrefs := loadPreferences(ctx, tenantID, userID, "user") @@ -291,15 +292,15 @@ func (s *sTenant) NotificationPreferencesGet(ctx context.Context, req *v1.Tenant // NotificationPreferencesUpdate 更新通知偏好 func (s *sTenant) NotificationPreferencesUpdate(ctx context.Context, req *v1.TenantNotificationPreferencesUpdateReq) (*v1.TenantNotificationPreferencesUpdateRes, error) { - tenantID := ctxTenantID(ctx) - userID := ctxUserID(ctx) + tenantID := middleware.GetTenantID(ctx) + userID := middleware.GetUserID(ctx) if req.Scope != "user" && req.Scope != "org" { return nil, common.NewBadRequestError("scope 必须是 'user' 或 'org'") } // Safety categories cannot be fully disabled - if req.Scope == "org" && ctxUserRole(ctx) != "owner" { + if req.Scope == "org" && middleware.GetUserRole(ctx) != "owner" { return nil, common.NewForbiddenError("仅组织所有者可修改组织级通知偏好") } validateSafetyCategories(req.Preferences) @@ -524,15 +525,9 @@ func splitString(s, sep string) []string { // ExportNotifications exports the tenant notification list as CSV or Excel. func (s *sTenant) ExportNotifications(ctx context.Context, req *v1.TenantNotificationsExportReq) (*v1.TenantNotificationsExportRes, error) { - r := g.RequestFromCtx(ctx) - format := req.Format - if format == "" { - format = "csv" - } - - tenantID := ctxTenantID(ctx) - userID := ctxUserID(ctx) - role := ctxUserRole(ctx) + tenantID := middleware.GetTenantID(ctx) + userID := middleware.GetUserID(ctx) + role := middleware.GetUserRole(ctx) columns := []export.Column{ {Field: "id", Header: "ID"}, @@ -544,49 +539,12 @@ func (s *sTenant) ExportNotifications(ctx context.Context, req *v1.TenantNotific } config := export.Config{ - Format: format, + Format: req.Format, Filename: "通知列表_" + gtime.Now().Format("Ymd_His"), Columns: columns, } - if format == "xlsx" { - items := make([]*v1.TenantNotificationItem, 0) - err := dao.NtfMessages.Ctx(ctx). - Where("tenant_id", tenantID). - Where("(user_id = ? OR (user_id IS NULL AND is_broadcast = 1 AND (target_roles IS NULL OR ? = ANY(string_to_array(target_roles, ',')))))", userID, role). - OrderDesc("created_at"). - Scan(&items) - if err != nil { - return nil, err - } - - data := make([]map[string]any, 0, len(items)) - for _, item := range items { - isRead := 0 - if item.IsBroadcast == 1 { - readCount, _ := dao.NtfReadStatus.Ctx(ctx). - Where("message_id", item.Id). - Where("user_id", userID). - Count() - if readCount > 0 { - isRead = 1 - } - } else { - isRead = item.IsRead - } - data = append(data, map[string]any{ - "id": item.Id, - "type": item.Type, - "title": item.Title, - "channel": item.Channel, - "is_read": isRead, - "created_at": item.CreatedAt, - }) - } - return nil, export.WriteExcel(r, config, data) - } - - return nil, export.StreamCSV(r, config, func(yield func(map[string]any) bool) { + return nil, export.GenericExport(ctx, config, func(yield func(map[string]any) bool) { offset := 0 for { items := make([]*v1.TenantNotificationItem, 0) diff --git a/internal/logic/tenant/oauth.go b/internal/logic/tenant/oauth.go index 911a884..b9bf76d 100644 --- a/internal/logic/tenant/oauth.go +++ b/internal/logic/tenant/oauth.go @@ -13,6 +13,7 @@ import ( v1 "github.com/qianfree/team-api/api/tenant/v1" "github.com/qianfree/team-api/internal/logic/common" + "github.com/qianfree/team-api/internal/middleware" "github.com/gogf/gf/v2/frame/g" ) @@ -278,8 +279,8 @@ func (s *sTenant) LinkOAuth(ctx context.Context, req *v1.OAuthLinkReq) (*v1.OAut return nil, common.NewBusinessError(10059, "该 OAuth 供应商未启用") } - tenantID := ctxTenantID(ctx) - userID := ctxUserID(ctx) + tenantID := middleware.GetTenantID(ctx) + userID := middleware.GetUserID(ctx) oauthToken, err := provider.ExchangeToken(ctx, req.Code) if err != nil { @@ -336,8 +337,8 @@ func (s *sTenant) LinkOAuth(ctx context.Context, req *v1.OAuthLinkReq) (*v1.OAut // UnlinkOAuth 解绑 OAuth 账号 func (s *sTenant) UnlinkOAuth(ctx context.Context, req *v1.OAuthUnlinkReq) (*v1.OAuthUnlinkRes, error) { - tenantID := ctxTenantID(ctx) - userID := ctxUserID(ctx) + tenantID := middleware.GetTenantID(ctx) + userID := middleware.GetUserID(ctx) _, err := g.DB().Model("tnt_oauth_identities").Ctx(ctx). Where("tenant_id", tenantID). @@ -353,8 +354,8 @@ func (s *sTenant) UnlinkOAuth(ctx context.Context, req *v1.OAuthUnlinkReq) (*v1. // ListOAuthProviders 获取已绑定的 OAuth 供应商列表 func (s *sTenant) ListOAuthProviders(ctx context.Context, req *v1.OAuthListProvidersReq) (*v1.OAuthListProvidersRes, error) { - tenantID := ctxTenantID(ctx) - userID := ctxUserID(ctx) + tenantID := middleware.GetTenantID(ctx) + userID := middleware.GetUserID(ctx) type row struct { Provider string `json:"provider" orm:"provider"` diff --git a/internal/logic/tenant/open_platform.go b/internal/logic/tenant/open_platform.go index 1aecbd4..bf04679 100644 --- a/internal/logic/tenant/open_platform.go +++ b/internal/logic/tenant/open_platform.go @@ -15,6 +15,7 @@ import ( 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" do "github.com/qianfree/team-api/internal/model/do" "github.com/qianfree/team-api/internal/model/entity" "github.com/qianfree/team-api/internal/utility/crypto" @@ -25,11 +26,11 @@ import ( // ============================================================ func (s *sTenant) OpenAppList(ctx context.Context, req *v1.OpenAppListReq) (*v1.OpenAppListRes, error) { - role := ctxUserRole(ctx) + role := middleware.GetUserRole(ctx) if role != "owner" && role != "admin" { return nil, common.NewForbiddenError("需要 owner 或 admin 权限") } - tenantID := ctxTenantID(ctx) + tenantID := middleware.GetTenantID(ctx) page := req.Page pageSize := req.PageSize if page < 1 { @@ -81,11 +82,11 @@ func (s *sTenant) OpenAppList(ctx context.Context, req *v1.OpenAppListReq) (*v1. } func (s *sTenant) OpenAppCreate(ctx context.Context, req *v1.OpenAppCreateReq) (*v1.OpenAppCreateRes, error) { - role := ctxUserRole(ctx) + role := middleware.GetUserRole(ctx) if role != "owner" && role != "admin" { return nil, common.NewForbiddenError("需要 owner 或 admin 权限") } - tenantID := ctxTenantID(ctx) + tenantID := middleware.GetTenantID(ctx) // Generate app_id (opn_xxxxxxxx) appIDBytes := make([]byte, 12) @@ -152,11 +153,11 @@ func (s *sTenant) OpenAppCreate(ctx context.Context, req *v1.OpenAppCreateReq) ( } func (s *sTenant) OpenAppUpdate(ctx context.Context, req *v1.OpenAppUpdateReq) (*v1.OpenAppUpdateRes, error) { - role := ctxUserRole(ctx) + role := middleware.GetUserRole(ctx) if role != "owner" && role != "admin" { return nil, common.NewForbiddenError("需要 owner 或 admin 权限") } - tenantID := ctxTenantID(ctx) + tenantID := middleware.GetTenantID(ctx) data := g.Map{} if req.Name != nil { @@ -186,11 +187,11 @@ func (s *sTenant) OpenAppUpdate(ctx context.Context, req *v1.OpenAppUpdateReq) ( } func (s *sTenant) OpenAppDelete(ctx context.Context, req *v1.OpenAppDeleteReq) (*v1.OpenAppDeleteRes, error) { - role := ctxUserRole(ctx) + role := middleware.GetUserRole(ctx) if role != "owner" && role != "admin" { return nil, common.NewForbiddenError("需要 owner 或 admin 权限") } - tenantID := ctxTenantID(ctx) + tenantID := middleware.GetTenantID(ctx) _, err := dao.OpnApps.Ctx(ctx).Where("id", req.Id).Where("tenant_id", tenantID).Delete() if err != nil { return nil, err @@ -201,11 +202,11 @@ func (s *sTenant) OpenAppDelete(ctx context.Context, req *v1.OpenAppDeleteReq) ( } func (s *sTenant) OpenAppResetSecret(ctx context.Context, req *v1.OpenAppResetSecretReq) (*v1.OpenAppResetSecretRes, error) { - role := ctxUserRole(ctx) + role := middleware.GetUserRole(ctx) if role != "owner" && role != "admin" { return nil, common.NewForbiddenError("需要 owner 或 admin 权限") } - tenantID := ctxTenantID(ctx) + tenantID := middleware.GetTenantID(ctx) // Generate new secret secretBytes := make([]byte, 24) @@ -241,11 +242,11 @@ func (s *sTenant) OpenAppResetSecret(ctx context.Context, req *v1.OpenAppResetSe } func (s *sTenant) OpenAppToggleStatus(ctx context.Context, req *v1.OpenAppToggleStatusReq) (*v1.OpenAppToggleStatusRes, error) { - role := ctxUserRole(ctx) + role := middleware.GetUserRole(ctx) if role != "owner" && role != "admin" { return nil, common.NewForbiddenError("需要 owner 或 admin 权限") } - tenantID := ctxTenantID(ctx) + tenantID := middleware.GetTenantID(ctx) _, err := dao.OpnApps.Ctx(ctx).Where("id", req.Id).Where("tenant_id", tenantID).Data(g.Map{ "status": req.Status, }).Update() @@ -257,11 +258,11 @@ func (s *sTenant) OpenAppToggleStatus(ctx context.Context, req *v1.OpenAppToggle // ============================================================ func (s *sTenant) WebhookConfigList(ctx context.Context, _ *v1.WebhookConfigListReq) (*v1.WebhookConfigListRes, error) { - role := ctxUserRole(ctx) + role := middleware.GetUserRole(ctx) if role != "owner" && role != "admin" { return nil, common.NewForbiddenError("需要 owner 或 admin 权限") } - tenantID := ctxTenantID(ctx) + tenantID := middleware.GetTenantID(ctx) var configs []entity.OpnWebhookConfigs err := dao.OpnWebhookConfigs.Ctx(ctx).Where("tenant_id", tenantID).OrderDesc("created_at").Scan(&configs) if err != nil { @@ -293,11 +294,11 @@ func (s *sTenant) WebhookConfigList(ctx context.Context, _ *v1.WebhookConfigList } func (s *sTenant) WebhookConfigCreate(ctx context.Context, req *v1.WebhookConfigCreateReq) (*v1.WebhookConfigCreateRes, error) { - role := ctxUserRole(ctx) + role := middleware.GetUserRole(ctx) if role != "owner" && role != "admin" { return nil, common.NewForbiddenError("需要 owner 或 admin 权限") } - tenantID := ctxTenantID(ctx) + tenantID := middleware.GetTenantID(ctx) // Generate signing key keyBytes := make([]byte, 24) @@ -330,11 +331,11 @@ func (s *sTenant) WebhookConfigCreate(ctx context.Context, req *v1.WebhookConfig } func (s *sTenant) WebhookConfigUpdate(ctx context.Context, req *v1.WebhookConfigUpdateReq) (*v1.WebhookConfigUpdateRes, error) { - role := ctxUserRole(ctx) + role := middleware.GetUserRole(ctx) if role != "owner" && role != "admin" { return nil, common.NewForbiddenError("需要 owner 或 admin 权限") } - tenantID := ctxTenantID(ctx) + tenantID := middleware.GetTenantID(ctx) data := g.Map{} if req.Name != nil { @@ -363,21 +364,21 @@ func (s *sTenant) WebhookConfigUpdate(ctx context.Context, req *v1.WebhookConfig } func (s *sTenant) WebhookConfigDelete(ctx context.Context, req *v1.WebhookConfigDeleteReq) (*v1.WebhookConfigDeleteRes, error) { - role := ctxUserRole(ctx) + role := middleware.GetUserRole(ctx) if role != "owner" && role != "admin" { return nil, common.NewForbiddenError("需要 owner 或 admin 权限") } - tenantID := ctxTenantID(ctx) + tenantID := middleware.GetTenantID(ctx) _, err := dao.OpnWebhookConfigs.Ctx(ctx).Where("id", req.Id).Where("tenant_id", tenantID).Delete() return nil, err } func (s *sTenant) WebhookDeliveryLogs(ctx context.Context, req *v1.WebhookDeliveryLogsReq) (*v1.WebhookDeliveryLogsRes, error) { - role := ctxUserRole(ctx) + role := middleware.GetUserRole(ctx) if role != "owner" && role != "admin" { return nil, common.NewForbiddenError("需要 owner 或 admin 权限") } - tenantID := ctxTenantID(ctx) + tenantID := middleware.GetTenantID(ctx) page := req.Page pageSize := req.PageSize if page < 1 { @@ -423,11 +424,11 @@ func (s *sTenant) WebhookDeliveryLogs(ctx context.Context, req *v1.WebhookDelive } func (s *sTenant) WebhookRetry(ctx context.Context, req *v1.WebhookRetryReq) (*v1.WebhookRetryRes, error) { - role := ctxUserRole(ctx) + role := middleware.GetUserRole(ctx) if role != "owner" && role != "admin" { return nil, common.NewForbiddenError("需要 owner 或 admin 权限") } - tenantID := ctxTenantID(ctx) + tenantID := middleware.GetTenantID(ctx) result, err := dao.OpnWebhookEvents.Ctx(ctx). Where("id", req.EventId). diff --git a/internal/logic/tenant/order.go b/internal/logic/tenant/order.go index 9076dd3..1d65118 100644 --- a/internal/logic/tenant/order.go +++ b/internal/logic/tenant/order.go @@ -7,7 +7,6 @@ import ( v1 "github.com/qianfree/team-api/api/tenant/v1" - "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gtime" "github.com/qianfree/team-api/internal/dao" @@ -15,16 +14,17 @@ import ( lcommon "github.com/qianfree/team-api/internal/logic/common" "github.com/qianfree/team-api/internal/logic/payment" + "github.com/qianfree/team-api/internal/middleware" "github.com/qianfree/team-api/internal/utility/export" ) // OrderList 获取租户订单列表 func (s *sTenant) OrderList(ctx context.Context, req *v1.TenantOrderListReq) (*v1.TenantOrderListRes, error) { - role := ctxUserRole(ctx) + role := middleware.GetUserRole(ctx) if role != "owner" && role != "admin" { return nil, lcommon.NewForbiddenError("需要 owner 或 admin 权限") } - tenantID := ctxTenantID(ctx) + tenantID := middleware.GetTenantID(ctx) page, pageSize := lcommon.NormalizePagination(req.Page, req.PageSize) status := req.Status @@ -52,11 +52,11 @@ func (s *sTenant) OrderList(ctx context.Context, req *v1.TenantOrderListReq) (*v // OrderDetail 获取订单详情 func (s *sTenant) OrderDetail(ctx context.Context, req *v1.TenantOrderDetailReq) (*v1.TenantOrderDetailRes, error) { - role := ctxUserRole(ctx) + role := middleware.GetUserRole(ctx) if role != "owner" && role != "admin" { return nil, lcommon.NewForbiddenError("需要 owner 或 admin 权限") } - tenantID := ctxTenantID(ctx) + tenantID := middleware.GetTenantID(ctx) var order map[string]any err := dao.OrdOrders.Ctx(ctx). Where("id", req.Id). @@ -73,12 +73,12 @@ func (s *sTenant) OrderDetail(ctx context.Context, req *v1.TenantOrderDetailReq) // OrderCreate 创建订单 func (s *sTenant) OrderCreate(ctx context.Context, req *v1.TenantOrderCreateReq) (*v1.TenantOrderCreateRes, error) { - role := ctxUserRole(ctx) + role := middleware.GetUserRole(ctx) if role != "owner" && role != "admin" { return nil, lcommon.NewForbiddenError("需要 owner 或 admin 权限") } - tenantID := ctxTenantID(ctx) - userID := ctxUserID(ctx) + tenantID := middleware.GetTenantID(ctx) + userID := middleware.GetUserID(ctx) planID := req.PlanID months := req.Months @@ -141,11 +141,11 @@ func (s *sTenant) OrderCreate(ctx context.Context, req *v1.TenantOrderCreateReq) // OrderCancel 取消订单 func (s *sTenant) OrderCancel(ctx context.Context, req *v1.TenantOrderCancelReq) (*v1.TenantOrderCancelRes, error) { - role := ctxUserRole(ctx) + role := middleware.GetUserRole(ctx) if role != "owner" && role != "admin" { return nil, lcommon.NewForbiddenError("需要 owner 或 admin 权限") } - tenantID := ctxTenantID(ctx) + tenantID := middleware.GetTenantID(ctx) result, err := dao.OrdOrders.Ctx(ctx). Where("id", req.Id). Where("tenant_id", tenantID). @@ -166,11 +166,11 @@ func (s *sTenant) OrderCancel(ctx context.Context, req *v1.TenantOrderCancelReq) // OrderPay 支付订单 func (s *sTenant) OrderPay(ctx context.Context, req *v1.TenantOrderPayReq) (*v1.TenantOrderPayRes, error) { - role := ctxUserRole(ctx) + role := middleware.GetUserRole(ctx) if role != "owner" && role != "admin" { return nil, lcommon.NewForbiddenError("需要 owner 或 admin 权限") } - tenantID := ctxTenantID(ctx) + tenantID := middleware.GetTenantID(ctx) orderID := req.Id // 支付渠道 @@ -231,7 +231,7 @@ func (s *sTenant) OrderPay(ctx context.Context, req *v1.TenantOrderPayReq) (*v1. // PaymentInfo 获取租户可用的支付信息(渠道列表、金额选项、折扣) func (s *sTenant) PaymentInfo(ctx context.Context, req *v1.TenantPaymentInfoReq) (*v1.TenantPaymentInfoRes, error) { - role := ctxUserRole(ctx) + role := middleware.GetUserRole(ctx) if role != "owner" && role != "admin" { return nil, lcommon.NewForbiddenError("需要 owner 或 admin 权限") } @@ -294,12 +294,12 @@ func updateOrderPaymentChannel(ctx context.Context, orderID int64, channel, paym // RechargeCreate 创建充值订单并发起支付(一步完成) func (s *sTenant) RechargeCreate(ctx context.Context, req *v1.TenantRechargeCreateReq) (*v1.TenantRechargeCreateRes, error) { - role := ctxUserRole(ctx) + role := middleware.GetUserRole(ctx) if role != "owner" && role != "admin" { return nil, lcommon.NewForbiddenError("需要 owner 或 admin 权限") } - tenantID := ctxTenantID(ctx) - userID := ctxUserID(ctx) + tenantID := middleware.GetTenantID(ctx) + userID := middleware.GetUserID(ctx) // 1. 校验金额 settings, _ := payment.GetGlobalPaymentSettings(ctx) @@ -393,18 +393,12 @@ func (s *sTenant) RechargeCreate(ctx context.Context, req *v1.TenantRechargeCrea // ExportOrders exports the tenant order list as CSV or Excel. func (s *sTenant) ExportOrders(ctx context.Context, req *v1.TenantOrderExportReq) (*v1.TenantOrderExportRes, error) { - role := ctxUserRole(ctx) + role := middleware.GetUserRole(ctx) if role != "owner" && role != "admin" { return nil, lcommon.NewForbiddenError("需要 owner 或 admin 权限") } - r := g.RequestFromCtx(ctx) - format := req.Format - if format == "" { - format = "csv" - } - - tenantID := ctxTenantID(ctx) + tenantID := middleware.GetTenantID(ctx) columns := []export.Column{ {Field: "id", Header: "ID"}, @@ -418,39 +412,12 @@ func (s *sTenant) ExportOrders(ctx context.Context, req *v1.TenantOrderExportReq } config := export.Config{ - Format: format, + Format: req.Format, Filename: "订单列表_" + gtime.Now().Format("Ymd_His"), Columns: columns, } - if format == "xlsx" { - orders := make([]*v1.TenantOrderItem, 0) - query := dao.OrdOrders.Ctx(ctx).Where("tenant_id", tenantID) - if req.Status != "" { - query = query.Where("status", req.Status) - } - err := query.OrderDesc("created_at").Scan(&orders) - if err != nil { - return nil, err - } - - data := make([]map[string]any, 0, len(orders)) - for _, o := range orders { - data = append(data, map[string]any{ - "id": o.Id, - "order_no": o.OrderNo, - "order_type": o.OrderType, - "amount": o.Amount, - "final_amount": o.FinalAmount, - "payment_channel": o.PaymentChannel, - "status": o.Status, - "created_at": o.CreatedAt, - }) - } - return nil, export.WriteExcel(r, config, data) - } - - return nil, export.StreamCSV(r, config, func(yield func(map[string]any) bool) { + return nil, export.GenericExport(ctx, config, func(yield func(map[string]any) bool) { offset := 0 for { orders := make([]*v1.TenantOrderItem, 0) diff --git a/internal/logic/tenant/organization.go b/internal/logic/tenant/organization.go index b466e24..d6ba377 100644 --- a/internal/logic/tenant/organization.go +++ b/internal/logic/tenant/organization.go @@ -9,12 +9,13 @@ import ( 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" "github.com/qianfree/team-api/internal/utility/crypto" ) // ownerOnly checks if the current user is the tenant owner. func ownerOnly(ctx context.Context) error { - if ctxUserRole(ctx) != "owner" { + if middleware.GetUserRole(ctx) != "owner" { return common.NewForbiddenError("仅组织所有者可执行此操作") } return nil @@ -22,7 +23,7 @@ func ownerOnly(ctx context.Context) error { // GetOrgInfo returns tenant organization info. func (s *sTenant) GetOrgInfo(ctx context.Context, req *v1.TenantOrgInfoReq) (*v1.TenantOrgInfoRes, error) { - tenantID := ctxTenantID(ctx) + tenantID := middleware.GetTenantID(ctx) var tenant *struct { Id int64 `json:"id"` @@ -79,7 +80,7 @@ func (s *sTenant) UpdateOrgInfo(ctx context.Context, req *v1.TenantOrgUpdateReq) if err := ownerOnly(ctx); err != nil { return nil, err } - tenantID := ctxTenantID(ctx) + tenantID := middleware.GetTenantID(ctx) data := do.TntTenants{} if req.Name != nil { @@ -101,8 +102,8 @@ func (s *sTenant) TransferOwnership(ctx context.Context, req *v1.TenantOrgTransf if err := ownerOnly(ctx); err != nil { return nil, err } - tenantID := ctxTenantID(ctx) - currentOwnerID := ctxUserID(ctx) + tenantID := middleware.GetTenantID(ctx) + currentOwnerID := middleware.GetUserID(ctx) if req.NewOwnerID == currentOwnerID { return nil, common.NewBadRequestError("不能转让给自己") @@ -183,7 +184,7 @@ func (s *sTenant) TransferOwnership(ctx context.Context, req *v1.TenantOrgTransf // GetProfile returns current user's profile. func (s *sTenant) GetProfile(ctx context.Context, req *v1.TenantProfileReq) (*v1.TenantProfileRes, error) { - userID := ctxUserID(ctx) + userID := middleware.GetUserID(ctx) var user *struct { Id int64 `json:"id"` @@ -216,7 +217,7 @@ func (s *sTenant) GetProfile(ctx context.Context, req *v1.TenantProfileReq) (*v1 // UpdateProfile updates current user's profile. func (s *sTenant) UpdateProfile(ctx context.Context, req *v1.TenantProfileUpdateReq) (*v1.TenantProfileUpdateRes, error) { - userID := ctxUserID(ctx) + userID := middleware.GetUserID(ctx) data := do.TntUsers{} if req.DisplayName != nil { diff --git a/internal/logic/tenant/personal_dashboard.go b/internal/logic/tenant/personal_dashboard.go index 815adec..a3a0fc3 100644 --- a/internal/logic/tenant/personal_dashboard.go +++ b/internal/logic/tenant/personal_dashboard.go @@ -8,12 +8,13 @@ import ( "github.com/gogf/gf/v2/frame/g" v1 "github.com/qianfree/team-api/api/tenant/v1" + "github.com/qianfree/team-api/internal/middleware" ) // PersonalDashboard returns the personal dashboard overview for the current user. func (s *sTenant) PersonalDashboard(ctx context.Context, req *v1.PersonalDashboardReq) (*v1.PersonalDashboardRes, error) { - userID := ctxUserID(ctx) - tenantID := ctxTenantID(ctx) + userID := middleware.GetUserID(ctx) + tenantID := middleware.GetTenantID(ctx) now := time.Now() todayStart := now.Format("2006-01-02") + " 00:00:00" @@ -206,8 +207,8 @@ func (s *sTenant) PersonalDashboard(ctx context.Context, req *v1.PersonalDashboa // PersonalTokenTrends returns daily token usage trends for the current user. func (s *sTenant) PersonalTokenTrends(ctx context.Context, req *v1.PersonalTokenTrendsReq) (*v1.PersonalTokenTrendsRes, error) { - userID := ctxUserID(ctx) - tenantID := ctxTenantID(ctx) + userID := middleware.GetUserID(ctx) + tenantID := middleware.GetTenantID(ctx) days := req.Days if days <= 0 || days > 90 { days = 30 @@ -240,8 +241,8 @@ func (s *sTenant) PersonalTokenTrends(ctx context.Context, req *v1.PersonalToken // PersonalModelDistribution returns model usage distribution for the current user. func (s *sTenant) PersonalModelDistribution(ctx context.Context, req *v1.PersonalModelDistReq) (*v1.PersonalModelDistRes, error) { - userID := ctxUserID(ctx) - tenantID := ctxTenantID(ctx) + userID := middleware.GetUserID(ctx) + tenantID := middleware.GetTenantID(ctx) days := req.Days if days <= 0 || days > 90 { days = 30 @@ -275,8 +276,8 @@ func (s *sTenant) PersonalModelDistribution(ctx context.Context, req *v1.Persona // PersonalApiKeyUsage returns per-API-key usage breakdown for the current user. func (s *sTenant) PersonalApiKeyUsage(ctx context.Context, req *v1.PersonalApiKeyUsageReq) (*v1.PersonalApiKeyUsageRes, error) { - userID := ctxUserID(ctx) - tenantID := ctxTenantID(ctx) + userID := middleware.GetUserID(ctx) + tenantID := middleware.GetTenantID(ctx) days := req.Days if days <= 0 || days > 90 { days = 30 diff --git a/internal/logic/tenant/plan.go b/internal/logic/tenant/plan.go index 4bba674..0cd8471 100644 --- a/internal/logic/tenant/plan.go +++ b/internal/logic/tenant/plan.go @@ -11,11 +11,12 @@ import ( v1 "github.com/qianfree/team-api/api/tenant/v1" "github.com/gogf/gf/v2/frame/g" + "github.com/qianfree/team-api/internal/middleware" ) // PlanList 获取可购买的套餐列表(仅 active) func (s *sTenant) PlanList(ctx context.Context, req *v1.TenantPlanListReq) (*v1.TenantPlanListRes, error) { - role := ctxUserRole(ctx) + role := middleware.GetUserRole(ctx) if role != "owner" && role != "admin" { return nil, common.NewForbiddenError("需要 owner 或 admin 权限") } @@ -47,11 +48,11 @@ func (s *sTenant) PlanList(ctx context.Context, req *v1.TenantPlanListReq) (*v1. // PlanCurrent 获取租户当前套餐 func (s *sTenant) PlanCurrent(ctx context.Context, req *v1.TenantPlanCurrentReq) (*v1.TenantPlanCurrentRes, error) { - role := ctxUserRole(ctx) + role := middleware.GetUserRole(ctx) if role != "owner" && role != "admin" { return nil, common.NewForbiddenError("需要 owner 或 admin 权限") } - tenantID := ctxTenantID(ctx) + tenantID := middleware.GetTenantID(ctx) var plan v1.TenantPlanCurrentRes err := dao.PlnTenantPlans.Ctx(ctx).As("tp"). Fields("tp.id, tp.tenant_id, tp.plan_id, tp.status, tp.start_at, tp.end_at, tp.auto_renew, tp.monthly_quota_tokens, tp.used_tokens, tp.last_reset_at, p.name, p.identifier, p.description, p.monthly_price, p.yearly_price"). @@ -74,11 +75,11 @@ func (s *sTenant) PlanCurrent(ctx context.Context, req *v1.TenantPlanCurrentReq) // PlanCancelAutoRenew 取消自动续费 func (s *sTenant) PlanCancelAutoRenew(ctx context.Context, req *v1.TenantPlanCancelAutoRenewReq) (*v1.TenantPlanCancelAutoRenewRes, error) { - role := ctxUserRole(ctx) + role := middleware.GetUserRole(ctx) if role != "owner" && role != "admin" { return nil, common.NewForbiddenError("需要 owner 或 admin 权限") } - tenantID := ctxTenantID(ctx) + tenantID := middleware.GetTenantID(ctx) _, err := dao.PlnTenantPlans.Ctx(ctx). Where("tenant_id", tenantID). Where("status", "active"). diff --git a/internal/logic/tenant/playground.go b/internal/logic/tenant/playground.go index 6dda624..27e0195 100644 --- a/internal/logic/tenant/playground.go +++ b/internal/logic/tenant/playground.go @@ -17,6 +17,7 @@ import ( "github.com/qianfree/team-api/internal/logic/billing" "github.com/qianfree/team-api/internal/logic/common" "github.com/qianfree/team-api/internal/logic/relay" + "github.com/qianfree/team-api/internal/middleware" relaycommon "github.com/qianfree/team-api/relay/common" "github.com/qianfree/team-api/relay/constant" "github.com/qianfree/team-api/relay/handler" @@ -32,8 +33,8 @@ type playgroundApiKey struct { } func findActiveApiKey(ctx context.Context) (*playgroundApiKey, error) { - tenantID := ctxTenantID(ctx) - userID := ctxUserID(ctx) + tenantID := middleware.GetTenantID(ctx) + userID := middleware.GetUserID(ctx) var key *playgroundApiKey err := dao.ApiKeys.Ctx(ctx). @@ -57,8 +58,8 @@ func buildPlaygroundRelayContext(ctx context.Context, key *playgroundApiKey, rec requestID = v.(string) } return &handler.RelayContext{ - TenantID: ctxTenantID(ctx), - UserID: ctxUserID(ctx), + TenantID: middleware.GetTenantID(ctx), + UserID: middleware.GetUserID(ctx), ApiKeyID: key.Id, RequestID: requestID, Writer: recorder, @@ -439,7 +440,7 @@ func (s *sTenant) PlaygroundRerank(ctx context.Context, req *v1.PlaygroundRerank // ============================================================ func (s *sTenant) SandboxChat(ctx context.Context, req *v1.SandboxChatReq) (*v1.SandboxChatRes, error) { - tenantID := ctxTenantID(ctx) + tenantID := middleware.GetTenantID(ctx) now := time.Now() quotaKey := fmt.Sprintf("sandbox:quota:%d:%s", tenantID, now.Format("200601")) @@ -480,7 +481,7 @@ func (s *sTenant) SandboxChat(ctx context.Context, req *v1.SandboxChatReq) (*v1. } func (s *sTenant) SandboxQuota(ctx context.Context, req *v1.SandboxQuotaReq) (*v1.SandboxQuotaRes, error) { - tenantID := ctxTenantID(ctx) + tenantID := middleware.GetTenantID(ctx) defaultQuota := g.Cfg().MustGet(ctx, "sandbox.sandbox_default_quota").Int() if defaultQuota <= 0 { diff --git a/internal/logic/tenant/plugin.go b/internal/logic/tenant/plugin.go index 9a3e60c..09d6327 100644 --- a/internal/logic/tenant/plugin.go +++ b/internal/logic/tenant/plugin.go @@ -4,6 +4,7 @@ import ( "context" v1 "github.com/qianfree/team-api/api/tenant/v1" + "github.com/qianfree/team-api/internal/middleware" "github.com/qianfree/team-api/internal/plugin" "github.com/gogf/gf/v2/errors/gerror" @@ -11,7 +12,7 @@ import ( ) func (s *sTenant) TenantPluginList(ctx context.Context, req *v1.TenantPluginListReq) (*v1.TenantPluginListRes, error) { - tenantID := ctxTenantID(ctx) + tenantID := middleware.GetTenantID(ctx) entries := plugin.AllPlugins() items := make([]v1.TenantPluginItem, 0, len(entries)) @@ -44,7 +45,7 @@ func (s *sTenant) TenantPluginList(ctx context.Context, req *v1.TenantPluginList } func (s *sTenant) TenantPluginDetail(ctx context.Context, req *v1.TenantPluginDetailReq) (*v1.TenantPluginDetailRes, error) { - tenantID := ctxTenantID(ctx) + tenantID := middleware.GetTenantID(ctx) entry := plugin.GetPlugin(req.Name) if entry == nil { return nil, gerror.New("插件不存在") @@ -76,7 +77,7 @@ func (s *sTenant) TenantPluginDetail(ctx context.Context, req *v1.TenantPluginDe } func (s *sTenant) TenantPluginConfigUpdate(ctx context.Context, req *v1.TenantPluginConfigUpdateReq) (*v1.TenantPluginConfigUpdateRes, error) { - tenantID := ctxTenantID(ctx) + tenantID := middleware.GetTenantID(ctx) if err := plugin.UpdatePluginConfigForTenant(ctx, req.Name, tenantID, req.Config); err != nil { return nil, err } @@ -84,7 +85,7 @@ func (s *sTenant) TenantPluginConfigUpdate(ctx context.Context, req *v1.TenantPl } func (s *sTenant) TenantPluginEnable(ctx context.Context, req *v1.TenantPluginEnableReq) (*v1.TenantPluginEnableRes, error) { - tenantID := ctxTenantID(ctx) + tenantID := middleware.GetTenantID(ctx) if err := plugin.EnableForTenant(ctx, req.Name, tenantID); err != nil { return nil, err } @@ -92,7 +93,7 @@ func (s *sTenant) TenantPluginEnable(ctx context.Context, req *v1.TenantPluginEn } func (s *sTenant) TenantPluginDisable(ctx context.Context, req *v1.TenantPluginDisableReq) (*v1.TenantPluginDisableRes, error) { - tenantID := ctxTenantID(ctx) + tenantID := middleware.GetTenantID(ctx) if err := plugin.DisableForTenant(ctx, req.Name, tenantID); err != nil { return nil, err } diff --git a/internal/logic/tenant/project.go b/internal/logic/tenant/project.go index ed01821..6917e54 100644 --- a/internal/logic/tenant/project.go +++ b/internal/logic/tenant/project.go @@ -12,6 +12,7 @@ import ( "github.com/qianfree/team-api/internal/model/do" v1 "github.com/qianfree/team-api/api/tenant/v1" + "github.com/qianfree/team-api/internal/middleware" ) type projectRow struct { @@ -49,7 +50,7 @@ type usageLogRow struct { // ProjectList returns a paginated list of projects for a tenant. func (s *sTenant) ProjectList(ctx context.Context, req *v1.TenantProjectListReq) (*v1.TenantProjectListRes, error) { - tenantID := ctxTenantID(ctx) + tenantID := middleware.GetTenantID(ctx) page, pageSize := common.NormalizePagination(req.Page, req.PageSize) var projects []projectRow @@ -78,8 +79,8 @@ func (s *sTenant) ProjectList(ctx context.Context, req *v1.TenantProjectListReq) // ProjectCreate creates a new project for a tenant. func (s *sTenant) ProjectCreate(ctx context.Context, req *v1.TenantProjectCreateReq) (*v1.TenantProjectCreateRes, error) { - tenantID := ctxTenantID(ctx) - userID := ctxUserID(ctx) + tenantID := middleware.GetTenantID(ctx) + userID := middleware.GetUserID(ctx) name := req.Name if name == "" { @@ -112,7 +113,7 @@ func (s *sTenant) ProjectCreate(ctx context.Context, req *v1.TenantProjectCreate // ProjectUpdate updates a project. func (s *sTenant) ProjectUpdate(ctx context.Context, req *v1.TenantProjectUpdateReq) (*v1.TenantProjectUpdateRes, error) { - tenantID := ctxTenantID(ctx) + tenantID := middleware.GetTenantID(ctx) var project *struct { ID int64 `json:"id"` @@ -161,7 +162,7 @@ func (s *sTenant) ProjectUpdate(ctx context.Context, req *v1.TenantProjectUpdate // ProjectArchive archives a project and revokes all its keys. func (s *sTenant) ProjectArchive(ctx context.Context, req *v1.TenantProjectArchiveReq) (*v1.TenantProjectArchiveRes, error) { - tenantID := ctxTenantID(ctx) + tenantID := middleware.GetTenantID(ctx) var project *struct { ID int64 `json:"id"` @@ -203,7 +204,7 @@ func (s *sTenant) ProjectArchive(ctx context.Context, req *v1.TenantProjectArchi // ProjectUnarchive restores an archived project. Keys are NOT auto-restored. func (s *sTenant) ProjectUnarchive(ctx context.Context, req *v1.TenantProjectUnarchiveReq) (*v1.TenantProjectUnarchiveRes, error) { - tenantID := ctxTenantID(ctx) + tenantID := middleware.GetTenantID(ctx) var project *struct { ID int64 `json:"id"` @@ -287,7 +288,7 @@ func CheckBudgetExhausted(ctx context.Context) error { // ProjectGet 根据 ID 获取单个项目详情(含统计摘要) func (s *sTenant) ProjectGet(ctx context.Context, req *v1.TenantProjectGetReq) (*v1.TenantProjectGetRes, error) { - tenantID := ctxTenantID(ctx) + tenantID := middleware.GetTenantID(ctx) var p *projectRow err := dao.TntProjects.Ctx(ctx). @@ -350,12 +351,12 @@ func (s *sTenant) ProjectGet(ctx context.Context, req *v1.TenantProjectGetReq) ( // ProjectApiKeyList 获取项目密钥列表(owner/admin 权限) func (s *sTenant) ProjectApiKeyList(ctx context.Context, req *v1.TenantProjectApiKeyListReq) (*v1.TenantProjectApiKeyListRes, error) { - role := ctxUserRole(ctx) + role := middleware.GetUserRole(ctx) if role != "owner" && role != "admin" { return nil, common.NewForbiddenError("需要 owner 或 admin 权限") } - tenantID := ctxTenantID(ctx) + tenantID := middleware.GetTenantID(ctx) page, pageSize := common.NormalizePagination(req.Page, req.PageSize) var keys []apiKeyRow @@ -385,13 +386,13 @@ func (s *sTenant) ProjectApiKeyList(ctx context.Context, req *v1.TenantProjectAp // ProjectApiKeyCreate 创建项目密钥(owner/admin 权限) func (s *sTenant) ProjectApiKeyCreate(ctx context.Context, req *v1.TenantProjectApiKeyCreateReq) (*v1.TenantProjectApiKeyCreateRes, error) { - role := ctxUserRole(ctx) + role := middleware.GetUserRole(ctx) if role != "owner" && role != "admin" { return nil, common.NewForbiddenError("需要 owner 或 admin 权限") } - tenantID := ctxTenantID(ctx) - userID := ctxUserID(ctx) + tenantID := middleware.GetTenantID(ctx) + userID := middleware.GetUserID(ctx) // Verify project exists and belongs to tenant var project *struct { @@ -457,12 +458,12 @@ func (s *sTenant) ProjectApiKeyCreate(ctx context.Context, req *v1.TenantProject // ProjectApiKeyDelete 删除项目密钥(owner/admin 权限) func (s *sTenant) ProjectApiKeyDelete(ctx context.Context, req *v1.TenantProjectApiKeyDeleteReq) (*v1.TenantProjectApiKeyDeleteRes, error) { - role := ctxUserRole(ctx) + role := middleware.GetUserRole(ctx) if role != "owner" && role != "admin" { return nil, common.NewForbiddenError("需要 owner 或 admin 权限") } - tenantID := ctxTenantID(ctx) + tenantID := middleware.GetTenantID(ctx) // Verify the key belongs to the project and tenant var key *struct { @@ -495,12 +496,12 @@ func (s *sTenant) ProjectApiKeyDelete(ctx context.Context, req *v1.TenantProject // ProjectUsageStats 获取项目用量统计(按日汇总,近30天)(owner/admin 权限) func (s *sTenant) ProjectUsageStats(ctx context.Context, req *v1.TenantProjectUsageStatsReq) (*v1.TenantProjectUsageStatsRes, error) { - role := ctxUserRole(ctx) + role := middleware.GetUserRole(ctx) if role != "owner" && role != "admin" { return nil, common.NewForbiddenError("需要 owner 或 admin 权限") } - tenantID := ctxTenantID(ctx) + tenantID := middleware.GetTenantID(ctx) var project struct { ID int64 `json:"id"` @@ -603,12 +604,12 @@ func (s *sTenant) ProjectUsageStats(ctx context.Context, req *v1.TenantProjectUs // ProjectUsageLogs 获取项目用量日志(分页)(owner/admin 权限) func (s *sTenant) ProjectUsageLogs(ctx context.Context, req *v1.TenantProjectUsageLogsReq) (*v1.TenantProjectUsageLogsRes, error) { - role := ctxUserRole(ctx) + role := middleware.GetUserRole(ctx) if role != "owner" && role != "admin" { return nil, common.NewForbiddenError("需要 owner 或 admin 权限") } - tenantID := ctxTenantID(ctx) + tenantID := middleware.GetTenantID(ctx) page, pageSize := common.NormalizePagination(req.Page, req.PageSize) // 获取项目关联的 API Key ID 列表 diff --git a/internal/logic/tenant/promo_code.go b/internal/logic/tenant/promo_code.go index 24d008b..8096adc 100644 --- a/internal/logic/tenant/promo_code.go +++ b/internal/logic/tenant/promo_code.go @@ -10,13 +10,14 @@ import ( "github.com/gogf/gf/v2/frame/g" lcommon "github.com/qianfree/team-api/internal/logic/common" + "github.com/qianfree/team-api/internal/middleware" v1 "github.com/qianfree/team-api/api/tenant/v1" ) // ValidatePromoCode 校验优惠码并返回折扣金额 func (s *sTenant) ValidatePromoCode(ctx context.Context, req *v1.TenantValidatePromoCodeReq) (*v1.TenantValidatePromoCodeRes, error) { - tenantID := ctxTenantID(ctx) + tenantID := middleware.GetTenantID(ctx) var promo *struct { ID int64 `json:"id"` diff --git a/internal/logic/tenant/redemption.go b/internal/logic/tenant/redemption.go index 8876c4a..856d71d 100644 --- a/internal/logic/tenant/redemption.go +++ b/internal/logic/tenant/redemption.go @@ -18,16 +18,17 @@ import ( v1 "github.com/qianfree/team-api/api/tenant/v1" lcommon2 "github.com/qianfree/team-api/internal/logic/common" + "github.com/qianfree/team-api/internal/middleware" ) // RedeemCode 租户兑换码 func (s *sTenant) RedeemCode(ctx context.Context, req *v1.TenantRedeemCodeReq) (*v1.TenantRedeemCodeRes, error) { - role := ctxUserRole(ctx) + role := middleware.GetUserRole(ctx) if role != "owner" && role != "admin" { return nil, lcommon.NewForbiddenError("需要 owner 或 admin 权限") } - tenantID := ctxTenantID(ctx) - userID := ctxUserID(ctx) + tenantID := middleware.GetTenantID(ctx) + userID := middleware.GetUserID(ctx) var redemption *struct { ID int64 `json:"id"` @@ -190,11 +191,11 @@ func extendPlanDuration(ctx context.Context, tenantID int64, days int) { // ListRedemptionUsages 获取当前租户的兑换历史 func (s *sTenant) ListRedemptionUsages(ctx context.Context, req *v1.TenantRedemptionUsagesReq) (*v1.TenantRedemptionUsagesRes, error) { - role := ctxUserRole(ctx) + role := middleware.GetUserRole(ctx) if role != "owner" && role != "admin" { return nil, lcommon.NewForbiddenError("需要 owner 或 admin 权限") } - tenantID := ctxTenantID(ctx) + tenantID := middleware.GetTenantID(ctx) page, pageSize := lcommon2.NormalizePagination(req.Page, req.PageSize) fromClause := "ord_redemption_usages ru LEFT JOIN ord_redemptions r ON ru.redemption_id = r.id" diff --git a/internal/logic/tenant/security.go b/internal/logic/tenant/security.go index e474cd8..ed7fb1e 100644 --- a/internal/logic/tenant/security.go +++ b/internal/logic/tenant/security.go @@ -15,6 +15,7 @@ import ( "github.com/qianfree/team-api/internal/consts" "github.com/qianfree/team-api/internal/dao" "github.com/qianfree/team-api/internal/logic/common" + "github.com/qianfree/team-api/internal/middleware" do "github.com/qianfree/team-api/internal/model/do" "github.com/qianfree/team-api/internal/model/entity" "github.com/qianfree/team-api/internal/utility/crypto" @@ -105,7 +106,7 @@ func (s *sTenant) Verify2FA(ctx context.Context, req *v1.Tenant2FAVerifyReq) (*v // Setup2FA starts the 2FA setup process for the current tenant user. func (s *sTenant) Setup2FA(ctx context.Context, _ *v1.Tenant2FASetupReq) (*v1.Tenant2FASetupRes, error) { - userID := ctxUserID(ctx) + userID := middleware.GetUserID(ctx) secret, uri, err := common.Setup2FA(ctx, "tenant", userID) if err != nil { return nil, err @@ -127,7 +128,7 @@ func (s *sTenant) Setup2FA(ctx context.Context, _ *v1.Tenant2FASetupReq) (*v1.Te // Enable2FA confirms and enables 2FA. func (s *sTenant) Enable2FA(ctx context.Context, req *v1.Tenant2FAEnableReq) (*v1.Tenant2FAEnableRes, error) { - userID := ctxUserID(ctx) + userID := middleware.GetUserID(ctx) secret, err := getPendingTOTPSecret(ctx, userID) if err != nil || secret == "" { return nil, common.NewBusinessError(consts.CodeBadRequest, "请先执行2FA设置") @@ -144,7 +145,7 @@ func (s *sTenant) Enable2FA(ctx context.Context, req *v1.Tenant2FAEnableReq) (*v // Disable2FA disables 2FA for the current tenant user. func (s *sTenant) Disable2FA(ctx context.Context, req *v1.Tenant2FADisableReq) (*v1.Tenant2FADisableRes, error) { - userID := ctxUserID(ctx) + userID := middleware.GetUserID(ctx) err := common.Disable2FA(ctx, "tenant", userID, req.Code) if err != nil { return nil, err @@ -154,7 +155,7 @@ func (s *sTenant) Disable2FA(ctx context.Context, req *v1.Tenant2FADisableReq) ( // RegenerateBackupCodes generates new backup codes. func (s *sTenant) RegenerateBackupCodes(ctx context.Context, req *v1.Tenant2FARegenerateBackupCodesReq) (*v1.Tenant2FARegenerateBackupCodesRes, error) { - userID := ctxUserID(ctx) + userID := middleware.GetUserID(ctx) codes, err := common.RegenerateBackupCodes(ctx, "tenant", userID, req.Code) if err != nil { return nil, err @@ -164,7 +165,7 @@ func (s *sTenant) RegenerateBackupCodes(ctx context.Context, req *v1.Tenant2FARe // ConfirmHighRisk generates a confirm token for high-risk operations. func (s *sTenant) ConfirmHighRisk(ctx context.Context, req *v1.Tenant2FAConfirmReq) (*v1.Tenant2FAConfirmRes, error) { - userID := ctxUserID(ctx) + userID := middleware.GetUserID(ctx) enabled, err := common.Is2FAEnabled(ctx, "tenant", userID) if err != nil { @@ -193,9 +194,9 @@ func (s *sTenant) ConfirmHighRisk(ctx context.Context, req *v1.Tenant2FAConfirmR // LoginHistory returns the login history for tenant users. // owner/admin see all members in the tenant; member sees only own records. func (s *sTenant) LoginHistory(ctx context.Context, req *v1.TenantLoginHistoryReq) (*v1.TenantLoginHistoryRes, error) { - userID := ctxUserID(ctx) - tenantID := ctxTenantID(ctx) - role := ctxUserRole(ctx) + userID := middleware.GetUserID(ctx) + tenantID := middleware.GetTenantID(ctx) + role := middleware.GetUserRole(ctx) page, pageSize := common.NormalizePagination(req.Page, req.PageSize) q := dao.AudLoginHistory.Ctx(ctx). @@ -352,7 +353,7 @@ func getEncKey(ctx context.Context) []byte { // GetIPWhitelist returns the tenant's IP whitelist configuration. func (s *sTenant) GetIPWhitelist(ctx context.Context, _ *v1.TenantIPWhitelistGetReq) (*v1.TenantIPWhitelistGetRes, error) { - tenantID := ctxTenantID(ctx) + tenantID := middleware.GetTenantID(ctx) var tenant *entity.TntTenants err := dao.TntTenants.Ctx(ctx).Where("id", tenantID).Scan(&tenant) if err != nil { @@ -385,7 +386,7 @@ func (s *sTenant) GetIPWhitelist(ctx context.Context, _ *v1.TenantIPWhitelistGet // UpdateIPWhitelist updates the tenant's IP whitelist configuration. func (s *sTenant) UpdateIPWhitelist(ctx context.Context, req *v1.TenantIPWhitelistUpdateReq) (*v1.TenantIPWhitelistUpdateRes, error) { - tenantID := ctxTenantID(ctx) + tenantID := middleware.GetTenantID(ctx) var tenant *entity.TntTenants err := dao.TntTenants.Ctx(ctx).Where("id", tenantID).Scan(&tenant) if err != nil { diff --git a/internal/logic/tenant/task.go b/internal/logic/tenant/task.go index 9295905..cab0d89 100644 --- a/internal/logic/tenant/task.go +++ b/internal/logic/tenant/task.go @@ -6,14 +6,15 @@ import ( 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" ) // TenantTaskList 租户异步任务列表 // owner/admin 可查看租户所有任务,member 只能查看自己的任务 func (s *sTenant) TenantTaskList(ctx context.Context, req *v1.TenantTaskListReq) (*v1.TenantTaskListRes, error) { - tenantID := ctxTenantID(ctx) - role := ctxUserRole(ctx) - userID := ctxUserID(ctx) + tenantID := middleware.GetTenantID(ctx) + role := middleware.GetUserRole(ctx) + userID := middleware.GetUserID(ctx) m := dao.TskModelTasks.Ctx(ctx). LeftJoin("tnt_users u", "u.id = tsk_model_tasks.user_id AND u.tenant_id = tsk_model_tasks.tenant_id"). @@ -101,9 +102,9 @@ func (s *sTenant) TenantTaskList(ctx context.Context, req *v1.TenantTaskListReq) // TenantTaskDetail 租户异步任务详情 // owner/admin 可查看租户所有任务,member 只能查看自己的任务 func (s *sTenant) TenantTaskDetail(ctx context.Context, req *v1.TenantTaskDetailReq) (*v1.TenantTaskDetailRes, error) { - tenantID := ctxTenantID(ctx) - role := ctxUserRole(ctx) - userID := ctxUserID(ctx) + tenantID := middleware.GetTenantID(ctx) + role := middleware.GetUserRole(ctx) + userID := middleware.GetUserID(ctx) var task struct { Id int64 `json:"id"` diff --git a/internal/logic/tenant/tenant.go b/internal/logic/tenant/tenant.go index 5b103a0..3194fe0 100644 --- a/internal/logic/tenant/tenant.go +++ b/internal/logic/tenant/tenant.go @@ -1,8 +1,6 @@ package tenant import ( - "context" - "github.com/qianfree/team-api/internal/service" ) @@ -17,51 +15,3 @@ func New() *sTenant { func init() { service.RegisterTenant(New()) } - -// ctxUserID reads userId from context. -func ctxUserID(ctx context.Context) int64 { - v := ctx.Value("userId") - if v == nil { - return 0 - } - if id, ok := v.(int64); ok { - return id - } - return 0 -} - -// ctxTenantID reads tenantId from context. -func ctxTenantID(ctx context.Context) int64 { - v := ctx.Value("tenantId") - if v == nil { - return 0 - } - if id, ok := v.(int64); ok { - return id - } - return 0 -} - -// ctxSessionID reads sessionId from context. -func ctxSessionID(ctx context.Context) int64 { - v := ctx.Value("sessionId") - if v == nil { - return 0 - } - if id, ok := v.(int64); ok { - return id - } - return 0 -} - -// ctxUserRole reads role from context. -func ctxUserRole(ctx context.Context) string { - v := ctx.Value("role") - if v == nil { - return "" - } - if role, ok := v.(string); ok { - return role - } - return "" -} diff --git a/internal/logic/tenant/ticket.go b/internal/logic/tenant/ticket.go index cc6b199..6de46e0 100644 --- a/internal/logic/tenant/ticket.go +++ b/internal/logic/tenant/ticket.go @@ -7,17 +7,17 @@ import ( v1 "github.com/qianfree/team-api/api/tenant/v1" "github.com/qianfree/team-api/internal/logic/common" + "github.com/qianfree/team-api/internal/middleware" "github.com/gogf/gf/v2/database/gdb" - "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gtime" "github.com/qianfree/team-api/internal/utility/export" ) // TicketCreate 创建工单 func (s *sTenant) TicketCreate(ctx context.Context, req *v1.TenantTicketCreateReq) (*v1.TenantTicketCreateRes, error) { - tenantID := ctxTenantID(ctx) - userID := ctxUserID(ctx) + tenantID := middleware.GetTenantID(ctx) + userID := middleware.GetUserID(ctx) result, err := dao.SptTickets.Ctx(ctx).Insert(do.SptTickets{ TenantId: tenantID, @@ -38,8 +38,8 @@ func (s *sTenant) TicketCreate(ctx context.Context, req *v1.TenantTicketCreateRe // TicketList 获取租户工单列表 func (s *sTenant) TicketList(ctx context.Context, req *v1.TenantTicketListReq) (*v1.TenantTicketListRes, error) { - tenantID := ctxTenantID(ctx) - userID := ctxUserID(ctx) + tenantID := middleware.GetTenantID(ctx) + userID := middleware.GetUserID(ctx) page, pageSize := common.NormalizePagination(req.Page, req.PageSize) @@ -71,8 +71,8 @@ func (s *sTenant) TicketList(ctx context.Context, req *v1.TenantTicketListReq) ( // TicketGet 获取工单详情(含回复) func (s *sTenant) TicketGet(ctx context.Context, req *v1.TenantTicketGetReq) (*v1.TenantTicketGetRes, error) { - tenantID := ctxTenantID(ctx) - userID := ctxUserID(ctx) + tenantID := middleware.GetTenantID(ctx) + userID := middleware.GetUserID(ctx) var ticket map[string]any err := dao.SptTickets.Ctx(ctx). @@ -114,8 +114,8 @@ func (s *sTenant) TicketGet(ctx context.Context, req *v1.TenantTicketGetReq) (*v // TicketReply 租户用户回复工单 func (s *sTenant) TicketReply(ctx context.Context, req *v1.TenantTicketReplyReq) (*v1.TenantTicketReplyRes, error) { - tenantID := ctxTenantID(ctx) - userID := ctxUserID(ctx) + tenantID := middleware.GetTenantID(ctx) + userID := middleware.GetUserID(ctx) if req.Content == "" { return nil, common.NewBadRequestError("回复内容不能为空") @@ -172,8 +172,8 @@ func (s *sTenant) TicketReply(ctx context.Context, req *v1.TenantTicketReplyReq) // TicketClose 租户用户关闭工单 func (s *sTenant) TicketClose(ctx context.Context, req *v1.TenantTicketCloseReq) (*v1.TenantTicketCloseRes, error) { - tenantID := ctxTenantID(ctx) - userID := ctxUserID(ctx) + tenantID := middleware.GetTenantID(ctx) + userID := middleware.GetUserID(ctx) result, err := dao.SptTickets.Ctx(ctx). Where("id", req.Id). @@ -195,8 +195,8 @@ func (s *sTenant) TicketClose(ctx context.Context, req *v1.TenantTicketCloseReq) // TicketReopen 租户用户重新打开工单 func (s *sTenant) TicketReopen(ctx context.Context, req *v1.TenantTicketReopenReq) (*v1.TenantTicketReopenRes, error) { - tenantID := ctxTenantID(ctx) - userID := ctxUserID(ctx) + tenantID := middleware.GetTenantID(ctx) + userID := middleware.GetUserID(ctx) result, err := dao.SptTickets.Ctx(ctx). Where("id", req.Id). @@ -218,14 +218,8 @@ func (s *sTenant) TicketReopen(ctx context.Context, req *v1.TenantTicketReopenRe // ExportTickets exports the tenant ticket list as CSV or Excel. func (s *sTenant) ExportTickets(ctx context.Context, req *v1.TenantTicketExportReq) (*v1.TenantTicketExportRes, error) { - r := g.RequestFromCtx(ctx) - format := req.Format - if format == "" { - format = "csv" - } - - tenantID := ctxTenantID(ctx) - userID := ctxUserID(ctx) + tenantID := middleware.GetTenantID(ctx) + userID := middleware.GetUserID(ctx) columns := []export.Column{ {Field: "id", Header: "ID"}, @@ -238,7 +232,7 @@ func (s *sTenant) ExportTickets(ctx context.Context, req *v1.TenantTicketExportR } config := export.Config{ - Format: format, + Format: req.Format, Filename: "工单列表_" + gtime.Now().Format("Ymd_His"), Columns: columns, } @@ -255,29 +249,7 @@ func (s *sTenant) ExportTickets(ctx context.Context, req *v1.TenantTicketExportR return query } - if format == "xlsx" { - var items []*v1.TenantTicketItem - err := buildQuery().OrderDesc("spt_tickets.created_at").Scan(&items) - if err != nil { - return nil, err - } - - data := make([]map[string]any, 0, len(items)) - for _, item := range items { - data = append(data, map[string]any{ - "id": item.Id, - "category": item.Category, - "title": item.Title, - "urgency": item.Urgency, - "status": item.Status, - "assigned_admin_name": item.AssignedAdminName, - "created_at": item.CreatedAt, - }) - } - return nil, export.WriteExcel(r, config, data) - } - - return nil, export.StreamCSV(r, config, func(yield func(map[string]any) bool) { + return nil, export.GenericExport(ctx, config, func(yield func(map[string]any) bool) { offset := 0 for { var items []*v1.TenantTicketItem diff --git a/internal/middleware/open_auth.go b/internal/middleware/open_auth.go index b73e2ee..03836c6 100644 --- a/internal/middleware/open_auth.go +++ b/internal/middleware/open_auth.go @@ -147,3 +147,25 @@ func getEncKey(ctx context.Context) []byte { } return crypto.MustGetEncryptionKey(hexKey) } + +// GetOpenTenantID extracts the tenant ID injected by OpenPlatformAuth middleware. +func GetOpenTenantID(ctx context.Context) int64 { + val := ctx.Value("openTenantId") + if val != nil { + if id, ok := val.(int64); ok { + return id + } + } + return 0 +} + +// GetOpenAppID extracts the app ID injected by OpenPlatformAuth middleware. +func GetOpenAppID(ctx context.Context) int64 { + val := ctx.Value("openAppId") + if val != nil { + if id, ok := val.(int64); ok { + return id + } + } + return 0 +} diff --git a/internal/utility/export/export.go b/internal/utility/export/export.go index 4f191ea..658a837 100644 --- a/internal/utility/export/export.go +++ b/internal/utility/export/export.go @@ -1,5 +1,11 @@ package export +import ( + "context" + + "github.com/gogf/gf/v2/frame/g" +) + // Column defines one column in the exported file. type Column struct { Field string // Key in the data map @@ -22,3 +28,23 @@ func (c Config) GetMaxRows() int { } return c.MaxRows } + +// GenericExport handles format normalization and xlsx/csv branching. +// queryFn yields data rows — same callback as StreamCSV's queryFn. +// For xlsx: collects all rows from queryFn, then calls WriteExcel. +// For csv: passes queryFn directly to StreamCSV for streaming. +func GenericExport(ctx context.Context, config Config, queryFn func(yield func(map[string]any) bool)) error { + r := g.RequestFromCtx(ctx) + config.Format = detectFormat(config.Format) + + if config.Format == "xlsx" { + var data []map[string]any + queryFn(func(row map[string]any) bool { + data = append(data, row) + return true + }) + return WriteExcel(r, config, data) + } + + return StreamCSV(r, config, queryFn) +}