Browse Source

[update] message:

kelei 11 months ago
parent
commit
e46725eca9
45 changed files with 2506 additions and 17 deletions
  1. 1 4
      admin/pom.xml
  2. 1 1
      admin/src/main/java/com/flyer/foster/AdminApplication.java
  3. 45 0
      admin/src/main/java/com/flyer/foster/component/CustomMetaObjectHandler.java
  4. 29 0
      admin/src/main/java/com/flyer/foster/config/CorsConfig.java
  5. 48 0
      admin/src/main/java/com/flyer/foster/config/RedisTemplateConfig.java
  6. 22 0
      admin/src/main/java/com/flyer/foster/config/SaTokenConfigure.java
  7. 19 0
      admin/src/main/java/com/flyer/foster/consts/LoginDevice.java
  8. 11 0
      admin/src/main/java/com/flyer/foster/consts/RedisKeyConst.java
  9. 33 0
      admin/src/main/java/com/flyer/foster/controller/MenuController.java
  10. 28 2
      admin/src/main/java/com/flyer/foster/controller/RoleController.java
  11. 86 2
      admin/src/main/java/com/flyer/foster/controller/UserController.java
  12. 15 0
      admin/src/main/java/com/flyer/foster/dto/LoginDTO.java
  13. 24 0
      admin/src/main/java/com/flyer/foster/dto/RoleAddDTO.java
  14. 22 0
      admin/src/main/java/com/flyer/foster/dto/RoleMenuAddDTO.java
  15. 20 0
      admin/src/main/java/com/flyer/foster/dto/RoleRespDTO.java
  16. 38 0
      admin/src/main/java/com/flyer/foster/dto/UserAddOrUpdateDTO.java
  17. 71 0
      admin/src/main/java/com/flyer/foster/dto/UserSearchDTO.java
  18. 40 0
      admin/src/main/java/com/flyer/foster/enums/DeleteStatusEnum.java
  19. 40 0
      admin/src/main/java/com/flyer/foster/enums/IsAdminEnum.java
  20. 40 0
      admin/src/main/java/com/flyer/foster/enums/StatusEnum.java
  21. 40 0
      admin/src/main/java/com/flyer/foster/enums/TFEnum.java
  22. 34 0
      admin/src/main/java/com/flyer/foster/mapper/IMenuMapper.java
  23. 24 0
      admin/src/main/java/com/flyer/foster/mapper/IUserMapper.java
  24. 18 0
      admin/src/main/java/com/flyer/foster/pojo/CheckBox.java
  25. 17 0
      admin/src/main/java/com/flyer/foster/pojo/CustomMeta.java
  26. 39 0
      admin/src/main/java/com/flyer/foster/pojo/RoleAssignTreeMenu.java
  27. 47 0
      admin/src/main/java/com/flyer/foster/pojo/TreeAllMenu.java
  28. 59 0
      admin/src/main/java/com/flyer/foster/pojo/TreeMenu.java
  29. 16 0
      admin/src/main/java/com/flyer/foster/pojo/UserRespPOJO.java
  30. 39 0
      admin/src/main/java/com/flyer/foster/service/IMenuService.java
  31. 2 1
      admin/src/main/java/com/flyer/foster/service/IRoleMenuService.java
  32. 6 0
      admin/src/main/java/com/flyer/foster/service/IRoleService.java
  33. 36 0
      admin/src/main/java/com/flyer/foster/service/IUserService.java
  34. 168 0
      admin/src/main/java/com/flyer/foster/service/impl/MenuServiceImpl.java
  35. 24 1
      admin/src/main/java/com/flyer/foster/service/impl/RoleMenuServiceImpl.java
  36. 62 0
      admin/src/main/java/com/flyer/foster/service/impl/RoleServiceImpl.java
  37. 154 0
      admin/src/main/java/com/flyer/foster/service/impl/UserServiceImpl.java
  38. 26 6
      admin/src/main/resources/application-dev.yml
  39. 154 0
      admin/src/main/resources/logback-spring.xml
  40. 61 0
      admin/src/main/resources/mapper/MenuMapper.xml
  41. 54 0
      admin/src/main/resources/mapper/UserMapper.xml
  42. 5 0
      common/pom.xml
  43. 66 0
      common/src/main/java/com/flyer/exception/BusinessException.java
  44. 84 0
      common/src/main/java/com/flyer/exception/GlobalExceptionHandler.java
  45. 638 0
      common/src/main/java/com/flyer/util/RedisUtil.java

+ 1 - 4
admin/pom.xml

@@ -34,10 +34,7 @@
             <artifactId>easyexcel</artifactId>
         </dependency>
 
-        <dependency>
-            <groupId>cn.dev33</groupId>
-            <artifactId>sa-token-spring-boot-starter</artifactId>
-        </dependency>
+
 
     </dependencies>
 

+ 1 - 1
admin/src/main/java/com/flyer/foster/AdminApplication.java

@@ -10,7 +10,7 @@ import org.springframework.boot.autoconfigure.SpringBootApplication;
  * @author kelei
  * @since 2022/4/2/15:15
  */
-@SpringBootApplication
+@SpringBootApplication(scanBasePackages = {"com.flyer"})
 public class AdminApplication {
     public static void main(String[] args) {
         SpringApplication.run(AdminApplication.class, args);

+ 45 - 0
admin/src/main/java/com/flyer/foster/component/CustomMetaObjectHandler.java

@@ -0,0 +1,45 @@
+package com.flyer.foster.component;
+
+import cn.dev33.satoken.stp.StpUtil;
+import cn.hutool.core.util.StrUtil;
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONObject;
+import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
+import org.apache.ibatis.reflection.MetaObject;
+import org.springframework.stereotype.Component;
+
+import java.time.LocalDateTime;
+
+/**
+ * mybatisplus插入时间处理
+ *
+ * @author kelei
+ * @since 2022/3/28/16:42
+ */
+@Component
+public class CustomMetaObjectHandler implements MetaObjectHandler {
+
+    @Override
+    public void insertFill(MetaObject metaObject) {
+        String strObj = JSON.toJSONString(metaObject.getOriginalObject());
+        JSONObject jsonObject = JSON.parseObject(strObj);
+        if (StrUtil.isBlank(jsonObject.getString("createdBy"))) {
+            this.setFieldValByName("createdBy", StpUtil.getLoginId(), metaObject);
+        }
+        if (StrUtil.isBlank(jsonObject.getString("createdTime"))) {
+            this.setFieldValByName("createdTime", LocalDateTime.now(), metaObject);
+        }
+    }
+
+    @Override
+    public void updateFill(MetaObject metaObject) {
+        String strObj = JSON.toJSONString(metaObject.getOriginalObject());
+        JSONObject jsonObject = JSON.parseObject(strObj);
+        if (StrUtil.isBlank(jsonObject.getString("updatedBy"))) {
+            this.setFieldValByName("updatedBy", StpUtil.getLoginId(), metaObject);
+        }
+        if (StrUtil.isBlank(jsonObject.getString("updatedTime"))) {
+            this.setFieldValByName("updatedTime", LocalDateTime.now(), metaObject);
+        }
+    }
+}

+ 29 - 0
admin/src/main/java/com/flyer/foster/config/CorsConfig.java

@@ -0,0 +1,29 @@
+package com.flyer.foster.config;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.cors.CorsConfiguration;
+import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
+import org.springframework.web.filter.CorsFilter;
+
+/**
+ * 跨域配置
+ *
+ * @author kelei
+ * @since 2024/5/8/11:08
+ */
+@Configuration
+public class CorsConfig {
+    @Bean
+    public CorsFilter corsFilter() {
+        CorsConfiguration config = new CorsConfiguration();
+        config.addAllowedOrigin("*");
+        config.addAllowedMethod("*");
+        config.addAllowedHeader("*");
+        config.addExposedHeader("*");
+        config.setAllowCredentials(true);
+        UrlBasedCorsConfigurationSource corsConfigurationSource = new UrlBasedCorsConfigurationSource();
+        corsConfigurationSource.registerCorsConfiguration("/**", config);
+        return new CorsFilter(corsConfigurationSource);
+    }
+}

+ 48 - 0
admin/src/main/java/com/flyer/foster/config/RedisTemplateConfig.java

@@ -0,0 +1,48 @@
+package com.flyer.foster.config;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.SerializationFeature;
+import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
+import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
+import org.springframework.data.redis.serializer.StringRedisSerializer;
+
+/**
+ * RedisTemplateConfig
+ *
+ * @author kelei
+ * @since 2022/3/25/15:35
+ */
+@Configuration
+public class RedisTemplateConfig {
+    @Bean
+    public RedisTemplate<String, Object> redisTemplate(LettuceConnectionFactory lettuceConnectionFactory) {
+        RedisTemplate<String, Object> redisTemplate = new RedisTemplate();
+        // 设置连接池工厂
+        redisTemplate.setConnectionFactory(lettuceConnectionFactory);
+        // 首先解决key的序列化方式
+        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
+        redisTemplate.setKeySerializer(stringRedisSerializer);
+
+        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
+        redisTemplate.setHashValueSerializer(new StringRedisSerializer());
+
+        // 解决value的序列化方式
+        Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<Object>(Object.class);
+        ObjectMapper objectMapper = new ObjectMapper();
+        // 将当前对象的数据类型也存入序列化的结果字符串中
+        objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL);
+        // 解决jackson2无法反序列化LocalDateTime的问题
+        objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
+        objectMapper.registerModule(new JavaTimeModule());
+        jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
+
+        redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
+
+        return redisTemplate;
+    }
+}

+ 22 - 0
admin/src/main/java/com/flyer/foster/config/SaTokenConfigure.java

@@ -0,0 +1,22 @@
+package com.flyer.foster.config;
+
+import cn.dev33.satoken.interceptor.SaInterceptor;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
+
+/**
+ * SaTokenConfigure
+ *
+ * @author kelei
+ * @since 2024/5/7/12:12
+ */
+@Configuration
+public class SaTokenConfigure implements WebMvcConfigurer {
+    // 注册 Sa-Token 拦截器,打开注解式鉴权功能
+    @Override
+    public void addInterceptors(InterceptorRegistry registry) {
+        // 注册 Sa-Token 拦截器,打开注解式鉴权功能
+        registry.addInterceptor(new SaInterceptor()).addPathPatterns("/**");
+    }
+}

+ 19 - 0
admin/src/main/java/com/flyer/foster/consts/LoginDevice.java

@@ -0,0 +1,19 @@
+package com.flyer.foster.consts;
+
+/**
+ * LoginDevice
+ *
+ * @author kelei
+ * @since 2024/5/7/11:44
+ */
+public interface LoginDevice {
+    /**
+     * 电脑端
+     */
+    String PC = "PC";
+
+    /**
+     * 移动端
+     */
+    String MOBILE = "MOBILE";
+}

+ 11 - 0
admin/src/main/java/com/flyer/foster/consts/RedisKeyConst.java

@@ -0,0 +1,11 @@
+package com.flyer.foster.consts;
+
+/**
+ * RedisKeyConst
+ *
+ * @author kelei
+ * @since 2024/5/7/9:42
+ */
+public interface RedisKeyConst {
+    String RSA_PRIVATE_KEY = "rsa:private_key:";
+}

+ 33 - 0
admin/src/main/java/com/flyer/foster/controller/MenuController.java

@@ -1,5 +1,12 @@
 package com.flyer.foster.controller;
 
+import cn.dev33.satoken.annotation.SaCheckLogin;
+import cn.dev33.satoken.stp.StpUtil;
+import com.flyer.foster.service.IMenuService;
+import com.flyer.foster.service.ITenantService;
+import com.flyer.util.R;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.GetMapping;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RestController;
 
@@ -11,8 +18,34 @@ import org.springframework.web.bind.annotation.RestController;
  * @author flyer
  * @since 2024-05-06
  */
+@SaCheckLogin
 @RestController
 @RequestMapping("/menu")
 public class MenuController {
+    @Autowired
+    private IMenuService iMenuService;
 
+    @Autowired
+    private ITenantService iTenantService;
+
+    /**
+     * 获取菜单列表
+     *
+     * @return
+     */
+    @GetMapping("/list")
+    public R menuList() {
+        String userName = StpUtil.getLoginId().toString();
+        return R.ok().result(iMenuService.selectMenusByUserName(userName));
+    }
+
+    /**
+     * 角色分配时所有菜单列表
+     *
+     * @return
+     */
+    @GetMapping("/role-assign-menu/list")
+    public R dropdownMenuList() {
+        return R.ok().result(iMenuService.getRoleAssignTreeMenuList());
+    }
 }

+ 28 - 2
admin/src/main/java/com/flyer/foster/controller/RoleController.java

@@ -1,7 +1,13 @@
 package com.flyer.foster.controller;
 
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RestController;
+import cn.dev33.satoken.annotation.SaCheckLogin;
+import com.flyer.foster.dto.RoleAddDTO;
+import com.flyer.foster.service.IRoleService;
+import com.flyer.util.R;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import javax.validation.Valid;
 
 /**
  * <p>
@@ -11,8 +17,28 @@ import org.springframework.web.bind.annotation.RestController;
  * @author flyer
  * @since 2024-05-06
  */
+@SaCheckLogin
 @RestController
 @RequestMapping("/role")
 public class RoleController {
+    @Autowired
+    private IRoleService iRoleService;
+
+    /**
+     * 角色列表查看
+     */
+    @GetMapping("/list")
+    public R getRoleList() {
+        return R.ok().result(iRoleService.getRoleList());
+    }
 
+    /**
+     * 新增角色
+     *
+     * @return
+     */
+    @PostMapping("")
+    public R addRole(@Valid @RequestBody RoleAddDTO addDTO) {
+        return R.ok().result(iRoleService.addRole(addDTO));
+    }
 }

+ 86 - 2
admin/src/main/java/com/flyer/foster/controller/UserController.java

@@ -1,7 +1,25 @@
 package com.flyer.foster.controller;
 
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RestController;
+import cn.dev33.satoken.annotation.SaCheckLogin;
+import cn.dev33.satoken.annotation.SaIgnore;
+import cn.dev33.satoken.stp.StpUtil;
+import cn.hutool.core.bean.BeanUtil;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.flyer.foster.dto.LoginDTO;
+import com.flyer.foster.dto.UserAddOrUpdateDTO;
+import com.flyer.foster.dto.UserSearchDTO;
+import com.flyer.foster.entity.Tenant;
+import com.flyer.foster.entity.User;
+import com.flyer.foster.pojo.UserRespPOJO;
+import com.flyer.foster.service.IUserService;
+import com.flyer.util.R;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import javax.validation.Valid;
+import java.util.List;
 
 /**
  * <p>
@@ -11,8 +29,74 @@ import org.springframework.web.bind.annotation.RestController;
  * @author flyer
  * @since 2024-05-06
  */
+@Slf4j
+@SaCheckLogin
 @RestController
 @RequestMapping("/user")
 public class UserController {
+    @Autowired
+    private IUserService iUserService;
+
+    @SaIgnore
+    @PostMapping("/login")
+    public R login(@RequestBody LoginDTO loginDto) {
+        return R.ok().message("登录成功").result(iUserService.login(loginDto));
+    }
+
+    @SaIgnore
+    @GetMapping("/generate-key")
+    public R getRsaKey(String username) {
+        return R.ok().result(iUserService.getRsaKey(username));
+    }
+
+    /**
+     * 根据登录信息获取用户信息
+     *
+     * @return
+     */
+    @GetMapping("")
+    public R getUserInfo() {
+        String username = StpUtil.getLoginId().toString();
+        User user = iUserService.lambdaQuery().eq(User::getUsername, username).eq(User::getStatus, 1).one();
+        UserRespPOJO userRespPOJO = new UserRespPOJO();
+        if (user != null) {
+            BeanUtil.copyProperties(user, userRespPOJO);
+        }
+        return R.ok().result(userRespPOJO);
+    }
+
+    /**
+     * 添加用户
+     *
+     * @param addDTO 用户添加对象
+     * @return
+     */
+    @PostMapping("")
+    public R addUser(@Valid @RequestBody UserAddOrUpdateDTO addDTO) {
+        return R.ok().result(iUserService.addUserAndRole(addDTO));
+    }
+
+    /**
+     * 用户列表-分页
+     *
+     * @param current 当前页,默认1
+     * @param size    当前页显示条数,默认10
+     * @return
+     */
+    @GetMapping("/list/{current}/{size}")
+    public R getUsers(@PathVariable Integer current, @PathVariable Integer size, UserSearchDTO queryDTO) {
+        IPage<User> page = new Page<>(current, size);
+        return R.ok().result(iUserService.selectByPage(page, queryDTO));
+    }
 
+    /**
+     * 删除用户
+     *
+     * @param idList 用户id集合
+     * @return
+     */
+    @PostMapping("/delete")
+    public R deleteUser(@RequestBody List<Integer> idList) {
+        return R.ok().result(iUserService.removeBatchByIds(idList));
+    }
 }

+ 15 - 0
admin/src/main/java/com/flyer/foster/dto/LoginDTO.java

@@ -0,0 +1,15 @@
+package com.flyer.foster.dto;
+
+import lombok.Data;
+
+/**
+ * LoginDto
+ *
+ * @author kelei
+ * @since 2024/5/7/9:18
+ */
+@Data
+public class LoginDTO {
+    private String username;
+    private String password;
+}

+ 24 - 0
admin/src/main/java/com/flyer/foster/dto/RoleAddDTO.java

@@ -0,0 +1,24 @@
+package com.flyer.foster.dto;
+
+import lombok.Data;
+
+import javax.validation.constraints.NotBlank;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * RoleAddDTO
+ *
+ * @author kelei
+ * @since 2023/3/21/20:33
+ */
+@Data
+public class RoleAddDTO {
+    @NotBlank(message = "角色名称不能为空")
+    private String roleName;
+
+    @NotBlank(message = "角色编码不能为空")
+    private String roleCode;
+
+    private List<Integer>  menuIdList = new ArrayList<>();
+}

+ 22 - 0
admin/src/main/java/com/flyer/foster/dto/RoleMenuAddDTO.java

@@ -0,0 +1,22 @@
+package com.flyer.foster.dto;
+
+import lombok.Data;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * RoleMenuAddDTO
+ *
+ * @author kelei
+ * @since 2023/3/23/16:33
+ */
+@Data
+public class RoleMenuAddDTO {
+    private Integer roleId;
+
+    /**
+     * 菜单id集合
+     */
+    private List<Integer> menuIdList = new ArrayList<>();
+}

+ 20 - 0
admin/src/main/java/com/flyer/foster/dto/RoleRespDTO.java

@@ -0,0 +1,20 @@
+package com.flyer.foster.dto;
+
+import cn.hutool.core.annotation.Alias;
+import lombok.Data;
+
+/**
+ * RoleRespDTO
+ *
+ * @author kelei
+ * @since 2023/3/24/14:02
+ */
+@Data
+public class RoleRespDTO {
+    @Alias("id")
+    private Integer roleId;
+
+    private String roleName;
+
+    private String roleCode;
+}

+ 38 - 0
admin/src/main/java/com/flyer/foster/dto/UserAddOrUpdateDTO.java

@@ -0,0 +1,38 @@
+package com.flyer.foster.dto;
+
+import lombok.Data;
+
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.NotNull;
+import javax.validation.constraints.Pattern;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 用户更新DTO
+ *
+ * @author kelei
+ * @since 2022/3/29/11:32
+ */
+@Data
+public class UserAddOrUpdateDTO {
+    private Integer id;
+
+    @Pattern(regexp = "^[a-zA-Z]\\w{4,15}$", message = "用户名以字母开头,允许5-16字节,允许字母数字下划线")
+    private String username;
+
+//    @Pattern(regexp = "^(?=.*[A-Za-z])(?=.*\\d)[A-Za-z\\d|!-~]{5,18}$", message = "密码长度为5~18,至少含有一个字母和一个数字,不能以特殊字符开头")
+    private String password;
+
+    private String tenantId;
+
+    private String remarks;
+
+    private List<Integer> roleIdList = new ArrayList<>();
+
+    /**
+     * 组织机构管理员账号分配的模块列表
+     */
+    @NotNull(message = "请选择管理员用户的操作菜单")
+    private List<Integer> moduleIdList = new ArrayList<>();
+}

+ 71 - 0
admin/src/main/java/com/flyer/foster/dto/UserSearchDTO.java

@@ -0,0 +1,71 @@
+package com.flyer.foster.dto;
+
+import com.flyer.foster.entity.Role;
+import com.flyer.foster.pojo.CheckBox;
+import lombok.Data;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 用户列表数据传输对象
+ *
+ * @author kelei
+ * @since 2022/3/28/14:02
+ */
+@Data
+public class UserSearchDTO {
+    /**
+     * 主键id
+     */
+    private Integer id;
+
+    /**
+     * 登录名
+     */
+    private String username;
+
+    /**
+     * 昵称
+     */
+    private String nickname;
+
+    /**
+     * 备注信息
+     */
+    private String remarks;
+
+    /**
+     * 状态{0.不可用 1.可用}
+     */
+    private Integer status;
+
+    /**
+     * 角色对象集合
+     */
+    private List<Role> roles = new ArrayList();
+
+    private List<CheckBox> moduleList = new ArrayList<>();
+
+    /**
+     * 角色描述
+     */
+    private String description;
+
+
+    /**
+     * 1:经销商,2:医院
+     */
+    private Integer tenantType;
+
+    private Integer tenantId;
+
+    private String tenantName;
+
+    /**
+     * 是否是默认经销商-{0:非,1:是}
+     */
+    private Integer isDefault;
+
+    private Integer adminUserId;
+}

+ 40 - 0
admin/src/main/java/com/flyer/foster/enums/DeleteStatusEnum.java

@@ -0,0 +1,40 @@
+package com.flyer.foster.enums;
+
+import lombok.Getter;
+
+/**
+ * DeleteStatusEnum
+ *
+ * @author kelei
+ * @since 2023/3/21/22:04
+ */
+@Getter
+public enum DeleteStatusEnum {
+    UNDELETED(0, "未删除"),
+
+    DELETED(1, "已删除");
+
+    private final Integer id;
+
+    private final String name;
+
+    DeleteStatusEnum(Integer id, String name) {
+        this.id = id;
+        this.name = name;
+    }
+
+    /**
+     * 根据id获取值
+     *
+     * @param id id
+     * @return
+     */
+    public static String getDeleteStatusName(Integer id) {
+        for (DeleteStatusEnum value : DeleteStatusEnum.values()) {
+            if (value.getId().equals(id)) {
+                return value.getName();
+            }
+        }
+        return null;
+    }
+}

+ 40 - 0
admin/src/main/java/com/flyer/foster/enums/IsAdminEnum.java

@@ -0,0 +1,40 @@
+package com.flyer.foster.enums;
+
+import lombok.Getter;
+
+/**
+ * IsAdminEnum
+ *
+ * @author kelei
+ * @since 2023/3/24/21:42
+ */
+@Getter
+public enum IsAdminEnum {
+    NO(0, "不是管理员"),
+
+    YES(1, "是管理员");
+
+    private final Integer id;
+
+    private final String name;
+
+    IsAdminEnum(Integer id, String name) {
+        this.id = id;
+        this.name = name;
+    }
+
+    /**
+     * 根据id获取值
+     *
+     * @param id id
+     * @return
+     */
+    public static String getIsAdminName(Integer id) {
+        for (IsAdminEnum value : IsAdminEnum.values()) {
+            if (value.getId().equals(id)) {
+                return value.getName();
+            }
+        }
+        return null;
+    }
+}

+ 40 - 0
admin/src/main/java/com/flyer/foster/enums/StatusEnum.java

@@ -0,0 +1,40 @@
+package com.flyer.foster.enums;
+
+import lombok.Getter;
+
+/**
+ * StatusEnum
+ *
+ * @author kelei
+ * @since 2023/3/21/21:03
+ */
+@Getter
+public enum StatusEnum {
+    DISABLE(0, "不可用"),
+
+    ENABLE(1, "可用");
+
+    private final Integer id;
+
+    private final String name;
+
+    StatusEnum(Integer id, String name) {
+        this.id = id;
+        this.name = name;
+    }
+
+    /**
+     * 根据id获取值
+     *
+     * @param id id
+     * @return
+     */
+    public static String getStatusName(Integer id) {
+        for (StatusEnum value : StatusEnum.values()) {
+            if (value.getId().equals(id)) {
+                return value.getName();
+            }
+        }
+        return null;
+    }
+}

+ 40 - 0
admin/src/main/java/com/flyer/foster/enums/TFEnum.java

@@ -0,0 +1,40 @@
+package com.flyer.foster.enums;
+
+import lombok.Getter;
+
+/**
+ * TFEnum
+ *
+ * @author kelei
+ * @since 2023/3/24/14:20
+ */
+@Getter
+public enum TFEnum {
+    FALSE(0,"false"),
+
+    TRUE(1,"true");
+
+    private final Integer id;
+
+    private final String name;
+
+    TFEnum(Integer id, String name) {
+        this.id = id;
+        this.name = name;
+    }
+
+    /**
+     * 根据id获取值
+     *
+     * @param id id
+     * @return
+     */
+    public static String getTFName(Integer id) {
+        for (TFEnum value : TFEnum.values()) {
+            if (value.getId().equals(id)) {
+                return value.getName();
+            }
+        }
+        return null;
+    }
+}

+ 34 - 0
admin/src/main/java/com/flyer/foster/mapper/IMenuMapper.java

@@ -2,6 +2,11 @@ package com.flyer.foster.mapper;
 
 import com.flyer.foster.entity.Menu;
 import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.flyer.foster.pojo.TreeAllMenu;
+import com.flyer.foster.pojo.TreeMenu;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
 
 /**
  * <p>
@@ -12,5 +17,34 @@ import com.baomidou.mybatisplus.core.mapper.BaseMapper;
  * @since 2024-05-06
  */
 public interface IMenuMapper extends BaseMapper<Menu> {
+    /**
+     * 根据用户id获取权限
+     *
+     * @param userId 用户id
+     * @return list
+     */
+    List<String> selectPermissionsByUserId(@Param("userId") Integer userId);
+
+    /**
+     * 根据用户id获取菜单列表
+     *
+     * @param userId 用户id
+     * @return list
+     */
+    List<TreeMenu> selectListByUserIdConvert2DTO(@Param("userId") Integer userId);
+
+    /**
+     * 根据角色id获取菜单列表
+     *
+     * @param roleId 角色id
+     * @return list
+     */
+    List<TreeAllMenu> selectListByRoleIdConvert2DTO(@Param("roleId") Integer roleId);
 
+    /**
+     * 获取新增角色界面的所有类型菜单
+     *
+     * @return
+     */
+    List<TreeAllMenu> selectAllTypeMenus(@Param("menuIdList") List<Integer> menuIdList);
 }

+ 24 - 0
admin/src/main/java/com/flyer/foster/mapper/IUserMapper.java

@@ -1,7 +1,12 @@
 package com.flyer.foster.mapper;
 
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.flyer.foster.dto.UserSearchDTO;
 import com.flyer.foster.entity.User;
 import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
 
 /**
  * <p>
@@ -12,5 +17,24 @@ import com.baomidou.mybatisplus.core.mapper.BaseMapper;
  * @since 2024-05-06
  */
 public interface IUserMapper extends BaseMapper<User> {
+    /**
+     * 分页查询用户信息及用户的角色信息
+     *
+     * @param page     分页对象
+     * @param queryDTO 查询对象
+     * @return 分页数据集
+     */
+    IPage<UserSearchDTO> selectByPage(@Param("page") IPage<User> page, @Param("queryDTO") UserSearchDTO queryDTO);
 
+    /**
+     * 组织机构管理员分页列表
+     *
+     * @param page
+     * @param searchDTO
+     * @param userIdList
+     * @return
+     */
+    IPage<UserSearchDTO> adminListByPage(@Param("page") IPage<UserSearchDTO> page,
+                                         @Param("searchDTO") UserSearchDTO searchDTO,
+                                         @Param("userIdList") List<Integer> userIdList);
 }

+ 18 - 0
admin/src/main/java/com/flyer/foster/pojo/CheckBox.java

@@ -0,0 +1,18 @@
+package com.flyer.foster.pojo;
+
+import lombok.Data;
+
+/**
+ * 前端checkbox对象
+ *
+ * @author kelei
+ * @since 2022/6/18/12:01
+ */
+@Data
+public class CheckBox {
+    private Integer id;
+
+    private String name;
+
+    private int selected;
+}

+ 17 - 0
admin/src/main/java/com/flyer/foster/pojo/CustomMeta.java

@@ -0,0 +1,17 @@
+package com.flyer.foster.pojo;
+
+import lombok.Data;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * CsutomMeta
+ *
+ * @author kelei
+ * @since 2022/8/3/10:02
+ */
+@Data
+public class CustomMeta {
+    private List<String> permission = new ArrayList<>();
+}

+ 39 - 0
admin/src/main/java/com/flyer/foster/pojo/RoleAssignTreeMenu.java

@@ -0,0 +1,39 @@
+package com.flyer.foster.pojo;
+
+import lombok.Data;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 角色分配菜单对象
+ *
+ * @author kelei
+ * @since 2023/3/21/21:32
+ */
+@Data
+public class RoleAssignTreeMenu {
+    private Integer id;
+
+    /**
+     * 菜单名称
+     */
+    private String title;
+
+    private Integer type;
+
+    /**
+     * 父节点
+     */
+    private Integer parentId;
+
+    /**
+     * 是否选中-{1:选中,0:未选中}
+     */
+    private boolean selected;
+
+    /**
+     * 子节点
+     */
+    private List<RoleAssignTreeMenu> children = new ArrayList<>();
+}

+ 47 - 0
admin/src/main/java/com/flyer/foster/pojo/TreeAllMenu.java

@@ -0,0 +1,47 @@
+package com.flyer.foster.pojo;
+
+import lombok.Data;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * TreeAllMenu
+ *
+ * @author kelei
+ * @since 2022/8/3/10:54
+ */
+@Data
+public class TreeAllMenu {
+    private Integer id;
+
+    /**
+     * 菜单名称
+     */
+    private String title;
+
+    /**
+     * 菜单编码
+     */
+    private String component;
+
+    /**
+     * 父节点
+     */
+    private Integer parentId;
+
+    /**
+     * 菜单排序
+     */
+    private Integer menuSort;
+
+    /**
+     * 是否选中-{1:选中,0:未选中}
+     */
+    private boolean selected;
+
+    /**
+     * 子节点
+     */
+    private List<TreeAllMenu> children = new ArrayList<>();
+}

+ 59 - 0
admin/src/main/java/com/flyer/foster/pojo/TreeMenu.java

@@ -0,0 +1,59 @@
+package com.flyer.foster.pojo;
+
+import lombok.Data;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * TreeMenu
+ *
+ * @author kelei
+ * @since 2022/8/3/10:53
+ */
+@Data
+public class TreeMenu {
+    private Integer id;
+
+    /**
+     * 菜单名称
+     */
+    private String title;
+
+    /**
+     * 菜单编码
+     */
+    private String component;
+
+    /**
+     * 父节点
+     */
+    private Integer parentId;
+
+    /**
+     * 菜单排序
+     */
+    private Integer menuSort;
+
+    /**
+     * 是否选中-{1:选中,0:未选中}
+     */
+    private boolean selected;
+
+    /**
+     * 菜单类型
+     */
+    private Integer type;
+
+    /**
+     * 权限
+     */
+    private CustomMeta meta;
+
+    private String permission;
+
+    /**
+     * 子节点
+     */
+    private List<TreeMenu> children = new ArrayList<>();
+}

+ 16 - 0
admin/src/main/java/com/flyer/foster/pojo/UserRespPOJO.java

@@ -0,0 +1,16 @@
+package com.flyer.foster.pojo;
+
+import lombok.Data;
+
+/**
+ * UserRespPOJO
+ *
+ * @author kelei
+ * @since 2024/5/8/11:48
+ */
+@Data
+public class UserRespPOJO {
+    private Integer id;
+
+    private String username;
+}

+ 39 - 0
admin/src/main/java/com/flyer/foster/service/IMenuService.java

@@ -2,6 +2,11 @@ package com.flyer.foster.service;
 
 import com.flyer.foster.entity.Menu;
 import com.baomidou.mybatisplus.extension.service.IService;
+import com.flyer.foster.pojo.RoleAssignTreeMenu;
+import com.flyer.foster.pojo.TreeAllMenu;
+import com.flyer.foster.pojo.TreeMenu;
+
+import java.util.List;
 
 /**
  * <p>
@@ -12,5 +17,39 @@ import com.baomidou.mybatisplus.extension.service.IService;
  * @since 2024-05-06
  */
 public interface IMenuService extends IService<Menu> {
+    /**
+     * 获取登录用户的所有权限
+     *
+     * @param userId 用户id
+     * @return list
+     */
+    List<String> selectPermissionsByUserId(Integer userId);
+
+    /**
+     * 获取登录用户的菜单列表
+     *
+     * @param userName 用户名
+     * @return 菜单集合
+     */
+    List<TreeMenu> selectMenusByUserName(String userName);
+
+    /**
+     * 根据角色获取菜单列表
+     *
+     * @param roleId 角色id
+     */
+    List<TreeAllMenu> selectMenusByRoleId(Integer roleId);
+
+    /**
+     * 获取新增角色界面的所有类型菜单
+     * @return
+     */
+    List<TreeAllMenu> getAllTypeMenuList();
 
+    /**
+     * 角色分配菜单,获取所有菜单列表
+     *
+     * @return
+     */
+    List<RoleAssignTreeMenu> getRoleAssignTreeMenuList();
 }

+ 2 - 1
admin/src/main/java/com/flyer/foster/service/IRoleMenuService.java

@@ -1,5 +1,6 @@
 package com.flyer.foster.service;
 
+import com.flyer.foster.dto.RoleMenuAddDTO;
 import com.flyer.foster.entity.RoleMenu;
 import com.baomidou.mybatisplus.extension.service.IService;
 
@@ -12,5 +13,5 @@ import com.baomidou.mybatisplus.extension.service.IService;
  * @since 2024-05-06
  */
 public interface IRoleMenuService extends IService<RoleMenu> {
-
+    boolean addRoleMenu(RoleMenuAddDTO addDTO);
 }

+ 6 - 0
admin/src/main/java/com/flyer/foster/service/IRoleService.java

@@ -1,8 +1,12 @@
 package com.flyer.foster.service;
 
+import com.flyer.foster.dto.RoleAddDTO;
+import com.flyer.foster.dto.RoleRespDTO;
 import com.flyer.foster.entity.Role;
 import com.baomidou.mybatisplus.extension.service.IService;
 
+import java.util.List;
+
 /**
  * <p>
  * 角色表 服务类
@@ -12,5 +16,7 @@ import com.baomidou.mybatisplus.extension.service.IService;
  * @since 2024-05-06
  */
 public interface IRoleService extends IService<Role> {
+    List<RoleRespDTO> getRoleList();
 
+    boolean addRole(RoleAddDTO addDTO);
 }

+ 36 - 0
admin/src/main/java/com/flyer/foster/service/IUserService.java

@@ -1,8 +1,15 @@
 package com.flyer.foster.service;
 
+import cn.hutool.crypto.asymmetric.RSA;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.flyer.foster.dto.LoginDTO;
+import com.flyer.foster.dto.UserAddOrUpdateDTO;
+import com.flyer.foster.dto.UserSearchDTO;
 import com.flyer.foster.entity.User;
 import com.baomidou.mybatisplus.extension.service.IService;
 
+import java.util.Map;
+
 /**
  * <p>
  * 用户表 服务类
@@ -12,5 +19,34 @@ import com.baomidou.mybatisplus.extension.service.IService;
  * @since 2024-05-06
  */
 public interface IUserService extends IService<User> {
+    /**
+     * 获取RSA的公钥
+     *
+     * @param username 登录账号
+     * @return
+     */
+    String getRsaKey(String username);
+
+    /**
+     * 登录
+     *
+     * @param loginDto
+     * @return
+     */
+    String login(LoginDTO loginDto);
+
+    /**
+     * 多表查询用户信息
+     *
+     * @return list
+     */
+    IPage<UserSearchDTO> selectByPage(IPage<User> page, UserSearchDTO queryDTO);
 
+    /**
+     * 添加用户和权限
+     *
+     * @param addDTO 新增用户对象
+     * @return true:成功,false:失败
+     */
+    boolean addUserAndRole(UserAddOrUpdateDTO addDTO);
 }

+ 168 - 0
admin/src/main/java/com/flyer/foster/service/impl/MenuServiceImpl.java

@@ -1,11 +1,33 @@
 package com.flyer.foster.service.impl;
 
+import cn.hutool.core.bean.BeanUtil;
+import cn.hutool.core.bean.copier.CopyOptions;
 import com.flyer.foster.entity.Menu;
+import com.flyer.foster.entity.Role;
+import com.flyer.foster.entity.RoleMenu;
+import com.flyer.foster.entity.User;
+import com.flyer.foster.enums.DeleteStatusEnum;
+import com.flyer.foster.enums.IsAdminEnum;
+import com.flyer.foster.enums.StatusEnum;
 import com.flyer.foster.mapper.IMenuMapper;
+import com.flyer.foster.pojo.CustomMeta;
+import com.flyer.foster.pojo.RoleAssignTreeMenu;
+import com.flyer.foster.pojo.TreeAllMenu;
+import com.flyer.foster.pojo.TreeMenu;
 import com.flyer.foster.service.IMenuService;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.flyer.foster.service.IRoleMenuService;
+import com.flyer.foster.service.IRoleService;
+import com.flyer.foster.service.IUserService;
+import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
 /**
  * <p>
  * 菜单表 服务实现类
@@ -17,4 +39,150 @@ import org.springframework.stereotype.Service;
 @Service
 public class MenuServiceImpl extends ServiceImpl<IMenuMapper, Menu> implements IMenuService {
 
+    @Autowired
+    private IUserService iUserService;
+
+    @Autowired
+    private IRoleService iRoleService;
+
+    @Autowired
+    private IRoleMenuService iRoleMenuService;
+
+    @Override
+    public List<String> selectPermissionsByUserId(Integer userId) {
+        return baseMapper.selectPermissionsByUserId(userId);
+    }
+
+    @Override
+    public List<TreeMenu> selectMenusByUserName(String userName) {
+        User user = iUserService.lambdaQuery().eq(User::getUsername, userName).one();
+        // 查询所有菜单,包括了type=2的数据
+        List<TreeMenu> menus = baseMapper.selectListByUserIdConvert2DTO(user.getId());
+        // 过滤根节点
+        List<TreeMenu> roots = menus.stream().filter(x -> x.getParentId().equals(0)).collect(Collectors.toList());
+        for (TreeMenu root : roots) {
+            this.buildMenus(root, menus);
+        }
+        return roots;
+    }
+
+    @Override
+    public List<TreeAllMenu> selectMenusByRoleId(Integer roleId) {
+        List<TreeAllMenu> menus = baseMapper.selectListByRoleIdConvert2DTO(roleId);
+        // 过滤根节点
+        List<TreeAllMenu> roots = menus.stream().filter(x -> x.getParentId().equals(0)).collect(Collectors.toList());
+        for (TreeAllMenu root : roots) {
+            this.buildAllMenus(root, menus);
+        }
+        return roots;
+    }
+
+    @Override
+    public List<TreeAllMenu> getAllTypeMenuList() {
+        Role adminRole = iRoleService.lambdaQuery().eq(Role::getIsAdmin, 1).one();
+        List<RoleMenu> roleMenuList = iRoleMenuService.lambdaQuery().eq(RoleMenu::getRoleId, adminRole.getId()).list();
+        List<TreeAllMenu> menus = baseMapper.selectAllTypeMenus(roleMenuList.stream().map(RoleMenu::getMenuId).collect(Collectors.toList()));
+        // 过滤根节点
+        List<TreeAllMenu> roots = menus.stream().filter(x -> x.getParentId().equals(0)).collect(Collectors.toList());
+        for (TreeAllMenu root : roots) {
+            this.assignMenu(root, menus);
+        }
+        return roots;
+    }
+
+    /**
+     * 递归构建菜单数据结构
+     *
+     * @return list
+     */
+    private void buildMenus(TreeMenu root, List<TreeMenu> data) {
+        CustomMeta meta = new CustomMeta();
+        for (TreeMenu menu : data) {
+            if (menu.getParentId().equals(root.getId())) {
+                if (menu.getType() == 1) {
+                    root.getChildren().add(menu);
+                } else if (menu.getType() == 2) {
+                    meta.getPermission().add(menu.getPermission());
+                }
+            }
+        }
+        root.setMeta(meta);
+        for (TreeMenu child : root.getChildren()) {
+            this.buildMenus(child, data);
+        }
+    }
+
+    /**
+     * 构建角色分配菜单弹出框的数据结构
+     *
+     * @param root
+     * @param data
+     */
+    private void buildAllMenus(TreeAllMenu root, List<TreeAllMenu> data) {
+        for (TreeAllMenu menu : data) {
+            if (menu.getParentId().equals(root.getId())) {
+                root.getChildren().add(menu);
+            }
+        }
+        for (TreeAllMenu child : root.getChildren()) {
+            this.buildAllMenus(child, data);
+        }
+    }
+
+    /**
+     * 给角色分配菜单的数据,过滤掉权限管理
+     *
+     * @return list
+     */
+    private void assignMenu(TreeAllMenu root, List<TreeAllMenu> data) {
+        for (TreeAllMenu menu : data) {
+            if (!menu.getTitle().equals("权限管理") && menu.getParentId().equals(root.getId())) {
+                root.getChildren().add(menu);
+            }
+        }
+        for (TreeAllMenu child : root.getChildren()) {
+            this.buildAllMenus(child, data);
+        }
+    }
+
+    @Override
+    public List<RoleAssignTreeMenu> getRoleAssignTreeMenuList() {
+        List<Menu> menuList = this.lambdaQuery()
+                .eq(Menu::getIsDeleted, DeleteStatusEnum.UNDELETED.getId())
+                .eq(Menu::getStatus, StatusEnum.ENABLE.getId())
+                .eq(Menu::getIsAdmin, IsAdminEnum.NO.getId())
+                .orderByAsc(Menu::getMenuSort)
+                .list();
+        List<RoleAssignTreeMenu> list = new ArrayList<>();
+        RoleAssignTreeMenu roleAssignTreeMenu;
+        for (Menu menu : menuList) {
+            roleAssignTreeMenu = new RoleAssignTreeMenu();
+            Map<String, String> mapping = new HashMap<>();
+            mapping.put("menuName", "title");
+            BeanUtil.copyProperties(menu, roleAssignTreeMenu, CopyOptions.create().setFieldMapping(mapping));
+            list.add(roleAssignTreeMenu);
+        }
+        List<RoleAssignTreeMenu> roots = list.stream().filter(x -> x.getParentId().equals(0)).collect(Collectors.toList());
+        for (RoleAssignTreeMenu root : roots) {
+            this.buildRoleAssignTreeMenuList(root, list);
+        }
+        return roots;
+    }
+
+    /**
+     * 角色分配菜单,构建所有菜单的树形结构
+     *
+     * @param root 根节点
+     * @param list 所有菜单集合
+     */
+    private void buildRoleAssignTreeMenuList(RoleAssignTreeMenu root, List<RoleAssignTreeMenu> list) {
+        for (RoleAssignTreeMenu menu : list) {
+            if (menu.getParentId().equals(root.getId())) {
+                root.getChildren().add(menu);
+            }
+        }
+        for (RoleAssignTreeMenu child : root.getChildren()) {
+            this.buildRoleAssignTreeMenuList(child, list);
+        }
+    }
 }

+ 24 - 1
admin/src/main/java/com/flyer/foster/service/impl/RoleMenuServiceImpl.java

@@ -1,10 +1,13 @@
 package com.flyer.foster.service.impl;
 
+import com.flyer.exception.BusinessException;
+import com.flyer.foster.dto.RoleMenuAddDTO;
 import com.flyer.foster.entity.RoleMenu;
 import com.flyer.foster.mapper.IRoleMenuMapper;
 import com.flyer.foster.service.IRoleMenuService;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
 import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
 
 /**
  * <p>
@@ -16,5 +19,25 @@ import org.springframework.stereotype.Service;
  */
 @Service
 public class RoleMenuServiceImpl extends ServiceImpl<IRoleMenuMapper, RoleMenu> implements IRoleMenuService {
-
+    @Transactional
+    @Override
+    public boolean addRoleMenu(RoleMenuAddDTO addDTO) {
+        if (addDTO.getRoleId() == null) {
+            throw new BusinessException("请选择角色");
+        }
+        if (addDTO.getMenuIdList().size() == 0) {
+            throw new BusinessException("请选择菜单");
+        }
+        // 删除角色菜单
+        this.removeBatchByIds(addDTO.getMenuIdList());
+        // 添加角色菜单
+        RoleMenu roleMenu;
+        for (Integer menuId : addDTO.getMenuIdList()) {
+            roleMenu = new RoleMenu();
+            roleMenu.setRoleId(addDTO.getRoleId());
+            roleMenu.setMenuId(menuId);
+            this.save(roleMenu);
+        }
+        return true;
+    }
 }

+ 62 - 0
admin/src/main/java/com/flyer/foster/service/impl/RoleServiceImpl.java

@@ -1,10 +1,23 @@
 package com.flyer.foster.service.impl;
 
+import cn.dev33.satoken.stp.StpUtil;
+import cn.hutool.core.bean.BeanUtil;
+import com.flyer.exception.BusinessException;
+import com.flyer.foster.dto.RoleAddDTO;
+import com.flyer.foster.dto.RoleMenuAddDTO;
+import com.flyer.foster.dto.RoleRespDTO;
 import com.flyer.foster.entity.Role;
+import com.flyer.foster.enums.TFEnum;
 import com.flyer.foster.mapper.IRoleMapper;
+import com.flyer.foster.service.IRoleMenuService;
 import com.flyer.foster.service.IRoleService;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.ArrayList;
+import java.util.List;
 
 /**
  * <p>
@@ -16,5 +29,54 @@ import org.springframework.stereotype.Service;
  */
 @Service
 public class RoleServiceImpl extends ServiceImpl<IRoleMapper, Role> implements IRoleService {
+    @Autowired
+    private IRoleMenuService iRoleMenuService;
+
+    @Override
+    public List<RoleRespDTO> getRoleList() {
+        int tenantId = StpUtil.getSession().getInt("tenantId");
+        List<RoleRespDTO> list = new ArrayList<>();
+        List<Role> roleList = this.lambdaQuery()
+                .eq(Role::getTenantId, tenantId)
+                .eq(Role::getIsAdmin, TFEnum.FALSE.getId())
+                .list();
+        RoleRespDTO roleRespDTO;
+        for (Role role : roleList) {
+            roleRespDTO = new RoleRespDTO();
+            BeanUtil.copyProperties(role, roleRespDTO);
+            list.add(roleRespDTO);
+        }
+        return list;
+    }
+
+    @Transactional
+    @Override
+    public boolean addRole(RoleAddDTO addDTO) {
+        int tenantId = StpUtil.getSession().getInt("tenantId");
+        addDTO.setRoleName(addDTO.getRoleName().trim());
+        addDTO.setRoleCode(addDTO.getRoleCode().trim());
+        if (addDTO.getMenuIdList().size() == 0) {
+            throw new BusinessException("请选择菜单");
+        }
+        // 1.判断(角色名称+角色编码)是否存在
+        Role role = this.lambdaQuery()
+                .eq(Role::getRoleName, addDTO.getRoleName())
+                .eq(Role::getRoleCode, addDTO.getRoleCode())
+                .one();
+        if (role != null) {
+            throw new BusinessException("角色编码+角色名称不能重复");
+        }
+        role = new Role();
+        BeanUtil.copyProperties(addDTO, role);
+        role.setTenantId(tenantId);
+        // 新增角色
+        this.save(role);
 
+        RoleMenuAddDTO roleMenuAddDTO = new RoleMenuAddDTO();
+        roleMenuAddDTO.setRoleId(role.getId());
+        roleMenuAddDTO.setMenuIdList(addDTO.getMenuIdList());
+        // 保存角色菜单
+        iRoleMenuService.addRoleMenu(roleMenuAddDTO);
+        return true;
+    }
 }

+ 154 - 0
admin/src/main/java/com/flyer/foster/service/impl/UserServiceImpl.java

@@ -1,10 +1,42 @@
 package com.flyer.foster.service.impl;
 
+import cn.dev33.satoken.session.SaSession;
+import cn.dev33.satoken.stp.StpUtil;
+import cn.hutool.core.bean.BeanUtil;
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.crypto.SecureUtil;
+import cn.hutool.crypto.asymmetric.KeyType;
+import cn.hutool.crypto.asymmetric.RSA;
+import cn.hutool.setting.dialect.Props;
+import com.alibaba.fastjson.JSON;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.flyer.exception.BusinessException;
+import com.flyer.foster.consts.LoginDevice;
+import com.flyer.foster.consts.RedisKeyConst;
+import com.flyer.foster.dto.LoginDTO;
+import com.flyer.foster.dto.UserAddOrUpdateDTO;
+import com.flyer.foster.dto.UserSearchDTO;
+import com.flyer.foster.entity.Role;
 import com.flyer.foster.entity.User;
+import com.flyer.foster.entity.UserRole;
 import com.flyer.foster.mapper.IUserMapper;
+import com.flyer.foster.service.IRoleService;
+import com.flyer.foster.service.IUserRoleService;
 import com.flyer.foster.service.IUserService;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.flyer.util.RedisUtil;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.core.env.Environment;
 import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
 
 /**
  * <p>
@@ -14,7 +46,129 @@ import org.springframework.stereotype.Service;
  * @author flyer
  * @since 2024-05-06
  */
+@Slf4j
 @Service
 public class UserServiceImpl extends ServiceImpl<IUserMapper, User> implements IUserService {
+    @Value("${use-local-private-key:false}")
+    private boolean useLocalPrivateKey;
+
+    @Value("${rsa-public-key}")
+    private String publicKey;
+
+    @Value("${rsa-private-key}")
+    private String privateKey;
+
+    @Value("${spring.profiles.active}")
+    private String env;
+
+    @Autowired
+    private RedisUtil redisUtil;
+
+    @Autowired
+    private IRoleService iRoleService;
+
+    @Autowired
+    private IUserRoleService iUserRoleService;
+
+    @Override
+    public String getRsaKey(String username) {
+        if (useLocalPrivateKey) {
+            return publicKey;
+        } else {
+            RSA rsa = new RSA();
+            // 私钥写入缓存,当登录成功之后则删除私钥缓存
+            redisUtil.set(RedisKeyConst.RSA_PRIVATE_KEY + username, rsa.getPrivateKeyBase64());
+            // 把公钥返回给前端
+            return rsa.getPublicKeyBase64();
+        }
+    }
+
+    @Override
+    public String login(LoginDTO loginDto) {
+        String password = this.decryptPassword(loginDto);
+        User user = this.lambdaQuery().eq(User::getUsername, loginDto.getUsername())
+                .eq(User::getPassword, SecureUtil.md5(password))
+                .one();
+        if (user == null) {
+            throw new BusinessException("账号密码错误!");
+        }
+        StpUtil.login(user.getUsername(), LoginDevice.PC);
+        SaSession tokenSession = StpUtil.getTokenSession();
+        tokenSession.set("tenantId", user.getTenantId());
+        return StpUtil.getTokenValue();
+    }
+
+    /**
+     * 对加密的密码进行解密
+     *
+     * @param loginDTO 登录对象
+     */
+    private String decryptPassword(LoginDTO loginDTO) {
+        RSA rsa;
+        String pwd = loginDTO.getPassword();
+        if ("dev".equals(env)) {
+            rsa = new RSA(privateKey, null);
+        } else {
+            log.info("rsa:private_key:" + loginDTO.getUsername());
+            String privateKey = redisUtil.get(RedisKeyConst.RSA_PRIVATE_KEY + loginDTO.getUsername()).toString();
+            rsa = new RSA(privateKey, null);
+        }
+        return rsa.decryptStr(pwd, KeyType.PrivateKey);
+    }
+
+    @Override
+    public IPage<UserSearchDTO> selectByPage(IPage<User> page, UserSearchDTO searchDTO) {
+        int tenantId = Integer.parseInt(StpUtil.getTokenSession().get("tenantId").toString());
+        searchDTO.setTenantId(tenantId);
+        // 查询管理员角色的账号
+        Role adminRole = iRoleService.lambdaQuery().eq(Role::getIsAdmin, 1).eq(Role::getTenantId, tenantId).one();
+        // 获取管理员用户id
+        UserRole userRole = iUserRoleService.lambdaQuery().eq(UserRole::getRoleId, adminRole.getId()).one();
+        searchDTO.setAdminUserId(userRole.getUserId());
+        IPage<UserSearchDTO> pageResult = baseMapper.selectByPage(page, searchDTO);
+        // 获取单个视频对象的标签集合
+        for (UserSearchDTO record : pageResult.getRecords()) {
+            List<Integer> roleIdList = iUserRoleService.lambdaQuery()
+                    .eq(UserRole::getUserId, record.getId()).list()
+                    .stream().map(UserRole::getRoleId).collect(Collectors.toList());
+            if (roleIdList.size() != 0) {
+                List<Role> roleList = iRoleService.listByIds(roleIdList);
+                record.setRoles(roleList);
+            }
+        }
+        return pageResult;
+    }
 
+    @Transactional
+    @Override
+    public boolean addUserAndRole(UserAddOrUpdateDTO addDTO) {
+        log.info("UserServiceImpl--->addUserAndRole params is {}", JSON.toJSONString(addDTO));
+        int tenantId = Integer.parseInt(StpUtil.getTokenSession().get("tenantId").toString());
+        // 查看用户名是否存在
+        String userName = addDTO.getUsername();
+        long count = this.lambdaQuery().eq(User::getUsername, userName).count();
+        if (count != 0) {
+            throw new BusinessException("用户已存在");
+        }
+        User user = new User();
+        BeanUtil.copyProperties(addDTO, user);
+        // 密码MD5加密存入
+        user.setPassword(SecureUtil.md5(user.getPassword()));
+        user.setTenantId(tenantId);
+        boolean saveResult = this.save(user);
+        if (!saveResult) {
+            throw new BusinessException("新增用户失败");
+        }
+        UserRole insertEntity;
+        for (Integer roleId : addDTO.getRoleIdList()) {
+            insertEntity = new UserRole();
+            insertEntity.setUserId(user.getId());
+            insertEntity.setRoleId(roleId);
+            boolean saveRoleResult = iUserRoleService.save(insertEntity);
+            if (!saveRoleResult) {
+                throw new BusinessException("新增用户角色关系数据失败");
+            }
+        }
+        return true;
+    }
 }

+ 26 - 6
admin/src/main/resources/application-dev.yml

@@ -2,17 +2,37 @@ spring:
   datasource:
     type: com.zaxxer.hikari.HikariDataSource
     driver-class-name: com.mysql.cj.jdbc.Driver
-    url: jdbc:mysql://118.195.172.253:3306/foster?serverTimezone=GMT%2B8&characterEncoding=utf-8
-    username: kelei
-    password: hx31Qk$Y6!cq$Mr^
+    url: jdbc:mysql://106.53.222.208:3306/foster?serverTimezone=GMT%2B8&characterEncoding=utf-8
+    username: foster
+    password: tKLGm42dSzaaNJPK
   redis:
     database: 1
-    host: 192.168.0.204
+    host: 106.53.222.208
     lettuce:
       pool:
         max-active: 20
         max-wait: -1
-    password: jiaguty
+    password: flyer_foster
 mybatis-plus:
   configuration:
-    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
+    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
+
+sa-token:
+  # token 名称(同时也是 cookie 名称)
+  token-name: token
+  # token 有效期(单位:秒) 默认30天,-1 代表永久有效
+  timeout: 3600
+  # token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结
+  active-timeout: -1
+  # 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录)
+  is-concurrent: true
+  # 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token)
+  is-share: true
+  # token 风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik)
+  token-style: uuid
+  # 是否输出操作日志
+  is-log: true
+
+use-local-private-key: true
+rsa-private-key: MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAIbvJIya52tj9POUUqqBPXQLuRXlkD+6afWC6ZX4Q3bSbmv+O+1BfFHdDoEuSsxydckTQckwQ5n+LQRC6pdldHpJxwaahMwOGQPS9XbrHy/7d6IIdkvG7+sv89W87kG2RrWXG8eaCBvxJHABOGf6q+N5PvwiL7gemba432qzoO1xAgMBAAECgYAWwDefZXbjioUAlN+jVAsyh897O6uosxuug5Yy7Rsi67QmjUU5abM2cllBurZt5lapwo9zBqo/SrX4Y/f98uNChHokiSKTq6EHoyqugu9Sckff1kJfkuTFxnNu0LFLnYlMb4NdMWB5S2PTyZh/GdvW48SMov1Yt+U5vjNCkfRiCQJBAMDnzHULgirBVYgg6osQwf18vln7MRddFHUb/201K6aNsoHXol86Mg72kERm+bDP9UdJ9x9Upd1stUE5GKqKbhsCQQCzEVaFISQe0fm4izUukxnDus45tlL0DGfqNzYVJzeK0C4b/Uh5LcPMp2ILZTkKovez8Hgp9nZDQx2m+wyNKptjAkBurI7BGDk2DnXkA/6MirDBnjAXr+YaYWy7Q7ToEvlYNTOVCwI9YEYYD531oJ7gsm8m12jQsN/4icX0Ba4BKirBAkEAgNK8V9Jb1gBhky4y+GrDYliF/Gb6jrBOIeXOdrFb9/WE9oXlGaie8CCLHH+Z5dkQMteQ2z+AHSuvrW12vigk2QJAIZDW6/EQmX4B2/w7MCCpNlJqwvUOrN2icbxC73hnjFvGgiRPkAxeSEiEi0Zk9QsYe1ljmRQn6vDyF64dbuUD7Q==
+rsa-public-key: MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCG7ySMmudrY/TzlFKqgT10C7kV5ZA/umn1gumV+EN20m5r/jvtQXxR3Q6BLkrMcnXJE0HJMEOZ/i0EQuqXZXR6SccGmoTMDhkD0vV26x8v+3eiCHZLxu/rL/PVvO5Btka1lxvHmggb8SRwAThn+qvjeT78Ii+4Hpm2uN9qs6DtcQIDAQAB

+ 154 - 0
admin/src/main/resources/logback-spring.xml

@@ -0,0 +1,154 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 日志级别从低到高分为TRACE < DEBUG < INFO < WARN < ERROR < FATAL,如果设置为WARN,则低于WARN的信息都不会输出 -->
+<!-- scan:当此属性设置为true时,配置文件如果发生改变,将会被重新加载,默认值为true -->
+<!-- scanPeriod:设置监测配置文件是否有修改的时间间隔,如果没有给出时间单位,默认单位是毫秒。当scan为true时,此属性生效。默认的时间间隔为1分钟。 -->
+<!-- debug:当此属性设置为true时,将打印出logback内部日志信息,实时查看logback运行状态。默认值为false。 -->
+<configuration scan="true" scanPeriod="10 seconds">
+    <contextName>logback</contextName>
+    <!-- name的值是变量的名称,value的值时变量定义的值。通过定义的值会被插入到logger上下文中。定义变量后,可以使“${}”来使用变量。 -->
+    <property name="LOG_PATH" value="/usr/logs/flyer-foster"/>
+
+    <!-- 彩色日志 -->
+    <!-- 配置格式变量:CONSOLE_LOG_PATTERN 彩色日志格式 -->
+    <!-- magenta:洋红 -->
+    <!-- boldMagenta:粗红-->
+    <!-- cyan:青色 -->
+    <!-- white:白色 -->
+    <property name="CONSOLE_LOG_PATTERN"
+              value="%yellow(%date{yyyy-MM-dd HH:mm:ss.SSS}) %blue([%thread]) %magenta(%-5level) %green(%logger) --- %cyan(%msg%n)"/>
+
+    <property name="LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} --- %msg%n"/>
+
+    <!--输出到控制台-->
+    <appender name="console" class="ch.qos.logback.core.ConsoleAppender">
+        <!--此日志appender是为开发使用,只配置最底级别,控制台输出的日志级别是大于或等于此级别的日志信息-->
+        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
+            <level>info</level>
+        </filter>
+        <encoder>
+            <Pattern>${CONSOLE_LOG_PATTERN}</Pattern>
+            <!-- 设置字符集 -->
+            <charset>UTF-8</charset>
+        </encoder>
+    </appender>
+
+    <!--输出到文件-->
+    <!-- 时间滚动输出 level为 DEBUG 日志 -->
+    <appender name="debug" class="ch.qos.logback.core.rolling.RollingFileAppender">
+        <!-- 正在记录的日志文件的路径及文件名 -->
+        <file>${LOG_PATH}/log-debug.log</file>
+        <!--日志文件输出格式-->
+        <encoder>
+            <pattern>${LOG_PATTERN}</pattern>
+            <charset>UTF-8</charset> <!-- 设置字符集 -->
+        </encoder>
+        <!-- 日志记录器的滚动策略,按日期,按大小记录 -->
+        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+            <!-- 日志归档 -->
+            <fileNamePattern>${LOG_PATH}/debug/log-debug-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
+            <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
+                <maxFileSize>100MB</maxFileSize>
+            </timeBasedFileNamingAndTriggeringPolicy>
+            <!--日志文件保留天数-->
+            <maxHistory>15</maxHistory>
+        </rollingPolicy>
+        <!-- 此日志文件只记录debug级别的 -->
+        <filter class="ch.qos.logback.classic.filter.LevelFilter">
+            <level>debug</level>
+            <onMatch>ACCEPT</onMatch>
+            <onMismatch>DENY</onMismatch>
+        </filter>
+    </appender>
+
+    <!-- 时间滚动输出 level为 INFO 日志 -->
+    <appender name="info" class="ch.qos.logback.core.rolling.RollingFileAppender">
+        <!-- 正在记录的日志文件的路径及文件名 -->
+        <file>${LOG_PATH}/log-info.log</file>
+        <!--日志文件输出格式-->
+        <encoder>
+            <pattern>${LOG_PATTERN}</pattern>
+            <charset>UTF-8</charset>
+        </encoder>
+        <!-- 日志记录器的滚动策略,按日期,按大小记录 -->
+        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+            <!-- 每天日志归档路径以及格式 -->
+            <fileNamePattern>${LOG_PATH}/info/log-info-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
+            <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
+                <maxFileSize>100MB</maxFileSize>
+            </timeBasedFileNamingAndTriggeringPolicy>
+            <!--日志文件保留天数-->
+            <maxHistory>15</maxHistory>
+        </rollingPolicy>
+        <!-- 此日志文件只记录info级别的 -->
+        <filter class="ch.qos.logback.classic.filter.LevelFilter">
+            <level>info</level>
+            <onMatch>ACCEPT</onMatch>
+            <onMismatch>DENY</onMismatch>
+        </filter>
+    </appender>
+
+    <!-- 时间滚动输出 level为 WARN 日志 -->
+    <appender name="warn" class="ch.qos.logback.core.rolling.RollingFileAppender">
+        <!-- 正在记录的日志文件的路径及文件名 -->
+        <file>${LOG_PATH}/log-warn.log</file>
+        <!--日志文件输出格式-->
+        <encoder>
+            <pattern>${LOG_PATTERN}</pattern>
+            <charset>UTF-8</charset> <!-- 此处设置字符集 -->
+        </encoder>
+        <!-- 日志记录器的滚动策略,按日期,按大小记录 -->
+        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+            <fileNamePattern>${LOG_PATH}/warn/log-warn-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
+            <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
+                <maxFileSize>100MB</maxFileSize>
+            </timeBasedFileNamingAndTriggeringPolicy>
+            <!--日志文件保留天数-->
+            <maxHistory>15</maxHistory>
+        </rollingPolicy>
+        <!-- 此日志文件只记录warn级别的 -->
+        <filter class="ch.qos.logback.classic.filter.LevelFilter">
+            <level>warn</level>
+            <onMatch>ACCEPT</onMatch>
+            <onMismatch>DENY</onMismatch>
+        </filter>
+    </appender>
+
+    <!-- 时间滚动输出 level为 ERROR 日志 -->
+    <appender name="error" class="ch.qos.logback.core.rolling.RollingFileAppender">
+        <!-- 正在记录的日志文件的路径及文件名 -->
+        <file>${LOG_PATH}/log-error.log</file>
+        <!--日志文件输出格式-->
+        <encoder>
+            <pattern>${LOG_PATTERN}</pattern>
+            <charset>UTF-8</charset> <!-- 此处设置字符集 -->
+        </encoder>
+        <!-- 日志记录器的滚动策略,按日期,按大小记录 -->
+        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+            <fileNamePattern>${LOG_PATH}/error/log-error-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
+            <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
+                <maxFileSize>100MB</maxFileSize>
+            </timeBasedFileNamingAndTriggeringPolicy>
+            <!--日志文件保留天数-->
+            <maxHistory>15</maxHistory>
+        </rollingPolicy>
+        <!-- 此日志文件只记录ERROR级别的 -->
+        <filter class="ch.qos.logback.classic.filter.LevelFilter">
+            <level>error</level>
+            <onMatch>ACCEPT</onMatch>
+            <onMismatch>DENY</onMismatch>
+        </filter>
+    </appender>
+
+    <!--开发环境:打印控制台-->
+    <springProfile name="dev">
+        <logger name="com.flyer.foster.service" level="debug"/>
+    </springProfile>
+
+    <root level="info">
+        <appender-ref ref="console"/>
+        <appender-ref ref="debug"/>
+        <appender-ref ref="info"/>
+        <appender-ref ref="warn"/>
+        <appender-ref ref="error"/>
+    </root>
+</configuration>

+ 61 - 0
admin/src/main/resources/mapper/MenuMapper.xml

@@ -1,5 +1,66 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
 <mapper namespace="com.flyer.foster.mapper.IMenuMapper">
+    <select id="selectPermissionsByUserId" resultType="java.lang.String">
+        select a.permission
+        from tb_menu a
+                 left join tb_role_menu b on a.id = b.menu_id and b.is_deleted = 0
+                 left join tb_user_role c on b.role_id = c.role_id and c.is_deleted = 0
+        where type = 2
+          and c.user_id = #{userId}
+    </select>
 
+    <select id="selectListByUserIdConvert2DTO" resultType="com.flyer.foster.pojo.TreeMenu">
+        select distinct a.id         as id,
+                        a.menu_name  as title,
+                        a.url        as component,
+                        a.parent_id  as parentId,
+                        a.menu_sort  as menuSort,
+                        a.type       as type,
+                        a.permission as permission
+        from tb_menu a
+                 inner join tb_role_menu b on a.id = b.menu_id and b.is_deleted = 0
+                 inner join tb_user_role c on b.role_id = c.role_id and c.is_deleted = 0
+        <where>
+            a.is_deleted = 0
+            <if test="userId!=null">
+                and c.user_id = #{userId}
+            </if>
+        </where>
+        order by a.menu_sort asc
+    </select>
+
+    <select id="selectListByRoleIdConvert2DTO" resultType="com.flyer.foster.pojo.TreeAllMenu">
+        select c.id        as id,
+               b.id        as roleMenuId,
+               c.menu_name as menuName,
+               c.url       as component,
+               c.parent_id as parentId
+        from tb_user_role a
+                 inner join tb_role_menu b on a.role_id = b.role_id
+                 inner join tb_menu c on b.menu_id = c.id
+        <where>
+            c.type = 1
+            <if test="roleId!=null">
+                and b.role_id = #{roleId}
+            </if>
+        </where>
+    </select>
+
+    <select id="selectAllTypeMenus" resultType="com.flyer.foster.pojo.TreeAllMenu">
+        select id         as id,
+               menu_name  as title,
+               parent_id  as parentId,
+               menu_sort  as menuSort,
+               url        as component,
+               type       as type,
+               permission as permission
+        from tb_menu
+        where is_deleted = 0
+        <if test="menuIdList.size!=0">
+            <foreach collection="menuIdList" item="menuId" open="and id in (" close=")" separator=",">
+                #{menuId}
+            </foreach>
+        </if>
+    </select>
 </mapper>

+ 54 - 0
admin/src/main/resources/mapper/UserMapper.xml

@@ -1,5 +1,59 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
 <mapper namespace="com.flyer.foster.mapper.IUserMapper">
+    <select id="selectByPage" resultType="com.flyer.foster.dto.UserSearchDTO">
+        select a.id        as id,
+               a.username  as userName,
+               a.status    as status,
+               a.tenant_id as tenantId
+        from tb_user a
+        where a.is_deleted = 0
+        <choose>
+            <when test="queryDTO.tenantId==0">
+                and a.tenant_id is not null
+            </when>
+            <otherwise>
+                and a.tenant_id = #{queryDTO.tenantId}
+            </otherwise>
+        </choose>
+        <if test="queryDTO.adminUserId!=null">
+            and a.id != #{queryDTO.adminUserId}
+        </if>
+        order by a.created_time desc
+    </select>
 
+    <select id="selectUserAndRoleByUserIdOrUserName" resultType="com.flyer.foster.dto.UserSearchDTO">
+        select a.username  as username,
+               c.role_name as roleName
+        from tb_user a
+                 left join tb_user_role b on a.id = b.user_id and b.is_deleted = 0
+                 left join tb_role c on b.role_id = c.id and c.is_deleted = 0
+        <where>
+            a.is_deleted = 0
+            <if test="searchDTO.id!=null">
+                and a.id = #{searchDTO.id}
+            </if>
+            <if test="searchDTO.username!=null and searchDTO.username!=''">
+                and a.username = #{searchDTO.username}
+            </if>
+        </where>
+    </select>
+
+    <select id="adminListByPage" resultType="com.flyer.foster.dto.UserSearchDTO">
+        select a.id          as id,
+               a.username    as userName,
+               a.status      as status,
+               a.tenant_id   as tenantId,
+               b.tenant_name as tenantName
+        from tb_user a
+                 left join tb_tenant b on a.tenant_id = b.id
+        where a.is_deleted = 0
+          and a.tenant_id is not null
+        <if test="userIdList.size!=0">
+            <foreach collection="userIdList" item="userId" open="and a.id in (" close=")" separator=",">
+                #{userId}
+            </foreach>
+        </if>
+        order by a.created_time desc
+    </select>
 </mapper>

+ 5 - 0
common/pom.xml

@@ -89,5 +89,10 @@
             <groupId>cn.hutool</groupId>
             <artifactId>hutool-all</artifactId>
         </dependency>
+
+        <dependency>
+            <groupId>cn.dev33</groupId>
+            <artifactId>sa-token-spring-boot-starter</artifactId>
+        </dependency>
     </dependencies>
 </project>

+ 66 - 0
common/src/main/java/com/flyer/exception/BusinessException.java

@@ -0,0 +1,66 @@
+package com.flyer.exception;
+
+import com.flyer.enums.StatusCodeEnum;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * BusinessException
+ *
+ * @author kelei
+ * @since 2022/3/16/9:15
+ */
+@Data
+@NoArgsConstructor
+public class BusinessException extends RuntimeException {
+    //错误码
+    private Integer code;
+    //错误消息
+    private String message;
+
+    /**
+     * @param message 错误消息
+     */
+    public BusinessException(String message) {
+        this.code = StatusCodeEnum.ERROR.getCode();
+        this.message = message;
+    }
+
+    /**
+     * @param message 错误消息
+     * @param code    错误码
+     */
+    public BusinessException(String message, Integer code) {
+        this.message = message;
+        this.code = code;
+    }
+
+    /**
+     * @param message 错误消息
+     * @param code    错误码
+     * @param cause   原始异常对象
+     */
+    public BusinessException(String message, Integer code, Throwable cause) {
+        super(cause);
+        this.message = message;
+        this.code = code;
+    }
+
+    /**
+     * @param statusCodeEnum 接收枚举类型
+     */
+    public BusinessException(StatusCodeEnum statusCodeEnum) {
+        this.message = statusCodeEnum.getMessage();
+        this.code = statusCodeEnum.getCode();
+    }
+
+    /**
+     * @param statusCodeEnum 接收枚举类型
+     * @param cause          原始异常对象
+     */
+    public BusinessException(StatusCodeEnum statusCodeEnum, Throwable cause) {
+        super(cause);
+        this.message = statusCodeEnum.getMessage();
+        this.code = statusCodeEnum.getCode();
+    }
+}

+ 84 - 0
common/src/main/java/com/flyer/exception/GlobalExceptionHandler.java

@@ -0,0 +1,84 @@
+package com.flyer.exception;
+
+import cn.dev33.satoken.exception.NotLoginException;
+import com.flyer.enums.StatusCodeEnum;
+import com.flyer.util.R;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.validation.BindingResult;
+import org.springframework.validation.FieldError;
+import org.springframework.web.bind.MethodArgumentNotValidException;
+import org.springframework.web.bind.ServletRequestBindingException;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.RestControllerAdvice;
+
+import javax.validation.ConstraintViolation;
+import javax.validation.ConstraintViolationException;
+import java.util.stream.Collectors;
+
+/**
+ * 全局异常处理
+ *
+ * @author kelei
+ * @since 2022/4/2/15:18
+ */
+@Slf4j
+@RestControllerAdvice
+public class GlobalExceptionHandler {
+    @ExceptionHandler(value = MethodArgumentNotValidException.class)
+    public R handleVaildException(MethodArgumentNotValidException e) {
+        log.error(e.getMessage(), e);
+        BindingResult bindingResult = e.getBindingResult();
+        StringBuilder errMsgBuilder = new StringBuilder();
+        for (FieldError fieldError : bindingResult.getFieldErrors()) {
+            errMsgBuilder.append(fieldError.getDefaultMessage());
+            break;
+        }
+        return R.error().message(errMsgBuilder.toString());
+    }
+
+    @ExceptionHandler(value = ConstraintViolationException.class)
+    public R handleVaildException(ConstraintViolationException e) {
+        log.error(e.getMessage(), e);
+        String message = e.getConstraintViolations().stream().map(ConstraintViolation::getMessage).collect(Collectors.joining());
+        return R.error().message(message);
+    }
+
+    @ExceptionHandler(value = {
+            Throwable.class,
+            ServletRequestBindingException.class
+    })
+    public R handleException(Throwable throwable) {
+        log.error(throwable.getMessage(), throwable);
+        return R.error(StatusCodeEnum.ERROR);
+    }
+
+    @ExceptionHandler(value = BusinessException.class)
+    public R handleException(BusinessException e) {
+        log.error(e.getMessage(), e);
+        return R.error().message(e.getMessage()).code(e.getCode());
+    }
+
+    @ExceptionHandler(value = NotLoginException.class)
+    public R handleException(NotLoginException e) {
+        log.error(e.getMessage(), e);
+        String msg = "";
+        switch (e.getType()) {
+            case "-1":
+            case "-2":
+                msg = "无效的token";
+                break;
+            case "-3":
+                msg = "token已过期";
+                break;
+            case "-4":
+                break;
+            case "-5":
+                break;
+            case "-6":
+                break;
+            case "-7":
+                break;
+        }
+        return R.error().message(msg).code(e.getCode());
+    }
+}

+ 638 - 0
common/src/main/java/com/flyer/util/RedisUtil.java

@@ -0,0 +1,638 @@
+package com.flyer.util;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.stereotype.Component;
+import org.springframework.util.CollectionUtils;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * redisTemplate封装
+ */
+@Component
+public class RedisUtil {
+
+    @Autowired
+    private RedisTemplate<String, Object> redisTemplate;
+
+    /**
+     * 指定缓存失效时间
+     *
+     * @param key  键
+     * @param time 时间(秒)
+     * @return
+     */
+    public boolean expire(String key, long time) {
+        try {
+            if (time > 0) {
+                redisTemplate.expire(key, time, TimeUnit.SECONDS);
+            }
+            return true;
+        } catch (Exception e) {
+            e.printStackTrace();
+            return false;
+        }
+    }
+
+    /**
+     * 根据key 获取过期时间
+     *
+     * @param key 键 不能为null
+     * @return 时间(秒) 返回0代表为永久有效
+     */
+    public long getExpire(String key) {
+        return redisTemplate.getExpire(key, TimeUnit.SECONDS);
+    }
+
+    /**
+     * 判断key是否存在
+     *
+     * @param key 键
+     * @return true 存在 false不存在
+     */
+    public boolean hasKey(String key) {
+        try {
+            return redisTemplate.hasKey(key);
+        } catch (Exception e) {
+            e.printStackTrace();
+            return false;
+        }
+    }
+
+    /**
+     * 删除缓存
+     *
+     * @param key 可以传一个值 或多个
+     */
+    @SuppressWarnings("unchecked")
+    public void del(String... key) {
+        if (key != null && key.length > 0) {
+            if (key.length == 1) {
+                redisTemplate.delete(key[0]);
+            } else {
+                redisTemplate.delete(CollectionUtils.arrayToList(key));
+            }
+        }
+    }
+
+    /**
+     * 原子操作
+     *
+     * @param key
+     * @param value
+     * @param time  失效时间
+     * @return
+     */
+    public boolean setIfAbsent(String key, Object value, long time) {
+        return redisTemplate.opsForValue().setIfAbsent(key, value, time, TimeUnit.SECONDS);
+    }
+
+    /**
+     * 原子操作
+     *
+     * @param key
+     * @param value
+     * @return
+     */
+    public boolean setIfAbsent(String key, Object value) {
+        return redisTemplate.opsForValue().setIfAbsent(key, value);
+    }
+
+    //============================String=============================
+
+    /**
+     * 普通缓存获取
+     *
+     * @param key 键
+     * @return 值
+     */
+    public Object get(String key) {
+        return key == null ? null : redisTemplate.opsForValue().get(key);
+    }
+
+    /**
+     * 普通缓存放入
+     *
+     * @param key   键
+     * @param value 值
+     * @return true成功 false失败
+     */
+    public boolean set(String key, Object value) {
+        try {
+            redisTemplate.opsForValue().set(key, value);
+            return true;
+        } catch (Exception e) {
+            e.printStackTrace();
+            return false;
+        }
+    }
+
+    /**
+     * 普通缓存放入并设置时间
+     *
+     * @param key   键
+     * @param value 值
+     * @param time  时间(秒) time要大于0 如果time小于等于0 将设置无限期
+     * @return true成功 false 失败
+     */
+    public boolean set(String key, Object value, long time) {
+        try {
+            if (time > 0) {
+                redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
+            } else {
+                set(key, value);
+            }
+            return true;
+        } catch (Exception e) {
+            e.printStackTrace();
+            return false;
+        }
+    }
+
+    /**
+     * 递增
+     *
+     * @param key   键
+     * @param delta 要增加几(大于0)
+     * @return
+     */
+    public long incr(String key, long delta) {
+        if (delta < 0) {
+            throw new RuntimeException("递增因子必须大于0");
+        }
+        return redisTemplate.opsForValue().increment(key, delta);
+    }
+
+    /**
+     * 递减
+     *
+     * @param key   键
+     * @param delta 要减少几(小于0)
+     * @return
+     */
+    public long decr(String key, long delta) {
+        if (delta < 0) {
+            throw new RuntimeException("递减因子必须大于0");
+        }
+        return redisTemplate.opsForValue().increment(key, -delta);
+    }
+
+    //================================Map=================================
+
+    /**
+     * HashGet
+     *
+     * @param key 键 不能为null
+     * @return 值
+     */
+    public long hlen(String key) {
+        return redisTemplate.opsForHash().size(key);
+    }
+
+    /**
+     * HashGet
+     *
+     * @param key  键 不能为null
+     * @param item 项 不能为null
+     * @return 值
+     */
+    public Object hget(String key, String item) {
+        return redisTemplate.opsForHash().get(key, item);
+    }
+
+    /**
+     * 获取hashKey对应的所有键值
+     *
+     * @param key 键
+     * @return 对应的多个键值
+     */
+    public Map<Object, Object> hmget(String key) {
+        return redisTemplate.opsForHash().entries(key);
+    }
+
+    /**
+     * HashSet
+     *
+     * @param key 键
+     * @param map 对应多个键值
+     * @return true 成功 false 失败
+     */
+    public boolean hmset(String key, Map<String, Object> map) {
+        try {
+            redisTemplate.opsForHash().putAll(key, map);
+            return true;
+        } catch (Exception e) {
+            e.printStackTrace();
+            return false;
+        }
+    }
+
+    /**
+     * HashSet 并设置时间
+     *
+     * @param key  键
+     * @param map  对应多个键值
+     * @param time 时间(秒)
+     * @return true成功 false失败
+     */
+    public boolean hmset(String key, Map<String, Object> map, long time) {
+        try {
+            redisTemplate.opsForHash().putAll(key, map);
+            if (time > 0) {
+                expire(key, time);
+            }
+            return true;
+        } catch (Exception e) {
+            e.printStackTrace();
+            return false;
+        }
+    }
+
+    /**
+     * 向一张hash表中放入数据,如果不存在将创建
+     *
+     * @param key   键
+     * @param item  项
+     * @param value 值
+     * @return true 成功 false失败
+     */
+    public boolean hset(String key, String item, Object value) {
+        try {
+            redisTemplate.opsForHash().put(key, item, value);
+            return true;
+        } catch (Exception e) {
+            e.printStackTrace();
+            return false;
+        }
+    }
+
+    /**
+     * 向一张hash表中放入数据,如果不存在将创建
+     *
+     * @param key   键
+     * @param item  项
+     * @param value 值
+     * @param time  时间(秒)  注意:如果已存在的hash表有时间,这里将会替换原有的时间
+     * @return true 成功 false失败
+     */
+    public boolean hset(String key, String item, Object value, long time) {
+        try {
+            redisTemplate.opsForHash().put(key, item, value);
+            if (time > 0) {
+                expire(key, time);
+            }
+            return true;
+        } catch (Exception e) {
+            e.printStackTrace();
+            return false;
+        }
+    }
+
+    /**
+     * 删除hash表中的值
+     *
+     * @param key  键 不能为null
+     * @param item 项 可以使多个 不能为null
+     */
+    public void hdel(String key, Object... item) {
+        redisTemplate.opsForHash().delete(key, item);
+    }
+
+    /**
+     * 判断hash表中是否有该项的值
+     *
+     * @param key  键 不能为null
+     * @param item 项 不能为null
+     * @return true 存在 false不存在
+     */
+    public boolean hHasKey(String key, String item) {
+        return redisTemplate.opsForHash().hasKey(key, item);
+    }
+
+    /**
+     * hash递增 如果不存在,就会创建一个 并把新增后的值返回
+     *
+     * @param key  键
+     * @param item 项
+     * @param by   要增加几(大于0)
+     * @return
+     */
+    public long hincr(String key, String item, long by) {
+        return redisTemplate.opsForHash().increment(key, item, by);
+    }
+
+    /**
+     * hash递减
+     *
+     * @param key  键
+     * @param item 项
+     * @param by   要减少记(小于0)
+     * @return
+     */
+    public long hdecr(String key, String item, long by) {
+        return redisTemplate.opsForHash().increment(key, item, -by);
+    }
+
+    //============================set=============================
+
+    /**
+     * 根据key获取Set中的所有值
+     *
+     * @param key 键
+     * @return
+     */
+    public Set<Object> sGet(String key) {
+        try {
+            return redisTemplate.opsForSet().members(key);
+        } catch (Exception e) {
+            e.printStackTrace();
+            return null;
+        }
+    }
+
+    /**
+     * 根据value从一个set中查询,是否存在
+     *
+     * @param key   键
+     * @param value 值
+     * @return true 存在 false不存在
+     */
+    public boolean sHasKey(String key, Object value) {
+        try {
+            return redisTemplate.opsForSet().isMember(key, value);
+        } catch (Exception e) {
+            e.printStackTrace();
+            return false;
+        }
+    }
+
+    /**
+     * 将数据放入set缓存
+     *
+     * @param key    键
+     * @param values 值 可以是多个
+     * @return 成功个数
+     */
+    public long sSet(String key, Object... values) {
+        try {
+            return redisTemplate.opsForSet().add(key, values);
+        } catch (Exception e) {
+            e.printStackTrace();
+            return 0;
+        }
+    }
+
+    /**
+     * 将set数据放入缓存
+     *
+     * @param key    键
+     * @param time   时间(秒)
+     * @param values 值 可以是多个
+     * @return 成功个数
+     */
+    public long sSetAndTime(String key, long time, Object... values) {
+        try {
+            Long count = redisTemplate.opsForSet().add(key, values);
+            if (time > 0) {
+                expire(key, time);
+            }
+            return count;
+        } catch (Exception e) {
+            e.printStackTrace();
+            return 0;
+        }
+    }
+
+    /**
+     * 获取set缓存的长度
+     *
+     * @param key 键
+     * @return
+     */
+    public long sGetSetSize(String key) {
+        try {
+            return redisTemplate.opsForSet().size(key);
+        } catch (Exception e) {
+            e.printStackTrace();
+            return 0;
+        }
+    }
+
+    /**
+     * 移除值为value的
+     *
+     * @param key    键
+     * @param values 值 可以是多个
+     * @return 移除的个数
+     */
+    public long setRemove(String key, Object... values) {
+        try {
+            Long count = redisTemplate.opsForSet().remove(key, values);
+            return count;
+        } catch (Exception e) {
+            e.printStackTrace();
+            return 0;
+        }
+    }
+    //===============================list=================================
+
+    /**
+     * 获取list缓存的内容
+     *
+     * @param key   键
+     * @param start 开始
+     * @param end   结束  0 到 -1代表所有值
+     * @return
+     */
+    public List<Object> lGet(String key, long start, long end) {
+        try {
+            return redisTemplate.opsForList().range(key, start, end);
+        } catch (Exception e) {
+            e.printStackTrace();
+            return null;
+        }
+    }
+
+    /**
+     * 获取list缓存的长度
+     *
+     * @param key 键
+     * @return
+     */
+    public long lGetListSize(String key) {
+        try {
+            return redisTemplate.opsForList().size(key);
+        } catch (Exception e) {
+            e.printStackTrace();
+            return 0;
+        }
+    }
+
+    /**
+     * 通过索引 获取list中的值
+     *
+     * @param key   键
+     * @param index 索引  index>=0时, 0 表头,1 第二个元素,依次类推;index<0时,-1,表尾,-2倒数第二个元素,依次类推
+     * @return
+     */
+    public Object lGetIndex(String key, long index) {
+        try {
+            return redisTemplate.opsForList().index(key, index);
+        } catch (Exception e) {
+            e.printStackTrace();
+            return null;
+        }
+    }
+
+    /**
+     * 将list放入缓存
+     *
+     * @param key   键
+     * @param value 值
+     * @return
+     */
+    public boolean lSetMIn(String key, Object value) {
+        try {
+            redisTemplate.opsForList().rightPush(key, value);
+            expire(key, 60);
+            return true;
+        } catch (Exception e) {
+            e.printStackTrace();
+            return false;
+        }
+    }
+
+    /**
+     * 将list放入缓存
+     *
+     * @param key   键
+     * @param value 值
+     * @return
+     */
+    public boolean lSet(String key, Object value) {
+        try {
+            redisTemplate.opsForList().rightPush(key, value);
+            return true;
+        } catch (Exception e) {
+            e.printStackTrace();
+            return false;
+        }
+    }
+
+    /**
+     * 将list放入缓存
+     *
+     * @param key   键
+     * @param value 值
+     * @param time  时间(秒)
+     * @return
+     */
+    public boolean lSet(String key, Object value, long time) {
+        try {
+            redisTemplate.opsForList().rightPush(key, value);
+            if (time > 0) {
+                expire(key, time);
+            }
+            return true;
+        } catch (Exception e) {
+            e.printStackTrace();
+            return false;
+        }
+    }
+
+    /**
+     * 将list放入缓存
+     *
+     * @param key   键
+     * @param value 值
+     * @return
+     */
+    public boolean lSetList(String key, List<Object> value) {
+        try {
+            redisTemplate.opsForList().rightPushAll(key, value);
+            return true;
+        } catch (Exception e) {
+            e.printStackTrace();
+            return false;
+        }
+    }
+
+    /**
+     * 将list放入缓存
+     *
+     * @param key   键
+     * @param value 值
+     * @param time  时间(秒)
+     * @return
+     */
+    public boolean lSetList(String key, List<Object> value, long time) {
+        try {
+            redisTemplate.opsForList().rightPushAll(key, value);
+            if (time > 0) {
+                expire(key, time);
+            }
+            return true;
+        } catch (Exception e) {
+            e.printStackTrace();
+            return false;
+        }
+    }
+
+    /**
+     * 根据索引修改list中的某条数据
+     *
+     * @param key   键
+     * @param index 索引
+     * @param value 值
+     * @return
+     */
+    public boolean lUpdateIndex(String key, long index, Object value) {
+        try {
+            redisTemplate.opsForList().set(key, index, value);
+            return true;
+        } catch (Exception e) {
+            e.printStackTrace();
+            return false;
+        }
+    }
+
+    /**
+     * 根据索引弹出list中的首条数据
+     *
+     * @param key   键
+     * @param index 等待时间(秒)
+     * @return
+     */
+    public Object lPop(String key, long index) {
+        try {
+            Object o = redisTemplate.opsForList().leftPop(key, index, TimeUnit.SECONDS);
+            return o;
+        } catch (Exception e) {
+            e.printStackTrace();
+            return null;
+        }
+    }
+
+    /**
+     * 移除N个值为value
+     *
+     * @param key   键
+     * @param count 移除多少个
+     * @param value 值
+     * @return 移除的个数
+     */
+    public long lRemove(String key, long count, Object value) {
+        try {
+            Long remove = redisTemplate.opsForList().remove(key, count, value);
+            return remove;
+        } catch (Exception e) {
+            e.printStackTrace();
+            return 0;
+        }
+    }
+
+}