From fa70d8790cc0dfbdb0b3d5d51e5a0076c92726bf Mon Sep 17 00:00:00 2001 From: chengqian Date: Tue, 2 Jun 2026 18:33:48 +0800 Subject: [PATCH] =?UTF-8?q?fix(auth):=E4=BF=AE=E5=A4=8D=E7=AE=A1=E7=90=86?= =?UTF-8?q?=E5=91=98=E6=9D=83=E9=99=90=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/admin/v1/audit.go | 2 + internal/logic/admin/admin_user.go | 55 ++++++++++------------- internal/logic/admin/audit.go | 34 +++++++++++++-- internal/logic/admin/auth.go | 16 ++++--- internal/logic/admin/cron_job.go | 10 ++--- internal/logic/admin/data_governance.go | 58 ++++++++++++++++++------- internal/logic/admin/error_log.go | 7 +-- internal/logic/admin/permission.go | 20 +++++++++ internal/logic/admin/security.go | 44 ++++++++++++++----- internal/logic/admin/settings.go | 5 +-- internal/logic/admin/setup.go | 9 +++- 11 files changed, 180 insertions(+), 80 deletions(-) diff --git a/api/admin/v1/audit.go b/api/admin/v1/audit.go index 01da006..e91d470 100644 --- a/api/admin/v1/audit.go +++ b/api/admin/v1/audit.go @@ -47,6 +47,8 @@ type SensitiveLogListReq struct { PageSize int `json:"page_size" d:"20"` UserID int `json:"user_id" dc:"用户ID"` ResourceType string `json:"resource_type" dc:"资源类型"` + StartDate string `json:"start_date" dc:"开始日期 YYYY-MM-DD"` + EndDate string `json:"end_date" dc:"结束日期 YYYY-MM-DD"` } type SensitiveLogListRes struct { diff --git a/internal/logic/admin/admin_user.go b/internal/logic/admin/admin_user.go index eb1f59a..61d7227 100644 --- a/internal/logic/admin/admin_user.go +++ b/internal/logic/admin/admin_user.go @@ -20,36 +20,12 @@ import ( func (s *sAdmin) ListUsers(ctx context.Context, req *v1.AdminUserListReq) (*v1.AdminUserListRes, error) { page, pageSize := common.NormalizePagination(req.Page, req.PageSize) - m := dao.SysAdminUsers.Ctx(ctx) - - if req.Keyword != "" { - keyword := "%" + strings.TrimSpace(req.Keyword) + "%" - m = m.Where("username LIKE ? OR email LIKE ?", keyword, keyword) - } - if req.Role != "" { - m = m.Where("role", req.Role) - } - if req.Status != "" { - m = m.Where("status", req.Status) - } - - total, err := m.Count() + total, err := buildUserFilters(dao.SysAdminUsers.Ctx(ctx), req.Keyword, req.Role, req.Status).Count() if err != nil { return nil, err } - // Rebuild model for data query (Count() modifies internal state) - m = dao.SysAdminUsers.Ctx(ctx) - if req.Keyword != "" { - keyword := "%" + strings.TrimSpace(req.Keyword) + "%" - m = m.Where("username LIKE ? OR email LIKE ?", keyword, keyword) - } - if req.Role != "" { - m = m.Where("role", req.Role) - } - if req.Status != "" { - m = m.Where("status", req.Status) - } + m := buildUserFilters(dao.SysAdminUsers.Ctx(ctx), req.Keyword, req.Role, req.Status) var users []struct { Id int64 `json:"id"` @@ -156,6 +132,22 @@ func (s *sAdmin) CreateUser(ctx context.Context, req *v1.AdminUserCreateReq) (*v // UpdateUser updates an admin user. func (s *sAdmin) UpdateUser(ctx context.Context, req *v1.AdminUserUpdateReq) (*v1.AdminUserUpdateRes, error) { + // Prevent modification of super_admin accounts + var targetUser *struct { + Role string `json:"role"` + } + err := dao.SysAdminUsers.Ctx(ctx). + Where("id", req.Id).Scan(&targetUser) + if err = common.IgnoreScanNoRows(err); err != nil { + return nil, err + } + if targetUser == nil { + return nil, common.NewNotFoundError("管理员") + } + if targetUser.Role == "super_admin" { + return nil, common.NewBadRequestError("不能修改超级管理员信息") + } + data := do.SysAdminUsers{} if req.DisplayName != nil { data.DisplayName = *req.DisplayName @@ -180,7 +172,7 @@ func (s *sAdmin) UpdateUser(ctx context.Context, req *v1.AdminUserUpdateReq) (*v data.Role = *req.Role } - _, err := dao.SysAdminUsers.Ctx(ctx).Where("id", req.Id).Update(data) + _, err = dao.SysAdminUsers.Ctx(ctx).Where("id", req.Id).Update(data) if err != nil { return nil, err } @@ -212,15 +204,16 @@ func (s *sAdmin) DeleteUser(ctx context.Context, req *v1.AdminUserDeleteReq) (*v return nil, common.NewBadRequestError("不能删除超级管理员") } - // Revoke all sessions - common.RevokeAllSessions(ctx, "admin", req.Id) - - // Delete user + // Delete user first, then revoke sessions to avoid leaving the user + // in a state where sessions are gone but the user still exists. _, err = dao.SysAdminUsers.Ctx(ctx).Where("id", req.Id).Delete() if err != nil { return nil, err } + // Revoke all sessions + common.RevokeAllSessions(ctx, "admin", req.Id) + return nil, nil } diff --git a/internal/logic/admin/audit.go b/internal/logic/admin/audit.go index 03f0e18..6dab038 100644 --- a/internal/logic/admin/audit.go +++ b/internal/logic/admin/audit.go @@ -36,9 +36,9 @@ var validAuditLevels = map[string]bool{ AuditLevelNone: true, } -// GetAuditConfig retrieves the global audit level from sys_options. +// GetGlobalAuditConfig retrieves the global audit level from sys_options. // Package-level wrapper for use by tenant package. -func GetAuditConfig(ctx context.Context) (string, error) { +func GetGlobalAuditConfig(ctx context.Context) (string, error) { return common.GetAuditLevel(ctx), nil } @@ -88,6 +88,8 @@ func (s *sAdmin) UpdateAuditConfig(ctx context.Context, req *v1.AuditConfigUpdat } // queryPage 使用 GoFrame ORM 分页查询,返回 []map[string]any。 +// 注意:该函数使用 g.DB().Model(table) 直接操作动态表名而非 DAO, +// 适用于通用分页工具场景(多表共用)。固定表查询应优先使用对应的 DAO。 // 分开调用 Count 和 All 避免 ScanAndCount 对 map[string]any 的 bug。 func queryPage(ctx context.Context, table, fields, where, orderBy string, page, pageSize int, args ...any) ([]map[string]any, int, error) { m := g.DB().Ctx(ctx).Model(table).Safe() @@ -198,6 +200,14 @@ func (s *sAdmin) ListSensitiveAccessLogs(ctx context.Context, req *v1.SensitiveL conditions = append(conditions, "resource_type = ?") args = append(args, req.ResourceType) } + if req.StartDate != "" { + conditions = append(conditions, "created_at >= ?") + args = append(args, req.StartDate+" 00:00:00") + } + if req.EndDate != "" { + conditions = append(conditions, "created_at <= ?") + args = append(args, req.EndDate+" 23:59:59") + } where := strings.Join(conditions, " AND ") items, total, err := queryPage(ctx, @@ -311,6 +321,9 @@ func (s *sAdmin) ListRequestAuditLogs(ctx context.Context, req *v1.RequestAuditL total = countResult[0]["total"].Int() } + // NOTE: fromClause and whereClause are constructed from hardcoded strings + // and parameterized conditions (using ? placeholders). No user input is + // directly interpolated into the SQL template. dataSQL := fmt.Sprintf( `SELECT a.id, a.tenant_id, COALESCE(tn.name, '') AS tenant_name, a.user_id, COALESCE(t.username, '') AS username, a.project_id, COALESCE(p.name, '') AS project_name, a.api_key_id, COALESCE(ak.name, '') AS api_key_name, a.request_id, a.method, a.path, a.query_params, a.status_code, a.client_ip, a.user_agent, a.latency_ms, a.first_token_ms, a.created_at, a.updated_at, a.audit_level FROM %s%s ORDER BY a.created_at DESC LIMIT ? OFFSET ?`, @@ -417,7 +430,22 @@ func (s *sAdmin) ExportOperationLogs(ctx context.Context, req *v1.OperationLogEx offset := 0 for { where, args := buildOpLogWhere() - items, _, err := queryPage(ctx, "aud_operation_logs", selectFields, where, "created_at DESC", offset/1000+1, 1000, args...) + q := g.DB().Ctx(ctx).Model("aud_operation_logs").Fields(selectFields).OrderDesc("created_at").Offset(offset).Limit(1000) + if where != "" { + q = q.Where(where, args...) + } + result, err := q.All() + if err != nil { + return + } + items := make([]map[string]any, 0, len(result)) + for _, row := range result { + m := make(map[string]any, len(row)) + for k, v := range row { + m[k] = v.Val() + } + items = append(items, m) + } if err != nil { return } diff --git a/internal/logic/admin/auth.go b/internal/logic/admin/auth.go index c6e393f..abd566e 100644 --- a/internal/logic/admin/auth.go +++ b/internal/logic/admin/auth.go @@ -104,13 +104,15 @@ func (s *sAdmin) Login(ctx context.Context, req *v1.AdminLoginReq) (*v1.AdminLog return nil, err } - // Update last login - dao.SysAdminUsers.Ctx(ctx). + // Update last login (non-critical, log errors only) + if _, err := dao.SysAdminUsers.Ctx(ctx). Where("id", user.Id). Data(do.SysAdminUsers{ LastLoginAt: gtime.Now(), LastLoginIp: ipAddress, - }).Update() + }).Update(); err != nil { + g.Log().Warningf(ctx, "update last_login for user %d: %v", user.Id, err) + } // Record login history _ = common.RecordLoginHistory(ctx, "admin", user.Id, 0, "password", ipAddress, ua, deviceFP, true, "") @@ -195,12 +197,12 @@ func (s *sAdmin) Refresh(ctx context.Context, req *v1.AdminRefreshReq) (*v1.Admi // Fetch current role from user table var adminUser *entity.SysAdminUsers - err = dao.SysAdminUsers.Ctx(ctx).Where("id", session.UserId).Fields("role").Scan(&adminUser) + err = dao.SysAdminUsers.Ctx(ctx).Where("id", session.UserId).Fields("role, status").Scan(&adminUser) if err = common.IgnoreScanNoRows(err); err != nil { return nil, err } - if adminUser == nil { - return nil, common.NewUnauthorizedError("用户不存在") + if adminUser == nil || adminUser.Status != "active" { + return nil, common.NewUnauthorizedError("账号已被禁用或不存在") } // Generate new token pair with same session ID and jti @@ -339,7 +341,7 @@ func (s *sAdmin) ChangePassword(ctx context.Context, req *v1.AdminChangePassword return nil, err } if user == nil { - return nil, common.NewUnauthorizedError("用户不存在") + return nil, common.NewUnauthorizedError("账号已被禁用或不存在") } // Verify old password diff --git a/internal/logic/admin/cron_job.go b/internal/logic/admin/cron_job.go index 1f45486..accdd47 100644 --- a/internal/logic/admin/cron_job.go +++ b/internal/logic/admin/cron_job.go @@ -2,14 +2,14 @@ package admin import ( "context" - "fmt" "strings" + "github.com/gogf/gf/v2/errors/gerror" + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/util/gconv" + v1 "github.com/qianfree/team-api/api/admin/v1" "github.com/qianfree/team-api/internal/logic/common" - - "github.com/gogf/gf/v2/frame/g" - "github.com/gogf/gf/v2/util/gconv" ) // CronJobList returns all registered cron jobs with their last execution status. @@ -121,7 +121,7 @@ func (s *sAdmin) CronJobExecutions(ctx context.Context, req *v1.CronJobExecution func (s *sAdmin) CronJobTrigger(ctx context.Context, req *v1.CronJobTriggerReq) (*v1.CronJobTriggerRes, error) { err := common.GetCronScheduler().TriggerJob(ctx, req.Name) if err != nil { - return nil, fmt.Errorf("触发任务失败: %w", err) + return nil, gerror.Wrapf(err, "触发定时任务失败") } return &v1.CronJobTriggerRes{Message: "任务已触发"}, nil } diff --git a/internal/logic/admin/data_governance.go b/internal/logic/admin/data_governance.go index 39a1c3e..5730a81 100644 --- a/internal/logic/admin/data_governance.go +++ b/internal/logic/admin/data_governance.go @@ -6,6 +6,7 @@ import ( "fmt" "time" + "github.com/gogf/gf/v2/database/gdb" "github.com/gogf/gf/v2/errors/gcode" "github.com/gogf/gf/v2/errors/gerror" "github.com/gogf/gf/v2/frame/g" @@ -46,7 +47,7 @@ func (s *sAdmin) DataGovernanceExport(ctx context.Context, req *v1.DataGovernanc count, err := dao.TskTasks.Ctx(ctx). Where("handler", "data_export"). Where("status", "pending"). - Where("payload->>'tenant_id'", fmt.Sprintf("%d", req.TenantID)). + Where("(payload->>'tenant_id')::bigint = ?", req.TenantID). Count() if err != nil { return nil, err @@ -80,7 +81,7 @@ func (s *sAdmin) DataGovernanceDeletion(ctx context.Context, req *v1.DataGoverna count, err := dao.TskTasks.Ctx(ctx). Where("handler", "data_deletion_request"). Where("status IN (?)", []string{"pending", "running"}). - Where("payload->>'tenant_id'", fmt.Sprintf("%d", req.TenantID)). + Where("(payload->>'tenant_id')::bigint = ?", req.TenantID). Count() if err != nil { return nil, err @@ -193,13 +194,30 @@ func cleanupTableByDate(ctx context.Context, table, dateColumn string, days int) cutoff := time.Now().AddDate(0, 0, -days) batchSize := 5000 + + // Build safe DELETE SQL via switch/case to avoid fmt.Sprintf on table/column names. + // The whitelist above guarantees table and dateColumn are from a trusted set. + var deleteSQL string + switch table { + case "aud_request_logs": + deleteSQL = "DELETE FROM aud_request_logs WHERE id IN (SELECT id FROM aud_request_logs WHERE created_at < ? LIMIT ?)" + case "aud_operation_logs": + deleteSQL = "DELETE FROM aud_operation_logs WHERE id IN (SELECT id FROM aud_operation_logs WHERE created_at < ? LIMIT ?)" + case "aud_sensitive_access_logs": + deleteSQL = "DELETE FROM aud_sensitive_access_logs WHERE id IN (SELECT id FROM aud_sensitive_access_logs WHERE created_at < ? LIMIT ?)" + default: + return fmt.Errorf("invalid table for cleanup: %s", table) + } + for { - result, err := g.DB().Ctx(ctx).Exec(ctx, - fmt.Sprintf("DELETE FROM %s WHERE id IN (SELECT id FROM %s WHERE %s < $1 LIMIT %d)", table, table, dateColumn, batchSize), cutoff) + result, err := g.DB().Ctx(ctx).Exec(ctx, deleteSQL, cutoff, batchSize) + if err != nil { + return err + } + rowsAffected, err := result.RowsAffected() if err != nil { return err } - rowsAffected, _ := result.RowsAffected() if rowsAffected == 0 { break } @@ -224,16 +242,26 @@ func cleanupDeactivatedTenants(ctx context.Context) error { } for _, t := range tenants { g.Log().Infof(ctx, "cleaning data for deactivated tenant %d (status=%s)", t.ID, t.StatusCode) - if _, err := dao.TntUsers.Ctx(ctx).Where("tenant_id", t.ID). - Data(do.TntUsers{DisplayName: "[deleted]", Email: fmt.Sprintf("deleted_%d@deleted.local", t.ID)}).Update(); err != nil { - return gerror.Wrapf(err, "anonymize users for tenant %d", t.ID) - } - if _, err := dao.ApiKeys.Ctx(ctx).Where("tenant_id", t.ID).Delete(); err != nil { - return gerror.Wrapf(err, "delete api keys for tenant %d", t.ID) - } - if _, err := dao.TntTenants.Ctx(ctx).Where("id", t.ID). - Data(do.TntTenants{DataRemovalAt: gtime.Now()}).Update(); err != nil { - return gerror.Wrapf(err, "update data removal for tenant %d", t.ID) + + // Use transaction to ensure atomicity: if any step fails, the tenant's data + // remains consistent (no partial anonymization/deletion). + err := g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error { + tenantID := t.ID + if _, err := tx.Model("tnt_users").Ctx(ctx).Where("tenant_id", tenantID). + Data(do.TntUsers{DisplayName: "[deleted]", Email: fmt.Sprintf("deleted_%d@deleted.local", tenantID)}).Update(); err != nil { + return gerror.Wrapf(err, "anonymize users for tenant %d", tenantID) + } + if _, err := tx.Model("api_keys").Ctx(ctx).Where("tenant_id", tenantID).Delete(); err != nil { + return gerror.Wrapf(err, "delete api keys for tenant %d", tenantID) + } + if _, err := tx.Model("tnt_tenants").Ctx(ctx).Where("id", tenantID). + Data(do.TntTenants{DataRemovalAt: gtime.Now()}).Update(); err != nil { + return gerror.Wrapf(err, "update data removal for tenant %d", tenantID) + } + return nil + }) + if err != nil { + return err } } return nil diff --git a/internal/logic/admin/error_log.go b/internal/logic/admin/error_log.go index eb76555..d5c5341 100644 --- a/internal/logic/admin/error_log.go +++ b/internal/logic/admin/error_log.go @@ -53,9 +53,10 @@ func (s *sAdmin) ErrorLogList(ctx context.Context, req *v1.ErrorLogListReq) (*v1 } where := strings.Join(conditions, " AND ") + page, pageSize := common.NormalizePagination(req.Page, req.PageSize) items, total, err := queryPage(ctx, "sys_error_logs", "*", where, "id DESC", - req.Page, req.PageSize, args...) + page, pageSize, args...) if err != nil { return nil, err } @@ -66,8 +67,8 @@ func (s *sAdmin) ErrorLogList(ctx context.Context, req *v1.ErrorLogListReq) (*v1 return &v1.ErrorLogListRes{ List: items, Total: total, - Page: req.Page, - PageSize: req.PageSize, + Page: page, + PageSize: pageSize, }, nil } diff --git a/internal/logic/admin/permission.go b/internal/logic/admin/permission.go index 5304eec..63a4c8e 100644 --- a/internal/logic/admin/permission.go +++ b/internal/logic/admin/permission.go @@ -6,6 +6,7 @@ import ( "github.com/gogf/gf/v2/database/gdb" do "github.com/qianfree/team-api/internal/model/do" + "github.com/gogf/gf/v2/errors/gerror" v1 "github.com/qianfree/team-api/api/admin/v1" "github.com/qianfree/team-api/internal/dao" "github.com/qianfree/team-api/internal/logic/common" @@ -181,6 +182,14 @@ func (s *sAdmin) UpdateUserPermissions(ctx context.Context, req *v1.AdminPermiss return err } + // Validate permission points against predefined set + validPerms := buildValidPermissionSet() + for _, p := range req.Permissions { + if !validPerms[p] { + return gerror.Newf("无效的权限点: %s", p) + } + } + // Insert new permissions if len(req.Permissions) > 0 { data := make([]do.SysAdminRolePerms, len(req.Permissions)) @@ -311,3 +320,14 @@ func GetDataScopes(ctx context.Context, userID int64, role string) ([]v1.DataSco } return result, nil } + +// buildValidPermissionSet returns a set of all valid permission points. +func buildValidPermissionSet() map[string]bool { + set := make(map[string]bool) + for _, g := range predefinedPermissionGroups { + for _, p := range g.Permissions { + set[p] = true + } + } + return set +} diff --git a/internal/logic/admin/security.go b/internal/logic/admin/security.go index 75301ac..0d06d8d 100644 --- a/internal/logic/admin/security.go +++ b/internal/logic/admin/security.go @@ -2,6 +2,7 @@ package admin import ( "context" + "encoding/hex" "fmt" "time" @@ -75,11 +76,13 @@ func (s *sAdmin) Verify2FA(ctx context.Context, req *v1.Admin2FAVerifyReq) (*v1. return nil, err } - // Update last login - dao.SysAdminUsers.Ctx(ctx).Where("id", user.Id).Data(do.SysAdminUsers{ + // Update last login (non-critical, log errors only) + if _, err := dao.SysAdminUsers.Ctx(ctx).Where("id", user.Id).Data(do.SysAdminUsers{ LastLoginAt: gtime.Now(), LastLoginIp: ipAddress, - }).Update() + }).Update(); err != nil { + g.Log().Warningf(ctx, "update last_login for user %d: %v", user.Id, err) + } // Record login history ua := g.RequestFromCtx(ctx).Header.Get("User-Agent") @@ -107,7 +110,10 @@ func (s *sAdmin) Setup2FA(ctx context.Context, _ *v1.Admin2FASetupReq) (*v1.Admi } // Store secret in Redis temporarily (5 min TTL) for the enable step - encKey := getEncKey(ctx) + encKey, err := getEncKey(ctx) + if err != nil { + return nil, err + } encrypted, err := crypto.EncryptString(encKey, secret) if err != nil { return nil, err @@ -305,10 +311,12 @@ func buildAdminUserMap(ctx context.Context, records []entity.AudLoginHistory) ma ids = append(ids, id) } var users []entity.SysAdminUsers - _ = dao.SysAdminUsers.Ctx(ctx). + if err := dao.SysAdminUsers.Ctx(ctx). Fields("id, username, display_name"). WhereIn("id", ids). - Scan(&users) + Scan(&users); err != nil { + g.Log().Warningf(ctx, "load admin users for session list: %v", err) + } for _, u := range users { m[u.Id] = adminUserBrief{Username: u.Username, DisplayName: u.DisplayName} } @@ -429,10 +437,12 @@ func buildTenantUserMap(ctx context.Context, records []entity.AudLoginHistory) m ids = append(ids, id) } var users []entity.TntUsers - _ = dao.TntUsers.Ctx(ctx). + if err := dao.TntUsers.Ctx(ctx). Fields("id, username, display_name"). WhereIn("id", ids). - Scan(&users) + Scan(&users); err != nil { + g.Log().Warningf(ctx, "load tenant users for session list: %v", err) + } for _, u := range users { m[u.Id] = tenantUserBrief{Username: u.Username, DisplayName: u.DisplayName} } @@ -450,7 +460,10 @@ func getPendingTOTPSecret(ctx context.Context, userID int64) (string, error) { return "", nil } // The secret is stored encrypted, decrypt it - encKey := getEncKey(ctx) + encKey, err := getEncKey(ctx) + if err != nil { + return "", err + } return crypto.DecryptString(encKey, val.String()) } @@ -459,10 +472,17 @@ func clearPendingTOTPSecret(ctx context.Context, userID int64) { _, _ = g.Redis().Do(ctx, "DEL", key) } -func getEncKey(ctx context.Context) []byte { +func getEncKey(ctx context.Context) ([]byte, error) { hexKey := g.Cfg().MustGet(ctx, "crypto.encryptionKey").String() if hexKey == "" { - hexKey = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + return nil, gerror.New("crypto.encryptionKey is not configured") } - return crypto.MustGetEncryptionKey(hexKey) + key, err := hex.DecodeString(hexKey) + if err != nil { + return nil, gerror.Wrapf(err, "invalid crypto.encryptionKey") + } + if len(key) != 32 { + return nil, gerror.Newf("crypto.encryptionKey must be 32 bytes, got %d", len(key)) + } + return key, nil } diff --git a/internal/logic/admin/settings.go b/internal/logic/admin/settings.go index cfc2d77..070d6e0 100644 --- a/internal/logic/admin/settings.go +++ b/internal/logic/admin/settings.go @@ -2,7 +2,6 @@ package admin import ( "context" - "fmt" v1 "github.com/qianfree/team-api/api/admin/v1" "github.com/qianfree/team-api/internal/logic/common" @@ -26,7 +25,7 @@ func (s *sAdmin) GetSettingsCategories(ctx context.Context, _ *v1.AdminSettingsC // GetSettings retrieves settings with schema for a given category. func (s *sAdmin) GetSettings(ctx context.Context, req *v1.AdminSettingsGetReq) (*v1.AdminSettingsGetRes, error) { if !isValidCategory(req.Category) { - return nil, fmt.Errorf("未知的设置分类: %s", req.Category) + return nil, common.NewBadRequestError("无效的设置分类") } items := common.Config().GetCategoryWithValues(ctx, req.Category) @@ -49,7 +48,7 @@ func (s *sAdmin) GetSettings(ctx context.Context, req *v1.AdminSettingsGetReq) ( // UpdateSettings batch-updates settings for a given category. func (s *sAdmin) UpdateSettings(ctx context.Context, req *v1.AdminSettingsUpdateReq) (*v1.AdminSettingsUpdateRes, error) { if !isValidCategory(req.Category) { - return nil, fmt.Errorf("未知的设置分类: %s", req.Category) + return nil, common.NewBadRequestError("无效的设置分类") } // Normalize interface{} values to strings for storage diff --git a/internal/logic/admin/setup.go b/internal/logic/admin/setup.go index 7e9906e..3bc31a7 100644 --- a/internal/logic/admin/setup.go +++ b/internal/logic/admin/setup.go @@ -102,6 +102,13 @@ func AutoInitAdmin(ctx context.Context) (bool, error) { return true, nil } +// isDuplicateKeyError checks if the error is a PostgreSQL unique constraint violation. +// PostgreSQL error code 23505 = unique_violation. +// NOTE: Uses string matching on the error message. Consider switching to +// pq.Error type assertion if github.com/lib/pq becomes a direct dependency. func isDuplicateKeyError(err error) bool { - return err != nil && strings.Contains(err.Error(), "23505") + if err == nil { + return false + } + return strings.Contains(err.Error(), "23505") || strings.Contains(err.Error(), "duplicate key") }