diff --git a/app/admin/apis/api_key.go b/app/admin/apis/api_key.go index b7cf2e2..7201e92 100644 --- a/app/admin/apis/api_key.go +++ b/app/admin/apis/api_key.go @@ -9,24 +9,31 @@ import ( "github.com/go-admin-team/go-admin-core/sdk/api" "github.com/go-admin-team/go-admin-core/sdk/pkg/jwtauth/user" "go-admin/app/admin/models" + "strconv" "strings" "time" + "unicode/utf8" ) type APIKey struct{ api.Api } func (e APIKey) List(c *gin.Context) { + e.MakeContext(c) db := primaryDB() - db.AutoMigrate(&models.APIKey{}) - var rows []models.APIKey - db.Order("id desc").Find(&rows) + rows := make([]models.APIKey, 0) + if err := db.Order("id desc").Find(&rows).Error; err != nil { + e.Error(500, err, "读取密钥失败,请重试") + return + } e.OK(rows, "查询成功") } func (e APIKey) Create(c *gin.Context) { + e.MakeContext(c) + c.Header("Cache-Control", "no-store") var in struct { Name string `json:"name"` } - if c.ShouldBindJSON(&in) != nil || strings.TrimSpace(in.Name) == "" { + if c.ShouldBindJSON(&in) != nil || strings.TrimSpace(in.Name) == "" || utf8.RuneCountInString(strings.TrimSpace(in.Name)) > 128 { e.Error(400, fmt.Errorf("name required"), "参数错误") return } @@ -40,7 +47,6 @@ func (e APIKey) Create(c *gin.Context) { row := models.APIKey{Name: strings.TrimSpace(in.Name), KeyHash: hex.EncodeToString(h[:]), Enabled: true} row.CreateBy = user.GetUserId(c) db := primaryDB() - db.AutoMigrate(&models.APIKey{}) if err := db.Create(&row).Error; err != nil { e.Error(500, err, "创建失败") return @@ -48,22 +54,42 @@ func (e APIKey) Create(c *gin.Context) { e.OK(gin.H{"id": row.Id, "name": row.Name, "apiKey": plain}, "创建成功") } func (e APIKey) Toggle(c *gin.Context) { + e.MakeContext(c) + id, err := strconv.Atoi(c.Param("id")) + if err != nil || id <= 0 { + e.Error(400, fmt.Errorf("invalid id"), "无效的密钥编号") + return + } var row models.APIKey db := primaryDB() - if db.First(&row, c.Param("id")).Error != nil { + if db.First(&row, id).Error != nil { e.Error(404, fmt.Errorf("not found"), "不存在") return } row.Enabled = !row.Enabled - db.Save(&row) + if err := db.Save(&row).Error; err != nil { + e.Error(500, err, "更新失败,请刷新后重试") + return + } e.OK(row, "更新成功") } func (e APIKey) Delete(c *gin.Context) { + e.MakeContext(c) + id, err := strconv.Atoi(c.Param("id")) + if err != nil || id <= 0 { + e.Error(400, fmt.Errorf("invalid id"), "无效的密钥编号") + return + } db := primaryDB() - if db.Delete(&models.APIKey{}, c.Param("id")).Error != nil { + result := db.Delete(&models.APIKey{}, id) + if result.Error != nil { e.Error(500, fmt.Errorf("delete failed"), "删除失败") return } + if result.RowsAffected == 0 { + e.Error(404, fmt.Errorf("not found"), "密钥不存在") + return + } e.OK(nil, "删除成功") } func ValidateAPIKey(c *gin.Context) bool { diff --git a/cmd/migrate/migration/version/1789171200000_api_key_page.go b/cmd/migrate/migration/version/1789171200000_api_key_page.go new file mode 100644 index 0000000..e9e757e --- /dev/null +++ b/cmd/migrate/migration/version/1789171200000_api_key_page.go @@ -0,0 +1,31 @@ +package version + +import ( + "fmt" + "go-admin/app/admin/models" + "go-admin/cmd/migrate/migration" + common "go-admin/common/models" + "gorm.io/gorm" +) + +func init() { migration.Migrate.SetVersion("1789171200000", migrateAPIKeyPage) } + +func migrateAPIKeyPage(db *gorm.DB, version string) error { + if err := db.AutoMigrate(&models.APIKey{}); err != nil { + return err + } + return db.Transaction(func(tx *gorm.DB) error { + var parent models.SysMenu + if err := tx.Where("title = ? AND menu_type = ?", "系统管理", "M").First(&parent).Error; err != nil { + return err + } + menu := models.SysMenu{MenuName: "APIKeyManagement", Title: "API Key 管理", Icon: "lock", Path: "/admin/api-keys", MenuType: "C", ParentId: parent.MenuId, Component: "/admin/api-key/index", Visible: "0", NoCache: true, Sort: 90, IsFrame: "1"} + if err := tx.Where("menu_name = ?", menu.MenuName).FirstOrCreate(&menu).Error; err != nil { + return err + } + if err := tx.Model(&menu).Update("paths", fmt.Sprintf("%s/%d", parent.Paths, menu.MenuId)).Error; err != nil { + return err + } + return tx.Create(&common.Migration{Version: version}).Error + }) +} diff --git a/tickets/LOCAL-005-api-key-page.md b/tickets/LOCAL-005-api-key-page.md new file mode 100644 index 0000000..8fefba2 --- /dev/null +++ b/tickets/LOCAL-005-api-key-page.md @@ -0,0 +1,22 @@ +# LOCAL-005:API Key 管理页面 + +- 状态:进行中 +- 来源:2026-09-12 用户「做005」 +- 前置:LOCAL-004 后端已验收 + +## 范围与方案 +沿用 GoAdmin / Element Plus,新增系统管理下的 API Key 页面。 +包含列表、名称筛选、创建及一次性密钥展示/复制、启用/禁用、删除确认、创建时间及最后使用时间。 +处理加载、空列表和失败状态;不把明文保存到浏览器存储。 +必要时修复接口响应和数据库错误处理以完成页面联调。 +前端定制文件保存在本仓库 web/,同步至现有 D:/chengma/erpgo-ui 实例。 + +## 验收 +- Admin 菜单和页面可访问。 +- 创建后显示一次明文;关闭后列表及刷新不显示明文。 +- 启停、删除操作持久化;取消确认不修改记录。 +- 空名称有提示;请求失败不产生未捕获异常。 +- 接口、前端编译及交互检查通过;只操作 erpgo 服务。 + +## 证据 +实施中。 diff --git a/web/README.md b/web/README.md new file mode 100644 index 0000000..b4d62e0 --- /dev/null +++ b/web/README.md @@ -0,0 +1,7 @@ +# erpgo 前端定制 + +基线是 D:/github/goadmin/go-admin-ui(Vue 3 / Element Plus)。 +本目录仅保存本项目定制文件;运行实例位于 D:/chengma/erpgo-ui。 +修改后运行 `Copy-Item web/src/* D:/chengma/erpgo-ui/src/ -Recurse -Force` 同步。 +开发服务支持热更新。需要重启时仅操作 erpgo-ui,禁止全局 reload/restart。 +菜单由后端数据库迁移建立,完成迁移后重新登录加载菜单。 diff --git a/web/src/api/admin/api-key.js b/web/src/api/admin/api-key.js new file mode 100644 index 0000000..0146e67 --- /dev/null +++ b/web/src/api/admin/api-key.js @@ -0,0 +1,6 @@ +import request from '@/utils/request' + +export const listKeys = () => request({ url: '/api/v1/api-keys', method: 'get' }) +export const createKey = name => request({ url: '/api/v1/api-keys', method: 'post', data: { name } }) +export const toggleKey = id => request({ url: `/api/v1/api-keys/${id}/toggle`, method: 'put' }) +export const deleteKey = id => request({ url: `/api/v1/api-keys/${id}`, method: 'delete' }) diff --git a/web/src/views/admin/api-key/index.vue b/web/src/views/admin/api-key/index.vue new file mode 100644 index 0000000..681ff98 --- /dev/null +++ b/web/src/views/admin/api-key/index.vue @@ -0,0 +1,124 @@ + + + + +