From 0b2e836c27d15a2bcb141ef3233f2b4bdc8531cc Mon Sep 17 00:00:00 2001 From: Odboy Date: Fri, 15 May 2026 21:09:40 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=B8=9A=E5=8A=A1=E7=BB=84=E5=92=8C?= =?UTF-8?q?=E5=BA=94=E7=94=A8=E6=94=AF=E6=8C=81=E5=A4=9A=E8=B4=9F=E8=B4=A3?= =?UTF-8?q?=E4=BA=BA=EF=BC=8C=E4=BF=AE=E5=A4=8D=E6=9D=83=E9=99=90=E6=9F=A5?= =?UTF-8?q?=E8=AF=A2=E8=A1=A8=E5=90=8D=E9=94=99=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 业务组 MasterUid 改为 MasterUids(逗号分隔多用户ID) - 应用 OwnerId 改为 OwnerIds(逗号分隔多用户ID) - 前端负责人下拉框改为多选 - 权限查询改用 FIND_IN_SET 匹配多负责人 - 修复 user_permissions 表名错误(应为 user_permission) - 所有数据库实体添加 TableName() 方法显式指定表名 - xorm tag 显式指定 owner_ids/master_uids 列名 - 启动时自动清理 owner_ids 中的空格 --- server/internal/dao/engine.go | 11 +++ server/internal/handler/app.go | 108 +++++++++++++++-------- server/internal/handler/group.go | 66 +++++++++----- server/internal/model/app_config.go | 8 ++ server/internal/model/application.go | 8 +- server/internal/model/business_group.go | 24 ++--- server/internal/model/client_instance.go | 4 + server/internal/model/profile.go | 8 ++ server/internal/model/user.go | 8 ++ server/internal/service/auth.go | 3 +- web/src/views/app/AppList.vue | 12 ++- web/src/views/group/GroupList.vue | 14 ++- 12 files changed, 193 insertions(+), 81 deletions(-) diff --git a/server/internal/dao/engine.go b/server/internal/dao/engine.go index 99d5459..800f4b5 100644 --- a/server/internal/dao/engine.go +++ b/server/internal/dao/engine.go @@ -22,6 +22,7 @@ func Init() error { } Engine.SetMaxOpenConns(20) Engine.SetMaxIdleConns(5) + Engine.ShowSQL(true) // sync table structures if err := Engine.Sync2( @@ -38,10 +39,20 @@ func Init() error { return err } + // 迁移旧数据 & 清理格式 + migrateOldData() + // insert initial data if tables are empty return initSeedData() } +func migrateOldData() { + // 清理 owner_ids 中可能存在的空格 + Engine.Exec("UPDATE application SET owner_ids = REPLACE(owner_ids, ' ', '') WHERE owner_ids LIKE '% %'") + // 清理 master_uids 中可能存在的空格 + Engine.Exec("UPDATE business_group SET master_uids = REPLACE(master_uids, ' ', '') WHERE master_uids LIKE '% %'") +} + func initSeedData() error { // check if profiles table is empty count, err := Engine.Count(new(model.Profile)) diff --git a/server/internal/handler/app.go b/server/internal/handler/app.go index d4280e4..76488c6 100644 --- a/server/internal/handler/app.go +++ b/server/internal/handler/app.go @@ -8,6 +8,7 @@ import ( "kenaito-conf/server/internal/model" "net/http" "strconv" + "strings" "time" "github.com/gin-gonic/gin" @@ -36,22 +37,30 @@ func (h *AppHandler) List(c *gin.Context) { query := dao.Engine.Where("enabled = 1") if !userInfo.IsSuperAdmin() { + userId, _ := strconv.Atoi(userInfo.Id) + userIdStr := strconv.Itoa(userId) + if userInfo.IsGroupAdmin() && userInfo.GroupId != nil { - query = query.And("group_id = ?", *userInfo.GroupId) + // 组管理员:本组应用 + 自己是负责人的应用 + query = query.And("(group_id = ? OR FIND_IN_SET('" + userIdStr + "', owner_ids) > 0 OR id IN (SELECT app_id FROM user_permission WHERE user_id = ?))", *userInfo.GroupId, userId) } else { - userId, _ := strconv.Atoi(userInfo.Id) - query = query.And("(owner_id = ? OR id IN (SELECT app_id FROM user_permissions WHERE user_id = ?))", userId, userId) + // 普通用户:自己是负责人的应用 + 被授权的应用 + query = query.And("(FIND_IN_SET('" + userIdStr + "', owner_ids) > 0 OR id IN (SELECT app_id FROM user_permission WHERE user_id = ?))", userId) } } var apps []model.Application whereClause := "enabled = 1" if !userInfo.IsSuperAdmin() { + userId, _ := strconv.Atoi(userInfo.Id) + userIdStr := strconv.Itoa(userId) + if userInfo.IsGroupAdmin() && userInfo.GroupId != nil { - whereClause += " AND group_id = " + strconv.Itoa(*userInfo.GroupId) + // 组管理员:本组应用 + 自己是负责人的应用 + whereClause += " AND (group_id = " + strconv.Itoa(*userInfo.GroupId) + " OR FIND_IN_SET('" + userIdStr + "', owner_ids) > 0 OR id IN (SELECT app_id FROM user_permission WHERE user_id = " + strconv.Itoa(userId) + "))" } else { - userId, _ := strconv.Atoi(userInfo.Id) - whereClause += " AND (owner_id = " + strconv.Itoa(userId) + " OR id IN (SELECT app_id FROM user_permissions WHERE user_id = " + strconv.Itoa(userId) + "))" + // 普通用户:自己是负责人的应用 + 被授权的应用 + whereClause += " AND (FIND_IN_SET('" + userIdStr + "', owner_ids) > 0 OR id IN (SELECT app_id FROM user_permission WHERE user_id = " + strconv.Itoa(userId) + "))" } } if req.AppType != "" { @@ -101,13 +110,13 @@ func (h *AppHandler) GetById(c *gin.Context) { } type AddAppRequest struct { - Id int `json:"id"` - GroupId *int `json:"groupId"` - Name string `json:"name" binding:"required"` - Code string `json:"code" binding:"required"` - ServiceId string `json:"serviceId"` - OwnerId *int `json:"ownerId"` - Remarks string `json:"remarks"` + Id int `json:"id"` + GroupId *int `json:"groupId"` + Name string `json:"name" binding:"required"` + Code string `json:"code" binding:"required"` + ServiceId string `json:"serviceId"` + OwnerIds []int `json:"ownerIds"` + Remarks string `json:"remarks"` } func (h *AppHandler) Add(c *gin.Context) { @@ -126,29 +135,39 @@ func (h *AppHandler) Add(c *gin.Context) { return } - ownerId := req.OwnerId - if ownerId == nil { + // 处理负责人:如果未指定,默认为当前用户 + ownerIds := req.OwnerIds + if len(ownerIds) == 0 { uid, _ := strconv.Atoi(userInfo.Id) - ownerId = &uid + ownerIds = []int{uid} } - var owner model.User - if has, _ := dao.Engine.ID(*ownerId).Get(&owner); !has { - c.JSON(http.StatusOK, model.WrapperResponse{Code: 400, Msg: "负责人不存在"}) + // 验证所有用户ID都存在 + var users []model.User + if err := dao.Engine.In("id", ownerIds).Find(&users); err != nil || len(users) != len(ownerIds) { + c.JSON(http.StatusOK, model.WrapperResponse{Code: 400, Msg: "部分负责人不存在"}) return } + // 构建逗号分隔的用户名和ID字符串 + ownerNames := make([]string, 0, len(users)) + ownerIdStrs := make([]string, 0, len(users)) + for _, u := range users { + ownerNames = append(ownerNames, u.Name) + ownerIdStrs = append(ownerIdStrs, strconv.Itoa(u.Id)) + } + app := model.Application{ - GroupId: req.GroupId, - Name: req.Name, - Code: req.Code, - ServiceId: req.ServiceId, - OwnerId: ownerId, - OwnerName: owner.Name, - Remarks: req.Remarks, - Enabled: true, - CreatedAt: func() *time.Time { t := time.Now(); return &t }(), - CreatedBy: userInfo.Name, + GroupId: req.GroupId, + Name: req.Name, + Code: req.Code, + ServiceId: req.ServiceId, + OwnerIds: strings.Join(ownerIdStrs, ","), + OwnerNames: strings.Join(ownerNames, ","), + Remarks: req.Remarks, + Enabled: true, + CreatedAt: func() *time.Time { t := time.Now(); return &t }(), + CreatedBy: userInfo.Name, } dao.Engine.Insert(&app) @@ -180,20 +199,32 @@ func (h *AppHandler) Update(c *gin.Context) { app.Name = req.Name app.ServiceId = req.ServiceId app.Remarks = req.Remarks - if req.OwnerId != nil { - if app.OwnerId == nil || *req.OwnerId != *app.OwnerId { - var owner model.User - if has, _ := dao.Engine.ID(*req.OwnerId).Get(&owner); has { - app.OwnerName = owner.Name - } + + // 更新负责人 + if len(req.OwnerIds) > 0 { + // 验证所有用户ID都存在 + var users []model.User + if err := dao.Engine.In("id", req.OwnerIds).Find(&users); err != nil || len(users) != len(req.OwnerIds) { + c.JSON(http.StatusOK, model.WrapperResponse{Code: 400, Msg: "部分负责人不存在"}) + return } - app.OwnerId = req.OwnerId + + // 构建逗号分隔的用户名和ID字符串 + ownerNames := make([]string, 0, len(users)) + ownerIdStrs := make([]string, 0, len(users)) + for _, u := range users { + ownerNames = append(ownerNames, u.Name) + ownerIdStrs = append(ownerIdStrs, strconv.Itoa(u.Id)) + } + app.OwnerIds = strings.Join(ownerIdStrs, ",") + app.OwnerNames = strings.Join(ownerNames, ",") } + app.UpdatedBy = userInfo.Name now := time.Now() app.UpdatedAt = &now - dao.Engine.ID(app.Id).Cols("name", "service_id", "remarks", "owner_id", "owner_name", "updated_by", "updated_at").Update(&app) + dao.Engine.ID(app.Id).Cols("name", "service_id", "remarks", "owner_ids", "owner_names", "updated_by", "updated_at").Update(&app) c.JSON(http.StatusOK, model.WrapperResponse{Code: 0, Data: true}) } @@ -255,7 +286,8 @@ func (h *AppHandler) Options(c *gin.Context) { query = query.And("group_id = ?", *userInfo.GroupId) } else { userId, _ := strconv.Atoi(userInfo.Id) - query = query.And("owner_id = ?", userId) + userIdStr := strconv.Itoa(userId) + query = query.And("FIND_IN_SET(?, owner_ids) > 0", userIdStr) } } diff --git a/server/internal/handler/group.go b/server/internal/handler/group.go index 1e7df96..4198028 100644 --- a/server/internal/handler/group.go +++ b/server/internal/handler/group.go @@ -6,6 +6,7 @@ import ( "kenaito-conf/server/internal/model" "net/http" "strconv" + "strings" "time" "github.com/gin-gonic/gin" @@ -35,10 +36,10 @@ func (h *GroupHandler) GetById(c *gin.Context) { } type AddGroupRequest struct { - Id int `json:"id"` - Name string `json:"name" binding:"required"` - MasterUid *int `json:"masterUid" binding:"required"` - Remarks string `json:"remarks"` + Id int `json:"id"` + Name string `json:"name" binding:"required"` + MasterUids []int `json:"masterUids" binding:"required"` + Remarks string `json:"remarks"` } func (h *GroupHandler) Add(c *gin.Context) { @@ -60,20 +61,29 @@ func (h *GroupHandler) Add(c *gin.Context) { return } - var master model.User - if has, _ := dao.Engine.ID(*req.MasterUid).Get(&master); !has { - c.JSON(http.StatusOK, model.WrapperResponse{Code: 400, Msg: "管理员不存在"}) + // 验证所有用户ID都存在 + var users []model.User + if err := dao.Engine.In("id", req.MasterUids).Find(&users); err != nil || len(users) != len(req.MasterUids) { + c.JSON(http.StatusOK, model.WrapperResponse{Code: 400, Msg: "部分管理员不存在"}) return } + // 构建逗号分隔的用户名和ID字符串 + masterNames := make([]string, 0, len(users)) + masterIds := make([]string, 0, len(users)) + for _, u := range users { + masterNames = append(masterNames, u.Name) + masterIds = append(masterIds, strconv.Itoa(u.Id)) + } + group := model.BusinessGroup{ - Name: req.Name, - Master: master.Name, - MasterUid: req.MasterUid, - Remarks: req.Remarks, - Enabled: true, - CreatedAt: func() *time.Time { t := time.Now(); return &t }(), - CreatedBy: userInfo.Name, + Name: req.Name, + Master: strings.Join(masterNames, ","), + MasterUids: strings.Join(masterIds, ","), + Remarks: req.Remarks, + Enabled: true, + CreatedAt: func() *time.Time { t := time.Now(); return &t }(), + CreatedBy: userInfo.Name, } dao.Engine.Insert(&group) @@ -106,20 +116,30 @@ func (h *GroupHandler) Update(c *gin.Context) { return } - group.Name = req.Name - group.Remarks = req.Remarks - if req.MasterUid != nil && (group.MasterUid == nil || *req.MasterUid != *group.MasterUid) { - var master model.User - if has, _ := dao.Engine.ID(*req.MasterUid).Get(&master); has { - group.Master = master.Name - group.MasterUid = req.MasterUid - } + // 验证所有用户ID都存在 + var users []model.User + if err := dao.Engine.In("id", req.MasterUids).Find(&users); err != nil || len(users) != len(req.MasterUids) { + c.JSON(http.StatusOK, model.WrapperResponse{Code: 400, Msg: "部分管理员不存在"}) + return } + + // 构建逗号分隔的用户名和ID字符串 + masterNames := make([]string, 0, len(users)) + masterIds := make([]string, 0, len(users)) + for _, u := range users { + masterNames = append(masterNames, u.Name) + masterIds = append(masterIds, strconv.Itoa(u.Id)) + } + + group.Name = req.Name + group.Master = strings.Join(masterNames, ",") + group.MasterUids = strings.Join(masterIds, ",") + group.Remarks = req.Remarks group.UpdatedBy = userInfo.Name now := time.Now() group.UpdatedAt = &now - dao.Engine.ID(group.Id).Cols("name", "remarks", "master", "master_uid", "updated_by", "updated_at").Update(&group) + dao.Engine.ID(group.Id).Cols("name", "remarks", "master", "master_uids", "updated_by", "updated_at").Update(&group) c.JSON(http.StatusOK, model.WrapperResponse{Code: 0}) } diff --git a/server/internal/model/app_config.go b/server/internal/model/app_config.go index 0b20327..87861f6 100644 --- a/server/internal/model/app_config.go +++ b/server/internal/model/app_config.go @@ -22,6 +22,10 @@ type AppConfig struct { ClientCount int `xorm:"-" json:"clientCount,omitempty"` } +func (AppConfig) TableName() string { + return "app_config" +} + type AppConfigHistory struct { Id int `xorm:"int autoincr pk" json:"id"` OriginId int `xorm:"int" json:"originId"` @@ -37,3 +41,7 @@ type AppConfigHistory struct { CreatedAt *time.Time `xorm:"datetime" json:"createdAt"` CreatedBy string `xorm:"varchar(32)" json:"createdBy"` } + +func (AppConfigHistory) TableName() string { + return "app_config_history" +} diff --git a/server/internal/model/application.go b/server/internal/model/application.go index bb768d1..3aa1608 100644 --- a/server/internal/model/application.go +++ b/server/internal/model/application.go @@ -9,8 +9,8 @@ type Application struct { Code string `xorm:"varchar(32)" json:"code"` ServiceId string `xorm:"varchar(64)" json:"serviceId"` Token string `xorm:"varchar(64)" json:"token"` - OwnerId *int `xorm:"int" json:"ownerId"` - OwnerName string `xorm:"varchar(16)" json:"ownerName"` + OwnerIds string `xorm:"varchar(255) 'owner_ids'" json:"ownerIds"` + OwnerNames string `xorm:"varchar(255) 'owner_names'" json:"ownerNames"` Remarks string `xorm:"text" json:"remarks"` Enabled bool `xorm:"tinyint(1) default 1" json:"enabled"` CreatedAt *time.Time `xorm:"datetime" json:"createdAt"` @@ -19,3 +19,7 @@ type Application struct { UpdatedBy string `xorm:"varchar(32)" json:"updatedBy"` GroupName string `xorm:"-" json:"groupName,omitempty"` } + +func (Application) TableName() string { + return "application" +} diff --git a/server/internal/model/business_group.go b/server/internal/model/business_group.go index bf64195..395bbc9 100644 --- a/server/internal/model/business_group.go +++ b/server/internal/model/business_group.go @@ -3,14 +3,18 @@ package model import "time" type BusinessGroup struct { - Id int `xorm:"int autoincr pk" json:"id"` - Name string `xorm:"varchar(32) notnull" json:"name"` - Master string `xorm:"varchar(16)" json:"master"` - MasterUid *int `xorm:"int" json:"masterUid"` - Remarks string `xorm:"text" json:"remarks"` - Enabled bool `xorm:"tinyint(1) default 1" json:"enabled"` - CreatedAt *time.Time `xorm:"datetime" json:"createdAt"` - CreatedBy string `xorm:"varchar(32)" json:"createdBy"` - UpdatedAt *time.Time `xorm:"datetime" json:"updatedAt"` - UpdatedBy string `xorm:"varchar(32)" json:"updatedBy"` + Id int `xorm:"int autoincr pk" json:"id"` + Name string `xorm:"varchar(32) notnull" json:"name"` + Master string `xorm:"varchar(255)" json:"master"` + MasterUids string `xorm:"varchar(255) 'master_uids'" json:"masterUids"` + Remarks string `xorm:"text" json:"remarks"` + Enabled bool `xorm:"tinyint(1) default 1" json:"enabled"` + CreatedAt *time.Time `xorm:"datetime" json:"createdAt"` + CreatedBy string `xorm:"varchar(32)" json:"createdBy"` + UpdatedAt *time.Time `xorm:"datetime" json:"updatedAt"` + UpdatedBy string `xorm:"varchar(32)" json:"updatedBy"` +} + +func (BusinessGroup) TableName() string { + return "business_group" } diff --git a/server/internal/model/client_instance.go b/server/internal/model/client_instance.go index f8f2304..72552f5 100644 --- a/server/internal/model/client_instance.go +++ b/server/internal/model/client_instance.go @@ -14,3 +14,7 @@ type ClientInstance struct { CreatedAt *time.Time `xorm:"datetime" json:"createdAt"` UpdatedAt *time.Time `xorm:"datetime" json:"updatedAt"` } + +func (ClientInstance) TableName() string { + return "client_instance" +} diff --git a/server/internal/model/profile.go b/server/internal/model/profile.go index 7788e8a..b41a3c0 100644 --- a/server/internal/model/profile.go +++ b/server/internal/model/profile.go @@ -12,9 +12,17 @@ type Profile struct { UpdatedAt *time.Time `xorm:"datetime" json:"updatedAt"` } +func (Profile) TableName() string { + return "profile" +} + type AppExtrAttr struct { AppId int `xorm:"int pk" json:"appId"` Env string `xorm:"varchar(16) pk" json:"env"` AttrName string `xorm:"varchar(32) pk" json:"attrName"` AttrValue string `xorm:"varchar(100)" json:"attrValue"` } + +func (AppExtrAttr) TableName() string { + return "app_extr_attr" +} diff --git a/server/internal/model/user.go b/server/internal/model/user.go index a406f70..8386b2e 100644 --- a/server/internal/model/user.go +++ b/server/internal/model/user.go @@ -18,6 +18,10 @@ type User struct { GroupMaster bool `xorm:"-" json:"groupMaster,omitempty"` } +func (User) TableName() string { + return "user" +} + type UserPermission struct { Id int `xorm:"int autoincr pk" json:"id"` UserId int `xorm:"int" json:"userId"` @@ -26,6 +30,10 @@ type UserPermission struct { Operate string `xorm:"varchar(8) default 'RW'" json:"operate"` // RO, RW } +func (UserPermission) TableName() string { + return "user_permission" +} + type LoginUserInfo struct { Id string `json:"id"` Name string `json:"name"` diff --git a/server/internal/service/auth.go b/server/internal/service/auth.go index 4f7fea9..009b8c5 100644 --- a/server/internal/service/auth.go +++ b/server/internal/service/auth.go @@ -141,7 +141,8 @@ func (s *AuthService) loadPermissions(userInfo *model.LoginUserInfo, userId int) if userInfo.IsGroupAdmin() { dao.Engine.Where("group_id = ?", userInfo.GroupId).Find(&apps) } else { - dao.Engine.Where("owner_id = ?", userId).Find(&apps) + userIdStr := strconv.Itoa(userId) + dao.Engine.Where("FIND_IN_SET(?, owner_ids) > 0", userIdStr).Find(&apps) } for _, app := range apps { userInfo.GrantAppIds = append(userInfo.GrantAppIds, app.Id) diff --git a/web/src/views/app/AppList.vue b/web/src/views/app/AppList.vue index fc5f866..6428240 100644 --- a/web/src/views/app/AppList.vue +++ b/web/src/views/app/AppList.vue @@ -12,7 +12,7 @@ - +