Browse Source

1、修改redis为公司的
2、修改后端端口为18888
3、新增管理员管理模块
4、更新用户、穴位、艾灸手法、方案等模块
5、更新前端路由、布局及相关页面

Made-with: Cursor

jiapu 5 months ago
parent
commit
67648fcc60
39 changed files with 1811 additions and 104 deletions
  1. 4 1
      .cursor/rules/project-info.mdc
  2. 18 0
      .idea/compiler.xml
  3. 6 0
      .idea/encodings.xml
  4. 25 0
      .idea/jarRepositories.xml
  5. 10 1
      .idea/misc.xml
  6. 11 1
      code/backend/src/main/java/com/aijiuyi/admin/common/constant/ResultCode.java
  7. 5 2
      code/backend/src/main/java/com/aijiuyi/admin/common/interceptor/TokenInterceptor.java
  8. 10 10
      code/backend/src/main/java/com/aijiuyi/admin/common/service/impl/SysLogServiceImpl.java
  9. 22 0
      code/backend/src/main/java/com/aijiuyi/admin/common/util/TokenUtil.java
  10. 18 0
      code/backend/src/main/java/com/aijiuyi/admin/controller/AcupointController.java
  11. 147 0
      code/backend/src/main/java/com/aijiuyi/admin/controller/AdminController.java
  12. 22 0
      code/backend/src/main/java/com/aijiuyi/admin/controller/AppUserController.java
  13. 27 11
      code/backend/src/main/java/com/aijiuyi/admin/controller/AuthController.java
  14. 18 0
      code/backend/src/main/java/com/aijiuyi/admin/controller/MoxibustionTechniqueController.java
  15. 28 0
      code/backend/src/main/java/com/aijiuyi/admin/controller/dto/AdminQueryDTO.java
  16. 1 1
      code/backend/src/main/java/com/aijiuyi/admin/controller/dto/AppUserQueryDTO.java
  17. 41 0
      code/backend/src/main/java/com/aijiuyi/admin/entity/AdminPermission.java
  18. 22 3
      code/backend/src/main/java/com/aijiuyi/admin/entity/AppUser.java
  19. 1 1
      code/backend/src/main/java/com/aijiuyi/admin/entity/Plan.java
  20. 12 0
      code/backend/src/main/java/com/aijiuyi/admin/mapper/AdminPermissionMapper.java
  21. 83 0
      code/backend/src/main/java/com/aijiuyi/admin/service/AdminService.java
  22. 22 0
      code/backend/src/main/java/com/aijiuyi/admin/service/AppUserService.java
  23. 236 0
      code/backend/src/main/java/com/aijiuyi/admin/service/impl/AdminServiceImpl.java
  24. 165 5
      code/backend/src/main/java/com/aijiuyi/admin/service/impl/AppUserServiceImpl.java
  25. 1 1
      code/backend/src/main/resources/application.yml
  26. 36 30
      code/backend/src/main/resources/sql/init.sql
  27. 8 0
      code/frontend/src/api/acupoint.js
  28. 80 0
      code/frontend/src/api/admin.js
  29. 9 0
      code/frontend/src/api/auth.js
  30. 3 0
      code/frontend/src/api/index.js
  31. 8 0
      code/frontend/src/api/moxibustion.js
  32. 3 1
      code/frontend/src/layout/index.vue
  33. 14 14
      code/frontend/src/router/index.js
  34. 41 3
      code/frontend/src/store/user.js
  35. 494 0
      code/frontend/src/views/admin/index.vue
  36. 120 13
      code/frontend/src/views/plan/index.vue
  37. 10 5
      code/frontend/src/views/user/app-user/index.vue
  38. 1 1
      code/frontend/vite.config.js
  39. 29 0
      code/管理员管理.md

+ 4 - 1
.cursor/rules/project-info.mdc

@@ -7,7 +7,10 @@ alwaysApply: true
 
 - 项目名称: aijiuyi
 - Git 仓库: http://git.iwintrue.com/liaoxg/ajy
-- 分支:从liyu_2026_04_07分支拉取代码,将代码放在一个文件夹:aijiuyi中,然后迁出一个分支:jiapu_aijiuyi分支,后续我将把代码同步到jiapu_aijiuyi这个分支。
+- 同事分支:liyu_2026_04_07
+- 我的分支:jiapu_aijiuyi
 - 项目路径(仓库内): code/aijiuyi
 - Git 用户名: jiapu
 - 分支: jiapu_aijiuyi
+- 拉取代码规则:将同事分支的openspec文件夹下的所有文件(不包括openspec目录)同步到我的code目录下。
+- git提交日志:在每次提交的时候要询问我,如果我选择空白,则你可以自己总结边写日志

+ 18 - 0
.idea/compiler.xml

@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="CompilerConfiguration">
+    <annotationProcessing>
+      <profile name="Maven default annotation processors profile" enabled="true">
+        <sourceOutputDir name="target/generated-sources/annotations" />
+        <sourceTestOutputDir name="target/generated-test-sources/test-annotations" />
+        <outputRelativeToContentRoot value="true" />
+        <module name="aijiuyi-admin" />
+      </profile>
+    </annotationProcessing>
+  </component>
+  <component name="JavacSettings">
+    <option name="ADDITIONAL_OPTIONS_OVERRIDE">
+      <module name="aijiuyi-admin" options="-parameters" />
+    </option>
+  </component>
+</project>

+ 6 - 0
.idea/encodings.xml

@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="Encoding">
+    <file url="file://$PROJECT_DIR$/code/backend/src/main/java" charset="UTF-8" />
+  </component>
+</project>

+ 25 - 0
.idea/jarRepositories.xml

@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="RemoteRepositoriesConfiguration">
+    <remote-repository>
+      <option name="id" value="nexus" />
+      <option name="name" value="Repository for nexus" />
+      <option name="url" value="http://mvn.iwintrue.com/nexus/content/groups/public/" />
+    </remote-repository>
+    <remote-repository>
+      <option name="id" value="central" />
+      <option name="name" value="Central Repository" />
+      <option name="url" value="http://mvn.iwintrue.com/nexus/content/groups/public/" />
+    </remote-repository>
+    <remote-repository>
+      <option name="id" value="central" />
+      <option name="name" value="Maven Central repository" />
+      <option name="url" value="https://repo1.maven.org/maven2" />
+    </remote-repository>
+    <remote-repository>
+      <option name="id" value="jboss.community" />
+      <option name="name" value="JBoss Community repository" />
+      <option name="url" value="https://repository.jboss.org/nexus/content/repositories/public/" />
+    </remote-repository>
+  </component>
+</project>

+ 10 - 1
.idea/misc.xml

@@ -1,6 +1,15 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <project version="4">
-  <component name="ProjectRootManager">
+  <component name="ExternalStorageConfigurationManager" enabled="true" />
+  <component name="MavenProjectsManager">
+    <option name="originalFiles">
+      <list>
+        <option value="$PROJECT_DIR$/moxibustion/pom.xml" />
+        <option value="$PROJECT_DIR$/code/backend/pom.xml" />
+      </list>
+    </option>
+  </component>
+  <component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" default="true" project-jdk-name="corretto-1.8" project-jdk-type="JavaSDK">
     <output url="file://$PROJECT_DIR$/out" />
   </component>
 </project>

+ 11 - 1
code/backend/src/main/java/com/aijiuyi/admin/common/constant/ResultCode.java

@@ -101,7 +101,17 @@ public enum ResultCode {
     /** 用户方案不存在 */
     USER_PLAN_NOT_FOUND(1705, "用户方案不存在"),
     /** 方案步骤至少保留1个 */
-    PLAN_STEP_MIN_ERROR(1706, "方案步骤至少保留1个");
+    PLAN_STEP_MIN_ERROR(1706, "方案步骤至少保留1个"),
+
+    // ======================== 管理员管理相关 1800-1899 ========================
+    /** 管理员不存在 */
+    ADMIN_NOT_FOUND(1800, "管理员不存在"),
+    /** 不能删除超级管理员 */
+    ADMIN_CANNOT_DELETE(1801, "超级管理员不可删除"),
+    /** 不能禁用自身账号 */
+    ADMIN_CANNOT_DISABLE_SELF(1802, "不能禁用当前登录账号"),
+    /** 管理员角色不合法 */
+    ADMIN_ROLE_INVALID(1803, "管理员角色不合法");
 
     // ======================== 其他业务错误码在此区间之后继续添加 ========================
 

+ 5 - 2
code/backend/src/main/java/com/aijiuyi/admin/common/interceptor/TokenInterceptor.java

@@ -116,10 +116,13 @@ public class TokenInterceptor implements HandlerInterceptor {
         // 将用户信息存入 ThreadLocal,供 Service 层使用
         Long userId = ((Number) claims.get("userId")).longValue();
         String username = (String) claims.get("username");
+        // 从 JWT Claims 中读取 userType(兼容旧 token 默认为 3=管理员)
+        Integer userType = claims.get("userType") != null
+                ? ((Number) claims.get("userType")).intValue()
+                : 3;
         RequestContext.setUserId(userId);
         RequestContext.setUserName(username);
-        // 当前 Token 体系为系统管理员(sys_user),固定设置 userType=1
-        RequestContext.setUserType(1);
+        RequestContext.setUserType(userType);
 
         LogUtil.debug(TokenInterceptor.class, "用户[{}]请求接口[{}]", username, requestUri);
         return true;

+ 10 - 10
code/backend/src/main/java/com/aijiuyi/admin/common/service/impl/SysLogServiceImpl.java

@@ -4,9 +4,9 @@ import com.aijiuyi.admin.common.entity.SysLog;
 import com.aijiuyi.admin.common.service.SysLogService;
 import com.aijiuyi.admin.common.util.LogUtil;
 import com.aijiuyi.admin.controller.dto.SysLogQueryDTO;
-import com.aijiuyi.admin.entity.SysUser;
+import com.aijiuyi.admin.entity.AppUser;
+import com.aijiuyi.admin.mapper.AppUserMapper;
 import com.aijiuyi.admin.mapper.SysLogMapper;
-import com.aijiuyi.admin.mapper.SysUserMapper;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.core.metadata.IPage;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
@@ -30,7 +30,7 @@ public class SysLogServiceImpl implements SysLogService {
     private SysLogMapper sysLogMapper;
 
     @Autowired
-    private SysUserMapper sysUserMapper;
+    private AppUserMapper appUserMapper;
 
     /**
      * 异步保存操作日志
@@ -73,20 +73,20 @@ public class SysLogServiceImpl implements SysLogService {
     }
 
     /**
-     * 获取系统管理员选项列表(用于前端操作人下拉框)
-     * 返回所有未删除的系统用户的 id、username 和 nickname
+     * 获取用户选项列表(用于前端操作人下拉框)
+     * 返回所有未删除的用户的 id、username 和 nickname
      *
      * @return 选项列表,每项含 id、label(nickname 或 username)
      */
     @Override
     public List<Map<String, Object>> getSysUserOptions() {
-        List<SysUser> users = sysUserMapper.selectList(
-                new LambdaQueryWrapper<SysUser>()
-                        .select(SysUser::getId, SysUser::getUsername, SysUser::getNickname)
-                        .orderByAsc(SysUser::getId)
+        List<AppUser> users = appUserMapper.selectList(
+                new LambdaQueryWrapper<AppUser>()
+                        .select(AppUser::getId, AppUser::getUsername, AppUser::getNickname)
+                        .orderByAsc(AppUser::getId)
         );
         List<Map<String, Object>> options = new ArrayList<>();
-        for (SysUser user : users) {
+        for (AppUser user : users) {
             Map<String, Object> option = new HashMap<>();
             option.put("id", user.getId());
             // 优先展示昵称,无昵称则展示用户名

+ 22 - 0
code/backend/src/main/java/com/aijiuyi/admin/common/util/TokenUtil.java

@@ -25,6 +25,28 @@ public class TokenUtil {
     /** Bearer 前缀 */
     private static final String TOKEN_PREFIX = "Bearer ";
 
+    /**
+     * 生成 JWT Token(携带 userType)
+     *
+     * @param userId   用户ID
+     * @param username 用户名
+     * @param userType 用户类型(角色)
+     * @return JWT Token 字符串
+     */
+    public static String generateToken(Long userId, String username, Integer userType) {
+        Map<String, Object> claims = new HashMap<>(4);
+        claims.put("userId", userId);
+        claims.put("username", username);
+        claims.put("userType", userType);
+
+        return Jwts.builder()
+                .setClaims(claims)
+                .setIssuedAt(new Date())
+                .setExpiration(new Date(System.currentTimeMillis() + EXPIRATION))
+                .signWith(SignatureAlgorithm.HS256, SECRET)
+                .compact();
+    }
+
     /**
      * 生成 JWT Token
      *

+ 18 - 0
code/backend/src/main/java/com/aijiuyi/admin/controller/AcupointController.java

@@ -6,6 +6,7 @@ import com.aijiuyi.admin.common.enums.OperationType;
 import com.aijiuyi.admin.controller.dto.AcupointQueryDTO;
 import com.aijiuyi.admin.entity.Acupoint;
 import com.aijiuyi.admin.service.AcupointService;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.core.metadata.IPage;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.web.bind.annotation.*;
@@ -23,6 +24,23 @@ public class AcupointController {
     @Autowired
     private AcupointService acupointService;
 
+    /**
+     * 获取所有启用穴位列表(用于选择框)
+     *
+     * @return 穴位列表(id + name)
+     */
+    @GetMapping("/list")
+    @Log(value = "获取穴位选项列表", module = "穴位管理", operationType = OperationType.QUERY)
+    public Result<List<Acupoint>> list() {
+        List<Acupoint> list = acupointService.list(
+                new LambdaQueryWrapper<Acupoint>()
+                        .eq(Acupoint::getStatus, 1)
+                        .select(Acupoint::getId, Acupoint::getName)
+                        .orderByAsc(Acupoint::getId)
+        );
+        return Result.success(list);
+    }
+
     /**
      * 分页查询穴位列表
      *

+ 147 - 0
code/backend/src/main/java/com/aijiuyi/admin/controller/AdminController.java

@@ -0,0 +1,147 @@
+package com.aijiuyi.admin.controller;
+
+import com.aijiuyi.admin.common.annotation.Log;
+import com.aijiuyi.admin.common.entity.Result;
+import com.aijiuyi.admin.common.enums.OperationType;
+import com.aijiuyi.admin.controller.dto.AdminQueryDTO;
+import com.aijiuyi.admin.entity.AppUser;
+import com.aijiuyi.admin.service.AdminService;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 管理员管理 Controller
+ * 提供管理员账号的增删改查、状态管理及页面权限配置接口
+ */
+@RestController
+@RequestMapping("/admin")
+public class AdminController {
+
+    @Autowired
+    private AdminService adminService;
+
+    /**
+     * 分页查询管理员列表
+     *
+     * @param queryDTO 查询参数(用户名/姓名模糊、角色、状态)
+     * @return 分页管理员列表
+     */
+    @GetMapping("/page")
+    @Log(value = "查询管理员列表", module = "管理员管理", operationType = OperationType.QUERY)
+    public Result<IPage<AppUser>> page(AdminQueryDTO queryDTO) {
+        return Result.success(adminService.pageList(queryDTO));
+    }
+
+    /**
+     * 查询管理员详情
+     *
+     * @param id 管理员ID
+     * @return 管理员信息
+     */
+    @GetMapping("/{id}")
+    @Log(value = "查询管理员详情", module = "管理员管理", operationType = OperationType.QUERY)
+    public Result<AppUser> getById(@PathVariable Long id) {
+        return Result.success(adminService.getById(id));
+    }
+
+    /**
+     * 新增管理员
+     *
+     * @param admin 管理员信息(username/password/nickname/adminRole/status 必填)
+     * @return 操作结果
+     */
+    @PostMapping
+    @Log(value = "新增管理员", module = "管理员管理", operationType = OperationType.INSERT)
+    public Result<Void> add(@RequestBody AppUser admin) {
+        adminService.addAdmin(admin);
+        return Result.success();
+    }
+
+    /**
+     * 修改管理员信息
+     *
+     * @param admin 管理员信息(需携带 id;密码为空则不修改)
+     * @return 操作结果
+     */
+    @PutMapping
+    @Log(value = "修改管理员信息", module = "管理员管理", operationType = OperationType.UPDATE)
+    public Result<Void> update(@RequestBody AppUser admin) {
+        adminService.updateAdmin(admin);
+        return Result.success();
+    }
+
+    /**
+     * 删除管理员(逻辑删除,超级管理员不可删除)
+     *
+     * @param id 管理员ID
+     * @return 操作结果
+     */
+    @DeleteMapping("/{id}")
+    @Log(value = "删除管理员", module = "管理员管理", operationType = OperationType.DELETE)
+    public Result<Void> delete(@PathVariable Long id) {
+        adminService.deleteAdmin(id);
+        return Result.success();
+    }
+
+    /**
+     * 批量删除管理员
+     *
+     * @param ids 管理员ID列表
+     * @return 操作结果
+     */
+    @DeleteMapping("/batch")
+    @Log(value = "批量删除管理员", module = "管理员管理", operationType = OperationType.DELETE)
+    public Result<Void> batchDelete(@RequestBody List<Long> ids) {
+        adminService.batchDeleteAdmin(ids);
+        return Result.success();
+    }
+
+    /**
+     * 修改管理员状态(禁用/启用)
+     *
+     * @param id     管理员ID
+     * @param status 目标状态:1=启用,0=禁用
+     * @return 操作结果
+     */
+    @PutMapping("/{id}/status")
+    @Log(value = "修改管理员状态", module = "管理员管理", operationType = OperationType.UPDATE)
+    public Result<Void> updateStatus(@PathVariable Long id, @RequestParam Integer status) {
+        adminService.updateStatus(id, status);
+        return Result.success();
+    }
+
+    /**
+     * 查询管理员权限路由列表
+     * 超级管理员返回 null,前端视为全部权限
+     *
+     * @param id 管理员ID
+     * @return 路由路径 JSON 数组字符串
+     */
+    @GetMapping("/{id}/permission")
+    @Log(value = "查询管理员权限", module = "管理员管理", operationType = OperationType.QUERY)
+    public Result<Map<String, Object>> getPermission(@PathVariable Long id) {
+        String routePaths = adminService.getPermissions(id);
+        Map<String, Object> result = new HashMap<>(2);
+        result.put("routePaths", routePaths);
+        return Result.success(result);
+    }
+
+    /**
+     * 更新管理员权限路由列表
+     *
+     * @param id      管理员ID
+     * @param payload 请求体,包含 routePaths(JSON数组字符串)
+     * @return 操作结果
+     */
+    @PutMapping("/{id}/permission")
+    @Log(value = "更新管理员权限", module = "管理员管理", operationType = OperationType.UPDATE)
+    public Result<Void> updatePermission(@PathVariable Long id, @RequestBody Map<String, String> payload) {
+        adminService.updatePermissions(id, payload.get("routePaths"));
+        return Result.success();
+    }
+}

+ 22 - 0
code/backend/src/main/java/com/aijiuyi/admin/controller/AppUserController.java

@@ -6,10 +6,13 @@ import com.aijiuyi.admin.common.enums.OperationType;
 import com.aijiuyi.admin.controller.dto.AppUserQueryDTO;
 import com.aijiuyi.admin.entity.AppUser;
 import com.aijiuyi.admin.service.AppUserService;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.core.metadata.IPage;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.web.bind.annotation.*;
 
+import java.util.List;
+
 /**
  * 应用用户管理 Controller(管理员后台)
  * 提供应用用户的增删改查和状态管理接口
@@ -21,6 +24,25 @@ public class AppUserController {
     @Autowired
     private AppUserService appUserService;
 
+    /**
+     * 获取应用用户选项列表(用于方案创作人选择框)
+     * 返回所有启用状态的用户,前端根据 userType 区分角色
+     *
+     * @param userType 可选过滤:1=普通用户,2=专家,3=管理员;null=全部
+     * @return 用户列表(id + nickname + username + userType)
+     */
+    @GetMapping("/list")
+    @Log(value = "获取用户选项列表", module = "用户管理", operationType = OperationType.QUERY)
+    public Result<List<AppUser>> list(@RequestParam(required = false) Integer userType) {
+        LambdaQueryWrapper<AppUser> wrapper = new LambdaQueryWrapper<AppUser>()
+                .eq(AppUser::getStatus, 1)
+                .eq(userType != null, AppUser::getUserType, userType)
+                .select(AppUser::getId, AppUser::getNickname, AppUser::getUsername, AppUser::getUserType)
+                .orderByAsc(AppUser::getUserType)
+                .orderByAsc(AppUser::getId);
+        return Result.success(appUserService.list(wrapper));
+    }
+
     /**
      * 分页查询应用用户列表
      *

+ 27 - 11
code/backend/src/main/java/com/aijiuyi/admin/controller/AuthController.java

@@ -5,30 +5,37 @@ import com.aijiuyi.admin.common.annotation.NoAuth;
 import com.aijiuyi.admin.common.context.RequestContext;
 import com.aijiuyi.admin.common.entity.Result;
 import com.aijiuyi.admin.common.enums.OperationType;
-import com.aijiuyi.admin.entity.SysUser;
+import com.aijiuyi.admin.entity.AppUser;
 import com.aijiuyi.admin.controller.dto.LoginDTO;
-import com.aijiuyi.admin.service.SysUserService;
+import com.aijiuyi.admin.service.AdminService;
+import com.aijiuyi.admin.service.AppUserService;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.web.bind.annotation.*;
 
 import javax.servlet.http.HttpServletRequest;
 import javax.validation.Valid;
+import java.util.HashMap;
 import java.util.Map;
 
 /**
  * 认证管理 Controller
  * 提供登录、退出、获取当前用户信息接口
+ * 统一使用应用用户表(app_user)进行身份验证
  */
 @RestController
 @RequestMapping("/auth")
 public class AuthController {
 
     @Autowired
-    private SysUserService sysUserService;
+    private AppUserService appUserService;
+
+    @Autowired
+    private AdminService adminService;
 
     /**
      * 用户登录接口
      * 免鉴权(使用 @NoAuth 注解跳过 Token 验证)
+     * 仅 userType=3(管理员)账号可登录后台
      *
      * @param loginDTO 登录参数(用户名、密码)
      * @return 包含 token 和用户信息的 Result
@@ -37,7 +44,7 @@ public class AuthController {
     @NoAuth
     @Log(value = "用户登录", module = "认证管理", operationType = OperationType.OTHER)
     public Result<Map<String, Object>> login(@RequestBody @Valid LoginDTO loginDTO) {
-        return sysUserService.login(loginDTO);
+        return appUserService.login(loginDTO);
     }
 
     /**
@@ -51,25 +58,34 @@ public class AuthController {
     @Log(value = "用户退出", module = "认证管理", operationType = OperationType.OTHER)
     public Result<Void> logout(HttpServletRequest request) {
         String token = request.getHeader("Authorization");
-        sysUserService.logout(token);
+        appUserService.logout(token);
         return Result.success();
     }
 
     /**
-     * 获取当前登录用户信息
+     * 获取当前登录用户信息(含权限路由列表)
      *
-     * @return 用户信息(不包含密码)
+     * @return 应用用户信息 + permissions 权限路由列表
      */
     @GetMapping("/info")
     @Log(value = "获取当前用户信息", module = "认证管理", operationType = OperationType.QUERY)
-    public Result<SysUser> getUserInfo() {
+    public Result<Map<String, Object>> getUserInfo() {
         // 从 ThreadLocal 获取当前用户ID(由 TokenInterceptor 解析 Token 后设置)
         Long userId = RequestContext.getUserId();
-        SysUser user = sysUserService.getById(userId);
+        AppUser user = appUserService.getById(userId);
         if (user != null) {
-            // 密码不返回给前端
             user.setPassword(null);
         }
-        return Result.success(user);
+        // 查询权限路由列表(超级管理员返回 null,前端视为全部权限)
+        String permissions = adminService.getPermissions(userId);
+        Map<String, Object> data = new HashMap<>(8);
+        data.put("userId", user != null ? user.getId() : null);
+        data.put("username", user != null ? user.getUsername() : null);
+        data.put("nickname", user != null ? user.getNickname() : null);
+        data.put("avatar", user != null ? user.getAvatar() : null);
+        data.put("userType", user != null ? user.getUserType() : null);
+        data.put("adminRole", user != null ? user.getAdminRole() : null);
+        data.put("permissions", permissions);
+        return Result.success(data);
     }
 }

+ 18 - 0
code/backend/src/main/java/com/aijiuyi/admin/controller/MoxibustionTechniqueController.java

@@ -6,6 +6,7 @@ import com.aijiuyi.admin.common.enums.OperationType;
 import com.aijiuyi.admin.controller.dto.MoxibustionTechniqueQueryDTO;
 import com.aijiuyi.admin.entity.MoxibustionTechnique;
 import com.aijiuyi.admin.service.MoxibustionTechniqueService;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.core.metadata.IPage;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.web.bind.annotation.*;
@@ -23,6 +24,23 @@ public class MoxibustionTechniqueController {
     @Autowired
     private MoxibustionTechniqueService techniqueService;
 
+    /**
+     * 获取所有启用艾灸手法列表(用于选择框)
+     *
+     * @return 手法列表(id + name)
+     */
+    @GetMapping("/list")
+    @Log(value = "获取手法选项列表", module = "艾灸手法管理", operationType = OperationType.QUERY)
+    public Result<List<MoxibustionTechnique>> list() {
+        List<MoxibustionTechnique> list = techniqueService.list(
+                new LambdaQueryWrapper<MoxibustionTechnique>()
+                        .eq(MoxibustionTechnique::getStatus, 1)
+                        .select(MoxibustionTechnique::getId, MoxibustionTechnique::getName)
+                        .orderByAsc(MoxibustionTechnique::getId)
+        );
+        return Result.success(list);
+    }
+
     /**
      * 分页查询艾灸手法列表
      *

+ 28 - 0
code/backend/src/main/java/com/aijiuyi/admin/controller/dto/AdminQueryDTO.java

@@ -0,0 +1,28 @@
+package com.aijiuyi.admin.controller.dto;
+
+import lombok.Data;
+
+/**
+ * 管理员分页查询参数 DTO
+ */
+@Data
+public class AdminQueryDTO {
+
+    /** 用户名(模糊查询) */
+    private String username;
+
+    /** 姓名/昵称(模糊查询) */
+    private String nickname;
+
+    /** 管理员角色:1=超级管理员,2=运营人员,3=只读用户 */
+    private Integer adminRole;
+
+    /** 状态:1=启用,0=禁用 */
+    private Integer status;
+
+    /** 当前页码,默认第1页 */
+    private Integer pageNum = 1;
+
+    /** 每页条数,默认10条 */
+    private Integer pageSize = 10;
+}

+ 1 - 1
code/backend/src/main/java/com/aijiuyi/admin/controller/dto/AppUserQueryDTO.java

@@ -14,7 +14,7 @@ public class AppUserQueryDTO {
     /** 昵称(模糊搜索) */
     private String nickname;
 
-    /** 用户类型:1=普通用户,2=专家 */
+    /** 用户类型:1=普通用户,2=专家,3=管理员 */
     private Integer userType;
 
     /** 状态:1=正常,0=禁用 */

+ 41 - 0
code/backend/src/main/java/com/aijiuyi/admin/entity/AdminPermission.java

@@ -0,0 +1,41 @@
+package com.aijiuyi.admin.entity;
+
+import com.baomidou.mybatisplus.annotation.*;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+/**
+ * 管理员权限配置实体
+ * 存储每个管理员可访问的前端路由路径列表(JSON格式)
+ * 超级管理员(adminRole=1)不受此表限制,默认拥有全部权限
+ */
+@Data
+@TableName("admin_permission")
+public class AdminPermission {
+
+    /** 主键ID */
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    /** 管理员ID(关联 app_user.id) */
+    private Long adminId;
+
+    /**
+     * 可访问的路由路径(JSON数组字符串,如 ["/dashboard","/device","/plan"])
+     * 超级管理员此字段为 null 或不使用
+     */
+    private String routePaths;
+
+    /** 逻辑删除:0=未删除,1=已删除 */
+    @TableLogic
+    private Integer deleted;
+
+    /** 创建时间(自动填充) */
+    @TableField(fill = FieldFill.INSERT)
+    private LocalDateTime createTime;
+
+    /** 更新时间(自动填充) */
+    @TableField(fill = FieldFill.INSERT_UPDATE)
+    private LocalDateTime updateTime;
+}

+ 22 - 3
code/backend/src/main/java/com/aijiuyi/admin/entity/AppUser.java

@@ -1,13 +1,15 @@
 package com.aijiuyi.admin.entity;
 
+import com.alibaba.fastjson.annotation.JSONField;
 import com.baomidou.mybatisplus.annotation.*;
 import lombok.Data;
 
 import java.time.LocalDateTime;
 
 /**
- * 应用用户实体(手机号验证码登录的普通用户/专家
+ * 应用用户实体(统一用户表,通过 userType 区分角色
  * 对应数据库 app_user 表
+ * userType: 1=普通用户,2=专家,3=管理员
  */
 @Data
 @TableName("app_user")
@@ -17,7 +19,14 @@ public class AppUser {
     @TableId(type = IdType.AUTO)
     private Long id;
 
-    /** 手机号(唯一,用于登录) */
+    /** 用户名(唯一,用于后台管理员登录) */
+    private String username;
+
+    /** 密码(MD5加密存储,后台账号使用;不序列化到响应) */
+    @JSONField(serialize = false)
+    private String password;
+
+    /** 手机号(唯一,用于App端登录) */
     private String phone;
 
     /** 昵称 */
@@ -26,12 +35,22 @@ public class AppUser {
     /** 头像URL */
     private String avatar;
 
-    /** 用户类型:1=普通用户,2=专家 */
+    /**
+     * 用户类型(角色):1=普通用户,2=专家,3=管理员
+     */
     private Integer userType;
 
+    /**
+     * 管理员角色:1=超级管理员,2=运营人员,3=只读用户(仅 userType=3 时有效)
+     */
+    private Integer adminRole;
+
     /** 状态:1=正常,0=禁用 */
     private Integer status;
 
+    /** 最后登录时间 */
+    private LocalDateTime lastLoginTime;
+
     /** 逻辑删除:0=未删除,1=已删除 */
     @TableLogic
     private Integer deleted;

+ 1 - 1
code/backend/src/main/java/com/aijiuyi/admin/entity/Plan.java

@@ -56,7 +56,7 @@ public class Plan {
      */
     private Integer authorType;
 
-    /** 创作人ID(对应 sys_user.id 或 app_user.id) */
+    /** 创作人ID(对应 app_user.id) */
     private Long authorId;
 
     /** 创作人名称(冗余存储,加速查询) */

+ 12 - 0
code/backend/src/main/java/com/aijiuyi/admin/mapper/AdminPermissionMapper.java

@@ -0,0 +1,12 @@
+package com.aijiuyi.admin.mapper;
+
+import com.aijiuyi.admin.entity.AdminPermission;
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * 管理员权限配置 Mapper
+ */
+@Mapper
+public interface AdminPermissionMapper extends BaseMapper<AdminPermission> {
+}

+ 83 - 0
code/backend/src/main/java/com/aijiuyi/admin/service/AdminService.java

@@ -0,0 +1,83 @@
+package com.aijiuyi.admin.service;
+
+import com.aijiuyi.admin.controller.dto.AdminQueryDTO;
+import com.aijiuyi.admin.entity.AppUser;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.service.IService;
+
+import java.util.List;
+
+/**
+ * 管理员管理 Service 接口
+ * 对 app_user 表中 userType=3 的管理员进行管理,
+ * 同时提供管理员权限配置的查询与更新
+ */
+public interface AdminService extends IService<AppUser> {
+
+    /**
+     * 分页查询管理员列表
+     *
+     * @param queryDTO 查询参数
+     * @return 分页结果
+     */
+    IPage<AppUser> pageList(AdminQueryDTO queryDTO);
+
+    /**
+     * 新增管理员
+     * 对密码进行 MD5 加密,校验用户名唯一性
+     *
+     * @param admin 管理员信息
+     */
+    void addAdmin(AppUser admin);
+
+    /**
+     * 修改管理员信息
+     * 若传入密码则重新加密,否则保留原密码
+     *
+     * @param admin 管理员信息(需携带 id)
+     */
+    void updateAdmin(AppUser admin);
+
+    /**
+     * 删除管理员(逻辑删除)
+     * 超级管理员不可删除
+     *
+     * @param id 管理员ID
+     */
+    void deleteAdmin(Long id);
+
+    /**
+     * 批量删除管理员(逻辑删除)
+     * 超级管理员不可删除
+     *
+     * @param ids 管理员ID列表
+     */
+    void batchDeleteAdmin(List<Long> ids);
+
+    /**
+     * 修改管理员状态(禁用/启用)
+     * 不允许禁用当前登录账号自身
+     *
+     * @param id     管理员ID
+     * @param status 目标状态:1=启用,0=禁用
+     */
+    void updateStatus(Long id, Integer status);
+
+    /**
+     * 查询管理员权限路由列表
+     * 超级管理员返回 null(前端解释为全部权限)
+     *
+     * @param adminId 管理员ID
+     * @return 路由路径列表(JSON数组字符串,如 ["/dashboard","/device"]);超级管理员返回 null
+     */
+    String getPermissions(Long adminId);
+
+    /**
+     * 更新管理员权限路由列表
+     * 若超级管理员调用此方法,视为无效操作直接返回
+     *
+     * @param adminId    管理员ID
+     * @param routePaths 路由路径列表(JSON数组字符串)
+     */
+    void updatePermissions(Long adminId, String routePaths);
+}

+ 22 - 0
code/backend/src/main/java/com/aijiuyi/admin/service/AppUserService.java

@@ -1,15 +1,37 @@
 package com.aijiuyi.admin.service;
 
+import com.aijiuyi.admin.common.entity.Result;
 import com.aijiuyi.admin.controller.dto.AppUserQueryDTO;
+import com.aijiuyi.admin.controller.dto.LoginDTO;
 import com.aijiuyi.admin.entity.AppUser;
 import com.baomidou.mybatisplus.core.metadata.IPage;
 import com.baomidou.mybatisplus.extension.service.IService;
 
+import java.util.Map;
+
 /**
  * 应用用户 Service
+ * 统一用户中心:普通用户、专家、管理员均使用此表,通过 userType 区分
  */
 public interface AppUserService extends IService<AppUser> {
 
+    /**
+     * 管理员登录
+     * 通过用户名+密码登录,返回 Token 和用户信息
+     *
+     * @param loginDTO 登录参数(用户名、密码)
+     * @return 包含 token 和用户基本信息的 Result
+     */
+    Result<Map<String, Object>> login(LoginDTO loginDTO);
+
+    /**
+     * 退出登录
+     * 从 Redis 中删除 Token,使 Token 立即失效
+     *
+     * @param token 当前用户的 Token(从请求头中获取)
+     */
+    void logout(String token);
+
     /**
      * 分页查询应用用户列表
      *

+ 236 - 0
code/backend/src/main/java/com/aijiuyi/admin/service/impl/AdminServiceImpl.java

@@ -0,0 +1,236 @@
+package com.aijiuyi.admin.service.impl;
+
+import com.aijiuyi.admin.common.constant.ResultCode;
+import com.aijiuyi.admin.common.context.RequestContext;
+import com.aijiuyi.admin.common.exception.BusinessException;
+import com.aijiuyi.admin.common.util.LogUtil;
+import com.aijiuyi.admin.controller.dto.AdminQueryDTO;
+import com.aijiuyi.admin.entity.AdminPermission;
+import com.aijiuyi.admin.entity.AppUser;
+import com.aijiuyi.admin.mapper.AdminPermissionMapper;
+import com.aijiuyi.admin.mapper.AppUserMapper;
+import com.aijiuyi.admin.service.AdminService;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.util.DigestUtils;
+import org.springframework.util.StringUtils;
+
+import java.util.List;
+
+/**
+ * 管理员管理 Service 实现类
+ * 操作 app_user 表中 userType=3 的管理员记录
+ * 同时维护 admin_permission 表中的路由权限配置
+ */
+@Service
+public class AdminServiceImpl extends ServiceImpl<AppUserMapper, AppUser> implements AdminService {
+
+    @Autowired
+    private AdminPermissionMapper adminPermissionMapper;
+
+    /**
+     * 分页查询管理员列表(仅 userType=3)
+     *
+     * @param queryDTO 查询条件
+     * @return 分页结果
+     */
+    @Override
+    public IPage<AppUser> pageList(AdminQueryDTO queryDTO) {
+        Page<AppUser> page = new Page<>(queryDTO.getPageNum(), queryDTO.getPageSize());
+        LambdaQueryWrapper<AppUser> wrapper = new LambdaQueryWrapper<AppUser>()
+                .eq(AppUser::getUserType, 3)
+                .eq(queryDTO.getAdminRole() != null, AppUser::getAdminRole, queryDTO.getAdminRole())
+                .eq(queryDTO.getStatus() != null, AppUser::getStatus, queryDTO.getStatus())
+                .orderByAsc(AppUser::getId);
+        // 用户名/姓名 OR 模糊查询
+        boolean hasKeyword = StringUtils.hasText(queryDTO.getUsername()) || StringUtils.hasText(queryDTO.getNickname());
+        if (hasKeyword) {
+            String kw = StringUtils.hasText(queryDTO.getUsername()) ? queryDTO.getUsername() : queryDTO.getNickname();
+            wrapper.and(w -> w.like(AppUser::getUsername, kw).or().like(AppUser::getNickname, kw));
+        }
+        return page(page, wrapper);
+    }
+
+    /**
+     * 新增管理员
+     * 校验用户名唯一性,对密码进行 MD5 加密,userType 强制设为 3
+     *
+     * @param admin 管理员信息
+     */
+    @Override
+    public void addAdmin(AppUser admin) {
+        // 校验用户名唯一性
+        if (!StringUtils.hasText(admin.getUsername())) {
+            throw new BusinessException(ResultCode.PARAM_ERROR);
+        }
+        long usernameCount = lambdaQuery().eq(AppUser::getUsername, admin.getUsername()).count();
+        if (usernameCount > 0) {
+            throw new BusinessException(ResultCode.USERNAME_EXISTS);
+        }
+        // 强制 userType=3(管理员)
+        admin.setUserType(3);
+        // 密码加密
+        if (StringUtils.hasText(admin.getPassword())) {
+            admin.setPassword(DigestUtils.md5DigestAsHex(admin.getPassword().getBytes()));
+        }
+        save(admin);
+        LogUtil.info(AdminServiceImpl.class, "新增管理员[{}],角色[{}]", admin.getUsername(), admin.getAdminRole());
+    }
+
+    /**
+     * 修改管理员信息
+     * 若传入密码则重新加密,否则保留原密码;不允许修改 userType
+     *
+     * @param admin 管理员信息(需携带 id)
+     */
+    @Override
+    public void updateAdmin(AppUser admin) {
+        AppUser exist = getById(admin.getId());
+        if (exist == null || !Integer.valueOf(3).equals(exist.getUserType())) {
+            throw new BusinessException(ResultCode.ADMIN_NOT_FOUND);
+        }
+        // 若修改了用户名,校验唯一性
+        if (StringUtils.hasText(admin.getUsername()) && !admin.getUsername().equals(exist.getUsername())) {
+            long count = lambdaQuery().eq(AppUser::getUsername, admin.getUsername()).count();
+            if (count > 0) {
+                throw new BusinessException(ResultCode.USERNAME_EXISTS);
+            }
+        }
+        // 密码处理
+        if (StringUtils.hasText(admin.getPassword())) {
+            admin.setPassword(DigestUtils.md5DigestAsHex(admin.getPassword().getBytes()));
+        } else {
+            admin.setPassword(null);
+        }
+        // 强制 userType 不变
+        admin.setUserType(null);
+        updateById(admin);
+        LogUtil.info(AdminServiceImpl.class, "修改管理员[{}]", admin.getId());
+    }
+
+    /**
+     * 删除管理员(逻辑删除)
+     * adminRole=1 超级管理员不可删除
+     *
+     * @param id 管理员ID
+     */
+    @Override
+    public void deleteAdmin(Long id) {
+        AppUser admin = getById(id);
+        if (admin == null || !Integer.valueOf(3).equals(admin.getUserType())) {
+            throw new BusinessException(ResultCode.ADMIN_NOT_FOUND);
+        }
+        if (Integer.valueOf(1).equals(admin.getAdminRole())) {
+            throw new BusinessException(ResultCode.ADMIN_CANNOT_DELETE);
+        }
+        removeById(id);
+        LogUtil.info(AdminServiceImpl.class, "删除管理员[{}]", id);
+    }
+
+    /**
+     * 批量删除管理员(逻辑删除)
+     * 超级管理员(adminRole=1)不可被批量删除
+     *
+     * @param ids 管理员ID列表
+     */
+    @Override
+    public void batchDeleteAdmin(List<Long> ids) {
+        for (Long id : ids) {
+            AppUser admin = getById(id);
+            if (admin != null && Integer.valueOf(1).equals(admin.getAdminRole())) {
+                throw new BusinessException(ResultCode.ADMIN_CANNOT_DELETE);
+            }
+        }
+        // 仅删除 userType=3 的记录,防止误删其他类型用户
+        LambdaQueryWrapper<AppUser> wrapper = new LambdaQueryWrapper<AppUser>()
+                .in(AppUser::getId, ids)
+                .eq(AppUser::getUserType, 3);
+        remove(wrapper);
+        LogUtil.info(AdminServiceImpl.class, "批量删除管理员,ID列表:{}", ids);
+    }
+
+    /**
+     * 修改管理员状态(禁用/启用)
+     * 不允许禁用当前登录账号自身
+     *
+     * @param id     管理员ID
+     * @param status 目标状态:1=启用,0=禁用
+     */
+    @Override
+    public void updateStatus(Long id, Integer status) {
+        // 不允许禁用自身
+        Long currentUserId = RequestContext.getUserId();
+        if (Integer.valueOf(0).equals(status) && id.equals(currentUserId)) {
+            throw new BusinessException(ResultCode.ADMIN_CANNOT_DISABLE_SELF);
+        }
+        AppUser admin = getById(id);
+        if (admin == null || !Integer.valueOf(3).equals(admin.getUserType())) {
+            throw new BusinessException(ResultCode.ADMIN_NOT_FOUND);
+        }
+        AppUser update = new AppUser();
+        update.setId(id);
+        update.setStatus(status);
+        updateById(update);
+        LogUtil.info(AdminServiceImpl.class, "管理员[{}]状态修改为[{}]", id, status);
+    }
+
+    /**
+     * 查询管理员权限路由列表
+     *
+     * @param adminId 管理员ID
+     * @return 路由路径 JSON 数组字符串;超级管理员返回 null
+     */
+    @Override
+    public String getPermissions(Long adminId) {
+        AppUser admin = getById(adminId);
+        if (admin == null) {
+            throw new BusinessException(ResultCode.ADMIN_NOT_FOUND);
+        }
+        // 超级管理员返回 null,前端视为全部权限
+        if (Integer.valueOf(1).equals(admin.getAdminRole())) {
+            return null;
+        }
+        AdminPermission permission = adminPermissionMapper.selectOne(
+                new LambdaQueryWrapper<AdminPermission>()
+                        .eq(AdminPermission::getAdminId, adminId)
+        );
+        return permission != null ? permission.getRoutePaths() : "[]";
+    }
+
+    /**
+     * 更新管理员权限路由列表
+     * 超级管理员(adminRole=1)调用时直接忽略,不修改
+     *
+     * @param adminId    管理员ID
+     * @param routePaths 路由路径 JSON 数组字符串
+     */
+    @Override
+    public void updatePermissions(Long adminId, String routePaths) {
+        AppUser admin = getById(adminId);
+        if (admin == null || !Integer.valueOf(3).equals(admin.getUserType())) {
+            throw new BusinessException(ResultCode.ADMIN_NOT_FOUND);
+        }
+        // 超级管理员不设置权限
+        if (Integer.valueOf(1).equals(admin.getAdminRole())) {
+            return;
+        }
+        AdminPermission exist = adminPermissionMapper.selectOne(
+                new LambdaQueryWrapper<AdminPermission>()
+                        .eq(AdminPermission::getAdminId, adminId)
+        );
+        if (exist == null) {
+            AdminPermission permission = new AdminPermission();
+            permission.setAdminId(adminId);
+            permission.setRoutePaths(routePaths);
+            adminPermissionMapper.insert(permission);
+        } else {
+            exist.setRoutePaths(routePaths);
+            adminPermissionMapper.updateById(exist);
+        }
+        LogUtil.info(AdminServiceImpl.class, "更新管理员[{}]权限:{}", adminId, routePaths);
+    }
+}

+ 165 - 5
code/backend/src/main/java/com/aijiuyi/admin/service/impl/AppUserServiceImpl.java

@@ -1,25 +1,155 @@
 package com.aijiuyi.admin.service.impl;
 
 import com.aijiuyi.admin.common.constant.ResultCode;
+import com.aijiuyi.admin.common.context.RequestContext;
+import com.aijiuyi.admin.common.entity.Result;
 import com.aijiuyi.admin.common.exception.BusinessException;
+import com.aijiuyi.admin.common.interceptor.TokenInterceptor;
 import com.aijiuyi.admin.common.util.LogUtil;
+import com.aijiuyi.admin.common.util.RedisUtil;
+import com.aijiuyi.admin.common.util.TokenUtil;
 import com.aijiuyi.admin.controller.dto.AppUserQueryDTO;
+import com.aijiuyi.admin.controller.dto.LoginDTO;
+import com.aijiuyi.admin.entity.AdminPermission;
 import com.aijiuyi.admin.entity.AppUser;
+import com.aijiuyi.admin.mapper.AdminPermissionMapper;
 import com.aijiuyi.admin.mapper.AppUserMapper;
 import com.aijiuyi.admin.service.AppUserService;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.core.metadata.IPage;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
+import org.springframework.util.DigestUtils;
 import org.springframework.util.StringUtils;
 
+import java.time.LocalDateTime;
+import java.util.HashMap;
+import java.util.Map;
+
 /**
  * 应用用户 Service 实现类
+ * 统一用户中心:普通用户、专家、管理员均使用此表
  */
 @Service
 public class AppUserServiceImpl extends ServiceImpl<AppUserMapper, AppUser> implements AppUserService {
 
+    /** Token 在 Redis 中的 key 前缀 */
+    private static final String TOKEN_REDIS_PREFIX = TokenInterceptor.REDIS_TOKEN_KEY_PREFIX;
+
+    /** Token 在 Redis 中的过期时间:3年(秒) */
+    private static final long TOKEN_EXPIRE_SECONDS = 3L * 365 * 24 * 60 * 60;
+
+    @Autowired
+    private RedisUtil redisUtil;
+
+    @Autowired
+    private AdminPermissionMapper adminPermissionMapper;
+
+    /**
+     * 管理员登录
+     * 通过用户名+密码校验身份,生成 JWT Token 并存入 Redis
+     *
+     * @param loginDTO 登录参数
+     * @return 包含 token 和用户基本信息的 Result
+     */
+    @Override
+    public Result<Map<String, Object>> login(LoginDTO loginDTO) {
+        LogUtil.info(AppUserServiceImpl.class, "用户[{}]尝试登录", loginDTO.getUsername());
+
+        // 根据用户名查询用户(仅管理员可通过用户名登录)
+        AppUser user = lambdaQuery()
+                .eq(AppUser::getUsername, loginDTO.getUsername())
+                .one();
+
+        // 用户不存在
+        if (user == null) {
+            LogUtil.warn(AppUserServiceImpl.class, "用户[{}]不存在", loginDTO.getUsername());
+            throw new BusinessException(ResultCode.USER_NOT_FOUND);
+        }
+
+        // 验证密码(前端传明文,后端 MD5 加密后比对)
+        String md5Password = DigestUtils.md5DigestAsHex(loginDTO.getPassword().getBytes());
+        if (!md5Password.equals(user.getPassword())) {
+            LogUtil.warn(AppUserServiceImpl.class, "用户[{}]密码错误", loginDTO.getUsername());
+            throw new BusinessException(ResultCode.USER_PASSWORD_ERROR);
+        }
+
+        // 验证用户状态
+        if (user.getStatus() != null && user.getStatus() == 0) {
+            LogUtil.warn(AppUserServiceImpl.class, "用户[{}]已被禁用", loginDTO.getUsername());
+            throw new BusinessException(ResultCode.USER_DISABLED);
+        }
+
+        // 验证角色(仅 userType=3=管理员 可登录后台)
+        if (user.getUserType() == null || user.getUserType() != 3) {
+            LogUtil.warn(AppUserServiceImpl.class, "用户[{}]无后台登录权限", loginDTO.getUsername());
+            throw new BusinessException(ResultCode.FORBIDDEN);
+        }
+
+        // 生成 JWT Token(携带 userType)
+        String token = TokenUtil.generateToken(user.getId(), user.getUsername(), user.getUserType());
+
+        // 将 Token 存入 Redis,3年过期
+        redisUtil.set(TOKEN_REDIS_PREFIX + token, user.getId(), TOKEN_EXPIRE_SECONDS);
+
+        // 更新最后登录时间
+        AppUser updateLogin = new AppUser();
+        updateLogin.setId(user.getId());
+        updateLogin.setLastLoginTime(LocalDateTime.now());
+        updateById(updateLogin);
+
+        // 登录成功后将用户信息写入 RequestContext,供 LogAspect 正确记录操作人
+        RequestContext.setUserId(user.getId());
+        RequestContext.setUserName(user.getUsername());
+        RequestContext.setUserType(user.getUserType());
+
+        LogUtil.info(AppUserServiceImpl.class, "用户[{}]登录成功", user.getUsername());
+
+        // 查询权限路由(超级管理员返回 null,代表全部权限)
+        String routePaths = null;
+        if (!Integer.valueOf(1).equals(user.getAdminRole())) {
+            AdminPermission permission = adminPermissionMapper.selectOne(
+                    new LambdaQueryWrapper<AdminPermission>()
+                            .eq(AdminPermission::getAdminId, user.getId())
+            );
+            routePaths = permission != null ? permission.getRoutePaths() : "[]";
+        }
+
+        // 组装返回数据
+        Map<String, Object> resultData = new HashMap<>(8);
+        resultData.put("token", token);
+        resultData.put("userId", user.getId());
+        resultData.put("username", user.getUsername());
+        resultData.put("nickname", user.getNickname());
+        resultData.put("avatar", user.getAvatar());
+        resultData.put("userType", user.getUserType());
+        resultData.put("adminRole", user.getAdminRole());
+        resultData.put("permissions", routePaths);
+
+        return Result.success(resultData);
+    }
+
+    /**
+     * 退出登录
+     * 从 Redis 中删除 Token,使 Token 立即失效
+     *
+     * @param token 当前用户的 Token
+     */
+    @Override
+    public void logout(String token) {
+        if (StringUtils.hasText(token)) {
+            // 去掉 Bearer 前缀
+            if (token.startsWith("Bearer ")) {
+                token = token.substring(7);
+            }
+            // 从 Redis 删除 Token
+            redisUtil.del(TOKEN_REDIS_PREFIX + token);
+            LogUtil.info(AppUserServiceImpl.class, "用户退出登录,Token 已失效");
+        }
+    }
+
     /**
      * 分页查询应用用户列表
      *
@@ -59,18 +189,34 @@ public class AppUserServiceImpl extends ServiceImpl<AppUserMapper, AppUser> impl
 
     /**
      * 新增应用用户
+     * 若为管理员(userType=3)则校验用户名唯一性并对密码做 MD5 加密
      *
      * @param appUser 用户信息
      */
     @Override
     public void addUser(AppUser appUser) {
-        // 手机号唯一性校验
-        long count = lambdaQuery().eq(AppUser::getPhone, appUser.getPhone()).count();
-        if (count > 0) {
-            throw new BusinessException(ResultCode.PHONE_EXISTS);
+        // 手机号唯一性校验(手机号不为空时校验)
+        if (StringUtils.hasText(appUser.getPhone())) {
+            long phoneCount = lambdaQuery().eq(AppUser::getPhone, appUser.getPhone()).count();
+            if (phoneCount > 0) {
+                throw new BusinessException(ResultCode.PHONE_EXISTS);
+            }
+        }
+        // 管理员需校验用户名唯一性,并对密码加密
+        if (Integer.valueOf(3).equals(appUser.getUserType())) {
+            if (StringUtils.hasText(appUser.getUsername())) {
+                long usernameCount = lambdaQuery().eq(AppUser::getUsername, appUser.getUsername()).count();
+                if (usernameCount > 0) {
+                    throw new BusinessException(ResultCode.USERNAME_EXISTS);
+                }
+            }
+            // 密码 MD5 加密
+            if (StringUtils.hasText(appUser.getPassword())) {
+                appUser.setPassword(DigestUtils.md5DigestAsHex(appUser.getPassword().getBytes()));
+            }
         }
         save(appUser);
-        LogUtil.info(AppUserServiceImpl.class, "新增应用用户,手机号[{}]", appUser.getPhone());
+        LogUtil.info(AppUserServiceImpl.class, "新增应用用户,手机号[{}],类型[{}]", appUser.getPhone(), appUser.getUserType());
     }
 
     /**
@@ -91,6 +237,20 @@ public class AppUserServiceImpl extends ServiceImpl<AppUserMapper, AppUser> impl
                 throw new BusinessException(ResultCode.PHONE_EXISTS);
             }
         }
+        // 如果修改了用户名,校验唯一性
+        if (StringUtils.hasText(appUser.getUsername()) && !appUser.getUsername().equals(exist.getUsername())) {
+            long count = lambdaQuery().eq(AppUser::getUsername, appUser.getUsername()).count();
+            if (count > 0) {
+                throw new BusinessException(ResultCode.USERNAME_EXISTS);
+            }
+        }
+        // 如果修改了密码,重新加密
+        if (StringUtils.hasText(appUser.getPassword())) {
+            appUser.setPassword(DigestUtils.md5DigestAsHex(appUser.getPassword().getBytes()));
+        } else {
+            // 不修改密码字段
+            appUser.setPassword(null);
+        }
         updateById(appUser);
         LogUtil.info(AppUserServiceImpl.class, "修改应用用户[{}]", appUser.getId());
     }

+ 1 - 1
code/backend/src/main/resources/application.yml

@@ -9,7 +9,7 @@ spring:
     default-property-inclusion: non_null
 
 server:
-  port: 8080
+  port: 18888
   servlet:
     context-path: /api
 

+ 36 - 30
code/backend/src/main/resources/sql/init.sql

@@ -5,31 +5,6 @@
 SET NAMES utf8mb4;
 SET FOREIGN_KEY_CHECKS = 0;
 
--- ----------------------------
--- 系统用户表
--- ----------------------------
-CREATE TABLE IF NOT EXISTS `sys_user` (
-    `id`          BIGINT       NOT NULL AUTO_INCREMENT COMMENT '主键ID',
-    `username`    VARCHAR(50)  NOT NULL COMMENT '用户名(唯一,用于登录)',
-    `password`    VARCHAR(100) NOT NULL COMMENT '密码(MD5加密存储)',
-    `nickname`    VARCHAR(100)          COMMENT '昵称(前端显示名称)',
-    `email`       VARCHAR(100)          COMMENT '邮箱',
-    `phone`       VARCHAR(20)           COMMENT '手机号',
-    `avatar`      VARCHAR(500)          COMMENT '头像URL',
-    `status`      TINYINT      NOT NULL DEFAULT 1 COMMENT '状态:1=正常,0=禁用',
-    `deleted`     TINYINT      NOT NULL DEFAULT 0 COMMENT '逻辑删除:0=未删除,1=已删除',
-    `create_time` DATETIME              COMMENT '创建时间',
-    `update_time` DATETIME              COMMENT '更新时间',
-    PRIMARY KEY (`id`),
-    UNIQUE KEY `uk_username` (`username`)
-) ENGINE = InnoDB
-  DEFAULT CHARSET = utf8mb4
-  COMMENT = '系统用户表';
-
--- 初始管理员账号(密码:admin123,MD5后为:0192023a7bbd73250516f069df18b500)
-INSERT INTO `sys_user` (`username`, `password`, `nickname`, `status`, `deleted`, `create_time`, `update_time`)
-VALUES ('admin', '0192023a7bbd73250516f069df18b500', '超级管理员', 1, 0, NOW(), NOW());
-
 -- ----------------------------
 -- 系统操作日志表
 -- ----------------------------
@@ -37,7 +12,7 @@ CREATE TABLE IF NOT EXISTS `sys_log` (
     `id`               BIGINT       NOT NULL AUTO_INCREMENT COMMENT '主键ID',
     `user_id`          BIGINT                COMMENT '请求用户ID',
     `user_name`        VARCHAR(100)          COMMENT '请求用户名',
-    `user_type`        TINYINT               COMMENT '操作者类型:1=系统管理员(sys_user),2=App用户(app_user)',
+    `user_type`        TINYINT               COMMENT '操作者类型:1=普通用户,2=专家,3=管理员(统一使用app_user表)',
     `request_url`      VARCHAR(500)          COMMENT '请求接口路径',
     `request_params`   TEXT                  COMMENT '请求参数(JSON格式)',
     `request_result`   TEXT                  COMMENT '请求结果(JSON格式,超2000字符截断)',
@@ -60,20 +35,27 @@ CREATE TABLE IF NOT EXISTS `sys_log` (
 -- ----------------------------
 CREATE TABLE IF NOT EXISTS `app_user` (
     `id`          BIGINT       NOT NULL AUTO_INCREMENT COMMENT '主键ID',
-    `phone`       VARCHAR(20)  NOT NULL COMMENT '手机号(唯一,用于登录)',
+    `phone`       VARCHAR(20)           COMMENT '手机号(唯一,用于登录)',
+    `username`    VARCHAR(64)           COMMENT '用户名(管理员登录用,唯一)',
+    `password`    VARCHAR(64)           COMMENT '密码(MD5加密,管理员用)',
     `nickname`    VARCHAR(100)          COMMENT '昵称',
     `avatar`      VARCHAR(500)          COMMENT '头像URL',
-    `user_type`   TINYINT      NOT NULL DEFAULT 1 COMMENT '用户类型:1=普通用户,2=专家',
+    `user_type`   TINYINT      NOT NULL DEFAULT 1 COMMENT '用户类型:1=普通用户,2=专家,3=管理员',
     `status`      TINYINT      NOT NULL DEFAULT 1 COMMENT '状态:1=正常,0=禁用',
     `deleted`     TINYINT      NOT NULL DEFAULT 0 COMMENT '逻辑删除:0=未删除,1=已删除',
     `create_time` DATETIME              COMMENT '创建时间',
     `update_time` DATETIME              COMMENT '更新时间',
     PRIMARY KEY (`id`),
-    UNIQUE KEY `uk_phone` (`phone`)
+    UNIQUE KEY `uk_phone` (`phone`),
+    UNIQUE KEY `uk_username` (`username`)
 ) ENGINE = InnoDB
   DEFAULT CHARSET = utf8mb4
   COMMENT = '应用用户表';
 
+-- 初始化管理员账号(密码:admin123,MD5后为:0192023a7bbd73250516f069df18b500)
+INSERT INTO `app_user` (`username`, `password`, `nickname`, `user_type`, `status`, `deleted`, `create_time`, `update_time`)
+VALUES ('admin', MD5('admin123'), '超级管理员', 3, 1, 0, NOW(), NOW());
+
 -- ----------------------------
 -- 子用户信息表(属于某个应用用户)
 -- ----------------------------
@@ -259,7 +241,7 @@ CREATE TABLE IF NOT EXISTS `plan` (
     `age_max`          INT           NOT NULL DEFAULT 120 COMMENT '适用年龄最大值(岁)',
     `description`      VARCHAR(200)           COMMENT '方案描述(最长200字符)',
     `author_type`      TINYINT       NOT NULL DEFAULT 1 COMMENT '创作人类型:1=管理员,2=普通用户,3=专家',
-    `author_id`        BIGINT                 COMMENT '创作人ID(对应 sys_user.id 或 app_user.id)',
+    `author_id`        BIGINT                 COMMENT '创作人ID(对应 app_user.id)',
     `author_name`      VARCHAR(100)           COMMENT '创作人名称(冗余存储,加速查询)',
     `status`           TINYINT       NOT NULL DEFAULT 0 COMMENT '状态:0=草稿,1=已发布,2=已下架',
     `use_count`        INT           NOT NULL DEFAULT 0 COMMENT '使用人数',
@@ -322,4 +304,28 @@ CREATE TABLE IF NOT EXISTS `user_plan` (
   DEFAULT CHARSET = utf8mb4
   COMMENT = '用户方案表(用户个性化工艺参数方案)';
 
+-- ----------------------------
+-- 管理员权限配置表
+-- ----------------------------
+CREATE TABLE IF NOT EXISTS `admin_permission` (
+    `id`          BIGINT       NOT NULL AUTO_INCREMENT COMMENT '主键ID',
+    `admin_id`    BIGINT       NOT NULL COMMENT '管理员ID(关联 app_user.id)',
+    `route_paths` TEXT                  COMMENT '可访问的路由路径(JSON数组,如 ["/dashboard","/device"])',
+    `deleted`     TINYINT      NOT NULL DEFAULT 0 COMMENT '逻辑删除:0=未删除,1=已删除',
+    `create_time` DATETIME              COMMENT '创建时间',
+    `update_time` DATETIME              COMMENT '更新时间',
+    PRIMARY KEY (`id`),
+    UNIQUE KEY `uk_admin_id` (`admin_id`)
+) ENGINE = InnoDB
+  DEFAULT CHARSET = utf8mb4
+  COMMENT = '管理员权限配置表';
+
+-- app_user 表追加管理员相关字段(首次执行时添加,若字段已存在请注释掉此段)
+ALTER TABLE `app_user`
+    ADD COLUMN `admin_role`      TINYINT  DEFAULT NULL COMMENT '管理员角色:1=超级管理员,2=运营人员,3=只读用户(仅 userType=3 时有效)',
+    ADD COLUMN `last_login_time` DATETIME DEFAULT NULL COMMENT '最后登录时间';
+
+-- 更新初始管理员为超级管理员角色
+UPDATE `app_user` SET `admin_role` = 1 WHERE `username` = 'admin' AND `admin_role` IS NULL;
+
 SET FOREIGN_KEY_CHECKS = 1;

+ 8 - 0
code/frontend/src/api/acupoint.js

@@ -5,6 +5,14 @@ import request from '@/utils/request'
  * 对应后端 AcupointController
  */
 
+/**
+ * 获取所有启用穴位列表(用于选择框)
+ * @returns {Promise} 穴位列表(id + name)
+ */
+export function getAcupointList() {
+  return request.get('/acupoint/list')
+}
+
 /**
  * 分页查询穴位列表
  * @param {Object} params 查询参数:name/meridian/locationType/status/pageNum/pageSize

+ 80 - 0
code/frontend/src/api/admin.js

@@ -0,0 +1,80 @@
+import request from '@/utils/request'
+
+/**
+ * 管理员管理模块 API
+ * 对应后端 AdminController
+ */
+
+/**
+ * 分页查询管理员列表
+ * @param {Object} params 查询参数:username/nickname/adminRole/status/pageNum/pageSize
+ */
+export function getAdminPage(params) {
+  return request.get('/admin/page', { params })
+}
+
+/**
+ * 查询管理员详情
+ * @param {number} id 管理员ID
+ */
+export function getAdminById(id) {
+  return request.get(`/admin/${id}`)
+}
+
+/**
+ * 新增管理员
+ * @param {Object} data 管理员信息(username/password/nickname/adminRole/status)
+ */
+export function addAdmin(data) {
+  return request.post('/admin', data)
+}
+
+/**
+ * 修改管理员信息
+ * @param {Object} data 管理员信息(需携带 id;密码为空则不修改)
+ */
+export function updateAdmin(data) {
+  return request.put('/admin', data)
+}
+
+/**
+ * 删除管理员(逻辑删除)
+ * @param {number} id 管理员ID
+ */
+export function deleteAdmin(id) {
+  return request.delete(`/admin/${id}`)
+}
+
+/**
+ * 批量删除管理员
+ * @param {number[]} ids 管理员ID列表
+ */
+export function batchDeleteAdmin(ids) {
+  return request.delete('/admin/batch', { data: ids })
+}
+
+/**
+ * 修改管理员状态(禁用/启用)
+ * @param {number} id     管理员ID
+ * @param {number} status 目标状态:1=启用,0=禁用
+ */
+export function updateAdminStatus(id, status) {
+  return request.put(`/admin/${id}/status`, null, { params: { status } })
+}
+
+/**
+ * 查询管理员权限路由列表
+ * @param {number} id 管理员ID
+ */
+export function getAdminPermission(id) {
+  return request.get(`/admin/${id}/permission`)
+}
+
+/**
+ * 更新管理员权限路由列表
+ * @param {number} id         管理员ID
+ * @param {string} routePaths 路由路径 JSON 数组字符串(如 '["/dashboard","/device"]')
+ */
+export function updateAdminPermission(id, routePaths) {
+  return request.put(`/admin/${id}/permission`, { routePaths })
+}

+ 9 - 0
code/frontend/src/api/auth.js

@@ -31,3 +31,12 @@ export function logout() {
 export function getUserInfoApi() {
   return request.get('/auth/info')
 }
+
+/**
+ * 获取应用用户列表(用于方案创作人选择框)
+ * @param {number} [userType] 可选过滤:1=普通用户,2=专家,3=管理员;不传则返回全部
+ * @returns {Promise} 用户列表(id + nickname + username + userType)
+ */
+export function getAppUserList(userType) {
+  return request.get('/user/app/list', { params: userType != null ? { userType } : {} })
+}

+ 3 - 0
code/frontend/src/api/index.js

@@ -25,3 +25,6 @@ export * as moxibustionApi from './moxibustion'
 
 // 方案管理模块
 export * as planApi from './plan'
+
+// 管理员管理模块
+export * as adminApi from './admin'

+ 8 - 0
code/frontend/src/api/moxibustion.js

@@ -5,6 +5,14 @@ import request from '@/utils/request'
  * 对应后端 MoxibustionTechniqueController
  */
 
+/**
+ * 获取所有启用艾灸手法列表(用于选择框)
+ * @returns {Promise} 手法列表(id + name)
+ */
+export function getTechniqueList() {
+  return request.get('/moxibustion-technique/list')
+}
+
 /**
  * 分页查询艾灸手法列表
  * @param {Object} params 查询参数:name/motionMode/status/pageNum/pageSize

+ 3 - 1
code/frontend/src/layout/index.vue

@@ -101,14 +101,16 @@ const activeMenu = computed(() => route.path)
 const currentRoute = computed(() => route)
 
 // 动态菜单(从路由配置中读取 Layout 下的子路由,使用绝对路径避免相对路径导航问题)
+// 同时根据当前用户权限过滤:超级管理员(adminRole=1)或 permissions=null 显示全部,其他按配置过滤
 const menuRoutes = computed(() => {
-  return router.getRoutes().filter(r =>
+  const allRoutes = router.getRoutes().filter(r =>
     r.meta?.title &&
     !r.meta?.hidden &&
     r.path !== '/' &&
     r.path !== '/login' &&
     r.path !== '/404'
   )
+  return allRoutes.filter(r => userStore.hasPermission(r.path))
 })
 
 // 切换侧边栏折叠

+ 14 - 14
code/frontend/src/router/index.js

@@ -17,19 +17,13 @@ const routes = [
     path: '/',
     component: () => import('@/layout/index.vue'),
     meta: { requiresAuth: true },
-    redirect: '/dashboard',
+    redirect: '/user/app',
     children: [
-      {
-        path: 'dashboard',
-        name: 'Dashboard',
-        component: () => import('@/views/dashboard/index.vue'),
-        meta: { requiresAuth: true, title: '控制台', icon: 'Odometer' }
-      },
       {
         path: 'user/app',
         name: 'AppUser',
         component: () => import('@/views/user/app-user/index.vue'),
-        meta: { requiresAuth: true, title: '应用用户', icon: 'User' }
+        meta: { requiresAuth: true, title: '账号管理', icon: 'User', hidden: true }
       },
       {
         path: 'user/profile',
@@ -43,12 +37,6 @@ const routes = [
         component: () => import('@/views/user/device/index.vue'),
         meta: { requiresAuth: true, title: '用户设备管理', icon: 'Monitor', hidden: true }
       },
-      {
-        path: 'log',
-        name: 'Log',
-        component: () => import('@/views/log/index.vue'),
-        meta: { requiresAuth: true, title: '日志管理', icon: 'Document' }
-      },
       {
         path: 'acupoint',
         name: 'Acupoint',
@@ -84,6 +72,18 @@ const routes = [
         name: 'UserPlan',
         component: () => import('@/views/user/plan/index.vue'),
         meta: { requiresAuth: true, title: '用户方案管理', hidden: true }
+      },
+      {
+        path: 'admin',
+        name: 'Admin',
+        component: () => import('@/views/admin/index.vue'),
+        meta: { requiresAuth: true, title: '管理员管理', icon: 'Setting' }
+      },
+      {
+        path: 'log',
+        name: 'Log',
+        component: () => import('@/views/log/index.vue'),
+        meta: { requiresAuth: true, title: '日志管理', icon: 'Document' }
       }
     ]
   },

+ 41 - 3
code/frontend/src/store/user.js

@@ -6,7 +6,7 @@ import router from '@/router'
 
 /**
  * 用户状态 Store
- * 管理登录状态、用户信息、Token
+ * 管理登录状态、用户信息、Token、权限路由列表
  */
 export const useUserStore = defineStore('user', () => {
   // 状态
@@ -19,6 +19,41 @@ export const useUserStore = defineStore('user', () => {
   const nickname = computed(() => userInfo.value?.nickname || userInfo.value?.username || '')
   const avatar = computed(() => userInfo.value?.avatar || '')
 
+  /**
+   * 管理员角色:1=超级管理员,2=运营人员,3=只读用户
+   * 超级管理员拥有所有菜单权限
+   */
+  const adminRole = computed(() => userInfo.value?.adminRole || null)
+
+  /**
+   * 权限路由路径列表(JSON字符串解析后的数组)
+   * 超级管理员(adminRole=1)此值为 null,前端视为拥有全部权限
+   * 其他角色为可访问的路由路径数组,如 ['/dashboard', '/device']
+   */
+  const permissions = computed(() => {
+    const raw = userInfo.value?.permissions
+    if (raw === null || raw === undefined) return null   // 超级管理员:全部权限
+    if (typeof raw === 'string') {
+      try {
+        return JSON.parse(raw)
+      } catch {
+        return []
+      }
+    }
+    if (Array.isArray(raw)) return raw
+    return []
+  })
+
+  /**
+   * 判断是否有指定路由的权限
+   * @param {string} path 路由 path
+   */
+  function hasPermission(path) {
+    if (adminRole.value === 1) return true  // 超级管理员全部通过
+    if (permissions.value === null) return true  // null 也视为全部权限
+    return permissions.value.includes(path)
+  }
+
   /**
    * 登录
    * @param {Object} loginData 登录参数 { username, password }
@@ -29,7 +64,7 @@ export const useUserStore = defineStore('user', () => {
     // 保存 Token 到内存和 localStorage
     token.value = newToken
     setToken(newToken)
-    // 保存用户基本信息
+    // 保存用户基本信息(含 adminRole 和 permissions)
     userInfo.value = info
     setUserInfo(info)
     return res
@@ -50,7 +85,7 @@ export const useUserStore = defineStore('user', () => {
   }
 
   /**
-   * 获取最新用户信息(从后端刷新)
+   * 获取最新用户信息(从后端刷新,含权限路由
    */
   async function fetchUserInfo() {
     const res = await getUserInfoApi()
@@ -76,6 +111,9 @@ export const useUserStore = defineStore('user', () => {
     username,
     nickname,
     avatar,
+    adminRole,
+    permissions,
+    hasPermission,
     login,
     logout,
     fetchUserInfo,

+ 494 - 0
code/frontend/src/views/admin/index.vue

@@ -0,0 +1,494 @@
+<template>
+  <div class="page-container">
+    <!-- 搜索栏 -->
+    <el-card class="search-card" shadow="never">
+      <el-form :model="query" inline>
+        <el-form-item label="用户名/姓名">
+          <el-input
+            v-model="query.keyword"
+            placeholder="请输入用户名或姓名"
+            clearable
+            style="width: 200px"
+            @keyup.enter="handleSearch"
+          />
+        </el-form-item>
+        <el-form-item label="角色">
+          <el-select v-model="query.adminRole" placeholder="全部" clearable style="width: 130px">
+            <el-option label="超级管理员" :value="1" />
+            <el-option label="运营人员" :value="2" />
+            <el-option label="只读用户" :value="3" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="状态">
+          <el-select v-model="query.status" placeholder="全部" clearable style="width: 100px">
+            <el-option label="启用" :value="1" />
+            <el-option label="禁用" :value="0" />
+          </el-select>
+        </el-form-item>
+        <el-form-item>
+          <el-button type="primary" :icon="Search" @click="handleSearch">查询</el-button>
+          <el-button :icon="Refresh" @click="handleReset">重置</el-button>
+        </el-form-item>
+      </el-form>
+    </el-card>
+
+    <!-- 表格卡片 -->
+    <el-card class="table-card" shadow="never">
+      <!-- 操作按钮 -->
+      <div class="toolbar">
+        <div class="toolbar-left">
+          <el-button type="primary" :icon="Plus" @click="handleAdd">新增管理员</el-button>
+          <el-button
+            type="danger"
+            :icon="Delete"
+            :disabled="selectedIds.length === 0"
+            @click="handleBatchDelete"
+          >
+            批量删除 {{ selectedIds.length > 0 ? `(${selectedIds.length})` : '' }}
+          </el-button>
+        </div>
+      </div>
+
+      <!-- 数据表格 -->
+      <el-table
+        v-loading="loading"
+        :data="tableData"
+        border
+        stripe
+        @selection-change="handleSelectionChange"
+      >
+        <el-table-column type="selection" width="50" align="center" />
+        <el-table-column label="序号" type="index" width="60" align="center" />
+        <el-table-column label="管理员ID" prop="id" width="100" align="center" />
+        <el-table-column label="用户名" prop="username" min-width="120" />
+        <el-table-column label="姓名" prop="nickname" min-width="120" />
+        <el-table-column label="角色" prop="adminRole" width="120" align="center">
+          <template #default="{ row }">
+            <el-tag :type="roleTagType(row.adminRole)" size="small">
+              {{ roleLabel(row.adminRole) }}
+            </el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column label="状态" prop="status" width="90" align="center">
+          <template #default="{ row }">
+            <el-tag :type="row.status === 1 ? 'success' : 'danger'" size="small">
+              {{ row.status === 1 ? '启用' : '禁用' }}
+            </el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column label="最后登录时间" prop="lastLoginTime" width="170" align="center">
+          <template #default="{ row }">{{ row.lastLoginTime || '—' }}</template>
+        </el-table-column>
+        <el-table-column label="操作" width="230" align="center" fixed="right">
+          <template #default="{ row }">
+            <el-button type="primary" link size="small" @click="handleEdit(row)">编辑</el-button>
+            <el-button
+              :type="row.status === 1 ? 'warning' : 'success'"
+              link
+              size="small"
+              @click="handleToggleStatus(row)"
+            >
+              {{ row.status === 1 ? '禁用' : '启用' }}
+            </el-button>
+            <el-button
+              type="primary"
+              link
+              size="small"
+              :disabled="row.adminRole === 1"
+              @click="handlePermission(row)"
+            >
+              权限配置
+            </el-button>
+            <el-button
+              type="danger"
+              link
+              size="small"
+              :disabled="row.adminRole === 1"
+              @click="handleDelete(row)"
+            >
+              删除
+            </el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <!-- 分页 -->
+      <div class="pagination">
+        <el-pagination
+          v-model:current-page="query.pageNum"
+          v-model:page-size="query.pageSize"
+          :page-sizes="[10, 20, 50]"
+          :total="total"
+          layout="total, sizes, prev, pager, next, jumper"
+          background
+          @size-change="loadData"
+          @current-change="loadData"
+        />
+      </div>
+    </el-card>
+
+    <!-- 新增/编辑对话框 -->
+    <el-dialog
+      v-model="formVisible"
+      :title="formMode === 'add' ? '新增管理员' : '编辑管理员'"
+      width="500px"
+      :close-on-click-modal="false"
+      @close="handleFormClose"
+    >
+      <el-form ref="formRef" :model="formData" :rules="formRules" label-width="90px">
+        <el-form-item label="用户名" prop="username">
+          <el-input v-model="formData.username" placeholder="4-20个字符,唯一" :disabled="formMode === 'edit'" />
+        </el-form-item>
+        <el-form-item label="密码" prop="password">
+          <el-input
+            v-model="formData.password"
+            type="password"
+            show-password
+            :placeholder="formMode === 'edit' ? '留空则不修改密码' : '8-20位,含字母和数字'"
+          />
+        </el-form-item>
+        <el-form-item label="姓名" prop="nickname">
+          <el-input v-model="formData.nickname" placeholder="最长50字符" />
+        </el-form-item>
+        <el-form-item label="角色" prop="adminRole">
+          <el-select v-model="formData.adminRole" placeholder="请选择角色" style="width: 100%">
+            <el-option label="超级管理员" :value="1" />
+            <el-option label="运营人员" :value="2" />
+            <el-option label="只读用户" :value="3" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="状态" prop="status">
+          <el-radio-group v-model="formData.status">
+            <el-radio :label="1">启用</el-radio>
+            <el-radio :label="0">禁用</el-radio>
+          </el-radio-group>
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <el-button @click="formVisible = false">取消</el-button>
+        <el-button type="primary" :loading="submitLoading" @click="handleSubmit">确定</el-button>
+      </template>
+    </el-dialog>
+
+    <!-- 权限配置对话框 -->
+    <el-dialog
+      v-model="permissionVisible"
+      title="页面权限配置"
+      width="520px"
+      :close-on-click-modal="false"
+    >
+      <div class="permission-tip">
+        <el-alert
+          title="配置该管理员可访问的页面菜单,超级管理员默认拥有全部权限(不可配置)"
+          type="info"
+          :closable="false"
+          show-icon
+        />
+      </div>
+      <div class="permission-admin-name">
+        当前管理员:<strong>{{ currentAdmin?.nickname }} ({{ currentAdmin?.username }})</strong>
+      </div>
+      <el-checkbox-group v-model="selectedRoutes" class="permission-checkboxes">
+        <el-row>
+          <el-col v-for="item in allMenuRoutes" :key="item.path" :span="12">
+            <el-checkbox :label="item.path" style="margin-bottom: 10px;">
+              {{ item.title }}
+            </el-checkbox>
+          </el-col>
+        </el-row>
+      </el-checkbox-group>
+      <template #footer>
+        <el-button @click="permissionVisible = false">取消</el-button>
+        <el-button type="primary" :loading="permissionLoading" @click="handlePermissionSubmit">
+          保存权限
+        </el-button>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup>
+import { ref, reactive, onMounted } from 'vue'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import { Search, Refresh, Plus, Delete } from '@element-plus/icons-vue'
+import { adminApi } from '@/api'
+
+// ======================== 可配置的菜单路由列表 ========================
+const allMenuRoutes = [
+  { path: '/user/profile', title: '用户管理' },
+  { path: '/acupoint', title: '穴位管理' },
+  { path: '/device', title: '设备管理' },
+  { path: '/moxibustion', title: '艾灸手法管理' },
+  { path: '/plan', title: '方案管理' },
+  { path: '/admin', title: '管理员管理' },
+  { path: '/log', title: '日志管理' },
+]
+
+// ======================== 列表 ========================
+const loading = ref(false)
+const tableData = ref([])
+const total = ref(0)
+const selectedIds = ref([])
+
+const query = reactive({
+  keyword: '',
+  adminRole: null,
+  status: null,
+  pageNum: 1,
+  pageSize: 10
+})
+
+async function loadData() {
+  loading.value = true
+  try {
+    const params = {
+      username: query.keyword || undefined,
+      nickname: query.keyword || undefined,
+      adminRole: query.adminRole ?? undefined,
+      status: query.status ?? undefined,
+      pageNum: query.pageNum,
+      pageSize: query.pageSize
+    }
+    const res = await adminApi.getAdminPage(params)
+    tableData.value = res.data.records || []
+    total.value = res.data.total || 0
+  } finally {
+    loading.value = false
+  }
+}
+
+function handleSearch() {
+  query.pageNum = 1
+  loadData()
+}
+
+function handleReset() {
+  query.keyword = ''
+  query.adminRole = null
+  query.status = null
+  query.pageNum = 1
+  loadData()
+}
+
+function handleSelectionChange(rows) {
+  selectedIds.value = rows.map(r => r.id)
+}
+
+// ======================== 角色标签 ========================
+function roleLabel(role) {
+  const map = { 1: '超级管理员', 2: '运营人员', 3: '只读用户' }
+  return map[role] || '未知'
+}
+
+function roleTagType(role) {
+  const map = { 1: 'danger', 2: 'primary', 3: 'info' }
+  return map[role] || 'default'
+}
+
+// ======================== 新增/编辑表单 ========================
+const formVisible = ref(false)
+const formMode = ref('add')
+const submitLoading = ref(false)
+const formRef = ref()
+
+const formData = reactive({
+  id: null,
+  username: '',
+  password: '',
+  nickname: '',
+  adminRole: null,
+  status: 1
+})
+
+const formRules = {
+  username: [
+    { required: true, message: '请输入用户名', trigger: 'blur' },
+    { min: 4, max: 20, message: '用户名长度4-20字符', trigger: 'blur' }
+  ],
+  password: [
+    {
+      validator: (rule, value, callback) => {
+        if (formMode.value === 'add' && !value) {
+          callback(new Error('请输入密码'))
+        } else if (value && (value.length < 8 || value.length > 20)) {
+          callback(new Error('密码长度8-20位'))
+        } else if (value && !/(?=.*[a-zA-Z])(?=.*\d)/.test(value)) {
+          callback(new Error('密码必须包含字母和数字'))
+        } else {
+          callback()
+        }
+      },
+      trigger: 'blur'
+    }
+  ],
+  nickname: [
+    { required: true, message: '请输入姓名', trigger: 'blur' },
+    { max: 50, message: '姓名最长50字符', trigger: 'blur' }
+  ],
+  adminRole: [{ required: true, message: '请选择角色', trigger: 'change' }],
+  status: [{ required: true, message: '请选择状态', trigger: 'change' }]
+}
+
+function handleAdd() {
+  formMode.value = 'add'
+  Object.assign(formData, { id: null, username: '', password: '', nickname: '', adminRole: null, status: 1 })
+  formVisible.value = true
+}
+
+function handleEdit(row) {
+  formMode.value = 'edit'
+  Object.assign(formData, {
+    id: row.id,
+    username: row.username,
+    password: '',
+    nickname: row.nickname,
+    adminRole: row.adminRole,
+    status: row.status
+  })
+  formVisible.value = true
+}
+
+function handleFormClose() {
+  formRef.value?.resetFields()
+}
+
+async function handleSubmit() {
+  await formRef.value?.validate()
+  submitLoading.value = true
+  try {
+    const payload = { ...formData }
+    if (formMode.value === 'add') {
+      await adminApi.addAdmin(payload)
+      ElMessage.success('新增管理员成功')
+    } else {
+      if (!payload.password) delete payload.password
+      await adminApi.updateAdmin(payload)
+      ElMessage.success('修改成功')
+    }
+    formVisible.value = false
+    loadData()
+  } finally {
+    submitLoading.value = false
+  }
+}
+
+// ======================== 状态切换 ========================
+async function handleToggleStatus(row) {
+  const newStatus = row.status === 1 ? 0 : 1
+  const label = newStatus === 1 ? '启用' : '禁用'
+  await ElMessageBox.confirm(`确认${label}管理员【${row.nickname}】吗?`, '提示', {
+    confirmButtonText: '确认',
+    cancelButtonText: '取消',
+    type: 'warning'
+  })
+  await adminApi.updateAdminStatus(row.id, newStatus)
+  ElMessage.success(`${label}成功`)
+  loadData()
+}
+
+// ======================== 删除 ========================
+async function handleDelete(row) {
+  await ElMessageBox.confirm(`确认删除管理员【${row.nickname}】吗?此操作不可恢复`, '警告', {
+    confirmButtonText: '确认删除',
+    cancelButtonText: '取消',
+    type: 'warning'
+  })
+  await adminApi.deleteAdmin(row.id)
+  ElMessage.success('删除成功')
+  loadData()
+}
+
+async function handleBatchDelete() {
+  await ElMessageBox.confirm(`确认删除选中的 ${selectedIds.value.length} 条记录吗?`, '批量删除', {
+    confirmButtonText: '确认删除',
+    cancelButtonText: '取消',
+    type: 'warning'
+  })
+  await adminApi.batchDeleteAdmin(selectedIds.value)
+  ElMessage.success('批量删除成功')
+  selectedIds.value = []
+  loadData()
+}
+
+// ======================== 权限配置 ========================
+const permissionVisible = ref(false)
+const permissionLoading = ref(false)
+const currentAdmin = ref(null)
+const selectedRoutes = ref([])
+
+async function handlePermission(row) {
+  currentAdmin.value = row
+  const res = await adminApi.getAdminPermission(row.id)
+  const routePaths = res.data?.routePaths
+  if (routePaths) {
+    try {
+      selectedRoutes.value = JSON.parse(routePaths)
+    } catch {
+      selectedRoutes.value = []
+    }
+  } else {
+    selectedRoutes.value = []
+  }
+  permissionVisible.value = true
+}
+
+async function handlePermissionSubmit() {
+  permissionLoading.value = true
+  try {
+    const routePathsStr = JSON.stringify(selectedRoutes.value)
+    await adminApi.updateAdminPermission(currentAdmin.value.id, routePathsStr)
+    ElMessage.success('权限配置保存成功')
+    permissionVisible.value = false
+  } finally {
+    permissionLoading.value = false
+  }
+}
+
+// ======================== 初始化 ========================
+onMounted(() => {
+  loadData()
+})
+</script>
+
+<style lang="scss" scoped>
+.page-container {
+  display: flex;
+  flex-direction: column;
+  gap: 16px;
+}
+
+.search-card :deep(.el-card__body) {
+  padding-bottom: 0;
+}
+
+.toolbar {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 16px;
+}
+
+.pagination {
+  margin-top: 16px;
+  display: flex;
+  justify-content: flex-end;
+}
+
+.permission-tip {
+  margin-bottom: 16px;
+}
+
+.permission-admin-name {
+  margin-bottom: 16px;
+  font-size: 14px;
+  color: #606266;
+}
+
+.permission-checkboxes {
+  max-height: 300px;
+  overflow-y: auto;
+  padding: 8px;
+  border: 1px solid #ebeef5;
+  border-radius: 4px;
+}
+</style>

+ 120 - 13
code/frontend/src/views/plan/index.vue

@@ -181,13 +181,28 @@
         <el-row :gutter="20">
           <el-col :span="12">
             <el-form-item label="适用年龄" prop="ageMin">
-              <el-input-number v-model="formData.ageMin" :min="1" :max="119" style="width:90px" /> 岁 ~
-              <el-input-number v-model="formData.ageMax" :min="2" :max="120" style="width:90px" /> 岁
+              <el-input-number v-model="formData.ageMin" :min="1" :max="formData.ageMax - 1" style="width:110px" />
+              岁 ~
+              <el-input-number v-model="formData.ageMax" :min="formData.ageMin + 1" style="width:110px" />
+              岁
             </el-form-item>
           </el-col>
           <el-col :span="12">
-            <el-form-item label="创作人" prop="authorName">
-              <el-input v-model="formData.authorName" placeholder="默认 ADMIN" maxlength="100" />
+            <el-form-item label="创作人" prop="authorId">
+              <el-select
+                v-model="formData.authorId"
+                placeholder="请选择创作人"
+                style="width:100%"
+                filterable
+                @change="onAuthorChange"
+              >
+                <el-option
+                  v-for="u in sysUserOptions"
+                  :key="u.id"
+                  :label="`${u.nickname || u.username} (${userTypeLabel(u.userType)})`"
+                  :value="u.id"
+                />
+              </el-select>
             </el-form-item>
           </el-col>
         </el-row>
@@ -216,8 +231,21 @@
           </div>
           <el-row :gutter="12">
             <el-col :span="8">
-              <el-form-item :label="'穴位'" :prop="`steps.${index}.acupointName`" :rules="stepRules.acupointName">
-                <el-input v-model="step.acupointName" placeholder="穴位名称" />
+              <el-form-item :label="'穴位'" :prop="`steps.${index}.acupointId`" :rules="stepRules.acupointId">
+                <el-select
+                  v-model="step.acupointId"
+                  placeholder="请选择穴位"
+                  style="width:100%"
+                  filterable
+                  @change="(val) => onAcupointChange(val, index)"
+                >
+                  <el-option
+                    v-for="a in acupointOptions"
+                    :key="a.id"
+                    :label="a.name"
+                    :value="a.id"
+                  />
+                </el-select>
               </el-form-item>
             </el-col>
             <el-col :span="8">
@@ -231,8 +259,21 @@
               </el-form-item>
             </el-col>
             <el-col :span="8">
-              <el-form-item :label="'手法'" :prop="`steps.${index}.techniqueName`" :rules="stepRules.techniqueName">
-                <el-input v-model="step.techniqueName" placeholder="艾灸手法" />
+              <el-form-item :label="'手法'" :prop="`steps.${index}.techniqueId`" :rules="stepRules.techniqueId">
+                <el-select
+                  v-model="step.techniqueId"
+                  placeholder="请选择手法"
+                  style="width:100%"
+                  filterable
+                  @change="(val) => onTechniqueChange(val, index)"
+                >
+                  <el-option
+                    v-for="t in techniqueOptions"
+                    :key="t.id"
+                    :label="t.name"
+                    :value="t.id"
+                  />
+                </el-select>
               </el-form-item>
             </el-col>
           </el-row>
@@ -331,11 +372,36 @@ import {
   offshelfPlan,
   batchOffshelfPlan
 } from '@/api/plan'
-
+import { getAcupointList } from '@/api/acupoint'
+import { getTechniqueList } from '@/api/moxibustion'
+import { getAppUserList } from '@/api/auth'
 // ============ 枚举常量 ============
 const effectOptions = ['驱寒', '祛湿', '祛风', '化瘀', '活血', '化痰', '养颜', '扶阳']
 const symptomsOptions = ['怕冷', '感冒', '咳嗽', '痰多', '疼痛', '腰酸', '失眠', '疲劳', '皮肤暗沉', '手脚冰凉', '其他']
 
+// ============ 选项列表(用于选择框) ============
+const acupointOptions = ref([])
+const techniqueOptions = ref([])
+const sysUserOptions = ref([])
+
+/**
+ * 加载穴位、手法、系统用户选项列表
+ */
+async function loadOptions() {
+  try {
+    const [acupointRes, techniqueRes, userRes] = await Promise.all([
+      getAcupointList(),
+      getTechniqueList(),
+      getAppUserList()
+    ])
+    acupointOptions.value = acupointRes.data || []
+    techniqueOptions.value = techniqueRes.data || []
+    sysUserOptions.value = userRes.data || []
+  } catch (e) {
+    // 选项加载失败不影响主流程
+  }
+}
+
 /**
  * 模式类型标签文字
  */
@@ -438,10 +504,12 @@ const submitLoading = ref(false)
 const formRef = ref(null)
 
 const defaultStep = () => ({
+  acupointId: null,
   acupointName: '',
   side: 1,
   temperature: 42,
   duration: 5,
+  techniqueId: null,
   techniqueName: '',
   remark: ''
 })
@@ -456,7 +524,8 @@ const defaultForm = () => ({
   applicableGender: 0,
   ageMin: 1,
   ageMax: 120,
-  authorName: 'ADMIN',
+  authorId: null,
+  authorName: '',
   authorType: 1,
   description: '',
   steps: [defaultStep()]
@@ -470,11 +539,46 @@ const formRules = {
 }
 
 const stepRules = {
-  acupointName: [{ required: true, message: '请填写穴位名称', trigger: 'blur' }],
+  acupointId: [{ required: true, message: '请选择穴位', trigger: 'change' }],
   side: [{ required: true, message: '请选择侧别', trigger: 'change' }],
   temperature: [{ required: true, message: '请填写温度', trigger: 'blur' }],
   duration: [{ required: true, message: '请填写时间', trigger: 'blur' }],
-  techniqueName: [{ required: true, message: '请填写艾灸手法', trigger: 'blur' }]
+  techniqueId: [{ required: true, message: '请选择艾灸手法', trigger: 'change' }]
+}
+
+/**
+ * 穴位选择变更:同步填充 acupointName
+ */
+function onAcupointChange(id, index) {
+  const found = acupointOptions.value.find(a => a.id === id)
+  formData.steps[index].acupointName = found ? found.name : ''
+}
+
+/**
+ * 手法选择变更:同步填充 techniqueName
+ */
+function onTechniqueChange(id, index) {
+  const found = techniqueOptions.value.find(t => t.id === id)
+  formData.steps[index].techniqueName = found ? found.name : ''
+}
+
+/**
+ * 应用用户类型标签文字
+ */
+function userTypeLabel(val) {
+  return { 1: '普通用户', 2: '专家', 3: '管理员' }[val] || '未知'
+}
+
+/**
+ * 创作人选择变更:同步填充 authorName 和 authorType
+ * AppUser.userType → Plan.authorType 映射:1(普通用户)→2,2(专家)→3,3(管理员)→1
+ */
+function onAuthorChange(id) {
+  const found = sysUserOptions.value.find(u => u.id === id)
+  formData.authorName = found ? (found.nickname || found.username) : ''
+  // 角色映射
+  const typeMap = { 1: 2, 2: 3, 3: 1 }
+  formData.authorType = found ? (typeMap[found.userType] || 2) : 2
 }
 
 /**
@@ -623,7 +727,10 @@ async function handleBatchDelete() {
   loadData()
 }
 
-onMounted(loadData)
+onMounted(() => {
+  loadData()
+  loadOptions()
+})
 </script>
 
 <style lang="scss" scoped>

+ 10 - 5
code/frontend/src/views/user/app-user/index.vue

@@ -13,6 +13,7 @@
           <el-select v-model="queryForm.userType" placeholder="全部" clearable style="width:120px">
             <el-option label="普通用户" :value="1" />
             <el-option label="专家" :value="2" />
+            <el-option label="管理员" :value="3" />
           </el-select>
         </el-form-item>
         <el-form-item label="状态">
@@ -32,7 +33,7 @@
     <el-card class="table-card">
       <template #header>
         <div class="card-header">
-          <span class="title">应用用户列表</span>
+          <span class="title">账号管理列表</span>
           <el-button type="primary" :icon="Plus" @click="handleAdd">新增用户</el-button>
         </div>
       </template>
@@ -43,8 +44,11 @@
         <el-table-column prop="nickname" label="昵称" />
         <el-table-column prop="userType" label="用户类型" width="100" align="center">
           <template #default="{ row }">
-            <el-tag :type="row.userType === 2 ? 'warning' : 'primary'" size="small">
-              {{ row.userType === 2 ? '专家' : '普通用户' }}
+            <el-tag
+              :type="row.userType === 2 ? 'warning' : row.userType === 3 ? 'danger' : 'primary'"
+              size="small"
+            >
+              {{ row.userType === 2 ? '专家' : row.userType === 3 ? '管理员' : '普通用户' }}
             </el-tag>
           </template>
         </el-table-column>
@@ -106,6 +110,7 @@
           <el-select v-model="formData.userType" placeholder="请选择" style="width:100%">
             <el-option label="普通用户" :value="1" />
             <el-option label="专家" :value="2" />
+            <el-option label="管理员" :value="3" />
           </el-select>
         </el-form-item>
         <el-form-item label="状态" prop="status">
@@ -202,14 +207,14 @@ function handleReset() {
 // 新增
 function handleAdd() {
   Object.assign(formData, { id: null, phone: '', nickname: '', userType: 1, status: 1 })
-  dialogTitle.value = '新增应用用户'
+  dialogTitle.value = '新增账号'
   dialogVisible.value = true
 }
 
 // 编辑
 function handleEdit(row) {
   Object.assign(formData, { id: row.id, phone: row.phone, nickname: row.nickname, userType: row.userType, status: row.status })
-  dialogTitle.value = '编辑应用用户'
+  dialogTitle.value = '编辑账号'
   dialogVisible.value = true
 }
 

+ 1 - 1
code/frontend/vite.config.js

@@ -15,7 +15,7 @@ export default defineConfig({
     // 代理配置:开发时将 /api 请求转发到后端
     proxy: {
       '/api': {
-        target: 'http://localhost:8080',
+        target: 'http://localhost:18888',
         changeOrigin: true
       }
     }

+ 29 - 0
code/管理员管理.md

@@ -0,0 +1,29 @@
+### 7.5 系统管理
+
+#### 7.5.1 管理员管理
+
+**功能说明**:管理后台系统管理员账号,包括新增管理员、分配权限、禁用/启用账号。
+
+**管理员列表**:
+
+| 多选框 | 序号 | 管理员ID | 用户名 | 姓名 | 角色 | 状态 | 最后登录时间 | 操作 |
+|--------|------|----------|--------|------|------|------|--------------|------|
+| ☐ | 1 | A001 | admin | 张管理员 | 超级管理员 | 启用 | 2026-04-01 | 编辑/禁用 |
+| ☐ | 2 | A002 | operator | 李操作员 | 运营人员 | 启用 | 2026-03-31 | 编辑/禁用 |
+
+**查询条件**:用户名/姓名(模糊搜索)、角色(下拉)、状态(下拉)
+
+**操作按钮**:新增、批量删除
+
+**新增表单字段**:
+
+| 字段名 | 类型 | 必填 | 验证规则 | 说明 |
+|--------|------|------|----------|------|
+| 用户名 | 文本 | 是 | 4-20字符,唯一值 | 登录账号 |
+| 密码 | 密码 | 是 | 8-20字符,含字母和数字 | - |
+| 姓名 | 文本 | 是 | 最长50字符 | - |
+| 角色 | 下拉选择 | 是 | 超级管理员 / 运营人员 / 只读用户 | - |
+| 状态 | 单选 | 是 | 启用 / 禁用 | - |
+
+
+用应用用户表,帮我设计实现能添加管理员并且能给管理员操作一些页面的权限配置。