package app import ( "context" "database/sql" "net/http" "regexp" "sort" "strings" "time" ) type adminRoleView struct { ID int64 `json:"id"` Code string `json:"code"` Name string `json:"name"` Description string `json:"description"` BuiltIn bool `json:"builtIn"` Status int `json:"status"` Permissions []string `json:"permissions"` } var adminAccountPattern = regexp.MustCompile(`^[A-Za-z][A-Za-z0-9_.-]{2,49}$`) var adminRoleCodePattern = regexp.MustCompile(`^[a-z][a-z0-9_]{2,49}$`) func (a *App) adminAccess(ctx context.Context, adminID int64) ([]string, []string, error) { roleRows, err := a.db.QueryContext(ctx, `SELECT DISTINCT r.role_code FROM admin_user_roles ur JOIN admin_roles r ON r.id=ur.role_id WHERE ur.admin_user_id=? AND r.status=1 ORDER BY r.role_code`, adminID) if err != nil { return nil, nil, err } roles := []string{} for roleRows.Next() { var code string if err = roleRows.Scan(&code); err != nil { _ = roleRows.Close() return nil, nil, err } roles = append(roles, code) } if err = roleRows.Close(); err != nil { return nil, nil, err } permissionRows, err := a.db.QueryContext(ctx, `SELECT DISTINCT rp.permission_code FROM admin_user_roles ur JOIN admin_roles r ON r.id=ur.role_id AND r.status=1 JOIN admin_role_permissions rp ON rp.role_id=r.id WHERE ur.admin_user_id=? ORDER BY rp.permission_code`, adminID) if err != nil { return nil, nil, err } permissions := []string{} for permissionRows.Next() { var code string if err = permissionRows.Scan(&code); err != nil { _ = permissionRows.Close() return nil, nil, err } permissions = append(permissions, code) } err = permissionRows.Close() return roles, permissions, err } func (a *App) hasAdminPermission(ctx context.Context, adminID int64, permission string) bool { var allowed int err := a.db.QueryRowContext(ctx, `SELECT EXISTS( SELECT 1 FROM admin_user_roles ur JOIN admin_roles r ON r.id=ur.role_id AND r.status=1 JOIN admin_role_permissions rp ON rp.role_id=r.id WHERE ur.admin_user_id=? AND rp.permission_code=?)`, adminID, permission).Scan(&allowed) return err == nil && allowed == 1 } func (a *App) requireAdminPermission(permission string, next http.HandlerFunc) http.HandlerFunc { return a.requireAuth("admin", func(w http.ResponseWriter, r *http.Request) { if !a.hasAdminPermission(r.Context(), current(r).ID, permission) { fail(w, http.StatusForbidden, 10006, "没有执行该操作的权限") return } next(w, r) }) } func (a *App) adminCodes(w http.ResponseWriter, r *http.Request) { _, permissions, err := a.adminAccess(r.Context(), current(r).ID) if err != nil { fail(w, http.StatusInternalServerError, 50001, "读取权限失败") return } reply(w, permissions) } func (a *App) adminRoleIDs(ctx context.Context, adminID int64) []int64 { rows, err := a.db.QueryContext(ctx, `SELECT role_id FROM admin_user_roles WHERE admin_user_id=? ORDER BY role_id`, adminID) if err != nil { return []int64{} } defer rows.Close() result := []int64{} for rows.Next() { var id int64 if rows.Scan(&id) == nil { result = append(result, id) } } return result } func (a *App) adminAccounts(w http.ResponseWriter, r *http.Request) { page, size, offset := pagination(r) keyword := strings.TrimSpace(r.URL.Query().Get("keyword")) where := "" args := []any{} if keyword != "" { where = ` WHERE username LIKE ? OR real_name LIKE ?` like := "%" + keyword + "%" args = append(args, like, like) } var total int64 if err := a.db.QueryRowContext(r.Context(), `SELECT COUNT(*) FROM admin_users`+where, args...).Scan(&total); err != nil { fail(w, http.StatusInternalServerError, 50001, "查询管理员失败") return } queryArgs := append(append([]any{}, args...), size, offset) rows, err := a.db.QueryContext(r.Context(), `SELECT id,username,real_name,avatar_url,status,last_login_at,password_changed_at,created_at FROM admin_users`+where+` ORDER BY id LIMIT ? OFFSET ?`, queryArgs...) if err != nil { fail(w, http.StatusInternalServerError, 50001, "查询管理员失败") return } defer rows.Close() items := []map[string]any{} for rows.Next() { var id int64 var username, realName, avatar string var status int var lastLogin, passwordChanged sql.NullTime var createdAt time.Time if err = rows.Scan(&id, &username, &realName, &avatar, &status, &lastLogin, &passwordChanged, &createdAt); err != nil { fail(w, http.StatusInternalServerError, 50001, "读取管理员失败") return } roles, _, _ := a.adminAccess(r.Context(), id) items = append(items, map[string]any{ "id": id, "username": username, "realName": realName, "avatar": avatar, "status": status, "roles": roles, "roleIds": a.adminRoleIDs(r.Context(), id), "lastLoginAt": nullableTime(lastLogin), "passwordChangedAt": nullableTime(passwordChanged), "createdAt": createdAt, }) } reply(w, pageResult{Items: items, Total: total, Page: page, Size: size}) } func validateAdminRoleIDs(ctx context.Context, tx *sql.Tx, roleIDs []int64) error { if len(roleIDs) == 0 { return sql.ErrNoRows } seen := map[int64]bool{} for _, roleID := range roleIDs { if roleID <= 0 || seen[roleID] { return sql.ErrNoRows } seen[roleID] = true var status int if err := tx.QueryRowContext(ctx, `SELECT status FROM admin_roles WHERE id=?`, roleID).Scan(&status); err != nil || status != 1 { return sql.ErrNoRows } } return nil } func replaceAdminRoles(ctx context.Context, tx *sql.Tx, adminID int64, roleIDs []int64) error { if err := validateAdminRoleIDs(ctx, tx, roleIDs); err != nil { return err } if _, err := tx.ExecContext(ctx, `DELETE FROM admin_user_roles WHERE admin_user_id=?`, adminID); err != nil { return err } for _, roleID := range roleIDs { if _, err := tx.ExecContext(ctx, `INSERT INTO admin_user_roles(admin_user_id,role_id) VALUES(?,?)`, adminID, roleID); err != nil { return err } } return nil } func (a *App) adminCreateAccount(w http.ResponseWriter, r *http.Request) { var req struct { Username string `json:"username"` RealName string `json:"realName"` Password string `json:"password"` Avatar string `json:"avatar"` RoleIDs []int64 `json:"roleIds"` } if decode(r, &req) != nil { fail(w, http.StatusBadRequest, 20001, "管理员资料格式错误") return } req.Username = strings.TrimSpace(req.Username) req.RealName = strings.TrimSpace(req.RealName) if !adminAccountPattern.MatchString(req.Username) || req.RealName == "" || len([]rune(req.RealName)) > 50 { fail(w, http.StatusBadRequest, 20001, "账号或姓名格式无效") return } if req.Password == "" { fail(w, http.StatusBadRequest, 20001, "请输入密码") return } hash, err := hashPassword(req.Password) if err != nil { fail(w, http.StatusInternalServerError, 50001, "密码加密失败") return } tx, err := a.db.BeginTx(r.Context(), &sql.TxOptions{Isolation: sql.LevelReadCommitted}) if err != nil { fail(w, http.StatusInternalServerError, 50001, "创建管理员失败") return } defer func() { _ = tx.Rollback() }() if err = validateAdminRoleIDs(r.Context(), tx, req.RoleIDs); err != nil { fail(w, http.StatusBadRequest, 20001, "请至少选择一个有效角色") return } result, err := tx.ExecContext(r.Context(), `INSERT INTO admin_users(username,password_hash,real_name,avatar_url,status,password_changed_at) VALUES(?,?,?,?,1,NOW(3))`, req.Username, hash, req.RealName, strings.TrimSpace(req.Avatar)) if err != nil { fail(w, http.StatusConflict, 20001, "管理员账号已存在") return } adminID, _ := result.LastInsertId() if err = replaceAdminRoles(r.Context(), tx, adminID, req.RoleIDs); err != nil || tx.Commit() != nil { fail(w, http.StatusInternalServerError, 50001, "保存管理员角色失败") return } a.audit(r, "create", "admin_user", adminID, map[string]any{"username": req.Username, "roleIds": req.RoleIDs}) reply(w, map[string]any{"id": adminID, "success": true}) } func containsInt64(values []int64, expected int64) bool { for _, value := range values { if value == expected { return true } } return false } func (a *App) ensureSuperAdminRemains(ctx context.Context, tx *sql.Tx, targetID int64, newStatus int, roleIDs []int64) error { var superRoleID int64 if err := tx.QueryRowContext(ctx, `SELECT id FROM admin_roles WHERE role_code='super_admin'`).Scan(&superRoleID); err != nil { return err } var targetWasSuper int if err := tx.QueryRowContext(ctx, `SELECT EXISTS(SELECT 1 FROM admin_user_roles WHERE admin_user_id=? AND role_id=?)`, targetID, superRoleID).Scan(&targetWasSuper); err != nil { return err } if targetWasSuper == 0 || (newStatus == 1 && containsInt64(roleIDs, superRoleID)) { return nil } var remaining int if err := tx.QueryRowContext(ctx, `SELECT COUNT(DISTINCT a.id) FROM admin_users a JOIN admin_user_roles ur ON ur.admin_user_id=a.id WHERE a.status=1 AND a.id<>? AND ur.role_id=?`, targetID, superRoleID).Scan(&remaining); err != nil { return err } if remaining == 0 { return sql.ErrNoRows } return nil } func (a *App) adminUpdateAccount(w http.ResponseWriter, r *http.Request) { adminID, err := pathID(r) if err != nil { fail(w, http.StatusBadRequest, 20001, "管理员 ID 无效") return } var req struct { RealName string `json:"realName"` Avatar string `json:"avatar"` Password string `json:"password"` Status int `json:"status"` RoleIDs []int64 `json:"roleIds"` } if decode(r, &req) != nil || strings.TrimSpace(req.RealName) == "" || (req.Status != 0 && req.Status != 1) { fail(w, http.StatusBadRequest, 20001, "管理员资料格式错误") return } if current(r).ID == adminID && req.Status != 1 { fail(w, http.StatusBadRequest, 20001, "不能停用当前登录账号") return } tx, err := a.db.BeginTx(r.Context(), &sql.TxOptions{Isolation: sql.LevelReadCommitted}) if err != nil { fail(w, http.StatusInternalServerError, 50001, "更新管理员失败") return } defer func() { _ = tx.Rollback() }() var exists int64 if err = tx.QueryRowContext(r.Context(), `SELECT id FROM admin_users WHERE id=? FOR UPDATE`, adminID).Scan(&exists); err != nil { fail(w, http.StatusNotFound, 30001, "管理员不存在") return } if err = validateAdminRoleIDs(r.Context(), tx, req.RoleIDs); err != nil { fail(w, http.StatusBadRequest, 20001, "请至少选择一个有效角色") return } if err = a.ensureSuperAdminRemains(r.Context(), tx, adminID, req.Status, req.RoleIDs); err != nil { fail(w, http.StatusBadRequest, 20001, "必须保留至少一个启用的超级管理员") return } if _, err = tx.ExecContext(r.Context(), `UPDATE admin_users SET real_name=?,avatar_url=?,status=? WHERE id=?`, strings.TrimSpace(req.RealName), strings.TrimSpace(req.Avatar), req.Status, adminID); err != nil { fail(w, http.StatusInternalServerError, 50001, "更新管理员失败") return } if err = replaceAdminRoles(r.Context(), tx, adminID, req.RoleIDs); err != nil { fail(w, http.StatusInternalServerError, 50001, "更新管理员角色失败") return } passwordChanged := false if req.Password != "" { hash, hashErr := hashPassword(req.Password) if hashErr != nil { fail(w, http.StatusInternalServerError, 50001, "密码加密失败") return } if _, err = tx.ExecContext(r.Context(), `UPDATE admin_users SET password_hash=?,token_version=token_version+1,password_changed_at=NOW(3) WHERE id=?`, hash, adminID); err != nil { fail(w, http.StatusInternalServerError, 50001, "更新管理员密码失败") return } passwordChanged = true } if req.Status != 1 || passwordChanged { if _, err = tx.ExecContext(r.Context(), `UPDATE admin_sessions SET revoked_at=NOW(3) WHERE admin_user_id=? AND revoked_at IS NULL`, adminID); err != nil { fail(w, http.StatusInternalServerError, 50001, "撤销管理员会话失败") return } } if err = tx.Commit(); err != nil { fail(w, http.StatusInternalServerError, 50001, "更新管理员失败") return } a.audit(r, "update", "admin_user", adminID, map[string]any{"roleIds": req.RoleIDs, "status": req.Status, "passwordChanged": passwordChanged}) reply(w, map[string]bool{"success": true}) } func (a *App) rolePermissions(ctx context.Context, roleID int64) []string { rows, err := a.db.QueryContext(ctx, `SELECT permission_code FROM admin_role_permissions WHERE role_id=? ORDER BY permission_code`, roleID) if err != nil { return []string{} } defer rows.Close() permissions := []string{} for rows.Next() { var code string if rows.Scan(&code) == nil { permissions = append(permissions, code) } } return permissions } func (a *App) adminRoles(w http.ResponseWriter, r *http.Request) { rows, err := a.db.QueryContext(r.Context(), `SELECT id,role_code,role_name,description,built_in,status FROM admin_roles ORDER BY built_in DESC,id`) if err != nil { fail(w, http.StatusInternalServerError, 50001, "查询角色失败") return } defer rows.Close() items := []adminRoleView{} for rows.Next() { var item adminRoleView var builtIn int if err = rows.Scan(&item.ID, &item.Code, &item.Name, &item.Description, &builtIn, &item.Status); err != nil { fail(w, http.StatusInternalServerError, 50001, "读取角色失败") return } item.BuiltIn = builtIn == 1 item.Permissions = a.rolePermissions(r.Context(), item.ID) items = append(items, item) } reply(w, map[string]any{"items": items}) } func (a *App) adminPermissions(w http.ResponseWriter, r *http.Request) { rows, err := a.db.QueryContext(r.Context(), `SELECT permission_code,permission_name,permission_group FROM admin_permissions ORDER BY permission_group,sort_order,permission_code`) if err != nil { fail(w, http.StatusInternalServerError, 50001, "查询权限失败") return } defer rows.Close() items := []map[string]string{} for rows.Next() { var code, name, group string if rows.Scan(&code, &name, &group) == nil { items = append(items, map[string]string{"code": code, "name": name, "group": group}) } } reply(w, map[string]any{"items": items}) } func validatePermissionCodes(ctx context.Context, tx *sql.Tx, permissions []string) error { if len(permissions) == 0 { return sql.ErrNoRows } seen := map[string]bool{} for _, permission := range permissions { permission = strings.TrimSpace(permission) if permission == "" || seen[permission] { return sql.ErrNoRows } seen[permission] = true var exists int if err := tx.QueryRowContext(ctx, `SELECT EXISTS(SELECT 1 FROM admin_permissions WHERE permission_code=?)`, permission).Scan(&exists); err != nil || exists != 1 { return sql.ErrNoRows } } return nil } func replaceRolePermissions(ctx context.Context, tx *sql.Tx, roleID int64, permissions []string) error { if err := validatePermissionCodes(ctx, tx, permissions); err != nil { return err } if _, err := tx.ExecContext(ctx, `DELETE FROM admin_role_permissions WHERE role_id=?`, roleID); err != nil { return err } sort.Strings(permissions) for _, permission := range permissions { if _, err := tx.ExecContext(ctx, `INSERT INTO admin_role_permissions(role_id,permission_code) VALUES(?,?)`, roleID, permission); err != nil { return err } } return nil } func (a *App) adminCreateRole(w http.ResponseWriter, r *http.Request) { a.adminSaveRole(w, r, 0) } func (a *App) adminUpdateRole(w http.ResponseWriter, r *http.Request) { roleID, err := pathID(r) if err != nil { fail(w, http.StatusBadRequest, 20001, "角色 ID 无效") return } a.adminSaveRole(w, r, roleID) } func (a *App) adminSaveRole(w http.ResponseWriter, r *http.Request, roleID int64) { var req struct { Code string `json:"code"` Name string `json:"name"` Description string `json:"description"` Permissions []string `json:"permissions"` Status int `json:"status"` } if decode(r, &req) != nil { fail(w, http.StatusBadRequest, 20001, "角色资料格式错误") return } req.Code = strings.TrimSpace(req.Code) req.Name = strings.TrimSpace(req.Name) if !adminRoleCodePattern.MatchString(req.Code) || req.Name == "" || len([]rune(req.Name)) > 80 || (req.Status != 0 && req.Status != 1) { fail(w, http.StatusBadRequest, 20001, "角色编码、名称或状态无效") return } tx, err := a.db.BeginTx(r.Context(), &sql.TxOptions{Isolation: sql.LevelReadCommitted}) if err != nil { fail(w, http.StatusInternalServerError, 50001, "保存角色失败") return } defer func() { _ = tx.Rollback() }() if err = validatePermissionCodes(r.Context(), tx, req.Permissions); err != nil { fail(w, http.StatusBadRequest, 20001, "请至少选择一个有效权限") return } if roleID == 0 { result, insertErr := tx.ExecContext(r.Context(), `INSERT INTO admin_roles(role_code,role_name,description,status) VALUES(?,?,?,?)`, req.Code, req.Name, strings.TrimSpace(req.Description), req.Status) if insertErr != nil { fail(w, http.StatusConflict, 20001, "角色编码已存在") return } roleID, _ = result.LastInsertId() } else { var builtIn int if err = tx.QueryRowContext(r.Context(), `SELECT built_in FROM admin_roles WHERE id=? FOR UPDATE`, roleID).Scan(&builtIn); err != nil { fail(w, http.StatusNotFound, 30001, "角色不存在") return } if builtIn == 1 { fail(w, http.StatusBadRequest, 20001, "内置超级管理员角色不可修改") return } if _, err = tx.ExecContext(r.Context(), `UPDATE admin_roles SET role_code=?,role_name=?,description=?,status=? WHERE id=?`, req.Code, req.Name, strings.TrimSpace(req.Description), req.Status, roleID); err != nil { fail(w, http.StatusConflict, 20001, "角色编码已存在") return } } if err = replaceRolePermissions(r.Context(), tx, roleID, req.Permissions); err != nil || tx.Commit() != nil { fail(w, http.StatusInternalServerError, 50001, "保存角色权限失败") return } a.audit(r, "save", "admin_role", roleID, map[string]any{"code": req.Code, "permissions": req.Permissions, "status": req.Status}) reply(w, map[string]any{"id": roleID, "success": true}) } func (a *App) adminDeleteRole(w http.ResponseWriter, r *http.Request) { roleID, err := pathID(r) if err != nil { fail(w, http.StatusBadRequest, 20001, "角色 ID 无效") return } var builtIn, assigned int if err = a.db.QueryRowContext(r.Context(), `SELECT built_in,(SELECT COUNT(*) FROM admin_user_roles WHERE role_id=admin_roles.id) FROM admin_roles WHERE id=?`, roleID).Scan(&builtIn, &assigned); err != nil { fail(w, http.StatusNotFound, 30001, "角色不存在") return } if builtIn == 1 || assigned > 0 { fail(w, http.StatusBadRequest, 20001, "内置角色或仍被管理员使用的角色不能删除") return } if _, err = a.db.ExecContext(r.Context(), `DELETE FROM admin_roles WHERE id=?`, roleID); err != nil { fail(w, http.StatusInternalServerError, 50001, "删除角色失败") return } a.audit(r, "delete", "admin_role", roleID, nil) reply(w, map[string]bool{"success": true}) }