Ver código fonte

问题修改与优化

jiapu 3 meses atrás
pai
commit
f5853c574c
27 arquivos alterados com 192 adições e 136 exclusões
  1. 11 11
      code/backend/src/main/java/com/aijiuyi/admin/common/constant/ResultCode.java
  2. 3 2
      code/backend/src/main/java/com/aijiuyi/admin/controller/PlanController.java
  3. 20 20
      code/backend/src/main/java/com/aijiuyi/admin/controller/UserCategoryController.java
  4. 1 1
      code/backend/src/main/java/com/aijiuyi/admin/controller/UserProfileController.java
  5. 1 1
      code/backend/src/main/java/com/aijiuyi/admin/controller/dto/AppUserQueryDTO.java
  6. 3 1
      code/backend/src/main/java/com/aijiuyi/admin/controller/dto/PlanSaveDTO.java
  7. 1 1
      code/backend/src/main/java/com/aijiuyi/admin/controller/dto/UserCategoryQueryDTO.java
  8. 4 1
      code/backend/src/main/java/com/aijiuyi/admin/controller/dto/UserProfileDTO.java
  9. 2 2
      code/backend/src/main/java/com/aijiuyi/admin/controller/dto/UserProfileQueryDTO.java
  10. 1 1
      code/backend/src/main/java/com/aijiuyi/admin/entity/AppUser.java
  11. 1 1
      code/backend/src/main/java/com/aijiuyi/admin/entity/Plan.java
  12. 2 2
      code/backend/src/main/java/com/aijiuyi/admin/entity/UserCategory.java
  13. 8 0
      code/backend/src/main/java/com/aijiuyi/admin/entity/UserProfile.java
  14. 1 1
      code/backend/src/main/java/com/aijiuyi/admin/mapper/UserCategoryMapper.java
  15. 11 11
      code/backend/src/main/java/com/aijiuyi/admin/service/UserCategoryService.java
  16. 4 4
      code/backend/src/main/java/com/aijiuyi/admin/service/impl/UserCategoryServiceImpl.java
  17. 43 3
      code/backend/src/main/java/com/aijiuyi/admin/service/impl/UserProfileServiceImpl.java
  18. 12 3
      code/backend/src/main/resources/mapper/UserProfileMapper.xml
  19. 1 1
      code/frontend/src/api/index.js
  20. 1 1
      code/frontend/src/api/user.js
  21. 11 11
      code/frontend/src/api/userCategory.js
  22. 1 1
      code/frontend/src/router/index.js
  23. 1 1
      code/frontend/src/views/admin/index.vue
  24. 1 1
      code/frontend/src/views/plan/index.vue
  25. 11 11
      code/frontend/src/views/system/user-category/index.vue
  26. 5 5
      code/frontend/src/views/user/app-user/index.vue
  27. 31 38
      code/frontend/src/views/user/profile/index.vue

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

@@ -147,17 +147,17 @@ public enum ResultCode {
     /** 文件上传失败 */
     FILE_UPLOAD_FAILED(1953, "文件上传失败"),
 
-    // ======================== 用户类相关 1970-1989 ========================
-    /** 用户类不存在 */
-    USER_CATEGORY_NOT_FOUND(1970, "用户类不存在"),
-    /** 用户类编码已存在 */
-    USER_CATEGORY_CODE_EXISTS(1971, "用户类编码已存在"),
-    /** 用户类名称已存在 */
-    USER_CATEGORY_NAME_EXISTS(1972, "用户类名称已存在"),
-    /** 内置用户类不可删除 */
-    USER_CATEGORY_BUILTIN_FORBIDDEN(1973, "内置用户类不可删除或修改编码"),
-    /** 用户类已被使用 */
-    USER_CATEGORY_IN_USE(1974, "用户类已被用户使用,不可删除");
+    // ======================== 用户类相关 1970-1989 ========================
+    /** 用户类不存在 */
+    USER_CATEGORY_NOT_FOUND(1970, "用户类不存在"),
+    /** 用户类编码已存在 */
+    USER_CATEGORY_CODE_EXISTS(1971, "用户类编码已存在"),
+    /** 用户类名称已存在 */
+    USER_CATEGORY_NAME_EXISTS(1972, "用户类名称已存在"),
+    /** 内置用户类不可删除 */
+    USER_CATEGORY_BUILTIN_FORBIDDEN(1973, "内置用户类不可删除或修改编码"),
+    /** 用户类已被使用 */
+    USER_CATEGORY_IN_USE(1974, "用户类已被用户使用,不可删除");
 
     // ======================== 其他业务错误码在此区间之后继续添加 ========================
 

+ 3 - 2
code/backend/src/main/java/com/aijiuyi/admin/controller/PlanController.java

@@ -11,6 +11,7 @@ import com.baomidou.mybatisplus.core.metadata.IPage;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.web.bind.annotation.*;
 
+import javax.validation.Valid;
 import java.util.List;
 
 /**
@@ -56,7 +57,7 @@ public class PlanController {
      */
     @PostMapping
     @Log(value = "新增方案", module = "方案管理", operationType = OperationType.INSERT)
-    public Result<Void> add(@RequestBody PlanSaveDTO dto) {
+    public Result<Void> add(@RequestBody @Valid PlanSaveDTO dto) {
         planService.addPlan(dto);
         return Result.success();
     }
@@ -69,7 +70,7 @@ public class PlanController {
      */
     @PutMapping
     @Log(value = "修改方案信息", module = "方案管理", operationType = OperationType.UPDATE)
-    public Result<Void> update(@RequestBody PlanSaveDTO dto) {
+    public Result<Void> update(@RequestBody @Valid PlanSaveDTO dto) {
         planService.updatePlan(dto);
         return Result.success();
     }

+ 20 - 20
code/backend/src/main/java/com/aijiuyi/admin/controller/UserCategoryController.java

@@ -13,8 +13,8 @@ import org.springframework.web.bind.annotation.*;
 import java.util.List;
 
 /**
- * 用户类管理 Controller
- * 提供 app_user.user_type=1 用户分类的增删改查接口
+ * 用户类管理 Controller
+ * 提供 app_user.user_type=1 用户分类的增删改查接口
  */
 @RestController
 @RequestMapping("/user/category")
@@ -24,74 +24,74 @@ public class UserCategoryController {
     private UserCategoryService userCategoryService;
 
     /**
-     * 分页查询用户类
+     * 分页查询用户
      *
      * @param queryDTO 查询条件
      * @return 分页结果
      */
     @GetMapping("/page")
-    @Log(value = "查询用户类列表", module = "用户类管理", operationType = OperationType.QUERY)
+    @Log(value = "查询用户类列表", module = "用户类管理", operationType = OperationType.QUERY)
     public Result<IPage<UserCategory>> page(UserCategoryQueryDTO queryDTO) {
         return Result.success(userCategoryService.pageList(queryDTO));
     }
 
     /**
-     * 查询所有用户类(前端下拉选择使用)
+     * 查询所有用户类(前端下拉选择使用)
      *
-     * @return 启用的用户类列表
+     * @return 启用的用户类列表
      */
     @GetMapping("/list")
-    @Log(value = "获取用户类下拉列表", module = "用户类管理", operationType = OperationType.QUERY)
+    @Log(value = "获取用户类下拉列表", module = "用户类管理", operationType = OperationType.QUERY)
     public Result<List<UserCategory>> list() {
         return Result.success(userCategoryService.listEnabled());
     }
 
     /**
-     * 查询用户类详情
+     * 查询用户类详情
      *
-     * @param id 类ID
-     * @return 类详情
+     * @param id 类ID
+     * @return 类详情
      */
     @GetMapping("/{id}")
-    @Log(value = "查询用户类详情", module = "用户类管理", operationType = OperationType.QUERY)
+    @Log(value = "查询用户类详情", module = "用户类管理", operationType = OperationType.QUERY)
     public Result<UserCategory> getById(@PathVariable Long id) {
         return Result.success(userCategoryService.getById(id));
     }
 
     /**
-     * 新增用户类
+     * 新增用户
      *
-     * @param category 类信息
+     * @param category 类信息
      * @return 操作结果
      */
     @PostMapping
-    @Log(value = "新增用户类", module = "用户类管理", operationType = OperationType.INSERT)
+    @Log(value = "新增用户类", module = "用户类管理", operationType = OperationType.INSERT)
     public Result<Void> add(@RequestBody UserCategory category) {
         userCategoryService.addCategory(category);
         return Result.success();
     }
 
     /**
-     * 修改用户类
+     * 修改用户
      *
-     * @param category 类信息
+     * @param category 类信息
      * @return 操作结果
      */
     @PutMapping
-    @Log(value = "修改用户类", module = "用户类管理", operationType = OperationType.UPDATE)
+    @Log(value = "修改用户类", module = "用户类管理", operationType = OperationType.UPDATE)
     public Result<Void> update(@RequestBody UserCategory category) {
         userCategoryService.updateCategory(category);
         return Result.success();
     }
 
     /**
-     * 删除用户类(已被用户使用的类不可删除)
+     * 删除用户类(已被用户使用的类不可删除)
      *
-     * @param id 类ID
+     * @param id 类ID
      * @return 操作结果
      */
     @DeleteMapping("/{id}")
-    @Log(value = "删除用户类", module = "用户类管理", operationType = OperationType.DELETE)
+    @Log(value = "删除用户类", module = "用户类管理", operationType = OperationType.DELETE)
     public Result<Void> delete(@PathVariable Long id) {
         userCategoryService.deleteCategory(id);
         return Result.success();

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

@@ -28,7 +28,7 @@ public class UserProfileController {
     /**
      * 分页查询用户列表(联表获取设备编号)
      *
-     * @param queryDTO 查询条件(关键字/设备编号/性别/注册时间范围/穴位表状态)
+     * @param queryDTO 查询条件(关键字/设备编号/性别/用户分类/注册时间范围/穴位表状态)
      * @return 分页结果
      */
     @GetMapping("/page")

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

@@ -17,7 +17,7 @@ public class AppUserQueryDTO {
     /** 用户类型:1=用户,2=专家,3=管理员 */
     private Integer userType;
 
-    /** 用户类ID(仅 userType=1 用户有效) */
+    /** 用户类ID(仅 userType=1 用户有效) */
     private Long userCategoryId;
 
     /** 状态:1=正常,0=禁用 */

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

@@ -3,6 +3,7 @@ package com.aijiuyi.admin.controller.dto;
 import com.aijiuyi.admin.entity.PlanStep;
 import lombok.Data;
 
+import javax.validation.constraints.Size;
 import java.util.List;
 
 /**
@@ -38,7 +39,8 @@ public class PlanSaveDTO {
     /** 适用年龄最大值(岁) */
     private Integer ageMax;
 
-    /** 方案描述(最长200字符) */
+    /** 方案描述(最长500字符) */
+    @Size(max = 500, message = "方案描述最多500字")
     private String description;
 
     /** 创作人类型:1=管理员,2=用户,3=专家 */

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

@@ -3,7 +3,7 @@ package com.aijiuyi.admin.controller.dto;
 import lombok.Data;
 
 /**
- * 用户类分页查询参数 DTO
+ * 用户类分页查询参数 DTO
  */
 @Data
 public class UserCategoryQueryDTO {

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

@@ -38,9 +38,12 @@ public class UserProfileDTO {
     @Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确")
     private String phone;
 
-    /** 用户类型:1=用户,2=专家,3=管理员(选填,缺省按用户) */
+    /** 用户类型:保留兼容,用户列表统一按 1=用户 处理 */
     private Integer userType;
 
+    /** 用户分类ID(仅 userType=1 用户有效,关联 app_user_category.id) */
+    private Long userCategoryId;
+
     /** 省级行政区划编码(GB/T 2260) */
     private String provinceCode;
 

+ 2 - 2
code/backend/src/main/java/com/aijiuyi/admin/controller/dto/UserProfileQueryDTO.java

@@ -17,8 +17,8 @@ public class UserProfileQueryDTO {
     /** 性别:1=男,2=女,不传则查全部 */
     private Integer gender;
 
-    /** 用户类型:1=用户,2=专家,3=管理员,不传则查全部 */
-    private Integer userType;
+    /** 用户分类ID(仅查询身份为用户的数据) */
+    private Long userCategoryId;
 
     /** 省级行政区划编码(选填,可独立查询) */
     private String provinceCode;

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

@@ -47,7 +47,7 @@ public class AppUser {
     private Integer userType;
 
     /**
-     * 用户类ID(仅 userType=1 用户有效,关联 app_user_category.id)
+     * 用户类ID(仅 userType=1 用户有效,关联 app_user_category.id)
      */
     private Long userCategoryId;
 

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

@@ -48,7 +48,7 @@ public class Plan {
     /** 适用年龄最大值(岁) */
     private Integer ageMax;
 
-    /** 方案描述(最长200字符) */
+    /** 方案描述(最长500字符) */
     private String description;
 
     /**

+ 2 - 2
code/backend/src/main/java/com/aijiuyi/admin/entity/UserCategory.java

@@ -6,7 +6,7 @@ import lombok.Data;
 import java.time.LocalDateTime;
 
 /**
- * 用户类实体(用户子分类)
+ * 用户类实体
  * 对应数据库 app_user_category 表
  * 仅用于 app_user.user_type = 1 的用户分类
  */
@@ -21,7 +21,7 @@ public class UserCategory {
     /** 归属用户类型,固定为 1=用户 */
     private Integer userType;
 
-    /** 类名称 */
+    /** 类名称 */
     private String name;
 
     /** 备注说明 */

+ 8 - 0
code/backend/src/main/java/com/aijiuyi/admin/entity/UserProfile.java

@@ -36,6 +36,14 @@ public class UserProfile {
     /** 用户类型:1=用户,2=专家,3=管理员 */
     private Integer userType;
 
+    /** 用户分类ID(非数据库字段,查询时从 app_user 关联填充) */
+    @TableField(exist = false)
+    private Long userCategoryId;
+
+    /** 用户分类名称(非数据库字段,查询时从 app_user_category 关联填充) */
+    @TableField(exist = false)
+    private String userCategoryName;
+
     /** 省级行政区划编码(GB/T 2260) */
     private String provinceCode;
 

+ 1 - 1
code/backend/src/main/java/com/aijiuyi/admin/mapper/UserCategoryMapper.java

@@ -5,7 +5,7 @@ import com.baomidou.mybatisplus.core.mapper.BaseMapper;
 import org.apache.ibatis.annotations.Mapper;
 
 /**
- * 用户类 Mapper 接口
+ * 用户类 Mapper 接口
  */
 @Mapper
 public interface UserCategoryMapper extends BaseMapper<UserCategory> {

+ 11 - 11
code/backend/src/main/java/com/aijiuyi/admin/service/UserCategoryService.java

@@ -8,12 +8,12 @@ import com.baomidou.mybatisplus.extension.service.IService;
 import java.util.List;
 
 /**
- * 用户类 Service 接口
+ * 用户类 Service 接口
  */
 public interface UserCategoryService extends IService<UserCategory> {
 
     /**
-     * 分页查询用户类
+     * 分页查询用户
      *
      * @param queryDTO 查询条件
      * @return 分页结果
@@ -21,31 +21,31 @@ public interface UserCategoryService extends IService<UserCategory> {
     IPage<UserCategory> pageList(UserCategoryQueryDTO queryDTO);
 
     /**
-     * 查询所有用户类(用于前端下拉选择)
+     * 查询所有用户类(用于前端下拉选择)
      *
-     * @return 用户类列表
+     * @return 用户类列表
      */
     List<UserCategory> listEnabled();
 
     /**
-     * 新增用户类
+     * 新增用户
      *
-     * @param category 类信息
+     * @param category 类信息
      */
     void addCategory(UserCategory category);
 
     /**
-     * 修改用户类
+     * 修改用户
      *
-     * @param category 类信息
+     * @param category 类信息
      */
     void updateCategory(UserCategory category);
 
     /**
-     * 删除用户类(逻辑删除)
-     * 已被用户使用的类不可删除
+     * 删除用户类(逻辑删除)
+     * 已被用户使用的类不可删除
      *
-     * @param id 类ID
+     * @param id 类ID
      */
     void deleteCategory(Long id);
 

+ 4 - 4
code/backend/src/main/java/com/aijiuyi/admin/service/impl/UserCategoryServiceImpl.java

@@ -21,7 +21,7 @@ import java.time.LocalDateTime;
 import java.util.List;
 
 /**
- * 用户类 Service 实现类
+ * 用户类 Service 实现类
  */
 @Service
 public class UserCategoryServiceImpl extends ServiceImpl<UserCategoryMapper, UserCategory>
@@ -72,7 +72,7 @@ public class UserCategoryServiceImpl extends ServiceImpl<UserCategoryMapper, Use
         category.setUserType(NORMAL_USER_TYPE);
         category.setName(name);
         save(category);
-        LogUtil.info(UserCategoryServiceImpl.class, "新增用户类 name=[{}]", category.getName());
+        LogUtil.info(UserCategoryServiceImpl.class, "新增用户类 name=[{}]", category.getName());
     }
 
     @Override
@@ -103,7 +103,7 @@ public class UserCategoryServiceImpl extends ServiceImpl<UserCategoryMapper, Use
                 .eq(UserCategory::getId, category.getId())
                 .eq(UserCategory::getUserType, NORMAL_USER_TYPE)
                 .update();
-        LogUtil.info(UserCategoryServiceImpl.class, "修改用户类[{}]", category.getId());
+        LogUtil.info(UserCategoryServiceImpl.class, "修改用户类[{}]", category.getId());
     }
 
     @Override
@@ -123,6 +123,6 @@ public class UserCategoryServiceImpl extends ServiceImpl<UserCategoryMapper, Use
             throw new BusinessException(ResultCode.USER_CATEGORY_IN_USE);
         }
         removeById(id);
-        LogUtil.info(UserCategoryServiceImpl.class, "删除用户类[{}]", id);
+        LogUtil.info(UserCategoryServiceImpl.class, "删除用户类[{}]", id);
     }
 }

+ 43 - 3
code/backend/src/main/java/com/aijiuyi/admin/service/impl/UserProfileServiceImpl.java

@@ -6,8 +6,10 @@ import com.aijiuyi.admin.common.util.LogUtil;
 import com.aijiuyi.admin.controller.dto.UserProfileDTO;
 import com.aijiuyi.admin.controller.dto.UserProfileQueryDTO;
 import com.aijiuyi.admin.entity.AppUser;
+import com.aijiuyi.admin.entity.UserCategory;
 import com.aijiuyi.admin.entity.UserProfile;
 import com.aijiuyi.admin.mapper.AppUserMapper;
+import com.aijiuyi.admin.mapper.UserCategoryMapper;
 import com.aijiuyi.admin.mapper.UserProfileMapper;
 import com.aijiuyi.admin.service.UserAcupointService;
 import com.aijiuyi.admin.service.UserProfileService;
@@ -30,12 +32,17 @@ import java.util.List;
 @Service
 public class UserProfileServiceImpl extends ServiceImpl<UserProfileMapper, UserProfile> implements UserProfileService {
 
+    private static final int NORMAL_USER_TYPE = 1;
+
     @Autowired
     private UserAcupointService userAcupointService;
 
     @Autowired
     private AppUserMapper appUserMapper;
 
+    @Autowired
+    private UserCategoryMapper userCategoryMapper;
+
     @Override
     public IPage<UserProfile> pageList(UserProfileQueryDTO queryDTO) {
         Page<UserProfile> page = new Page<>(queryDTO.getPageNum(), queryDTO.getPageSize());
@@ -52,7 +59,8 @@ public class UserProfileServiceImpl extends ServiceImpl<UserProfileMapper, UserP
         save(profile);
         LogUtil.info(UserProfileServiceImpl.class, "新增子用户[{}],手机号[{}]", dto.getName(), dto.getPhone());
 
-        syncAppUserType(dto.getPhone(), dto.getUserType());
+        syncAppUserType(dto.getPhone(), NORMAL_USER_TYPE);
+        syncAppUserCategory(dto.getPhone(), dto.getUserCategoryId());
 
         regenerateUserAcupoints(profile.getId(), "新增");
     }
@@ -78,7 +86,8 @@ public class UserProfileServiceImpl extends ServiceImpl<UserProfileMapper, UserP
 
         // 优先以传入手机号同步;未传入时回退到已存记录的手机号
         String syncPhone = StringUtils.hasText(dto.getPhone()) ? dto.getPhone() : exist.getPhone();
-        syncAppUserType(syncPhone, dto.getUserType());
+        syncAppUserType(syncPhone, NORMAL_USER_TYPE);
+        syncAppUserCategory(syncPhone, dto.getUserCategoryId());
 
         regenerateUserAcupoints(dto.getId(), "修改");
     }
@@ -210,6 +219,37 @@ public class UserProfileServiceImpl extends ServiceImpl<UserProfileMapper, UserP
                 "同步 app_user[{}] 用户类型为[{}](手机号[{}])", appUser.getId(), userType, phone);
     }
 
+    /**
+     * 同步用户分类到 app_user:用户列表只维护身份为“用户”的档案,分类存储在 app_user.user_category_id。
+     */
+    private void syncAppUserCategory(String phone, Long userCategoryId) {
+        if (!StringUtils.hasText(phone) || userCategoryId == null) {
+            return;
+        }
+        long categoryCount = userCategoryMapper.selectCount(
+                new LambdaQueryWrapper<UserCategory>()
+                        .eq(UserCategory::getId, userCategoryId)
+                        .eq(UserCategory::getUserType, NORMAL_USER_TYPE));
+        if (categoryCount <= 0) {
+            throw new BusinessException(ResultCode.USER_CATEGORY_NOT_FOUND);
+        }
+
+        AppUser appUser = appUserMapper.selectOne(
+                new LambdaQueryWrapper<AppUser>().eq(AppUser::getPhone, phone));
+        if (appUser == null || !Integer.valueOf(NORMAL_USER_TYPE).equals(appUser.getUserType())) {
+            return;
+        }
+        if (userCategoryId.equals(appUser.getUserCategoryId())) {
+            return;
+        }
+        AppUser update = new AppUser();
+        update.setId(appUser.getId());
+        update.setUserCategoryId(userCategoryId);
+        appUserMapper.updateById(update);
+        LogUtil.info(UserProfileServiceImpl.class,
+                "同步 app_user[{}] 用户分类为[{}](手机号[{}])", appUser.getId(), userCategoryId, phone);
+    }
+
     private UserProfile buildProfileFromDto(UserProfileDTO dto) {
         UserProfile profile = new UserProfile();
         profile.setUserId(dto.getUserId());
@@ -217,7 +257,7 @@ public class UserProfileServiceImpl extends ServiceImpl<UserProfileMapper, UserP
         profile.setGender(dto.getGender());
         profile.setAge(dto.getAge());
         profile.setPhone(dto.getPhone());
-        profile.setUserType(dto.getUserType());
+        profile.setUserType(NORMAL_USER_TYPE);
         profile.setProvinceCode(dto.getProvinceCode());
         profile.setCityCode(dto.getCityCode());
         profile.setDistrictCode(dto.getDistrictCode());

+ 12 - 3
code/backend/src/main/resources/mapper/UserProfileMapper.xml

@@ -3,7 +3,8 @@
 <mapper namespace="com.aijiuyi.admin.mapper.UserProfileMapper">
 
     <!--
-        分页查询子用户列表,LEFT JOIN user_device 获取绑定设备编号
+        分页查询用户列表,LEFT JOIN user_device 获取绑定设备编号
+        该列表固定展示身份为“用户”的数据,并关联 app_user_category 获取用户分类
         device_codes: GROUP_CONCAT 逗号拼接已绑定设备编号
         设备编号过滤使用子查询,避免 LEFT JOIN 过滤导致无设备用户丢失
     -->
@@ -32,10 +33,18 @@
             up.deleted,
             up.create_time,
             up.update_time,
+            MAX(COALESCE(au_id.user_category_id, au_phone.user_category_id)) AS user_category_id,
+            MAX(uc.name) AS user_category_name,
             GROUP_CONCAT(ud.device_code ORDER BY ud.id SEPARATOR ',') AS device_codes
         FROM user_profile up
         LEFT JOIN user_device ud ON ud.user_id = up.id AND ud.status = 1
+        LEFT JOIN app_user au_id ON au_id.id = up.user_id AND au_id.deleted = 0
+        LEFT JOIN app_user au_phone ON au_id.id IS NULL AND au_phone.phone = up.phone AND au_phone.deleted = 0
+        LEFT JOIN app_user_category uc
+            ON uc.id = COALESCE(au_id.user_category_id, au_phone.user_category_id)
+            AND uc.deleted = 0
         WHERE up.deleted = 0
+          AND up.user_type = 1
         <if test="q.keyword != null and q.keyword != ''">
             AND (up.name LIKE CONCAT('%', #{q.keyword}, '%')
               OR up.phone LIKE CONCAT('%', #{q.keyword}, '%'))
@@ -43,8 +52,8 @@
         <if test="q.gender != null">
             AND up.gender = #{q.gender}
         </if>
-        <if test="q.userType != null">
-            AND up.user_type = #{q.userType}
+        <if test="q.userCategoryId != null">
+            AND COALESCE(au_id.user_category_id, au_phone.user_category_id) = #{q.userCategoryId}
         </if>
         <if test="q.provinceCode != null and q.provinceCode != ''">
             AND up.province_code = #{q.provinceCode}

+ 1 - 1
code/frontend/src/api/index.js

@@ -35,5 +35,5 @@ export * as simulationApi from './simulation'
 // 内容管理模块
 export * as contentApi from './content'
 
-// 用户类管理模块
+// 用户类管理模块
 export * as userCategoryApi from './userCategory'

+ 1 - 1
code/frontend/src/api/user.js

@@ -9,7 +9,7 @@ import request from '@/utils/request'
 
 /**
  * 分页查询用户列表(联表获取设备编号)
- * @param {Object} params 查询参数:keyword/deviceCode/gender/startTime/endTime/acupointStatus/pageNum/pageSize
+ * @param {Object} params 查询参数:keyword/deviceCode/gender/userCategoryId/startTime/endTime/acupointStatus/pageNum/pageSize
  */
 export function getProfilePage(params) {
   return request.get('/user/profile/page', { params })

+ 11 - 11
code/frontend/src/api/userCategory.js

@@ -1,12 +1,12 @@
 import request from '@/utils/request'
 
 /**
- * 用户类管理模块 API
+ * 用户类管理模块 API
  * 对应后端 UserCategoryController
  */
 
 /**
- * 分页查询用户类
+ * 分页查询用户
  * @param {Object} params 查询参数:keyword/pageNum/pageSize
  */
 export function getUserCategoryPage(params) {
@@ -14,39 +14,39 @@ export function getUserCategoryPage(params) {
 }
 
 /**
- * 查询所有用户类(前端下拉用)
+ * 查询所有用户类(前端下拉用)
  */
 export function getUserCategoryList() {
   return request.get('/user/category/list')
 }
 
 /**
- * 查询用户类详情
- * @param {string} id 类ID
+ * 查询用户类详情
+ * @param {string} id 类ID
  */
 export function getUserCategoryById(id) {
   return request.get(`/user/category/${id}`)
 }
 
 /**
- * 新增用户类
- * @param {Object} data 类信息:name/remark
+ * 新增用户
+ * @param {Object} data 类信息:name/remark
  */
 export function addUserCategory(data) {
   return request.post('/user/category', data)
 }
 
 /**
- * 修改用户类
- * @param {Object} data 类信息:id/name/remark
+ * 修改用户
+ * @param {Object} data 类信息:id/name/remark
  */
 export function updateUserCategory(data) {
   return request.put('/user/category', data)
 }
 
 /**
- * 删除用户类
- * @param {string} id 类ID
+ * 删除用户
+ * @param {string} id 类ID
  */
 export function deleteUserCategory(id) {
   return request.delete(`/user/category/${id}`)

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

@@ -122,7 +122,7 @@ const routes = [
             path: 'user-category',
             name: 'UserCategory',
             component: () => import('@/views/system/user-category/index.vue'),
-            meta: { requiresAuth: true, title: '用户类', icon: 'Collection' }
+            meta: { requiresAuth: true, title: '用户类', icon: 'Collection' }
           },
           {
             path: 'log',

+ 1 - 1
code/frontend/src/views/admin/index.vue

@@ -230,7 +230,7 @@ const allMenuRoutes = [
   { path: '/plan', title: '方案管理' },
   { path: '/simulation', title: '方案模拟测试' },
   { path: '/admin', title: '管理员管理' },
-  { path: '/system/user-category', title: '用户类' },
+  { path: '/system/user-category', title: '用户类' },
   { path: '/log', title: '日志管理' },
 ]
 

+ 1 - 1
code/frontend/src/views/plan/index.vue

@@ -230,7 +230,7 @@
           </div>
         </el-form-item>
         <el-form-item label="方案描述">
-          <el-input v-model="formData.description" type="textarea" :rows="2" maxlength="200" show-word-limit />
+          <el-input v-model="formData.description" type="textarea" :rows="2" maxlength="500" show-word-limit />
         </el-form-item>
 
         <!-- 灸方参数 -->

+ 11 - 11
code/frontend/src/views/system/user-category/index.vue

@@ -5,7 +5,7 @@
         <el-form-item label="关键字">
           <el-input
             v-model="query.keyword"
-            placeholder="类名称/备注"
+            placeholder="类名称/备注"
             clearable
             style="width: 220px"
             @keyup.enter="handleSearch"
@@ -20,13 +20,13 @@
 
     <el-card class="table-card" shadow="never">
       <div class="toolbar">
-        <el-button type="primary" :icon="Plus" @click="handleAdd">新增用户类</el-button>
+        <el-button type="primary" :icon="Plus" @click="handleAdd">新增用户类</el-button>
       </div>
 
       <el-table v-loading="loading" :data="tableData" border stripe>
         <el-table-column label="序号" type="index" width="70" align="center" />
-        <el-table-column label="类ID" prop="id" width="180" align="center" />
-        <el-table-column label="类名称" prop="name" min-width="180" />
+        <el-table-column label="类ID" prop="id" width="180" align="center" />
+        <el-table-column label="类名称" prop="name" min-width="180" />
         <el-table-column label="备注" prop="remark" min-width="260" show-overflow-tooltip>
           <template #default="{ row }">{{ row.remark || '-' }}</template>
         </el-table-column>
@@ -55,14 +55,14 @@
 
     <el-dialog
       v-model="formVisible"
-      :title="formMode === 'add' ? '新增用户类' : '编辑用户类'"
+      :title="formMode === 'add' ? '新增用户类' : '编辑用户类'"
       width="520px"
       :close-on-click-modal="false"
       @close="handleFormClose"
     >
       <el-form ref="formRef" :model="formData" :rules="formRules" label-width="90px">
-        <el-form-item label="类名称" prop="name">
-          <el-input v-model.trim="formData.name" placeholder="请输入类名称" maxlength="50" show-word-limit />
+        <el-form-item label="类名称" prop="name">
+          <el-input v-model.trim="formData.name" placeholder="请输入类名称" maxlength="50" show-word-limit />
         </el-form-item>
         <el-form-item label="备注" prop="remark">
           <el-input
@@ -139,8 +139,8 @@ const formData = reactive({
 
 const formRules = {
   name: [
-    { required: true, message: '请输入类名称', trigger: 'blur' },
-    { max: 50, message: '类名称最长50字符', trigger: 'blur' }
+    { required: true, message: '请输入类名称', trigger: 'blur' },
+    { max: 50, message: '类名称最长50字符', trigger: 'blur' }
   ]
 }
 
@@ -178,7 +178,7 @@ async function handleSubmit() {
   try {
     if (formMode.value === 'add') {
       await userCategoryApi.addUserCategory({ ...formData })
-      ElMessage.success('新增用户类成功')
+      ElMessage.success('新增用户类成功')
     } else {
       await userCategoryApi.updateUserCategory({ ...formData })
       ElMessage.success('修改成功')
@@ -191,7 +191,7 @@ async function handleSubmit() {
 }
 
 async function handleDelete(row) {
-  await ElMessageBox.confirm(`确认删除用户类【${row.name}】吗?此操作不可恢复`, '警告', {
+  await ElMessageBox.confirm(`确认删除用户类【${row.name}】吗?此操作不可恢复`, '警告', {
     confirmButtonText: '确认删除',
     cancelButtonText: '取消',
     type: 'warning'

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

@@ -19,7 +19,7 @@
             />
           </el-select>
         </el-form-item>
-        <el-form-item label="用户类">
+        <el-form-item label="用户类">
           <el-select
             v-model="queryForm.userCategoryId"
             placeholder="全部"
@@ -71,7 +71,7 @@
             </el-tag>
           </template>
         </el-table-column>
-        <el-table-column prop="userCategoryId" label="用户类" width="120" align="center">
+        <el-table-column prop="userCategoryId" label="用户类" width="120" align="center">
           <template #default="{ row }">
             <span v-if="row.userType === 1">{{ userCategoryText(row.userCategoryId) }}</span>
             <span v-else>-</span>
@@ -142,8 +142,8 @@
             />
           </el-select>
         </el-form-item>
-        <el-form-item v-if="formData.userType === 1" label="用户类" prop="userCategoryId">
-          <el-select v-model="formData.userCategoryId" placeholder="请选择用户类" clearable style="width:100%">
+        <el-form-item v-if="formData.userType === 1" label="用户类" prop="userCategoryId">
+          <el-select v-model="formData.userCategoryId" placeholder="请选择用户类" clearable style="width:100%">
             <el-option
               v-for="item in userCategoryOptions"
               :key="item.id"
@@ -191,7 +191,7 @@ const userTypeOptions = [
   { code: 3, name: '管理员', tagType: 'danger' }
 ]
 
-/** 用户类选项(用户 user_type=1 的分类) */
+/** 用户类选项(用户 user_type=1 的分类) */
 const userCategoryOptions = ref([])
 
 /** 用户身份文本 */

+ 31 - 38
code/frontend/src/views/user/profile/index.vue

@@ -15,13 +15,13 @@
             <el-option label="女" :value="2" />
           </el-select>
         </el-form-item>
-        <el-form-item label="用户身份">
-          <el-select v-model="queryForm.userType" placeholder="全部" clearable style="width:120px">
+        <el-form-item label="用户分类">
+          <el-select v-model="queryForm.userCategoryId" placeholder="全部" clearable style="width:120px">
             <el-option
               v-for="item in userCategoryOptions"
-              :key="item.code"
+              :key="item.id"
               :label="item.name"
-              :value="item.code"
+              :value="item.id"
             />
           </el-select>
         </el-form-item>
@@ -129,11 +129,9 @@
           <template #default="{ row }">{{ row.age != null ? row.age + '岁' : '-' }}</template>
         </el-table-column>
         <el-table-column prop="phone" label="手机号" width="130" />
-        <el-table-column prop="userType" label="用户身份" width="95" align="center">
+        <el-table-column prop="userCategoryName" label="用户分类" width="110" align="center">
           <template #default="{ row }">
-            <el-tag :type="userTypeTagType(row.userType)" size="small" v-if="row.userType">
-              {{ userTypeText(row.userType) }}
-            </el-tag>
+            <el-tag v-if="row.userCategoryName" type="primary" size="small">{{ row.userCategoryName }}</el-tag>
             <span v-else>-</span>
           </template>
         </el-table-column>
@@ -232,13 +230,13 @@
             </el-form-item>
           </el-col>
           <el-col v-if="formData.id" :span="12">
-            <el-form-item label="用户身份" prop="userType">
-              <el-select v-model="formData.userType" placeholder="请选择用户身份" style="width:100%">
+            <el-form-item label="用户分类" prop="userCategoryId">
+              <el-select v-model="formData.userCategoryId" placeholder="请选择用户分类" style="width:100%">
                 <el-option
                   v-for="item in userCategoryOptions"
-                  :key="item.code"
+                  :key="item.id"
                   :label="item.name"
-                  :value="item.code"
+                  :value="item.id"
                 />
               </el-select>
             </el-form-item>
@@ -354,6 +352,7 @@ import { regionData, codeToText } from 'element-china-area-data'
 import ExcelExport from '@/components/ExcelExport.vue'
 import ExcelImport from '@/components/ExcelImport.vue'
 import { dateTimeFormatter } from '@/utils/date'
+import { getUserCategoryList } from '@/api/userCategory'
 import {
   getProfilePage,
   addProfile,
@@ -371,7 +370,7 @@ const queryForm = reactive({
   keyword: '',
   deviceCode: '',
   gender: null,
-  userType: null,
+  userCategoryId: null,
   provinceCode: '',
   cityCode: '',
   districtCode: '',
@@ -393,7 +392,7 @@ const exportHeaders = [
   { label: '性别', prop: 'genderText' },
   { label: '年龄', prop: 'age' },
   { label: '手机号', prop: 'phone' },
-  { label: '用户身份', prop: 'userTypeLabel' },
+  { label: '用户分类', prop: 'userCategoryText' },
   { label: '所在地区', prop: 'regionText' },
   { label: '详细地址', prop: 'address' },
   { label: '设备编号', prop: 'deviceCodes' },
@@ -493,26 +492,8 @@ function onQueryCityChange(val) {
   }
 }
 
-/** 用户身份固定选项(user_type 主分类) */
-const userCategoryOptions = [
-  { code: 1, name: '用户', tagType: 'primary' },
-  { code: 2, name: '专家', tagType: 'warning' },
-  { code: 3, name: '管理员', tagType: 'danger' }
-]
-
-/** 用户身份文本 */
-function userTypeText(type) {
-  if (type == null) return '-'
-  const item = userCategoryOptions.find(c => c.code === type)
-  return item ? item.name : '-'
-}
-
-/** 用户身份标签颜色 */
-function userTypeTagType(type) {
-  if (type == null) return 'info'
-  const item = userCategoryOptions.find(c => c.code === type)
-  return item?.tagType || 'primary'
-}
+/** 用户分类选项 */
+const userCategoryOptions = ref([])
 
 /** 根据行政区划编码还原为 “省/市/区” 文本 */
 function formatRegion(row) {
@@ -530,7 +511,7 @@ async function loadData() {
       keyword: queryForm.keyword || undefined,
       deviceCode: queryForm.deviceCode || undefined,
       gender: queryForm.gender,
-      userType: queryForm.userType,
+      userCategoryId: queryForm.userCategoryId,
       provinceCode: queryForm.provinceCode || undefined,
       cityCode: queryForm.cityCode || undefined,
       districtCode: queryForm.districtCode || undefined,
@@ -545,7 +526,7 @@ async function loadData() {
     const records = (res.data?.records || []).map(r => ({
       ...r,
       genderText: r.gender === 1 ? '男' : r.gender === 2 ? '女' : '-',
-      userTypeLabel: userTypeText(r.userType),
+      userCategoryText: r.userCategoryName || '-',
       regionText: formatRegion(r),
       acupointText: r.acupointTableId ? '已生成' : '未生成'
     }))
@@ -565,7 +546,7 @@ function handleReset() {
   queryForm.keyword = ''
   queryForm.deviceCode = ''
   queryForm.gender = null
-  queryForm.userType = null
+  queryForm.userCategoryId = null
   queryForm.provinceCode = ''
   queryForm.cityCode = ''
   queryForm.districtCode = ''
@@ -592,6 +573,7 @@ const formData = reactive({
   age: null,
   phone: '',
   userType: 1,
+  userCategoryId: null,
   regionCodes: [],
   address: '',
   shoulderWidth: null,
@@ -678,7 +660,7 @@ const formRules = {
 function handleAdd() {
   Object.assign(formData, {
     id: null, name: '', gender: null, age: null, phone: '',
-    userType: 1, regionCodes: [], address: '',
+    userType: 1, userCategoryId: null, regionCodes: [], address: '',
     shoulderWidth: null, bodyHeight: null, spineLength: null, height: null, weight: null,
     fingerWidth1: null, fingerWidth15: null, fingerWidth3: null
   })
@@ -694,6 +676,7 @@ function handleEdit(row) {
     age: row.age,
     phone: row.phone,
     userType: row.userType ?? 1,
+    userCategoryId: row.userCategoryId || null,
     regionCodes: [row.provinceCode, row.cityCode, row.districtCode].filter(c => !!c),
     address: row.address || '',
     shoulderWidth: row.shoulderWidth,
@@ -780,6 +763,15 @@ function handleDeviceManager(row) {
   })
 }
 
+async function loadUserCategories() {
+  try {
+    const res = await getUserCategoryList()
+    userCategoryOptions.value = res.data || []
+  } catch (e) {
+    userCategoryOptions.value = []
+  }
+}
+
 // ======================== 生成用户穴位 ========================
 
 /**
@@ -802,6 +794,7 @@ async function handleGenerateAcupoint() {
 }
 
 onMounted(() => {
+  loadUserCategories()
   loadData()
 })
 </script>