feat: 查询条件优化和 xorm session 修复
This commit is contained in:
@@ -1,206 +0,0 @@
|
|||||||
# Server 代码缺陷清单 — 2026-10-15
|
|
||||||
|
|
||||||
## 🔴 严重缺陷(安全/数据风险)
|
|
||||||
|
|
||||||
### 1. `encryptIfRequired` 是空实现 — 敏感数据未加密
|
|
||||||
**文件**: `internal/handler/config.go`
|
|
||||||
|
|
||||||
`CryptoService` 已完整实现了 AES 加解密,但 `Add` 和 `Update` 配置时调用的 `encryptIfRequired` 是空函数,所有敏感配置值以明文存储。
|
|
||||||
|
|
||||||
```go
|
|
||||||
func (h *ConfigHandler) encryptIfRequired(config *model.AppConfig) {
|
|
||||||
// simplified: check if contents contain sensitive keys
|
|
||||||
// In production, parse the config and encrypt values with {Cipher} prefix
|
|
||||||
// For now, skip encryption logic
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. 重置密码使用 `math/rand` — 可预测的伪随机数
|
|
||||||
**文件**: `internal/handler/user.go`
|
|
||||||
|
|
||||||
`ResetPassword` 用 `math/rand` 生成 8 位随机密码。`math/rand` 不是密码学安全的随机源,攻击者可以预测生成的密码。应使用 `crypto/rand`。
|
|
||||||
|
|
||||||
```go
|
|
||||||
import "math/rand"
|
|
||||||
|
|
||||||
func randomString(n int) string {
|
|
||||||
const letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
|
||||||
b := make([]byte, n)
|
|
||||||
for i := range b {
|
|
||||||
b[i] = letters[rand.Intn(len(letters))] // ← 不安全
|
|
||||||
}
|
|
||||||
return string(b)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. JWT 验证未检查签名算法 — 存在 "none" 算法攻击风险
|
|
||||||
**文件**: `internal/service/auth.go`
|
|
||||||
|
|
||||||
没有检查 `token.Method` 是否为预期的 `jwt.SigningMethodHS256`,攻击者可以将算法改为 `none` 来伪造任意 token。
|
|
||||||
|
|
||||||
```go
|
|
||||||
func (s *AuthService) ValidateToken(tokenString string) (*model.LoginUserInfo, error) {
|
|
||||||
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
|
|
||||||
return []byte(s.jwtSecret), nil // ← 未验证 token.Method
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. 数据库真实凭据硬编码在配置文件中
|
|
||||||
**文件**: `application.properties`
|
|
||||||
|
|
||||||
外网 IP + 真实密码 + JWT 密钥 + 加密密钥全部明文提交到代码仓库。
|
|
||||||
|
|
||||||
```properties
|
|
||||||
database.host=103.236.98.135
|
|
||||||
database.password=?uWxt,CX2>+#*Ij@-xa+
|
|
||||||
jwt.secret=kenaito-conf-jwt-secret-2026
|
|
||||||
crypto.secretKey=k9#mP2$vL7qR4xN8
|
|
||||||
```
|
|
||||||
|
|
||||||
### 5. 加密注释声称 ECB 模式,实际使用 CBC + 零 IV
|
|
||||||
**文件**: `internal/service/crypto.go`
|
|
||||||
|
|
||||||
注释写的是 ECB,代码用的是 CBC(IV 全零)。如果 Java 端真的是 ECB,则两端加解密不兼容。且 CBC + 零 IV 等于 ECB 的安全性。
|
|
||||||
|
|
||||||
```go
|
|
||||||
// ECB mode (compatible with Java version)
|
|
||||||
mode := cipher.NewCBCEncrypter(block, make([]byte, aes.BlockSize))
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🟠 重要缺陷(功能/逻辑错误)
|
|
||||||
|
|
||||||
### 6. Logout 不做任何服务端失效处理
|
|
||||||
**文件**: `internal/handler/auth.go`
|
|
||||||
|
|
||||||
JWT 在过期前始终有效,登出后 token 仍可使用。
|
|
||||||
|
|
||||||
```go
|
|
||||||
func (h *AuthHandler Logout(c *gin.Context) {
|
|
||||||
c.JSON(http.StatusOK, model.WrapperResponse{Code: 0})
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 9. `ValidateToken` 不检查用户是否已被禁用
|
|
||||||
**文件**: `internal/service/auth.go`
|
|
||||||
|
|
||||||
用户被管理员禁用后,已签发的 JWT 在过期前仍然有效。
|
|
||||||
|
|
||||||
```go
|
|
||||||
has, err := dao.Engine.ID(userId).Get(&user)
|
|
||||||
if err != nil || !has {
|
|
||||||
return nil, errors.New("user not found")
|
|
||||||
}
|
|
||||||
// ← 没有检查 user.Enabled
|
|
||||||
```
|
|
||||||
|
|
||||||
### 10. 占位符解析存在循环引用风险
|
|
||||||
**文件**: `internal/service/config_parse.go`
|
|
||||||
|
|
||||||
没有检测循环引用,恶意或错误的配置(如 A=${B}, B=${A})会导致 CPU 空转 100 次迭代。
|
|
||||||
|
|
||||||
```go
|
|
||||||
func (s *ConfigParseService) replacePlaceholder(...) string {
|
|
||||||
maxIterations := 100
|
|
||||||
for i := 0; i < maxIterations; i++ {
|
|
||||||
// 循环引用时无限迭代
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 11. `FetchAllConfigs` / `FetchChangedConfigs` 未强制要求 Token
|
|
||||||
**文件**: `internal/handler/config_api.go`
|
|
||||||
|
|
||||||
当 `app.Token` 为空时,任何请求都能拉取配置,无需认证。如果应用未配置 Token,则配置接口完全开放。
|
|
||||||
|
|
||||||
```go
|
|
||||||
if app.Token != "" && app.Token != req.Token {
|
|
||||||
c.JSON(http.StatusForbidden, ...)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// Token 为空时直接放行
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🟡 一般缺陷(代码质量/健壮性)
|
|
||||||
|
|
||||||
### 12. 数据库操作错误被大规模忽略
|
|
||||||
整个代码库中大量数据库操作结果被忽略,包括关键的 Insert、Update、Count 操作:
|
|
||||||
|
|
||||||
| 文件 | 示例 |
|
|
||||||
|------|------|
|
|
||||||
| `handler/app.go` | `dao.Engine.Insert(&app)` — 插入失败无感知 |
|
|
||||||
| `handler/config.go` | `dao.Engine.Insert(&history)` — 历史记录保存失败无感知 |
|
|
||||||
| `handler/user.go` | `dao.Engine.Insert(&user)` — 用户创建失败无感知 |
|
|
||||||
| `handler/config_api.go` | `dao.Engine.Where(...).Find(&globalConfigs)` — 查询失败返回空结果 |
|
|
||||||
|
|
||||||
### 13. Token 校验使用字符串直接比较 — 时序攻击
|
|
||||||
**文件**: `internal/handler/config_api.go`
|
|
||||||
|
|
||||||
应使用 `crypto/subtle.ConstantTimeCompare` 防止时序攻击。
|
|
||||||
|
|
||||||
```go
|
|
||||||
if app.Token != "" && app.Token != req.Token {
|
|
||||||
```
|
|
||||||
|
|
||||||
### 14. `Delete` 用户是硬删除,与其他模块软删除不一致
|
|
||||||
**文件**: `internal/handler/user.go`
|
|
||||||
|
|
||||||
而 App、Config 等都是 `enabled = false` 软删除。硬删除用户后,关联的 `user_permissions` 记录和 `created_by` 引用会变成孤儿数据。
|
|
||||||
|
|
||||||
```go
|
|
||||||
dao.Engine.ID(id).Delete(&model.User{}) // 物理删除
|
|
||||||
```
|
|
||||||
|
|
||||||
### 15. `AppHandler.List` 存在 N+1 查询
|
|
||||||
**文件**: `internal/handler/app.go`
|
|
||||||
|
|
||||||
加载 group names 时,每个 group 单独查询一次,应改为 `WHERE id IN (...)` 批量查询。
|
|
||||||
|
|
||||||
```go
|
|
||||||
for gid := range groupIds {
|
|
||||||
var group model.BusinessGroup
|
|
||||||
if has, _ := dao.Engine.ID(gid).Get(&group); has { // ← 每个 group 一次查询
|
|
||||||
```
|
|
||||||
|
|
||||||
### 16. `ConfigHandler.List` 中 `app_code like ?` 可能过度匹配
|
|
||||||
**文件**: `internal/handler/config.go`
|
|
||||||
|
|
||||||
如果 `app.Code = "app"`,会匹配到 `app`、`app-web`、`app-api` 等所有以 `app` 开头的配置。
|
|
||||||
|
|
||||||
```go
|
|
||||||
query = query.And("app_code like ?", app.Code+"%")
|
|
||||||
```
|
|
||||||
|
|
||||||
### 17. `AppHandler.Tree` 返回已禁用的应用
|
|
||||||
**文件**: `internal/handler/app.go`
|
|
||||||
|
|
||||||
没有过滤 `enabled = 1`,禁用的应用也会出现在树中。
|
|
||||||
|
|
||||||
```go
|
|
||||||
func (h *AppHandler) Tree(c *gin.Context) {
|
|
||||||
var apps []model.Application
|
|
||||||
dao.Engine.Find(&apps) // ← 没有 WHERE enabled = 1
|
|
||||||
```
|
|
||||||
|
|
||||||
### 18. `Profile` 模型缺少 `CreatedAt`/`UpdatedAt` 字段
|
|
||||||
**文件**: `internal/model/profile.go`
|
|
||||||
|
|
||||||
其他所有模型都有时间审计字段,Profile 没有,无法追踪环境的创建和修改时间。
|
|
||||||
|
|
||||||
### 19. DSN 构建逻辑重复
|
|
||||||
**文件**: `internal/config/config.go` 的 `DSN()` 方法 vs `internal/dao/engine.go` 的 `Init()` 函数
|
|
||||||
|
|
||||||
两处各自拼接 DSN 字符串,且 `dao/engine.go` 没有复用 `config.DatabaseConfig.DSN()` 方法,还额外加了 `&loc=Local` 参数。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 总结
|
|
||||||
|
|
||||||
| 等级 | 数量 | 关键问题 |
|
|
||||||
|------|------|----------|
|
|
||||||
| 🔴 严重 | 5 | 加密空实现、不安全随机数、JWT 算法未校验、凭据泄露、加密模式不一致 |
|
|
||||||
| 🟠 重要 | 6 | 回滚不加密、权限表缺失、登出无效、禁用用户仍可访问等 |
|
|
||||||
| 🟡 一般 | 8 | 错误忽略、N+1 查询、时序攻击、硬删除不一致等 |
|
|
||||||
@@ -45,8 +45,21 @@ func (h *AppHandler) List(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var apps []model.Application
|
var apps []model.Application
|
||||||
total, _ := query.Count(&model.Application{})
|
whereClause := "enabled = 1"
|
||||||
query.Limit(req.PageSize, (req.PageNo-1)*req.PageSize).Find(&apps)
|
if !userInfo.IsSuperAdmin() {
|
||||||
|
if userInfo.IsGroupAdmin() && userInfo.GroupId != nil {
|
||||||
|
whereClause += " AND group_id = " + strconv.Itoa(*userInfo.GroupId)
|
||||||
|
} 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) + "))"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if req.AppType != "" {
|
||||||
|
whereClause += " AND service_id = '" + req.AppType + "'"
|
||||||
|
}
|
||||||
|
|
||||||
|
total, _ := dao.Engine.Where(whereClause).Count(&model.Application{})
|
||||||
|
dao.Engine.Where(whereClause).Limit(req.PageSize, (req.PageNo-1)*req.PageSize).Find(&apps)
|
||||||
|
|
||||||
// load group names - batch query to avoid N+1
|
// load group names - batch query to avoid N+1
|
||||||
groupIds := make(map[int]bool)
|
groupIds := make(map[int]bool)
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"kenaito-conf/server/internal/service"
|
"kenaito-conf/server/internal/service"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
@@ -36,34 +37,48 @@ func (h *ConfigHandler) List(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
userInfo := middleware.GetUserInfo(c)
|
userInfo := middleware.GetUserInfo(c)
|
||||||
query := dao.Engine.Where("enabled = 1")
|
|
||||||
|
|
||||||
if req.Env != "" {
|
// 检查权限
|
||||||
query = query.And("env = ?", req.Env)
|
|
||||||
}
|
|
||||||
if req.AppId != nil {
|
|
||||||
if *req.AppId <= 0 {
|
|
||||||
query = query.And("(app_id is null OR app_id = 0)")
|
|
||||||
} else {
|
|
||||||
query = query.And("app_id = ?", *req.AppId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// permission filter
|
|
||||||
if !userInfo.IsSuperAdmin() {
|
if !userInfo.IsSuperAdmin() {
|
||||||
if userInfo.IsGroupAdmin() && userInfo.GroupId != nil {
|
if !userInfo.IsGroupAdmin() && len(userInfo.GrantAppIds) == 0 {
|
||||||
query = query.And("group_id = ?", *userInfo.GroupId)
|
|
||||||
} else if len(userInfo.GrantAppIds) > 0 {
|
|
||||||
query = query.In("app_id", userInfo.GrantAppIds)
|
|
||||||
} else {
|
|
||||||
c.JSON(http.StatusOK, model.PageResult{PageNo: req.PageNo, PageSize: req.PageSize, Total: 0, Data: []interface{}{}})
|
c.JSON(http.StatusOK, model.PageResult{PageNo: req.PageNo, PageSize: req.PageSize, Total: 0, Data: []interface{}{}})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 构建查询条件
|
||||||
|
buildConditions := func() []string {
|
||||||
|
conditions := []string{"enabled = 1"}
|
||||||
|
if req.Env != "" {
|
||||||
|
conditions = append(conditions, "env = '"+req.Env+"'")
|
||||||
|
}
|
||||||
|
if req.AppId != nil {
|
||||||
|
if *req.AppId <= 0 {
|
||||||
|
conditions = append(conditions, "(app_id is null OR app_id = 0)")
|
||||||
|
} else {
|
||||||
|
conditions = append(conditions, "app_id = "+strconv.Itoa(*req.AppId))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !userInfo.IsSuperAdmin() {
|
||||||
|
if userInfo.IsGroupAdmin() && userInfo.GroupId != nil {
|
||||||
|
conditions = append(conditions, "group_id = "+strconv.Itoa(*userInfo.GroupId))
|
||||||
|
} else if len(userInfo.GrantAppIds) > 0 {
|
||||||
|
ids := make([]string, len(userInfo.GrantAppIds))
|
||||||
|
for i, id := range userInfo.GrantAppIds {
|
||||||
|
ids[i] = strconv.Itoa(id)
|
||||||
|
}
|
||||||
|
conditions = append(conditions, "app_id in ("+strings.Join(ids, ",")+")")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return conditions
|
||||||
|
}
|
||||||
|
|
||||||
|
conditions := buildConditions()
|
||||||
|
whereClause := strings.Join(conditions, " AND ")
|
||||||
|
|
||||||
var configs []model.AppConfig
|
var configs []model.AppConfig
|
||||||
total, _ := query.Count(&model.AppConfig{})
|
total, _ := dao.Engine.Where(whereClause).Count(&model.AppConfig{})
|
||||||
query.Limit(req.PageSize, (req.PageNo-1)*req.PageSize).Find(&configs)
|
dao.Engine.Where(whereClause).Limit(req.PageSize, (req.PageNo-1)*req.PageSize).Find(&configs)
|
||||||
|
|
||||||
// build app names
|
// build app names
|
||||||
for i := range configs {
|
for i := range configs {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
@@ -24,20 +25,35 @@ func NewUserHandler(perm *service.PermissionService) *UserHandler {
|
|||||||
|
|
||||||
func (h *UserHandler) List(c *gin.Context) {
|
func (h *UserHandler) List(c *gin.Context) {
|
||||||
groupIdStr := c.Query("groupId")
|
groupIdStr := c.Query("groupId")
|
||||||
|
name := c.Query("name")
|
||||||
|
userType := c.Query("type")
|
||||||
|
enabledStr := c.Query("enabled")
|
||||||
userInfo := middleware.GetUserInfo(c)
|
userInfo := middleware.GetUserInfo(c)
|
||||||
|
|
||||||
var users []model.User
|
buildConditions := func() []string {
|
||||||
query := dao.Engine.Where("enabled = 1")
|
conditions := []string{"1=1"}
|
||||||
|
|
||||||
if !userInfo.IsSuperAdmin() && userInfo.GroupId != nil {
|
if !userInfo.IsSuperAdmin() && userInfo.GroupId != nil {
|
||||||
query = query.And("group_id = ?", *userInfo.GroupId)
|
conditions = append(conditions, "group_id = "+strconv.Itoa(*userInfo.GroupId))
|
||||||
}
|
}
|
||||||
if groupIdStr != "" {
|
if groupIdStr != "" {
|
||||||
groupId, _ := strconv.Atoi(groupIdStr)
|
conditions = append(conditions, "group_id = "+groupIdStr)
|
||||||
query = query.And("group_id = ?", groupId)
|
}
|
||||||
|
if name != "" {
|
||||||
|
conditions = append(conditions, "name like '%"+name+"%'")
|
||||||
|
}
|
||||||
|
if userType != "" {
|
||||||
|
conditions = append(conditions, "type = '"+userType+"'")
|
||||||
|
}
|
||||||
|
if enabledStr != "" {
|
||||||
|
conditions = append(conditions, "enabled = "+enabledStr)
|
||||||
|
}
|
||||||
|
return conditions
|
||||||
}
|
}
|
||||||
|
|
||||||
query.Find(&users)
|
whereClause := strings.Join(buildConditions(), " AND ")
|
||||||
|
|
||||||
|
var users []model.User
|
||||||
|
dao.Engine.Where(whereClause).Find(&users)
|
||||||
|
|
||||||
// load group names
|
// load group names
|
||||||
var groups []model.BusinessGroup
|
var groups []model.BusinessGroup
|
||||||
|
|||||||
@@ -3,9 +3,12 @@
|
|||||||
<div slot="header" style="display: flex; justify-content: space-between; align-items: center">
|
<div slot="header" style="display: flex; justify-content: space-between; align-items: center">
|
||||||
<span>配置列表</span>
|
<span>配置列表</span>
|
||||||
<div>
|
<div>
|
||||||
<el-select v-model="query.env" placeholder="环境" clearable style="width: 120px; margin-right: 10px" @change="loadData">
|
<el-select v-model="query.env" placeholder="环境" clearable style="width: 160px; margin-right: 10px" @change="loadData">
|
||||||
<el-option v-for="p in profiles" :key="p.value" :label="p.label" :value="p.value"></el-option>
|
<el-option v-for="p in profiles" :key="p.value" :label="p.label" :value="p.value"></el-option>
|
||||||
</el-select>
|
</el-select>
|
||||||
|
<el-select v-model="query.appId" placeholder="应用" clearable filterable style="width: 200px; margin-right: 10px" @change="loadData">
|
||||||
|
<el-option v-for="app in apps" :key="app.value" :label="app.label" :value="app.value"></el-option>
|
||||||
|
</el-select>
|
||||||
<el-button type="primary" @click="$router.push('/config/add')">新建配置</el-button>
|
<el-button type="primary" @click="$router.push('/config/add')">新建配置</el-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -192,8 +195,9 @@ export default {
|
|||||||
list: [],
|
list: [],
|
||||||
loading: false,
|
loading: false,
|
||||||
total: 0,
|
total: 0,
|
||||||
query: { pageNo: 1, pageSize: 20, env: '' },
|
query: { pageNo: 1, pageSize: 20, env: '', appId: null },
|
||||||
profiles: [],
|
profiles: [],
|
||||||
|
apps: [],
|
||||||
historyVisible: false,
|
historyVisible: false,
|
||||||
historyList: [],
|
historyList: [],
|
||||||
currentConfig: null,
|
currentConfig: null,
|
||||||
@@ -206,12 +210,23 @@ export default {
|
|||||||
created() {
|
created() {
|
||||||
this.loadData()
|
this.loadData()
|
||||||
this.loadProfiles()
|
this.loadProfiles()
|
||||||
|
this.loadApps()
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
async loadData() {
|
async loadData() {
|
||||||
this.loading = true
|
this.loading = true
|
||||||
try {
|
try {
|
||||||
const res = await getConfigList(this.query)
|
const params = {
|
||||||
|
pageNo: this.query.pageNo,
|
||||||
|
pageSize: this.query.pageSize
|
||||||
|
}
|
||||||
|
if (this.query.env) {
|
||||||
|
params.env = this.query.env
|
||||||
|
}
|
||||||
|
if (this.query.appId) {
|
||||||
|
params.appId = this.query.appId
|
||||||
|
}
|
||||||
|
const res = await getConfigList(params)
|
||||||
this.list = res.data || []
|
this.list = res.data || []
|
||||||
this.total = res.total || 0
|
this.total = res.total || 0
|
||||||
} finally {
|
} finally {
|
||||||
@@ -224,6 +239,12 @@ export default {
|
|||||||
this.profiles = res.data || []
|
this.profiles = res.data || []
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
},
|
},
|
||||||
|
async loadApps() {
|
||||||
|
try {
|
||||||
|
const res = await request({ url: '/admin/app/options', method: 'get' })
|
||||||
|
this.apps = res.data || []
|
||||||
|
} catch (e) {}
|
||||||
|
},
|
||||||
handlePageChange(page) {
|
handlePageChange(page) {
|
||||||
this.query.pageNo = page
|
this.query.pageNo = page
|
||||||
this.loadData()
|
this.loadData()
|
||||||
|
|||||||
@@ -2,9 +2,14 @@
|
|||||||
<el-card>
|
<el-card>
|
||||||
<div slot="header" style="display: flex; justify-content: space-between; align-items: center">
|
<div slot="header" style="display: flex; justify-content: space-between; align-items: center">
|
||||||
<span>业务组管理</span>
|
<span>业务组管理</span>
|
||||||
|
<div>
|
||||||
|
<el-select v-model="filterName" placeholder="组名称" clearable filterable style="width: 200px; margin-right: 10px">
|
||||||
|
<el-option v-for="g in groupOptions" :key="g" :label="g" :value="g"></el-option>
|
||||||
|
</el-select>
|
||||||
<el-button type="primary" @click="showAdd">新建业务组</el-button>
|
<el-button type="primary" @click="showAdd">新建业务组</el-button>
|
||||||
</div>
|
</div>
|
||||||
<el-table :data="list" v-loading="loading" border stripe>
|
</div>
|
||||||
|
<el-table :data="filteredList" v-loading="loading" border stripe>
|
||||||
<el-table-column prop="id" label="ID" width="60"></el-table-column>
|
<el-table-column prop="id" label="ID" width="60"></el-table-column>
|
||||||
<el-table-column prop="name" label="组名称"></el-table-column>
|
<el-table-column prop="name" label="组名称"></el-table-column>
|
||||||
<el-table-column prop="master" label="负责人"></el-table-column>
|
<el-table-column prop="master" label="负责人"></el-table-column>
|
||||||
@@ -51,6 +56,7 @@ export default {
|
|||||||
return {
|
return {
|
||||||
list: [],
|
list: [],
|
||||||
loading: false,
|
loading: false,
|
||||||
|
filterName: '',
|
||||||
dialogVisible: false,
|
dialogVisible: false,
|
||||||
dialogTitle: '',
|
dialogTitle: '',
|
||||||
form: {},
|
form: {},
|
||||||
@@ -61,6 +67,15 @@ export default {
|
|||||||
users: []
|
users: []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
computed: {
|
||||||
|
groupOptions() {
|
||||||
|
return [...new Set(this.list.map(g => g.name))].filter(name => name && name.trim())
|
||||||
|
},
|
||||||
|
filteredList() {
|
||||||
|
if (!this.filterName) return this.list
|
||||||
|
return this.list.filter(g => g.name === this.filterName)
|
||||||
|
}
|
||||||
|
},
|
||||||
created() {
|
created() {
|
||||||
this.loadData()
|
this.loadData()
|
||||||
this.loadUsers()
|
this.loadUsers()
|
||||||
|
|||||||
@@ -2,8 +2,23 @@
|
|||||||
<el-card>
|
<el-card>
|
||||||
<div slot="header" style="display: flex; justify-content: space-between; align-items: center">
|
<div slot="header" style="display: flex; justify-content: space-between; align-items: center">
|
||||||
<span>用户管理</span>
|
<span>用户管理</span>
|
||||||
|
<div>
|
||||||
|
<el-input v-model="query.name" placeholder="用户名" clearable style="width: 140px; margin-right: 10px" @clear="loadData" @keyup.enter.native="loadData"></el-input>
|
||||||
|
<el-select v-model="query.type" placeholder="类型" clearable style="width: 120px; margin-right: 10px" @change="loadData">
|
||||||
|
<el-option value="user" label="普通用户"></el-option>
|
||||||
|
<el-option value="groupAdmin" label="组管理员"></el-option>
|
||||||
|
<el-option value="superAdmin" label="超级管理员"></el-option>
|
||||||
|
</el-select>
|
||||||
|
<el-select v-model="query.groupId" placeholder="所属组" clearable filterable style="width: 160px; margin-right: 10px" @change="loadData">
|
||||||
|
<el-option v-for="g in groups" :key="g.value" :label="g.label" :value="g.value"></el-option>
|
||||||
|
</el-select>
|
||||||
|
<el-select v-model="query.enabled" placeholder="状态" clearable style="width: 100px; margin-right: 10px" @change="loadData">
|
||||||
|
<el-option :value="1" label="启用"></el-option>
|
||||||
|
<el-option :value="0" label="禁用"></el-option>
|
||||||
|
</el-select>
|
||||||
<el-button type="primary" @click="showAdd">新建用户</el-button>
|
<el-button type="primary" @click="showAdd">新建用户</el-button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<el-table :data="list" v-loading="loading" border stripe>
|
<el-table :data="list" v-loading="loading" border stripe>
|
||||||
<el-table-column prop="id" label="ID" width="60"></el-table-column>
|
<el-table-column prop="id" label="ID" width="60"></el-table-column>
|
||||||
<el-table-column prop="name" label="用户名"></el-table-column>
|
<el-table-column prop="name" label="用户名"></el-table-column>
|
||||||
@@ -73,6 +88,13 @@ export default {
|
|||||||
return {
|
return {
|
||||||
list: [],
|
list: [],
|
||||||
loading: false,
|
loading: false,
|
||||||
|
query: {
|
||||||
|
name: '',
|
||||||
|
type: '',
|
||||||
|
groupId: '',
|
||||||
|
enabled: ''
|
||||||
|
},
|
||||||
|
groups: [],
|
||||||
dialogVisible: false,
|
dialogVisible: false,
|
||||||
dialogTitle: '',
|
dialogTitle: '',
|
||||||
form: {},
|
form: {},
|
||||||
@@ -85,17 +107,29 @@ export default {
|
|||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
this.loadData()
|
this.loadData()
|
||||||
|
this.loadGroups()
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
async loadData() {
|
async loadData() {
|
||||||
this.loading = true
|
this.loading = true
|
||||||
try {
|
try {
|
||||||
const res = await request({ url: '/admin/user/list', method: 'get' })
|
const params = {}
|
||||||
|
if (this.query.name) params.name = this.query.name
|
||||||
|
if (this.query.type) params.type = this.query.type
|
||||||
|
if (this.query.groupId) params.groupId = this.query.groupId
|
||||||
|
if (this.query.enabled !== '') params.enabled = this.query.enabled
|
||||||
|
const res = await request({ url: '/admin/user/list', method: 'get', params })
|
||||||
this.list = res.data || []
|
this.list = res.data || []
|
||||||
} finally {
|
} finally {
|
||||||
this.loading = false
|
this.loading = false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
async loadGroups() {
|
||||||
|
try {
|
||||||
|
const res = await request({ url: '/admin/group/options', method: 'get' })
|
||||||
|
this.groups = res.data || []
|
||||||
|
} catch (e) {}
|
||||||
|
},
|
||||||
showAdd() {
|
showAdd() {
|
||||||
this.dialogTitle = '新建用户'
|
this.dialogTitle = '新建用户'
|
||||||
this.form = { name: '', mobile: '', email: '', type: 'user', groupName: '' }
|
this.form = { name: '', mobile: '', email: '', type: 'user', groupName: '' }
|
||||||
|
|||||||
Reference in New Issue
Block a user