first commit.

This commit is contained in:
2022-12-07 18:14:38 +08:00
commit e916d067f3
183 changed files with 9649 additions and 0 deletions

View File

@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<artifactId>plusone-system</artifactId>
<groupId>xyz.zhouxy</groupId>
<version>1.0.0-SNAPSHOT</version>
</parent>
<artifactId>plusone-system-application</artifactId>
<dependencies>
<dependency>
<groupId>xyz.zhouxy</groupId>
<artifactId>plusone-system-domain</artifactId>
</dependency>
<dependency>
<groupId>xyz.zhouxy</groupId>
<artifactId>plusone-system-infrastructure</artifactId>
</dependency>
<dependency>
<groupId>xyz.zhouxy</groupId>
<artifactId>plusone-basic-application</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,61 @@
package xyz.zhouxy.plusone.system.application.common.util;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import xyz.zhouxy.plusone.domain.IWithOrderNumber;
import xyz.zhouxy.plusone.system.application.query.result.MenuViewObject;
import xyz.zhouxy.plusone.system.domain.model.menu.Menu.MenuType;
/**
* 菜单工具类
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
*/
public class MenuUtil {
private MenuUtil() {
throw new IllegalStateException("Utility class");
}
/**
* 构建菜单树
*
* @param allMenus 菜单列表
* @return 菜单树
*/
public static List<MenuViewObject> buildMenuTree(Collection<MenuViewObject> allMenus) {
// 先排序,保证添加到 rootMenus 中的顺序,以及 addChild 添加的子菜单的顺序
allMenus = allMenus.stream()
.sorted(Comparator.comparing(IWithOrderNumber::getOrderNumber))
.toList();
// 一级菜单
List<MenuViewObject> rootMenus = new ArrayList<>();
// key: 菜单 id; value: 菜单对象. 方便根据 id 查找相应对象。
Map<Long, MenuViewObject> menuListMap = new HashMap<>();
for (var menu : allMenus) {
// 添加 MENU_LIST 到 map 中,方便后面调用对象的方法
if (menu.getType() == MenuType.MENU_LIST.ordinal()) {
menuListMap.put(menu.getId(), menu);
}
// 一级菜单
if (menu.getParentId() == 0) {
rootMenus.add(menu);
}
}
for (var menu : allMenus) {
var parent = menuListMap.getOrDefault(menu.getParentId(), null);
// 父菜单存在于 map 中,调用父菜单的 addChild 方法将当前菜单添加为父菜单的子菜单。
if (parent != null) {
parent.addChild(menu);
}
}
return rootMenus;
}
}

View File

@@ -0,0 +1,18 @@
package xyz.zhouxy.plusone.system.application.common.util;
import lombok.Getter;
import xyz.zhouxy.plusone.constant.RegexConsts;
public enum PrincipalType {
EMAIL(RegexConsts.EMAIL),
MOBILE_PHONE(RegexConsts.MOBILE_PHONE),
USERNAME(RegexConsts.USERNAME)
;
@Getter
private final String regex;
PrincipalType(String regex) {
this.regex = regex;
}
}

View File

@@ -0,0 +1,64 @@
package xyz.zhouxy.plusone.system.application.common.util;
import javax.annotation.Nullable;
import xyz.zhouxy.plusone.exception.InvalidInputException;
import xyz.zhouxy.plusone.system.domain.model.account.Email;
import xyz.zhouxy.plusone.system.domain.model.account.MobilePhone;
import xyz.zhouxy.plusone.system.domain.model.account.Principal;
import xyz.zhouxy.plusone.system.domain.model.account.Username;
/**
* 根据字面值,判断并生成 {@link Principal} 值对象。
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
* @see Principal
* @see Username
* @see Email
* @see MobilePhone
* @see InvalidInputException
*/
public class PrincipalUtil {
private PrincipalUtil() {
throw new IllegalStateException("Utility class");
}
public static PrincipalType getPrincipalType(@Nullable String principal) {
if (principal == null) {
throw new IllegalArgumentException("principal 不能为空");
}
PrincipalType[] principalTypes = PrincipalType.values();
for (var principalType : principalTypes) {
if (principal.matches(principalType.getRegex())) {
return principalType;
}
}
throw InvalidInputException.unsupportedPrincipalTypeException();
}
public static Principal getPrincipal(@Nullable String principal) {
PrincipalType principalType = getPrincipalType(principal);
if (principalType == PrincipalType.EMAIL) {
return Email.of(principal);
}
if (principalType == PrincipalType.MOBILE_PHONE) {
return MobilePhone.of(principal);
}
if (principalType == PrincipalType.USERNAME) {
return Username.of(principal);
}
throw InvalidInputException.unsupportedPrincipalTypeException();
}
public static Principal getEmailOrMobilePhone(@Nullable String principal) {
PrincipalType principalType = getPrincipalType(principal);
if (principalType == PrincipalType.EMAIL) {
return Email.of(principal);
}
if (principalType == PrincipalType.MOBILE_PHONE) {
return MobilePhone.of(principal);
}
throw InvalidInputException.unsupportedPrincipalTypeException("输入邮箱地址或手机号");
}
}

View File

@@ -0,0 +1,40 @@
package xyz.zhouxy.plusone.system.application.controller;
import static xyz.zhouxy.plusone.system.constant.AuthLogic.adminAuthLogic;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import xyz.zhouxy.plusone.system.application.service.AccountContextService;
import xyz.zhouxy.plusone.util.RestfulResult;
/**
* 账号查询本身相关信息
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
*/
@RestController
@RequestMapping("account")
public class AccountContextController {
private final AccountContextService service;
public AccountContextController(AccountContextService service) {
this.service = service;
}
@GetMapping("info")
public RestfulResult getAccountInfo() {
adminAuthLogic.checkLogin();
var result = service.getAccountInfo();
return RestfulResult.success("查询成功", result);
}
@GetMapping("menus")
public RestfulResult getMenuTree() {
adminAuthLogic.checkLogin();
var result = service.getMenuTree();
return RestfulResult.success("查询成功", result);
}
}

View File

@@ -0,0 +1,83 @@
package xyz.zhouxy.plusone.system.application.controller;
import static xyz.zhouxy.plusone.system.constant.AuthLogic.adminAuthLogic;
import static xyz.zhouxy.plusone.util.RestfulResult.success;
import java.util.List;
import javax.validation.Valid;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import xyz.zhouxy.plusone.system.application.query.params.AccountQueryParams;
import xyz.zhouxy.plusone.system.application.service.AccountManagementService;
import xyz.zhouxy.plusone.system.application.service.command.CreateAccountCommand;
import xyz.zhouxy.plusone.system.application.service.command.UpdateAccountCommand;
import xyz.zhouxy.plusone.util.AssertResult;
import xyz.zhouxy.plusone.util.RestfulResult;
/**
* 账号管理
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
*/
@RestController
@RequestMapping("sys/account")
public class AccountManagementController {
private final AccountManagementService service;
public AccountManagementController(AccountManagementService service) {
this.service = service;
}
@PostMapping
public RestfulResult createAccount(@RequestBody @Valid CreateAccountCommand command) {
adminAuthLogic.checkLogin();
adminAuthLogic.checkPermission("sys-account-create");
service.createAccount(command);
return success();
}
@DeleteMapping
public RestfulResult deleteAccounts(@RequestBody List<Long> ids) {
adminAuthLogic.checkLogin();
adminAuthLogic.checkPermission("sys-account-delete");
service.deleteAccounts(ids);
return success();
}
@PatchMapping("{id}")
public RestfulResult updateAccountInfo(
@PathVariable("id") Long id,
@RequestBody @Valid UpdateAccountCommand command) {
adminAuthLogic.checkLogin();
adminAuthLogic.checkPermission("sys-account-update");
service.updateAccountInfo(id, command);
return success();
}
@GetMapping("query")
public RestfulResult queryAccountOverviewList(AccountQueryParams queryParams) {
adminAuthLogic.checkLogin();
adminAuthLogic.checkPermission("sys-account-list");
var accountOverviewList = service.queryAccountOverviewList(queryParams);
return success("查询成功", accountOverviewList);
}
@GetMapping("{accountId}")
public RestfulResult queryAccountDetails(@PathVariable("accountId") Long accountId) {
adminAuthLogic.checkLogin();
adminAuthLogic.checkPermission("sys-account-details");
var accountDetails = service.queryAccountDetails(accountId);
AssertResult.nonNull(accountDetails);
return success("查询成功", accountDetails);
}
}

View File

@@ -0,0 +1,49 @@
package xyz.zhouxy.plusone.system.application.controller;
import static xyz.zhouxy.plusone.util.RestfulResult.success;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import xyz.zhouxy.plusone.system.application.service.AdminLoginService;
import xyz.zhouxy.plusone.system.application.service.command.LoginByOtpCommand;
import xyz.zhouxy.plusone.system.application.service.command.LoginByPasswordCommand;
import xyz.zhouxy.plusone.util.RestfulResult;
/**
* Admin 账号登录
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
*/
@RestController
@RequestMapping("login")
public class AdminLoginController {
private final AdminLoginService service;
public AdminLoginController(AdminLoginService service) {
this.service = service;
}
@PostMapping("byPassword")
public RestfulResult loginByPassword(@RequestBody LoginByPasswordCommand command) {
var loginInfo = service.loginByPassword(command);
return success("登录成功", loginInfo);
}
@PostMapping("byOtp")
public RestfulResult loginByOtp(@RequestBody LoginByOtpCommand command) {
var loginInfo = service.loginByOtp(command);
return success("登录成功", loginInfo);
}
@GetMapping("sendOtp")
public RestfulResult sendOtp(@RequestParam String principal) {
service.sendOtp(principal);
return success("发送成功");
}
}

View File

@@ -0,0 +1,30 @@
package xyz.zhouxy.plusone.system.application.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import xyz.zhouxy.plusone.system.application.service.AdminLogoutService;
import xyz.zhouxy.plusone.util.RestfulResult;
/**
* Admin 账号登出
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
*/
@RestController
@RequestMapping("logout")
public class AdminLogoutController {
private final AdminLogoutService service;
public AdminLogoutController(AdminLogoutService service) {
this.service = service;
}
@GetMapping
public RestfulResult execute() {
service.execute();
return RestfulResult.success("注销成功");
}
}

View File

@@ -0,0 +1,83 @@
package xyz.zhouxy.plusone.system.application.controller;
import static xyz.zhouxy.plusone.system.constant.AuthLogic.adminAuthLogic;
import static xyz.zhouxy.plusone.util.RestfulResult.success;
import java.util.List;
import javax.validation.Valid;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import xyz.zhouxy.plusone.system.application.query.params.DictQueryParams;
import xyz.zhouxy.plusone.system.application.service.DictManagementService;
import xyz.zhouxy.plusone.system.application.service.command.CreateDictCommand;
import xyz.zhouxy.plusone.system.application.service.command.UpdateDictCommand;
import xyz.zhouxy.plusone.util.RestfulResult;
/**
* 数据字典管理
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
*/
@RestController
@RequestMapping("sys/dict")
public class DictManagementController {
private final DictManagementService service;
public DictManagementController(DictManagementService service) {
this.service = service;
}
@PostMapping
public RestfulResult createDict(@RequestBody @Valid CreateDictCommand command) {
adminAuthLogic.checkPermission("sys-dict-create");
service.createDict(command);
return success();
}
@DeleteMapping
public RestfulResult deleteDicts(@RequestBody List<Long> ids) {
adminAuthLogic.checkPermission("sys-dict-delete");
service.deleteDicts(ids);
return success();
}
@PatchMapping("{id}")
public RestfulResult updateDict(
@PathVariable("id") Long id,
@RequestBody @Valid UpdateDictCommand command) {
adminAuthLogic.checkPermission("sys-dict-update");
service.updateDict(id, command);
return success();
}
@GetMapping("{dictId}")
public RestfulResult findDictDetails(@PathVariable("dictId") Long dictId) {
adminAuthLogic.checkPermission("sys-dict-details");
var dictDetails = service.findDictDetails(dictId);
return success("查询成功", dictDetails);
}
@GetMapping("all")
public RestfulResult loadAllDicts() {
adminAuthLogic.checkPermissionAnd("sys-dict-list", "sys-dict-details");
var dicts = service.loadAllDicts();
return success("查询成功", dicts);
}
@GetMapping("query")
public RestfulResult queryDictOverviewList(@Valid DictQueryParams queryParams) {
adminAuthLogic.checkPermission("sys-dict-list");
var dicts = service.queryDictOverviewList(queryParams);
return success("查询成功", dicts);
}
}

View File

@@ -0,0 +1,85 @@
package xyz.zhouxy.plusone.system.application.controller;
import static xyz.zhouxy.plusone.system.constant.AuthLogic.adminAuthLogic;
import static xyz.zhouxy.plusone.util.RestfulResult.success;
import javax.validation.Valid;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import xyz.zhouxy.plusone.system.application.service.MenuManagementService;
import xyz.zhouxy.plusone.system.application.service.command.CreateMenuCommand;
import xyz.zhouxy.plusone.system.application.service.command.UpdateMenuCommand;
import xyz.zhouxy.plusone.util.RestfulResult;
/**
* 菜单管理
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
*/
@RestController
@RequestMapping("sys/menu")
public class MenuManagementController {
private final MenuManagementService service;
public MenuManagementController(MenuManagementService service) {
this.service = service;
}
// ==================== create ====================
@PostMapping
public RestfulResult createMenu(@RequestBody @Valid CreateMenuCommand command) {
adminAuthLogic.checkPermission("sys-menu-create");
service.createMenu(command);
return success();
}
// ==================== delete ====================
@DeleteMapping("{id}")
public RestfulResult deleteMenu(@PathVariable("id") Long id) {
adminAuthLogic.checkPermission("sys-menu-delete");
service.deleteMenu(id);
return success();
}
// ==================== update ====================
@PatchMapping("{id}")
public RestfulResult updateMenu(
@PathVariable("id") Long id,
@RequestBody @Valid UpdateMenuCommand command) {
adminAuthLogic.checkPermission("sys-menu-update");
service.updateMenu(id, command);
return success();
}
// ==================== query ====================
@GetMapping("{id}")
public RestfulResult findById(@PathVariable("id") Long id) {
adminAuthLogic.checkPermission("sys-menu-details");
var result = service.findById(id);
return RestfulResult.success("查询成功", result);
}
@GetMapping("queryByAccountId")
public RestfulResult queryByAccountId(@RequestParam Long accountId) {
adminAuthLogic.checkPermission("sys-menu-details");
var result = service.queryByAccountId(accountId);
return success("查询成功", result);
}
@GetMapping("queryByRoleId")
public RestfulResult queryByRoleId(@RequestParam Long roleId) {
adminAuthLogic.checkPermission("sys-menu-details");
var result = service.queryByRoleId(roleId);
return success("查询成功", result);
}
}

View File

@@ -0,0 +1,42 @@
package xyz.zhouxy.plusone.system.application.controller;
import static xyz.zhouxy.plusone.util.RestfulResult.success;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import xyz.zhouxy.plusone.system.application.service.RegisterAccountService;
import xyz.zhouxy.plusone.system.application.service.command.RegisterAccountCommand;
import xyz.zhouxy.plusone.util.RestfulResult;
/**
* 注册账号服务
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
*/
@RestController
@RequestMapping("register")
public class RegisterAccountController {
private final RegisterAccountService service;
public RegisterAccountController(RegisterAccountService service) {
this.service = service;
}
@PostMapping
public RestfulResult registerAccount(@RequestBody RegisterAccountCommand command) {
service.registerAccount(command);
return success("注册成功");
}
@GetMapping("sendCode")
public RestfulResult sendCode(@RequestParam String principal) {
service.sendCode(principal);
return success("发送成功");
}
}

View File

@@ -0,0 +1,78 @@
package xyz.zhouxy.plusone.system.application.controller;
import javax.validation.Valid;
import static xyz.zhouxy.plusone.system.constant.AuthLogic.adminAuthLogic;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import xyz.zhouxy.plusone.system.application.query.params.RoleQueryParams;
import xyz.zhouxy.plusone.system.application.service.RoleManagementService;
import xyz.zhouxy.plusone.system.application.service.command.CreateRoleCommand;
import xyz.zhouxy.plusone.system.application.service.command.UpdateRoleCommand;
import xyz.zhouxy.plusone.util.RestfulResult;
/**
* 角色管理服务
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
*/
@RestController
@RequestMapping("sys/role")
public class RoleManagementController {
private final RoleManagementService service;
public RoleManagementController(RoleManagementService service) {
this.service = service;
}
@PostMapping
public RestfulResult createRole(@RequestBody @Valid CreateRoleCommand command) {
adminAuthLogic.checkPermission("sys-role-create");
service.createRole(command);
return RestfulResult.success();
}
@PatchMapping
public RestfulResult updateRole(@RequestBody @Valid UpdateRoleCommand command) {
adminAuthLogic.checkPermission("sys-role-update");
service.updateRole(command);
return RestfulResult.success();
}
@DeleteMapping("{id}")
public RestfulResult delete(@PathVariable("id") Long id) {
adminAuthLogic.checkPermission("sys-role-delete");
service.delete(id);
return RestfulResult.success();
}
@GetMapping("exists")
public RestfulResult exists(@RequestParam("id") Long id) {
adminAuthLogic.checkPermissionOr("sys-role-list", "sys-role-details");
var isExists = service.exists(id);
return RestfulResult.success(isExists ? "存在" : "不存在", isExists);
}
@GetMapping("{id}")
public RestfulResult findById(@PathVariable("id") Long id) {
adminAuthLogic.checkPermission("sys-role-details");
var result = service.findById(id);
return RestfulResult.success("查询成功", result);
}
@GetMapping("query")
public RestfulResult query(RoleQueryParams params) {
adminAuthLogic.checkPermission("sys-role-list");
var result = service.query(params);
return RestfulResult.success("查询成功", result);
}
}

View File

@@ -0,0 +1,38 @@
package xyz.zhouxy.plusone.system.application.exception;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;
import xyz.zhouxy.plusone.exception.PlusoneException;
@ResponseStatus(HttpStatus.BAD_REQUEST)
public class AccountLoginException extends PlusoneException {
@java.io.Serial
private static final long serialVersionUID = -3040996790739138556L;
private static final int DEFAULT_ERR_CODE = 4030000;
private AccountLoginException() {
super(DEFAULT_ERR_CODE, "用户登录异常");
}
private AccountLoginException(int code, String message) {
super(code, message);
}
public static AccountLoginException accountNotExistException() {
return new AccountLoginException(4030101, "用户账户不存在");
}
public static AccountLoginException otpErrorException() {
return new AccountLoginException(4030501, "验证码错误");
}
public static AccountLoginException otpNotExistsException() {
return new AccountLoginException(4030502, "验证码不存在或已过期");
}
public static AccountLoginException passwordErrorException() {
return new AccountLoginException(4030200, "用户密码错误");
}
}

View File

@@ -0,0 +1,57 @@
package xyz.zhouxy.plusone.system.application.exception;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;
import xyz.zhouxy.plusone.exception.PlusoneException;
@ResponseStatus(HttpStatus.BAD_REQUEST)
public class AccountRegisterException extends PlusoneException {
@java.io.Serial
private static final long serialVersionUID = 7580245181633370195L;
public AccountRegisterException() {
this(4020000, "用户注册错误");
}
public AccountRegisterException(String message) {
this(4020000, message);
}
public AccountRegisterException(Throwable cause) {
super(4020000, cause);
}
public AccountRegisterException(int code, String message) {
super(code, message);
}
public AccountRegisterException(int code, Throwable cause) {
super(code, cause);
}
public static AccountRegisterException emailOrMobilePhoneRequiredException() {
return new AccountRegisterException(4020300, "邮箱和手机号应至少绑定一个");
}
public static AccountRegisterException usernameAlreadyExists(String username) {
return new AccountRegisterException(4020400, String.format("用户名 %s 已存在", username));
}
public static AccountRegisterException emailAlreadyExists(String value) {
return new AccountRegisterException(4020500, String.format("邮箱 %s 已存在", value));
}
public static AccountRegisterException mobilePhoneAlreadyExists(String value) {
return new AccountRegisterException(4020600, String.format("手机号 %s 已存在", value));
}
public static AccountRegisterException codeErrorException() {
return new AccountRegisterException(4020701, "校验码错误");
}
public static AccountRegisterException codeNotExistsException() {
return new AccountRegisterException(4020702, "校验码不存在或已过期");
}
}

View File

@@ -0,0 +1,17 @@
package xyz.zhouxy.plusone.system.application.exception;
import xyz.zhouxy.plusone.exception.InvalidInputException;
public class UnsupportedMenuTypeException extends InvalidInputException {
@java.io.Serial
private static final long serialVersionUID = -769169844015637730L;
public UnsupportedMenuTypeException() {
this("不支持的菜单类型");
}
public UnsupportedMenuTypeException(String message) {
super(message);
}
}

View File

@@ -0,0 +1,56 @@
package xyz.zhouxy.plusone.system.application.exception.handler;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import cn.dev33.satoken.exception.DisableServiceException;
import cn.dev33.satoken.exception.SameTokenInvalidException;
import cn.dev33.satoken.exception.NotBasicAuthException;
import cn.dev33.satoken.exception.NotLoginException;
import cn.dev33.satoken.exception.NotPermissionException;
import cn.dev33.satoken.exception.NotRoleException;
import cn.dev33.satoken.exception.NotSafeException;
import cn.dev33.satoken.exception.SaTokenException;
import lombok.extern.slf4j.Slf4j;
import xyz.zhouxy.plusone.exception.handler.BaseExceptionHandler;
import xyz.zhouxy.plusone.util.RestfulResult;
/**
* Sa-Token 异常处理器
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
*/
@RestControllerAdvice
@Slf4j
public class SaTokenExceptionHandler extends BaseExceptionHandler {
public SaTokenExceptionHandler(ExceptionInfoHolder exceptionInfoHolder) {
super(exceptionInfoHolder);
set(NotPermissionException.class, 4030103, "会话未能通过权限认证", HttpStatus.FORBIDDEN);
set(NotRoleException.class, 4030103, "会话未能通过角色认证", HttpStatus.FORBIDDEN);
set(DisableServiceException.class, 4030202, "账号指定服务已被封禁", HttpStatus.FORBIDDEN);
set(SameTokenInvalidException.class, 4030400, "提供的 Same-Token 无效", HttpStatus.UNAUTHORIZED);
set(NotBasicAuthException.class, 4030000, "会话未能通过 Http Basic 认证", HttpStatus.UNAUTHORIZED);
set(NotSafeException.class, 4020300, "会话未能通过二级认证", HttpStatus.UNAUTHORIZED);
set(NotLoginException.class,
4020400,
e -> switch (((NotLoginException) e).getType()) {
case NotLoginException.NOT_TOKEN -> "未提供 Token";
case NotLoginException.INVALID_TOKEN -> "Token 无效";
case NotLoginException.TOKEN_TIMEOUT -> "Token 已过期";
case NotLoginException.BE_REPLACED -> "Token 已被顶下线";
case NotLoginException.KICK_OUT -> "Token 已被踢下线";
default -> "当前会话未登录";
},
HttpStatus.UNAUTHORIZED);
set(SaTokenException.class, 4020300, "未通过身份认证或权限认证", HttpStatus.FORBIDDEN);
}
@ExceptionHandler(SaTokenException.class)
public ResponseEntity<RestfulResult> handleSaTokenException(SaTokenException e) {
log.error(e.getMessage(), e);
return buildExceptionResponse(e);
}
}

View File

@@ -0,0 +1,32 @@
package xyz.zhouxy.plusone.system.application.query;
import java.util.List;
import org.apache.ibatis.annotations.Mapper;
import xyz.zhouxy.plusone.system.application.query.params.AccountQueryParams;
import xyz.zhouxy.plusone.system.application.query.result.AccountDetails;
import xyz.zhouxy.plusone.system.application.query.result.AccountOverview;
import xyz.zhouxy.plusone.util.PageDTO;
/**
* 账号信息查询器
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
*/
@Mapper
public interface AccountQueries {
default PageDTO<AccountOverview> queryAccountOverviewPage(AccountQueryParams queryParams) {
List<AccountOverview> content = queryAccountOverview(queryParams);
long total = count(queryParams);
return PageDTO.of(content, total);
}
List<AccountOverview> queryAccountOverview(AccountQueryParams queryParams);
long count(AccountQueryParams queryParams);
AccountDetails queryAccountDetails(Long accountId);
}

View File

@@ -0,0 +1,39 @@
package xyz.zhouxy.plusone.system.application.query;
import java.util.List;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.namedparam.BeanPropertySqlParameterSource;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.stereotype.Component;
import xyz.zhouxy.plusone.sql.SQL;
import xyz.zhouxy.plusone.system.application.query.params.DictQueryParams;
import xyz.zhouxy.plusone.system.application.query.result.DictOverview;
/**
* 数据字典查询器
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
*/
@Component
public class DictQueries {
private final NamedParameterJdbcTemplate jdbcTemplate;
public DictQueries(NamedParameterJdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
public List<DictOverview> queryDictOverviewList(DictQueryParams queryParams) {
String sql = new SQL()
.SELECT("id", "dict_type", "dict_label",
"created_by", "create_time", "updated_by", "update_time", "count")
.FROM("view_sys_dict_overview")
.WHERE_IF(queryParams.getDictType() != null, "dict_type LIKE '%:dictType%'")
.WHERE_IF(queryParams.getDictLabel() != null, "dict_label LIKE '%:dictLabel%'")
.toString();
return this.jdbcTemplate
.query(sql, new BeanPropertySqlParameterSource(queryParams), new BeanPropertyRowMapper<>(DictOverview.class));
}
}

View File

@@ -0,0 +1,10 @@
package xyz.zhouxy.plusone.system.application.query;
/**
*
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
*/
public interface PermissionQueries {
// TODO【添加】 权限信息查询器
}

View File

@@ -0,0 +1,64 @@
package xyz.zhouxy.plusone.system.application.query;
import java.util.List;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.namedparam.BeanPropertySqlParameterSource;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.stereotype.Repository;
import xyz.zhouxy.plusone.sql.SQL;
import xyz.zhouxy.plusone.system.application.query.params.RoleQueryParams;
import xyz.zhouxy.plusone.system.application.query.result.RoleOverview;
/**
* 角色信息查询器
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
*/
@Repository
public class RoleQueries {
private final NamedParameterJdbcTemplate jdbcTemplate;
public RoleQueries(NamedParameterJdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
/**
* 查询 Role
* <p>
* <b> !!!注意:此方法内存在字符串拼接,勿在循环内使用。</b>
* </p>
*
* @param params 查询参数
* @return 查询结果
*/
public List<RoleOverview> query(RoleQueryParams params) {
String b = new SQL()
.SELECT("id")
.FROM("sys_role")
.WHERE_IF_NOT_NULL(params.getId(), "id = :id")
.WHERE_IF_NOT_NULL(params.getName(), "name = :name")
.WHERE_IF_NOT_NULL(params.getIdentifier(), "identifier = :identifier")
.WHERE_IF_NOT_NULL(params.getStatus(), "status = :status")
.WHERE_IF_NOT_NULL(params.getCreateTimeStart(), "create_time >= :createTimeStart")
.WHERE_IF_NOT_NULL(params.getCreateTimeEnd(), "create_time < :createTimeEnd")
.WHERE_IF_NOT_NULL(params.getUpdateTimeStart(), "update_time >= :updateTimeStart")
.WHERE_IF_NOT_NULL(params.getUpdateTimeEnd(), "update_time < :updateTimeEnd")
.LIMIT(params.getSize())
.OFFSET(params.getOffset())
.toString();
var sql = """
SELECT a.id AS id, a.name AS name, a.identifier AS identifier, a.status AS status
FROM sys_role AS a, (
""" + b + """
) AS b
WHERE a.id = b.id
""";
return this.jdbcTemplate
.query(sql, new BeanPropertySqlParameterSource(params),
new BeanPropertyRowMapper<>(RoleOverview.class));
}
}

View File

@@ -0,0 +1,55 @@
package xyz.zhouxy.plusone.system.application.query.params;
import java.time.LocalDate;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import xyz.zhouxy.plusone.util.PagingAndSortingQueryParams;
/**
* 账号信息查询参数
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
*/
@ToString
public class AccountQueryParams extends PagingAndSortingQueryParams {
public AccountQueryParams() {
super("id",
"username",
"email",
"mobile_phone",
"status",
"nickname",
"sex",
"created_by",
"create_time",
"updated_by",
"update_time");
}
// TODO【添加】 注解参数校验
private @Getter @Setter Long id;
private @Getter @Setter String username;
private @Getter @Setter String email;
private @Getter @Setter String mobilePhone;
private @Getter @Setter Integer status;
private @Getter @Setter String nickname;
private @Getter @Setter Integer sex;
private @Getter @Setter Long createdBy;
private @Getter @Setter LocalDate createTimeStart;
private @Getter LocalDate createTimeEnd;
private @Getter @Setter Long updatedBy;
private @Getter @Setter LocalDate updateTimeStart;
private @Getter LocalDate updateTimeEnd;
private @Getter @Setter Long roleId;
public void setCreateTimeEnd(LocalDate createTimeEnd) {
this.createTimeEnd = createTimeEnd.plusDays(1);
}
public void setUpdateTimeEnd(LocalDate updateTimeEnd) {
this.updateTimeEnd = updateTimeEnd.plusDays(1);
}
}

View File

@@ -0,0 +1,21 @@
package xyz.zhouxy.plusone.system.application.query.params;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import lombok.experimental.Accessors;
import xyz.zhouxy.plusone.util.PagingAndSortingQueryParams;
/**
* 数据字典查询参数
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
*/
@Getter
@Setter
@Accessors(chain = true)
@ToString(callSuper = true)
public class DictQueryParams extends PagingAndSortingQueryParams {
String dictType;
String dictLabel;
}

View File

@@ -0,0 +1,34 @@
package xyz.zhouxy.plusone.system.application.query.params;
import java.time.LocalDate;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import xyz.zhouxy.plusone.util.PagingAndSortingQueryParams;
/**
* 角色信息查询参数
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
*/
@ToString(callSuper = true)
public class RoleQueryParams extends PagingAndSortingQueryParams {
private @Getter @Setter Long id;
private @Getter @Setter String name;
private @Getter @Setter String identifier;
private @Getter @Setter Integer status;
private @Getter @Setter LocalDate createTimeStart;
private @Getter LocalDate createTimeEnd;
private @Getter @Setter LocalDate updateTimeStart;
private @Getter LocalDate updateTimeEnd;
public void setCreateTimeEnd(LocalDate createTimeEnd) {
this.createTimeEnd = createTimeEnd.plusDays(1);
}
public void setUpdateTimeEnd(LocalDate updateTimeEnd) {
this.updateTimeEnd = updateTimeEnd.plusDays(1);
}
}

View File

@@ -0,0 +1,29 @@
package xyz.zhouxy.plusone.system.application.query.result;
import java.util.Set;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;
/**
* 账号详细信息
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
*/
@Getter
@Setter
@NoArgsConstructor
@ToString
public class AccountDetails {
Long id;
String username;
String email;
String mobilePhone;
Integer status;
Set<RoleOverview> roles;
String nickname;
String avatar;
Integer sex;
}

View File

@@ -0,0 +1,38 @@
package xyz.zhouxy.plusone.system.application.query.result;
import java.time.LocalDateTime;
import java.util.Set;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;
import lombok.experimental.Accessors;
/**
* 账号概述信息
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
*/
@Getter
@Setter
@Accessors(chain = true)
@NoArgsConstructor
@AllArgsConstructor
@ToString
public class AccountOverview {
Long id;
String username;
String email;
String mobilePhone;
Integer status;
Set<String> roles;
String nickname;
String avatar;
Integer sex;
Long createdBy;
LocalDateTime createTime;
Long updatedBy;
LocalDateTime updateTime;
}

View File

@@ -0,0 +1,22 @@
package xyz.zhouxy.plusone.system.application.query.result;
import java.time.LocalDateTime;
import lombok.Data;
/**
* 数据字典概述信息
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
*/
@Data
public class DictOverview {
Long id;
String dictType;
String dictLabel;
Integer count;
Long createdBy;
LocalDateTime createTime;
Long updatedBy;
LocalDateTime updateTime;
}

View File

@@ -0,0 +1,24 @@
package xyz.zhouxy.plusone.system.application.query.result;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.ToString;
/**
* 登录结果
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
*/
@Getter
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@ToString
public class LoginInfoViewObject {
private String token;
private AccountDetails account;
public static LoginInfoViewObject of(String token, AccountDetails accountDetails) {
return new LoginInfoViewObject(token, accountDetails);
}
}

View File

@@ -0,0 +1,131 @@
package xyz.zhouxy.plusone.system.application.query.result;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import xyz.zhouxy.plusone.domain.IWithOrderNumber;
import xyz.zhouxy.plusone.system.domain.model.menu.Action;
import xyz.zhouxy.plusone.system.domain.model.menu.Menu;
import xyz.zhouxy.plusone.system.domain.model.menu.Menu.MenuType;
/**
* 菜单信息
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
*/
@NoArgsConstructor
@JsonInclude(JsonInclude.Include.NON_NULL)
public class MenuViewObject implements IWithOrderNumber {
@Getter
@Setter
Integer type;
@Getter
@Setter
String typeName;
@Getter
@Setter
Long id;
@Getter
@Setter
Long parentId;
@Getter
@Setter
String name;
// 若 type 为 MENU_ITEM 且 path 以 http:// 或 https:// 开头则被识别为外链
@Getter
@Setter
String path;
@Getter
@Setter
String title;
@Getter
@Setter
String icon;
@Getter
@Setter
boolean hidden;
@Getter
@Setter
int orderNumber;
@Getter
@Setter
Integer status;
@Getter
@Setter
String remarks;
// MENU_ITEM
@Getter
@Setter
String component;
@Getter
@Setter
Boolean cache;
@Getter
@Setter
String resource;
@Getter
@Setter
List<Action> actions;
// MENU_LIST
List<MenuViewObject> children;
public void addChild(MenuViewObject child) {
if (this.children == null) {
this.children = new ArrayList<>();
}
this.children.add(child);
}
public void addChildren(Collection<MenuViewObject> children) {
if (this.children == null) {
this.children = new ArrayList<>();
}
this.children.addAll(children);
}
public static MenuViewObject of(Menu menu) {
var viewObject = new MenuViewObject();
viewObject.type = menu.getType().ordinal();
viewObject.typeName = menu.getType().name();
viewObject.id = menu.getId().orElseThrow();
viewObject.parentId = menu.getParentId();
viewObject.name = menu.getName();
viewObject.path = menu.getPath();
viewObject.title = menu.getTitle();
viewObject.icon = menu.getIcon();
viewObject.hidden = menu.isHidden();
viewObject.orderNumber = menu.getOrderNumber();
viewObject.status = menu.getStatus().getValue();
viewObject.remarks = menu.getRemarks();
if (viewObject.type == MenuType.MENU_ITEM.ordinal()) {
viewObject.component = menu.getComponent();
viewObject.cache = menu.getCache();
viewObject.resource = menu.getResource();
viewObject.actions = menu.getActions();
}
return viewObject;
}
public List<MenuViewObject> getChildren() {
return Objects.nonNull(this.children)
? this.children
.stream()
.sorted(Comparator.comparing(IWithOrderNumber::getOrderNumber))
.toList()
: null;
}
}

View File

@@ -0,0 +1,23 @@
package xyz.zhouxy.plusone.system.application.query.result;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 角色信息
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class RoleOverview {
// 角色 id
Long id;
// 角色名
String name;
// 标识符(安全框架校验权限所用)
String identifier;
Integer status;
}

View File

@@ -0,0 +1,38 @@
package xyz.zhouxy.plusone.system.application.service;
import static xyz.zhouxy.plusone.system.constant.AuthLogic.adminAuthLogic;
import java.util.List;
import org.springframework.stereotype.Service;
import xyz.zhouxy.plusone.system.application.query.AccountQueries;
import xyz.zhouxy.plusone.system.application.query.result.AccountDetails;
import xyz.zhouxy.plusone.system.application.query.result.MenuViewObject;
/**
* 账号查询本身相关信息
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
*/
@Service
public class AccountContextService {
private final AccountQueries accountQueries;
private final MenuManagementService menuManagementService;
public AccountContextService(AccountQueries accountQueries, MenuManagementService menuManagementService) {
this.accountQueries = accountQueries;
this.menuManagementService = menuManagementService;
}
public AccountDetails getAccountInfo() {
long accountId = adminAuthLogic.getLoginIdAsLong();
return accountQueries.queryAccountDetails(accountId);
}
public List<MenuViewObject> getMenuTree() {
long accountId = adminAuthLogic.getLoginIdAsLong();
return menuManagementService.queryByAccountId(accountId);
}
}

View File

@@ -0,0 +1,105 @@
package xyz.zhouxy.plusone.system.application.service;
import static xyz.zhouxy.plusone.system.constant.AuthLogic.adminAuthLogic;
import java.util.List;
import java.util.Objects;
import javax.validation.Valid;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.PathVariable;
import xyz.zhouxy.plusone.system.application.exception.AccountRegisterException;
import xyz.zhouxy.plusone.system.application.query.AccountQueries;
import xyz.zhouxy.plusone.system.application.query.params.AccountQueryParams;
import xyz.zhouxy.plusone.system.application.query.result.AccountDetails;
import xyz.zhouxy.plusone.system.application.query.result.AccountOverview;
import xyz.zhouxy.plusone.system.application.service.command.CreateAccountCommand;
import xyz.zhouxy.plusone.system.application.service.command.UpdateAccountCommand;
import xyz.zhouxy.plusone.system.domain.model.account.Account;
import xyz.zhouxy.plusone.system.domain.model.account.AccountInfo;
import xyz.zhouxy.plusone.system.domain.model.account.AccountRepository;
import xyz.zhouxy.plusone.system.domain.model.account.Email;
import xyz.zhouxy.plusone.system.domain.model.account.MobilePhone;
import xyz.zhouxy.plusone.system.domain.model.account.Username;
import xyz.zhouxy.plusone.util.AssertResult;
import xyz.zhouxy.plusone.util.PageDTO;
/**
* 账号管理
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
*/
@Service
@Transactional
public class AccountManagementService {
private final AccountRepository accountRepository;
private final AccountQueries accountQueries;
public AccountManagementService(AccountRepository accountRepository, AccountQueries accountQueries) {
this.accountRepository = accountRepository;
this.accountQueries = accountQueries;
}
public void createAccount(@Valid CreateAccountCommand command) {
String username = command.getUsername();
if (accountRepository.existsUsername(Username.of(username))) {
throw AccountRegisterException.usernameAlreadyExists(username);
}
String email = command.getEmail();
if (StringUtils.hasText(email) && accountRepository.existsEmail(Email.of(email))) {
throw AccountRegisterException.emailAlreadyExists(email);
}
String mobilePhone = command.getMobilePhone();
if (StringUtils.hasText(mobilePhone) && accountRepository.existsMobilePhone(MobilePhone.of(mobilePhone))) {
throw AccountRegisterException.mobilePhoneAlreadyExists(mobilePhone);
}
Account account = Account.newInstance(
username,
email,
mobilePhone,
command.getPassword(),
command.getPasswordConfirmation(),
command.getStatus(),
command.getRoleRefs(),
AccountInfo.of(command.getNickname(), command.getAvatar(), command.getSex()),
adminAuthLogic.getLoginIdAsLong());
accountRepository.save(account);
}
public void deleteAccounts(List<Long> ids) {
Account accountToDelete;
for (var id : ids) {
accountToDelete = accountRepository.find(id);
AssertResult.nonNull(accountToDelete);
accountRepository.delete(accountToDelete);
}
}
public void updateAccountInfo(Long id, @Valid UpdateAccountCommand command) {
Assert.isTrue(Objects.equals(id, command.getId()), "参数错误: id 不匹配");
Account account = accountRepository.find(id);
AssertResult.nonNull(account, "该账号不存在");
account.setAccountInfo(command.getNickname(), command.getAvatar(), command.getSex());
account.setUpdatedBy(adminAuthLogic.getLoginIdAsLong());
accountRepository.save(account);
}
@Transactional(propagation = Propagation.SUPPORTS)
public PageDTO<AccountOverview> queryAccountOverviewList(AccountQueryParams queryParams) {
return accountQueries.queryAccountOverviewPage(queryParams);
}
@Transactional(propagation = Propagation.SUPPORTS)
public AccountDetails queryAccountDetails(@PathVariable("accountId") Long accountId) {
var accountDetails = accountQueries.queryAccountDetails(accountId);
AssertResult.nonNull(accountDetails);
return accountDetails;
}
}

View File

@@ -0,0 +1,104 @@
package xyz.zhouxy.plusone.system.application.service;
import static xyz.zhouxy.plusone.system.constant.AuthLogic.adminAuthLogic;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import xyz.zhouxy.plusone.exception.InvalidInputException;
import xyz.zhouxy.plusone.system.application.common.util.PrincipalType;
import xyz.zhouxy.plusone.system.application.common.util.PrincipalUtil;
import xyz.zhouxy.plusone.system.application.exception.AccountLoginException;
import xyz.zhouxy.plusone.system.application.query.AccountQueries;
import xyz.zhouxy.plusone.system.application.query.result.LoginInfoViewObject;
import xyz.zhouxy.plusone.system.application.service.command.LoginByOtpCommand;
import xyz.zhouxy.plusone.system.application.service.command.LoginByPasswordCommand;
import xyz.zhouxy.plusone.system.domain.model.account.Account;
import xyz.zhouxy.plusone.system.domain.model.account.AccountRepository;
import xyz.zhouxy.plusone.system.domain.model.account.Email;
import xyz.zhouxy.plusone.system.domain.model.account.MobilePhone;
import xyz.zhouxy.plusone.system.domain.model.account.Principal;
import xyz.zhouxy.plusone.system.domain.model.account.Username;
import xyz.zhouxy.plusone.validator.ValidateDto;
/**
* Admin 账号登录
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
*/
@Service
@Transactional
public class AdminLoginService {
private final AccountRepository accountRepository;
private final AccountQueries accountQueries;
private final MailAndSmsVerifyService mailAndSmsVerifyService;
AdminLoginService(AccountRepository accountRepository, AccountQueries accountQueries,
MailAndSmsVerifyService mailAndSmsVerifyService) {
this.accountRepository = accountRepository;
this.accountQueries = accountQueries;
this.mailAndSmsVerifyService = mailAndSmsVerifyService;
}
@ValidateDto
public LoginInfoViewObject loginByPassword(LoginByPasswordCommand command) {
Principal principal = PrincipalUtil.getPrincipal(command.getPrincipal());
Account account;
if (principal instanceof Email) {
account = accountRepository.findByEmail((Email) principal);
} else if (principal instanceof MobilePhone) {
account = accountRepository.findByMobilePhone((MobilePhone) principal);
} else {
account = accountRepository.findByUsername((Username) principal);
}
if (account == null) {
throw AccountLoginException.accountNotExistException();
}
@SuppressWarnings("null")
boolean isPasswordCorrect = account.checkPassword(command.getPassword());
if (!isPasswordCorrect) {
throw AccountLoginException.passwordErrorException();
}
adminAuthLogic.login(account.getId().orElseThrow(), command.isRememberMe());
var accountDetails = accountQueries.queryAccountDetails(account.getId().orElseThrow());
return LoginInfoViewObject.of(adminAuthLogic.getTokenValue(), accountDetails);
}
@ValidateDto
public LoginInfoViewObject loginByOtp(LoginByOtpCommand command) {
String principal = command.getPrincipal();
PrincipalType principalType = PrincipalUtil.getPrincipalType(principal);
String otp = command.getOtp();
boolean rememberMe = command.isRememberMe();
Account account;
if (principalType == PrincipalType.EMAIL) {
account = accountRepository.findByEmail(Email.of(principal));
} else if (principalType == PrincipalType.MOBILE_PHONE) {
account = accountRepository.findByMobilePhone(MobilePhone.of(principal));
} else {
throw InvalidInputException.unsupportedPrincipalTypeException("输入邮箱地址或手机号");
}
if (account == null) {
throw AccountLoginException.accountNotExistException();
}
mailAndSmsVerifyService.checkOtp(principal, otp);
adminAuthLogic.login(account.getId().orElseThrow(), rememberMe);
var accountDetails = accountQueries.queryAccountDetails(account.getId().orElseThrow());
return LoginInfoViewObject.of(adminAuthLogic.getTokenValue(), accountDetails);
}
public void sendOtp(String principal) {
Principal emailOrMobilePhone = PrincipalUtil.getEmailOrMobilePhone(principal);
if (emailOrMobilePhone instanceof Email) {
mailAndSmsVerifyService.sendOtpToEmail((Email) emailOrMobilePhone);
} else {
mailAndSmsVerifyService.sendOtpToMobilePhone((MobilePhone) emailOrMobilePhone);
}
}
}

View File

@@ -0,0 +1,19 @@
package xyz.zhouxy.plusone.system.application.service;
import static xyz.zhouxy.plusone.system.constant.AuthLogic.adminAuthLogic;
import org.springframework.stereotype.Service;
/**
* Admin 账号登出
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
*/
@Service
public class AdminLogoutService {
public void execute() {
adminAuthLogic.checkLogin();
adminAuthLogic.logout();
}
}

View File

@@ -0,0 +1,60 @@
package xyz.zhouxy.plusone.system.application.service;
import static xyz.zhouxy.plusone.system.constant.AuthLogic.*;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.springframework.stereotype.Service;
import cn.dev33.satoken.session.SaSession;
import cn.dev33.satoken.stp.StpInterface;
import cn.dev33.satoken.stp.StpUtil;
import xyz.zhouxy.plusone.system.domain.model.menu.Action;
import xyz.zhouxy.plusone.system.domain.model.menu.MenuRepository;
import xyz.zhouxy.plusone.system.domain.model.role.ActionRef;
import xyz.zhouxy.plusone.system.domain.model.role.Role;
import xyz.zhouxy.plusone.system.domain.model.role.RoleRepository;
@Service
public class AuthService implements StpInterface {
private RoleRepository roleRepository;
private MenuRepository menuRepository;
public AuthService(RoleRepository roleRepository, MenuRepository menuRepository) {
this.roleRepository = roleRepository;
this.menuRepository = menuRepository;
}
@Override
public List<String> getPermissionList(Object loginId, String loginType) {
Collection<Role> roles = getRoleList(loginId);
Set<Long> permissionIds = new HashSet<>();
roles.forEach(role -> permissionIds.addAll(
role.getPermissions()
.stream()
.map(ActionRef::actionId)
.toList()));
List<String> permValList = menuRepository.findPermissionsByIdIn(permissionIds)
.stream().map(Action::value).toList();
return permValList;
}
@Override
public List<String> getRoleList(Object loginId, String loginType) {
SaSession session = switch (loginType) {
case ADMIN_LOGIN_TYPE -> adminAuthLogic.getSessionByLoginId(loginId);
case USER_LOGIN_TYPE -> userAuthLogic.getSessionByLoginId(loginId);
default -> StpUtil.getSessionByLoginId(loginId);
};
return session.get("RoleList",
() -> getRoleList(loginId).stream().map(Role::getIdentifier).toList());
}
private Collection<Role> getRoleList(Object loginId) {
return roleRepository.findByAccountId(Long.valueOf((String) loginId));
}
}

View File

@@ -0,0 +1,74 @@
package xyz.zhouxy.plusone.system.application.service;
import java.util.List;
import java.util.Objects;
import javax.validation.Valid;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.Assert;
import xyz.zhouxy.plusone.system.application.query.DictQueries;
import xyz.zhouxy.plusone.system.application.query.params.DictQueryParams;
import xyz.zhouxy.plusone.system.application.query.result.DictOverview;
import xyz.zhouxy.plusone.system.application.service.command.CreateDictCommand;
import xyz.zhouxy.plusone.system.application.service.command.UpdateDictCommand;
import xyz.zhouxy.plusone.system.domain.model.dict.Dict;
import xyz.zhouxy.plusone.system.domain.model.dict.DictRepository;
/**
* 数据字典管理
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
*/
@Service
@Transactional
public class DictManagementService {
private final DictRepository dictRepository;
private final DictQueries dictQueries;
public DictManagementService(DictRepository dictRepository, DictQueries dictQueries) {
this.dictRepository = dictRepository;
this.dictQueries = dictQueries;
}
public void createDict(@Valid CreateDictCommand command) {
var dictToSave = Dict.newInstance(command.getDictType(),
command.getDictLabel(),
command.getKeyLabelMap());
dictRepository.save(dictToSave);
}
public void deleteDicts(List<Long> ids) {
Dict dictToDelete;
for (Long id : ids) {
dictToDelete = dictRepository.find(id);
dictRepository.delete(dictToDelete);
}
}
public void updateDict(Long id, @Valid UpdateDictCommand command) {
Assert.isTrue(Objects.equals(id, command.getId()), "id 不匹配");
Dict dictToUpdate = dictRepository.find(command.getId());
dictToUpdate.updateDict(command.getDictType(), command.getDictLabel(), command.getKeyLabelMap());
dictRepository.save(dictToUpdate);
}
@Transactional(propagation = Propagation.SUPPORTS)
public Dict findDictDetails(Long dictId) {
return dictRepository.find(dictId);
}
@Transactional(propagation = Propagation.SUPPORTS)
public List<Dict> loadAllDicts() {
return dictRepository.findAll();
}
@Transactional(propagation = Propagation.SUPPORTS)
public List<DictOverview> queryDictOverviewList(@Valid DictQueryParams queryParams) {
return dictQueries.queryDictOverviewList(queryParams);
}
}

View File

@@ -0,0 +1,113 @@
package xyz.zhouxy.plusone.system.application.service;
import java.util.Objects;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.util.Assert;
import cn.hutool.core.util.RandomUtil;
import xyz.zhouxy.plusone.mail.MailService;
import xyz.zhouxy.plusone.sms.SmsService;
import xyz.zhouxy.plusone.system.application.exception.AccountLoginException;
import xyz.zhouxy.plusone.system.application.exception.AccountRegisterException;
import xyz.zhouxy.plusone.system.domain.model.account.AccountRepository;
import xyz.zhouxy.plusone.system.domain.model.account.Email;
import xyz.zhouxy.plusone.system.domain.model.account.MobilePhone;
/**
* 邮箱和短信的验证服务
*/
@Service
public class MailAndSmsVerifyService {
private static final int CODE_LENGTH = 6;
private final AccountRepository accountRepository;
private final MailService mailService;
private final SmsService smsService;
private final StringRedisTemplate redisTemplate;
public MailAndSmsVerifyService(AccountRepository accountRepository, MailService mailService, SmsService smsService,
StringRedisTemplate redisTemplate) {
this.accountRepository = accountRepository;
this.mailService = mailService;
this.smsService = smsService;
this.redisTemplate = redisTemplate;
}
/**
* 发送一次性密码到邮箱
*
* @param email 要求邮箱必须已注册
*/
public void sendOtpToEmail(Email email) {
Assert.isTrue(accountRepository.existsEmail(email), "该邮箱未绑定任何帐号");
var otp = generateCode();
mailService.sendCodeMail(otp, email.value());
redisTemplate.opsForValue().set("OTP-" + email.value(), otp);
}
/**
* 发送一次性密码到手机号
*
* @param mobilePhone 要求手机号必须已注册
*/
public void sendOtpToMobilePhone(MobilePhone mobilePhone) {
Assert.isTrue(accountRepository.existsMobilePhone(mobilePhone), "该手机号未绑定任何帐号");
var otp = generateCode();
smsService.sendCodeMessage(otp, mobilePhone.value());
redisTemplate.opsForValue().set("OTP-" + mobilePhone.value(), otp);
}
/**
* 发送校验码到邮箱
*
* @param email 要求邮箱必须未注册
*/
public void sendCodeToEmail(Email email) {
Assert.isTrue(!accountRepository.existsEmail(email), "该邮箱未绑定任何帐号");
var code = generateCode();
mailService.sendCodeMail(code, email.value());
redisTemplate.opsForValue().set("Code-" + email.value(), code);
}
/**
* 发送校验码到手机号
*
* @param mobilePhone 要求手机号必须未注册
*/
public void sendCodeToMobilePhone(MobilePhone mobilePhone) {
Assert.isTrue(!accountRepository.existsMobilePhone(mobilePhone), "该手机号未绑定任何帐号");
var code = generateCode();
smsService.sendCodeMessage(code, mobilePhone.value());
redisTemplate.opsForValue().set("Code-" + mobilePhone.value(), code);
}
public void checkOtp(String emailOrMobilePhone, String otp) {
String key = "OTP-" + emailOrMobilePhone;
String otpInRedis = redisTemplate.opsForValue().get(key);
if (otpInRedis == null) {
throw AccountLoginException.otpNotExistsException();
}
if (!Objects.equals(otpInRedis, otp)) {
throw AccountLoginException.otpErrorException();
}
redisTemplate.delete(key);
}
public void checkCode(String emailOrMobilePhone, String code) {
String key = "Code-" + emailOrMobilePhone;
String codeInRedis = redisTemplate.opsForValue().get(key);
if (codeInRedis == null) {
throw AccountRegisterException.codeNotExistsException();
}
if (!Objects.equals(codeInRedis, code)) {
throw AccountRegisterException.codeErrorException();
}
redisTemplate.delete(key);
}
private static String generateCode() {
return RandomUtil.randomString(CODE_LENGTH);
}
}

View File

@@ -0,0 +1,174 @@
package xyz.zhouxy.plusone.system.application.service;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import javax.validation.Valid;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.Assert;
import xyz.zhouxy.plusone.domain.IWithOrderNumber;
import xyz.zhouxy.plusone.system.application.exception.UnsupportedMenuTypeException;
import xyz.zhouxy.plusone.system.application.query.result.MenuViewObject;
import xyz.zhouxy.plusone.system.application.service.command.CreateMenuCommand;
import xyz.zhouxy.plusone.system.application.service.command.UpdateMenuCommand;
import xyz.zhouxy.plusone.system.domain.model.menu.Menu;
import xyz.zhouxy.plusone.system.domain.model.menu.MenuConstructor;
import xyz.zhouxy.plusone.system.domain.model.menu.MenuRepository;
import xyz.zhouxy.plusone.system.domain.service.MenuService;
import xyz.zhouxy.plusone.util.AssertResult;
/**
* 菜单管理
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
*/
@Service
@Transactional
public class MenuManagementService {
private final MenuService menuService;
private final MenuRepository menuRepository;
MenuManagementService(MenuService roleRepository, MenuRepository menuRepository) {
this.menuService = roleRepository;
this.menuRepository = menuRepository;
}
// ==================== create ====================
public void createMenu(@Valid CreateMenuCommand command) {
Menu menuToInsert;
switch (command.getMenuType()) {
case MENU_LIST:
menuToInsert = createMenuList(command);
break;
case MENU_ITEM:
menuToInsert = createMenuItem(command);
break;
default:
throw new UnsupportedMenuTypeException();
}
menuRepository.save(menuToInsert);
}
private Menu createMenuList(CreateMenuCommand command) {
return MenuConstructor.newMenuList(
command.getParentId(),
command.getPath(),
command.getName(),
command.getTitle(),
command.getIcon(),
command.getHidden(),
command.getOrderNumber(),
command.getStatus(),
command.getRemarks());
}
private Menu createMenuItem(CreateMenuCommand command) {
return MenuConstructor.newMenuItem(
command.getParentId(),
command.getPath(),
command.getName(),
command.getTitle(),
command.getIcon(),
command.getHidden(),
command.getOrderNumber(),
command.getStatus(),
command.getComponent(),
command.getResource(),
command.getCache(),
command.getRemarks());
}
// ==================== delete ====================
public void deleteMenu(Long id) {
Menu menuToDelete = menuRepository.find(id);
AssertResult.nonNull(menuToDelete);
menuRepository.delete(menuToDelete);
}
// ==================== update ====================
public void updateMenu(Long id, @Valid UpdateMenuCommand command) {
Assert.isTrue(Objects.equals(id, command.getId()), "id 不匹配");
Menu menuToUpdate = menuRepository.find(command.getId());
menuToUpdate.updateMenuInfo(
command.getMenuType(),
command.getParentId(),
command.getPath(),
command.getName(),
command.getTitle(),
command.getIcon(),
command.getHidden(),
command.getOrderNumber(),
command.getStatus(),
command.getComponent(),
command.getResource(),
command.getCache(),
command.getRemarks());
menuRepository.save(menuToUpdate);
}
// ==================== query ====================
@Transactional(propagation = Propagation.SUPPORTS)
public MenuViewObject findById(Long id) {
var menu = menuRepository.find(id);
return MenuViewObject.of(menu);
}
@Transactional(propagation = Propagation.SUPPORTS)
public List<MenuViewObject> queryByAccountId(Long accountId) {
var menus = menuService.queryAllMenuListByAccountId(accountId);
var menuViewObjects = menus.stream().map(MenuViewObject::of).toList();
return buildMenuTree(menuViewObjects);
}
@Transactional(propagation = Propagation.SUPPORTS)
public List<MenuViewObject> queryByRoleId(Long roleId) {
var menus = menuRepository.queryByRoleId(roleId);
var menuViewObjects = menus.stream().map(MenuViewObject::of).toList();
return buildMenuTree(menuViewObjects);
}
@Transactional(propagation = Propagation.SUPPORTS)
public List<MenuViewObject> buildMenuTree(List<MenuViewObject> menus) {
List<MenuViewObject> rootMenus = menus
.stream()
.filter(menu -> Objects.equals(menu.getParentId(), 0L))
.toList();
Map<Long, MenuViewObject> allMenus = new HashMap<>();
for (var item : menus) {
allMenus.put(item.getId(), item);
}
for (MenuViewObject menu : menus) {
long parentId = menu.getParentId();
while (parentId != 0 && !allMenus.containsKey(parentId)) {
MenuViewObject parent = findById(parentId);
if (parent == null) {
break;
}
allMenus.put(parent.getId(), parent);
parentId = parent.getParentId();
}
}
for (var menu : allMenus.values()) {
var parent = allMenus.getOrDefault(menu.getParentId(), null);
if (parent != null) {
parent.addChild(menu);
}
}
return rootMenus
.stream()
.sorted(Comparator.comparing(IWithOrderNumber::getOrderNumber))
.toList();
}
}

View File

@@ -0,0 +1,98 @@
package xyz.zhouxy.plusone.system.application.service;
import java.util.Set;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import xyz.zhouxy.plusone.exception.InvalidInputException;
import xyz.zhouxy.plusone.system.application.common.util.PrincipalType;
import xyz.zhouxy.plusone.system.application.common.util.PrincipalUtil;
import xyz.zhouxy.plusone.system.application.exception.AccountRegisterException;
import xyz.zhouxy.plusone.system.application.service.command.RegisterAccountCommand;
import xyz.zhouxy.plusone.system.domain.model.account.Account;
import xyz.zhouxy.plusone.system.domain.model.account.AccountInfo;
import xyz.zhouxy.plusone.system.domain.model.account.AccountRepository;
import xyz.zhouxy.plusone.system.domain.model.account.AccountStatus;
import xyz.zhouxy.plusone.system.domain.model.account.Email;
import xyz.zhouxy.plusone.system.domain.model.account.MobilePhone;
import xyz.zhouxy.plusone.system.domain.model.account.Password;
import xyz.zhouxy.plusone.system.domain.model.account.Username;
/**
* 注册账号服务
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
*/
@Service
@Transactional
public class RegisterAccountService {
private static final long DEFAULT_ROLE_ID = 1L;
private final AccountRepository accountRepository;
private final MailAndSmsVerifyService verifyService;
public RegisterAccountService(AccountRepository accountRepository, MailAndSmsVerifyService verifyService) {
this.accountRepository = accountRepository;
this.verifyService = verifyService;
}
public void registerAccount(RegisterAccountCommand command) {
String username = command.getUsername();
var existsUsername = accountRepository.existsUsername(Username.of(username));
if (existsUsername) {
throw AccountRegisterException.usernameAlreadyExists(username);
}
// 1. 确定是使用邮箱地址还是手机号进行注册
String emailOrMobilePhone = command.getEmailOrMobilePhone();
if (emailOrMobilePhone == null) {
throw new IllegalArgumentException("邮箱地址或手机号不能为空");
}
PrincipalType principalType = PrincipalUtil.getPrincipalType(emailOrMobilePhone);
// 2. 确定该邮箱地址或手机号是否存在,比对校验码
Email email = null;
MobilePhone mobilePhone = null;
boolean isExists;
if (principalType == PrincipalType.EMAIL) {
email = Email.of(emailOrMobilePhone);
isExists = accountRepository.existsEmail(email);
if (isExists) {
throw AccountRegisterException.emailAlreadyExists(email.value());
}
} else if (principalType == PrincipalType.MOBILE_PHONE) {
mobilePhone = MobilePhone.of(emailOrMobilePhone);
isExists = accountRepository.existsMobilePhone(mobilePhone);
if (isExists) {
throw AccountRegisterException.emailAlreadyExists(mobilePhone.value());
}
} else {
throw InvalidInputException.unsupportedPrincipalTypeException();
}
verifyService.checkCode(emailOrMobilePhone, command.getCode());
Account accountToSave = Account.register(
Username.of(username),
email,
mobilePhone,
Password.newPassword(command.getPassword(), command.getPasswordConfirmation()),
AccountStatus.AVAILABLE,
Set.of(DEFAULT_ROLE_ID),
AccountInfo.of(command.getNickname(), command.getAvatar(), command.getSex()));
accountRepository.save(accountToSave);
}
public void sendCode(String principal) {
PrincipalType principalType = PrincipalUtil.getPrincipalType(principal);
if (principalType == PrincipalType.EMAIL) {
verifyService.sendCodeToEmail(Email.of(principal));
} else if (principalType == PrincipalType.MOBILE_PHONE) {
verifyService.sendCodeToMobilePhone(MobilePhone.of(principal));
} else {
throw InvalidInputException.unsupportedPrincipalTypeException();
}
}
}

View File

@@ -0,0 +1,95 @@
package xyz.zhouxy.plusone.system.application.service;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import javax.validation.Valid;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import xyz.zhouxy.plusone.system.application.query.RoleQueries;
import xyz.zhouxy.plusone.system.application.query.params.RoleQueryParams;
import xyz.zhouxy.plusone.system.application.query.result.RoleOverview;
import xyz.zhouxy.plusone.system.application.service.command.CreateRoleCommand;
import xyz.zhouxy.plusone.system.application.service.command.UpdateRoleCommand;
import xyz.zhouxy.plusone.system.domain.model.menu.MenuRepository;
import xyz.zhouxy.plusone.system.domain.model.role.ActionRef;
import xyz.zhouxy.plusone.system.domain.model.role.MenuRef;
import xyz.zhouxy.plusone.system.domain.model.role.Role;
import xyz.zhouxy.plusone.system.domain.model.role.RoleRepository;
/**
* 角色管理服务
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
*/
@Service
@Transactional
public class RoleManagementService {
private final RoleRepository _roleRepository;
private final MenuRepository _menuRepository;
private final RoleQueries _roleQueries;
public RoleManagementService(RoleRepository roleRepository, MenuRepository menuRepository,
RoleQueries roleQueries) {
_roleRepository = roleRepository;
_menuRepository = menuRepository;
_roleQueries = roleQueries;
}
public void createRole(@Valid CreateRoleCommand command) {
Set<MenuRef> menuRefs = _menuRepository.findByIdIn(command.getMenus())
.stream()
.map(menu -> new MenuRef(menu.getId().orElseThrow()))
.collect(Collectors.toSet());
Set<ActionRef> permissionRefs = _menuRepository.findPermissionsByIdIn(command.getPermissions())
.stream()
.map(permission -> new ActionRef(permission.getId().orElseThrow()))
.collect(Collectors.toSet());
Role roleToCreate = Role.newInstance(
command.getName(),
command.getIdentifier(),
command.getStatus(),
command.getRemarks(),
menuRefs,
permissionRefs);
_roleRepository.save(roleToCreate);
}
public void updateRole(@Valid UpdateRoleCommand command) {
Long roleId = command.getId();
Role roleToUpdate = _roleRepository.find(roleId);
roleToUpdate.update(
command.getName(),
command.getIdentifier(),
command.getStatus(),
command.getRemarks(),
Set.copyOf(_menuRepository.findByIdIn(command.getMenus())),
Set.copyOf(_menuRepository.findPermissionsByIdIn(command.getPermissions())));
_roleRepository.save(roleToUpdate);
}
public void delete(Long id) {
Role role = _roleRepository.find(id);
_roleRepository.delete(role);
}
@Transactional(propagation = Propagation.SUPPORTS)
public boolean exists(Long id) {
return _roleRepository.exists(id);
}
@Transactional(propagation = Propagation.SUPPORTS)
public Role findById(Long id) {
return _roleRepository.find(id);
}
@Transactional(propagation = Propagation.SUPPORTS)
public List<RoleOverview> query(RoleQueryParams params) {
return _roleQueries.query(params);
}
}

View File

@@ -0,0 +1,56 @@
package xyz.zhouxy.plusone.system.application.service.command;
import java.util.Set;
import javax.validation.constraints.Email;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Pattern;
import org.hibernate.validator.constraints.URL;
import lombok.Data;
import xyz.zhouxy.plusone.constant.RegexConsts;
import xyz.zhouxy.plusone.domain.ICommand;
import xyz.zhouxy.plusone.system.domain.model.account.AccountStatus;
import xyz.zhouxy.plusone.system.domain.model.account.Sex;
/**
* 创建账号命令
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
*/
@Data
public class CreateAccountCommand implements ICommand {
@NotBlank
@Pattern(regexp = RegexConsts.USERNAME, message = "用户名格式错误")
String username;
@Email(message = "邮箱地址格式错误")
String email;
@Pattern(regexp = RegexConsts.MOBILE_PHONE, message = "手机号格式错误")
String mobilePhone;
@NotBlank
@Pattern(regexp = RegexConsts.PASSWORD, message = "密码不符合要求")
String password;
@NotBlank
@Pattern(regexp = RegexConsts.PASSWORD, message = "密码不符合要求")
String passwordConfirmation;
@NotNull
AccountStatus status;
Set<Long> roleRefs;
@Pattern(regexp = RegexConsts.NICKNAME, message = "昵称格式错误")
String nickname;
@NotBlank
@URL
String avatar;
Sex sex;
}

View File

@@ -0,0 +1,27 @@
package xyz.zhouxy.plusone.system.application.service.command;
import java.util.Map;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import lombok.Data;
import xyz.zhouxy.plusone.domain.ICommand;
/**
* 创建数据字典命令
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
*/
@Data
public class CreateDictCommand implements ICommand {
@NotBlank
String dictType;
@NotBlank
String dictLabel;
@NotNull
Map<Integer, String> keyLabelMap;
}

View File

@@ -0,0 +1,55 @@
package xyz.zhouxy.plusone.system.application.service.command;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import lombok.Getter;
import lombok.Setter;
import xyz.zhouxy.plusone.constant.EntityStatus;
import xyz.zhouxy.plusone.domain.ICommand;
import xyz.zhouxy.plusone.system.domain.model.menu.Menu.MenuType;
/**
* 创建菜单命令
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
*/
@Getter
@Setter
public class CreateMenuCommand implements ICommand {
@NotNull
private MenuType menuType;
@NotNull
private Long parentId;
@NotBlank
private String path;
@NotBlank
private String name;
@NotBlank
private String title;
@NotBlank
private String icon;
@NotNull
private Boolean hidden;
@NotNull
private Integer orderNumber;
@NotNull
private EntityStatus status;
private String component;
private String resource;
@NotNull
private Boolean cache;
private String remarks;
}

View File

@@ -0,0 +1,31 @@
package xyz.zhouxy.plusone.system.application.service.command;
import java.util.Set;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import lombok.Data;
import xyz.zhouxy.plusone.constant.EntityStatus;
import xyz.zhouxy.plusone.domain.ICommand;
/**
* 创建角色命令
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
*/
@Data
public class CreateRoleCommand implements ICommand {
@NotBlank
String name;
@NotBlank
String identifier;
@NotNull
EntityStatus status;
String remarks;
Set<Long> menus;
Set<Long> permissions;
}

View File

@@ -0,0 +1,21 @@
package xyz.zhouxy.plusone.system.application.service.command;
import lombok.Data;
import xyz.zhouxy.plusone.domain.ICommand;
/**
* 登录命令
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
*/
@Data
public class LoginByOtpCommand implements ICommand {
String principal; // 邮箱地址 / 手机号
String otp; // 密码
boolean rememberMe; // 记住我
// 进入登陆界面时或刷新验证码时,前端发送图形验证码的请求,后端生成 captcha 并暂存到 redis 中key 为 UUID将图形和 uuid 响应给前端。
// String uuid; // 校验码的 key
// String captcha; // 校验码
}

View File

@@ -0,0 +1,21 @@
package xyz.zhouxy.plusone.system.application.service.command;
import lombok.Data;
import xyz.zhouxy.plusone.domain.ICommand;
/**
* 登录命令
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
*/
@Data
public class LoginByPasswordCommand implements ICommand {
String principal; // 用户名 / 邮箱地址 / 手机号
String password; // 密码
boolean rememberMe; // 记住我
// 进入登陆界面时或刷新验证码时,前端发送图形验证码的请求,后端生成 captcha 并暂存到 redis 中key 为 UUID将图形和 uuid 响应给前端。
// String uuid; // 校验码的 key
// String captcha; // 校验码
}

View File

@@ -0,0 +1,45 @@
package xyz.zhouxy.plusone.system.application.service.command;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Pattern;
import org.hibernate.validator.constraints.URL;
import lombok.Data;
import xyz.zhouxy.plusone.constant.RegexConsts;
import xyz.zhouxy.plusone.domain.ICommand;
import xyz.zhouxy.plusone.system.domain.model.account.Sex;
/**
* 注册账号命令
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
*/
@Data
public class RegisterAccountCommand implements ICommand {
@NotBlank
String emailOrMobilePhone;
@NotBlank
@Pattern(regexp = RegexConsts.CAPTCHA)
String code; // 校验码
@NotBlank
@Pattern(regexp = RegexConsts.USERNAME)
String username;
@NotBlank
@Pattern(regexp = RegexConsts.PASSWORD)
String password;
String passwordConfirmation;
@Pattern(regexp = RegexConsts.NICKNAME)
String nickname;
@NotBlank
@URL
String avatar;
Sex sex;
}

View File

@@ -0,0 +1,31 @@
package xyz.zhouxy.plusone.system.application.service.command;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Pattern;
import org.hibernate.validator.constraints.URL;
import lombok.Data;
import xyz.zhouxy.plusone.constant.RegexConsts;
import xyz.zhouxy.plusone.domain.ICommand;
import xyz.zhouxy.plusone.system.domain.model.account.Sex;
/**
* 更新账号信息命令
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
*/
@Data
public class UpdateAccountCommand implements ICommand {
@NotNull
Long id;
@Pattern(regexp = RegexConsts.NICKNAME)
String nickname;
@URL
String avatar;
Sex sex;
}

View File

@@ -0,0 +1,30 @@
package xyz.zhouxy.plusone.system.application.service.command;
import java.util.Map;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import lombok.Data;
import xyz.zhouxy.plusone.domain.ICommand;
/**
* 更新数据字典命令
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
*/
@Data
public class UpdateDictCommand implements ICommand {
@NotNull
Long id;
@NotBlank
String dictType;
@NotBlank
String dictLabel;
@NotNull
Map<Integer, String> keyLabelMap;
}

View File

@@ -0,0 +1,59 @@
package xyz.zhouxy.plusone.system.application.service.command;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import lombok.Getter;
import lombok.Setter;
import xyz.zhouxy.plusone.constant.EntityStatus;
import xyz.zhouxy.plusone.domain.ICommand;
import xyz.zhouxy.plusone.system.domain.model.menu.Menu.MenuType;
/**
* 更新菜单信息命令
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
*/
@Getter
@Setter
public class UpdateMenuCommand implements ICommand {
@NotNull
private Long id;
@NotNull
private MenuType menuType;
@NotNull
private Long parentId;
@NotBlank
private String path;
@NotBlank
private String name;
@NotBlank
private String title;
@NotBlank
private String icon;
@NotNull
private Boolean hidden;
@NotNull
private Integer orderNumber;
@NotNull
private EntityStatus status;
private String component;
private String resource;
@NotNull
private Boolean cache;
private String remarks;
}

View File

@@ -0,0 +1,40 @@
package xyz.zhouxy.plusone.system.application.service.command;
import java.util.Set;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import lombok.Data;
import xyz.zhouxy.plusone.constant.EntityStatus;
import xyz.zhouxy.plusone.domain.ICommand;
/**
* 更新角色信息命令
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
*/
@Data
public class UpdateRoleCommand implements ICommand {
@NotNull
Long id;
@NotBlank
String name;
@NotBlank
String identifier;
@NotNull
EntityStatus status;
@NotBlank
String remarks;
@NotNull
Set<Long> menus;
@NotNull
Set<Long> permissions;
}

View File

@@ -0,0 +1,29 @@
package xyz.zhouxy.plusone.system.application.service.command.validator;
import java.util.regex.Pattern;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import xyz.zhouxy.plusone.constant.RegexConsts;
import xyz.zhouxy.plusone.system.application.service.command.LoginByOtpCommand;
import xyz.zhouxy.plusone.util.RegexUtil;
import xyz.zhouxy.plusone.validator.BaseValidator;
import xyz.zhouxy.plusone.validator.DtoValidator;
@Component
@DtoValidator(LoginByOtpCommand.class)
public class LoginByOtpCommandValidator extends BaseValidator<LoginByOtpCommand> {
public LoginByOtpCommandValidator() {
ruleFor(loginCommand -> {
String principal = loginCommand.getPrincipal();
return StringUtils.hasText(principal)
&&
RegexUtil.matchesOr(principal, RegexConsts.EMAIL, RegexConsts.MOBILE_PHONE);
}, "输入邮箱地址或手机号");
ruleFor(loginCommand -> {
String otp = loginCommand.getOtp();
return StringUtils.hasText(otp) && Pattern.matches(RegexConsts.CAPTCHA, otp);
}, "验证码不符合要求");
}
}

View File

@@ -0,0 +1,31 @@
package xyz.zhouxy.plusone.system.application.service.command.validator;
import java.util.regex.Pattern;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import xyz.zhouxy.plusone.constant.RegexConsts;
import xyz.zhouxy.plusone.system.application.service.command.LoginByPasswordCommand;
import xyz.zhouxy.plusone.util.RegexUtil;
import xyz.zhouxy.plusone.validator.BaseValidator;
import xyz.zhouxy.plusone.validator.DtoValidator;
@Component
@DtoValidator(LoginByPasswordCommand.class)
public class LoginByPasswordCommandValidator extends BaseValidator<LoginByPasswordCommand> {
public LoginByPasswordCommandValidator() {
ruleFor(loginCommand -> {
String principal = loginCommand.getPrincipal();
return StringUtils.hasText(principal)
&&
RegexUtil.matchesOr(principal, RegexConsts.USERNAME, RegexConsts.EMAIL, RegexConsts.MOBILE_PHONE, principal);
}, "输入用户名、邮箱地址或手机号");
ruleFor(loginCommand -> {
String password = loginCommand.getPassword();
return StringUtils.hasText(password)
&&
Pattern.matches(RegexConsts.PASSWORD, password);
}, "密码格式不正确");
}
}

View File

@@ -0,0 +1,196 @@
<?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="xyz.zhouxy.plusone.system.application.query.AccountQueries">
<!-- //////////////////// SELECT //////////////////// -->
<resultMap id="AccountOverview_ResultMap" type="xyz.zhouxy.plusone.system.application.query.result.AccountOverview">
<id column="a_id" property="id" javaType="Long" />
<result column="a_username" property="username" />
<result column="a_email" property="email" />
<result column="a_mobile_phone" property="mobilePhone" />
<result column="a_status" property="status" />
<result column="a_nickname" property="nickname" />
<result column="a_avatar" property="avatar" />
<result column="a_sex" property="sex" />
<result column="a_created_by" property="createdBy" javaType="Long" />
<result column="a_create_time" property="createTime" javaType="java.time.LocalDateTime" />
<result column="a_updated_by" property="updatedBy" javaType="Long" />
<result column="a_update_time" property="updateTime" javaType="java.time.LocalDateTime" />
<collection property="roles" select="xyz.zhouxy.plusone.system.application.query.AccountQueries.getRoleNameListByAccount" column="a_id" />
</resultMap>
<!-- List<AccountOverview> queryAccountOverview(AccountQueryParams queryParams); -->
<select id="queryAccountOverview" resultMap="AccountOverview_ResultMap">
SELECT
a.id AS a_id,
a.username AS a_username,
a.email AS a_email,
a.mobile_phone AS a_mobile_phone,
a.avatar AS a_avatar,
a.sex AS a_sex,
a.nickname AS a_nickname,
a.status AS a_status,
a.created_by AS a_created_by,
a.create_time AS a_create_time,
a.updated_by AS a_updated_by,
a.update_time AS a_update_time
FROM
sys_account AS a,
(
SELECT sa.id
FROM sys_account sa
<if test="roleId != null">
JOIN sys_account_role AS sar ON sa.id = sar.account_id
JOIN sys_role AS sr ON sr.id = sar.role_id
</if>
WHERE sa.deleted = 0
<if test="id != null">
AND sa.id = #{id}
</if>
<if test="username != null">
AND sa.username = #{username}
</if>
<if test="email != null">
AND sa.email = #{email}
</if>
<if test="mobilePhone != null">
AND sa.mobile_phone = #{mobilePhone}
</if>
<if test="status != null">
AND sa.status = #{status}
</if>
<if test="nickname != null">
AND sa.nickname = #{nickname}
</if>
<if test="sex != null">
AND sa.sex = #{sex}
</if>
<if test="createdBy != null">
AND sa.created_by = #{createdBy}
</if>
<if test="createTimeStart != null">
AND sa.create_time &gt;= #{createTimeStart}
</if>
<if test="createTimeEnd != null">
AND sa.create_time &lt; #{createTimeEnd}
</if>
<if test="updatedBy != null">
AND sa.updated_by = #{updatedBy}
</if>
<if test="updateTimeStart != null">
AND sa.update_time &gt;= #{updateTimeStart}
</if>
<if test="updateTimeEnd != null">
AND sa.update_time &lt; #{updateTimeEnd}
</if>
<if test="roleId != null">
AND sar.role_id = #{roleId}
AND sr.deleted = 0
</if>
<if test="orderBy != null">
ORDER BY sa.${orderBy}, sa.id
</if>
<if test="orderBy == null">
ORDER BY sa.id
</if>
LIMIT #{size} OFFSET #{offset}
) b
WHERE a.id = b.id
</select>
<!--
long count(SysAccountQuery queryParams);
-->
<select id="count" resultType="long">
SELECT COUNT(*)
FROM sys_account sa
<if test="roleId != null">
JOIN sys_account_role AS sar ON sa.id = sar.account_id
JOIN sys_role AS sr ON sr.id = sar.role_id
</if>
WHERE sa.deleted = 0
<if test="id != null">
AND sa.id = #{id}
</if>
<if test="username != null">
AND sa.username = #{username}
</if>
<if test="email != null">
AND sa.email = #{email}
</if>
<if test="mobilePhone != null">
AND sa.mobile_phone = #{mobilePhone}
</if>
<if test="status != null">
AND sa.status = #{status}
</if>
<if test="nickname != null">
AND sa.nickname = #{nickname}
</if>
<if test="sex != null">
AND sa.sex = #{sex}
</if>
<if test="createdBy != null">
AND sa.created_by = #{createdBy}
</if>
<if test="createTimeStart != null">
AND sa.create_time &gt;= #{createTimeStart}
</if>
<if test="createTimeEnd != null">
AND sa.create_time &lt; #{createTimeEnd}
</if>
<if test="updatedBy != null">
AND sa.updated_by = #{updatedBy}
</if>
<if test="updateTimeStart != null">
AND sa.update_time &gt;= #{updateTimeStart}
</if>
<if test="updateTimeEnd != null">
AND sa.update_time &lt; #{updateTimeEnd}
</if>
<if test="roleId != null">
AND sar.role_id = #{roleId}
AND sr.deleted = 0
</if>
</select>
<select id="getRoleNameListByAccount" resultType="java.lang.String">
SELECT r.name
FROM sys_role AS r
RIGHT JOIN sys_account_role AS ar ON r.id = ar.role_id
WHERE ar.account_id = #{id} AND r.deleted = 0
</select>
<!-- AccountDetails queryAccountDetails(Long accountId); -->
<resultMap id="RoleOverview_ResultMap" type="xyz.zhouxy.plusone.system.application.query.result.RoleOverview">
<id column="id" property="id" javaType="Long" />
<result column="name" property="name" />
<result column="identifier" property="identifier" />
<result column="status" property="status" />
</resultMap>
<select id="getRoleOverviewListByAccount" resultMap="RoleOverview_ResultMap">
SELECT r.id, r.name, r.identifier, r.status
FROM sys_role AS r
RIGHT JOIN sys_account_role AS ar ON r.id = ar.role_id
WHERE ar.account_id = #{id} AND r.deleted = 0
</select>
<resultMap id="AccountDetails_ResultMap" type="xyz.zhouxy.plusone.system.application.query.result.AccountDetails">
<id column="id" property="id" javaType="Long" />
<result column="username" property="username" />
<result column="email" property="email" />
<result column="mobile_phone" property="mobilePhone" />
<result column="status" property="status" />
<result column="nickname" property="nickname" />
<result column="avatar" property="avatar" />
<result column="sex" property="sex" />
<collection property="roles" select="xyz.zhouxy.plusone.system.application.query.AccountQueries.getRoleOverviewListByAccount" column="id" />
</resultMap>
<select id="queryAccountDetails" resultMap="AccountDetails_ResultMap">
SELECT id, username, email, mobile_phone, avatar, sex, nickname, status
FROM sys_account
WHERE id = #{id} AND deleted = 0
</select>
</mapper>