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>

View File

@@ -0,0 +1,23 @@
<?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-common</artifactId>
<dependencies>
<dependency>
<groupId>xyz.zhouxy</groupId>
<artifactId>plusone-basic-common</artifactId>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-crypto</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,20 @@
package xyz.zhouxy.plusone.system.constant;
import cn.dev33.satoken.stp.StpLogic;
/**
* 维护 StpLogic 的实例
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
*/
public class AuthLogic {
private AuthLogic() {
throw new IllegalStateException("Utility class");
}
public static final String ADMIN_LOGIN_TYPE = "Admin";
public static final StpLogic adminAuthLogic = new StpLogic(ADMIN_LOGIN_TYPE);
public static final String USER_LOGIN_TYPE = "User";
public static final StpLogic userAuthLogic = new StpLogic(USER_LOGIN_TYPE);
}

View File

@@ -0,0 +1,51 @@
package xyz.zhouxy.plusone.system.util;
import javax.annotation.Nonnull;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.crypto.digest.DigestUtil;
import xyz.zhouxy.plusone.exception.PlusoneException;
/**
* 密码工具类
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
*/
public final class PasswordUtil {
private static final String SALT_BASE_STRING = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ~`!@#$%^&*()_-+={}[]|\\:;\"',.<>?/";
/**
* 将密码和随机盐混合,并进行哈希加密。
*
* @param password 密文密码
* @param salt 随机盐
* @return 哈希加密的结果
*/
@Nonnull
public static String hashPassword(@Nonnull String password, @Nonnull String salt) {
int length = salt.length();
int i = length > 0 ? length / 2 : 0;
var passwordWithSalt = salt.substring(0, i)
+ password
+ salt.substring(1);
String sha512Hex = DigestUtil.sha512Hex(passwordWithSalt);
if (sha512Hex == null) {
throw new PlusoneException(9999999, "未知错误:哈希加密失败!");
}
return sha512Hex;
}
/**
* 生成 24 位的字符串
*
* @return 生成的随机盐
*/
public static String generateRandomSalt() {
return RandomUtil.randomString(SALT_BASE_STRING, 24);
}
private PasswordUtil() {
// 不允许实例化
throw new IllegalStateException("Utility class");
}
}

View File

@@ -0,0 +1,23 @@
<?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>
<groupId>xyz.zhouxy</groupId>
<artifactId>plusone-system</artifactId>
<version>1.0.0-SNAPSHOT</version>
</parent>
<artifactId>plusone-system-domain</artifactId>
<dependencies>
<dependency>
<groupId>xyz.zhouxy</groupId>
<artifactId>plusone-basic-domain</artifactId>
</dependency>
<dependency>
<groupId>xyz.zhouxy</groupId>
<artifactId>plusone-system-common</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,33 @@
package xyz.zhouxy.plusone.system.domain.event;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;
import xyz.zhouxy.plusone.domain.DomainEvent;
import xyz.zhouxy.plusone.system.domain.model.account.Account;
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;
/**
* 领域事件:创建账号事件
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
*/
@Getter
@Setter
@NoArgsConstructor
@ToString(callSuper = true)
public class AccountCreated extends DomainEvent {
private Username username;
private Email email;
private MobilePhone mobilePhone;
public AccountCreated(Account account) {
this.username = account.getUsername();
this.email = account.getEmail();
this.mobilePhone = account.getMobilePhone();
}
}

View File

@@ -0,0 +1,33 @@
package xyz.zhouxy.plusone.system.domain.event;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;
import xyz.zhouxy.plusone.domain.DomainEvent;
import xyz.zhouxy.plusone.system.domain.model.account.Account;
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;
/**
* 领域事件:账号被锁定事件
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
*/
@Getter
@Setter
@NoArgsConstructor
@ToString(callSuper = true)
public class AccountLocked extends DomainEvent {
private Username username;
private Email email;
private MobilePhone mobilePhone;
public AccountLocked(Account account) {
this.username = account.getUsername();
this.email = account.getEmail();
this.mobilePhone = account.getMobilePhone();
}
}

View File

@@ -0,0 +1,34 @@
package xyz.zhouxy.plusone.system.domain.event;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;
import xyz.zhouxy.plusone.domain.DomainEvent;
import xyz.zhouxy.plusone.system.domain.model.account.Account;
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;
/**
* 领域事件:账号密码被更改事件
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
*/
@Getter
@Setter
@NoArgsConstructor
@ToString(callSuper = true)
public class AccountPasswordChanged extends DomainEvent {
private Username username;
private Email email;
private MobilePhone mobilePhone;
public AccountPasswordChanged(Account account) {
this.username = account.getUsername();
this.email = account.getEmail();
this.mobilePhone = account.getMobilePhone();
}
}

View File

@@ -0,0 +1,27 @@
package xyz.zhouxy.plusone.system.domain.event;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;
import xyz.zhouxy.plusone.domain.DomainEvent;
import xyz.zhouxy.plusone.system.domain.model.account.Account;
import xyz.zhouxy.plusone.system.domain.model.account.Username;
/**
* 领域事件:账号恢复事件
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
*/
@Getter
@Setter
@NoArgsConstructor
@ToString(callSuper = true)
public class AccountRecovered extends DomainEvent {
private Username username;
public AccountRecovered(Account account) {
this.username = account.getUsername();
}
}

View File

@@ -0,0 +1,27 @@
package xyz.zhouxy.plusone.system.domain.event;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;
import xyz.zhouxy.plusone.domain.DomainEvent;
import xyz.zhouxy.plusone.system.domain.model.account.Account;
import xyz.zhouxy.plusone.system.domain.model.account.Username;
/**
* 领域事件:账号绑定角色事件
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
*/
@Getter
@Setter
@NoArgsConstructor
@ToString(callSuper = true)
public class AccountRolesBound extends DomainEvent {
private Username username;
public AccountRolesBound(Account account) {
this.username = account.getUsername();
}
}

View File

@@ -0,0 +1,30 @@
package xyz.zhouxy.plusone.system.domain.event;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;
import xyz.zhouxy.plusone.domain.DomainEvent;
import xyz.zhouxy.plusone.system.domain.model.account.Account;
import xyz.zhouxy.plusone.system.domain.model.account.Email;
import xyz.zhouxy.plusone.system.domain.model.account.Username;
/**
* 领域事件:账号邮箱更改事件
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
*/
@Getter
@Setter
@NoArgsConstructor
@ToString(callSuper = true)
public class EmailChanged extends DomainEvent {
private Username username;
private Email email;
public EmailChanged(Account account) {
this.username = account.getUsername();
this.email = account.getEmail();
}
}

View File

@@ -0,0 +1,30 @@
package xyz.zhouxy.plusone.system.domain.event;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;
import xyz.zhouxy.plusone.domain.DomainEvent;
import xyz.zhouxy.plusone.system.domain.model.account.Account;
import xyz.zhouxy.plusone.system.domain.model.account.MobilePhone;
import xyz.zhouxy.plusone.system.domain.model.account.Username;
/**
* 领域事件:账号手机号更改事件
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
*/
@Getter
@Setter
@NoArgsConstructor
@ToString(callSuper = true)
public class MobilePhoneChanged extends DomainEvent {
private Username username;
private @Getter @Setter MobilePhone mobilePhone;
public MobilePhoneChanged(Account account) {
this.username = account.getUsername();
this.mobilePhone = account.getMobilePhone();
}
}

View File

@@ -0,0 +1,33 @@
package xyz.zhouxy.plusone.system.domain.event;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;
import xyz.zhouxy.plusone.domain.DomainEvent;
import xyz.zhouxy.plusone.system.domain.model.account.Account;
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;
/**
* 领域事件:账号用户名修改事件
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
*/
@Getter
@Setter
@NoArgsConstructor
@ToString(callSuper = true)
public class UsernameChanged extends DomainEvent {
private Username username;
private Email email;
private MobilePhone mobilePhone;
public UsernameChanged(Account account) {
this.username = account.getUsername();
this.email = account.getEmail();
this.mobilePhone = account.getMobilePhone();
}
}

View File

@@ -0,0 +1,235 @@
package xyz.zhouxy.plusone.system.domain.model.account;
import java.net.URL;
import java.util.HashSet;
import java.util.Optional;
import java.util.Set;
import javax.annotation.Nonnull;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import xyz.zhouxy.plusone.domain.AggregateRoot;
import xyz.zhouxy.plusone.domain.IWithVersion;
import xyz.zhouxy.plusone.exception.UserOperationException;
import xyz.zhouxy.plusone.system.domain.event.AccountCreated;
import xyz.zhouxy.plusone.system.domain.event.AccountLocked;
import xyz.zhouxy.plusone.system.domain.event.AccountPasswordChanged;
import xyz.zhouxy.plusone.system.domain.event.AccountRecovered;
import xyz.zhouxy.plusone.system.domain.event.AccountRolesBound;
import xyz.zhouxy.plusone.system.domain.event.EmailChanged;
import xyz.zhouxy.plusone.system.domain.event.MobilePhoneChanged;
import xyz.zhouxy.plusone.system.domain.event.UsernameChanged;
/**
* 聚合根:账号
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
*/
@ToString
public class Account extends AggregateRoot<Long> implements IWithVersion {
// ===================== 字段 ====================
private Long id;
private @Getter Username username;
private @Getter Email email;
private @Getter MobilePhone mobilePhone;
private Password password;
private @Getter AccountStatus status;
private @Getter AccountInfo accountInfo;
private Set<Long> roleRefs = new HashSet<>();
private @Getter Long createdBy;
private @Getter @Setter Long updatedBy;
private @Getter long version;
public void setUsername(Username username) {
this.username = username;
addDomainEvent(new UsernameChanged(this));
}
public void setEmail(Email email) {
this.email = email;
addDomainEvent(new EmailChanged(this));
}
public void setMobilePhone(MobilePhone mobilePhone) {
this.mobilePhone = mobilePhone;
addDomainEvent(new MobilePhoneChanged(this));
}
public void changePassword(String newPassword, String passwordConfirmation) {
this.password = Password.newPassword(newPassword, passwordConfirmation);
addDomainEvent(new AccountPasswordChanged(this));
}
/**
* 锁定账号。如当前账号已锁定,则抛出 UserOperationException 异常
*
* @see UserOperationException
*/
public void lockAccount() {
if (this.status == AccountStatus.LOCKED) {
throw UserOperationException
.invalidOperation(String.format("账号 %d 的状态为:%s无法锁定", this.id, this.status.getName()));
}
this.status = AccountStatus.LOCKED;
addDomainEvent(new AccountLocked(this));
}
/**
* 恢复账号状态。如当前用户可用,则抛出 UserOperationException 异常
*
* @see UserOperationException
*/
public void recoveryAccount() {
if (this.status == AccountStatus.AVAILABLE) {
throw UserOperationException
.invalidOperation(String.format("账号 %d 的状态为:%s无法恢复", this.id, this.status.getName()));
}
this.status = AccountStatus.AVAILABLE;
addDomainEvent(new AccountRecovered(this));
}
public void setAccountInfo(AccountInfo accountInfo) {
this.accountInfo = accountInfo;
}
public void setAccountInfo(Nickname nickname, URL avatar, Sex sex) {
this.accountInfo = AccountInfo.of(nickname, avatar, sex);
}
public void setAccountInfo(String nickname, String avatar, Sex sex) {
this.accountInfo = AccountInfo.of(nickname, avatar, sex);
}
/**
* 绑定角色
*
* @param roleRefs 角色 id 集合
*/
public void bindRoles(Set<Long> roleRefs) {
this.roleRefs.clear();
this.roleRefs.addAll(roleRefs);
addDomainEvent(new AccountRolesBound(this));
}
public boolean checkPassword(@Nonnull String password) {
return this.password.check(password);
}
// ===================== 实例化 ====================
Account(Long id,
Username username,
Email email,
MobilePhone mobilePhone,
Password password,
AccountStatus status,
AccountInfo accountInfo,
Set<Long> roleRefs,
Long createdBy,
Long updatedBy,
long version) {
this.id = id;
this.username = username;
this.email = email;
this.mobilePhone = mobilePhone;
this.password = password;
this.status = status;
this.accountInfo = accountInfo;
this.bindRoles(roleRefs);
this.createdBy = createdBy;
this.updatedBy = updatedBy;
this.version = version;
}
public static Account newInstance(
Username username,
Email email,
MobilePhone mobilePhone,
Password password,
AccountStatus status,
Set<Long> roleRefs,
AccountInfo accountInfo,
Long createdBy) {
var newInstance = new Account(null, username, email, mobilePhone, password, status, accountInfo, roleRefs,
createdBy, null, 0);
newInstance.addDomainEvent(new AccountCreated(newInstance));
return newInstance;
}
public static Account register(
Username username,
Email email,
MobilePhone mobilePhone,
Password password,
AccountStatus status,
Set<Long> roleRefs,
AccountInfo accountInfo) {
var newInstance = new Account(null, username, email, mobilePhone, password, status, accountInfo, roleRefs,
0L, null, 0);
newInstance.addDomainEvent(new AccountCreated(newInstance));
return newInstance;
}
Account(Long id,
String username,
String email,
String mobilePhone,
Password password,
AccountStatus status,
AccountInfo accountInfo,
Set<Long> roleRefs,
Long createdBy,
Long updatedBy,
long version) {
this(id, Username.of(username), Email.ofNullable(email), MobilePhone.ofNullable(mobilePhone),
password, status, accountInfo, roleRefs, createdBy, updatedBy, version);
}
public static Account newInstance(
String username,
String email,
String mobilePhone,
String password,
String passwordConfirmation,
AccountStatus status,
Set<Long> roleRefs,
AccountInfo accountInfo,
long createdBy) {
var newInstance = new Account(null, username, email, mobilePhone,
Password.newPassword(password, passwordConfirmation), status, accountInfo, roleRefs,
createdBy, null, 0);
newInstance.addDomainEvent(new AccountCreated(newInstance));
return newInstance;
}
public static Account register(
String username,
String email,
String mobilePhone,
Password password,
AccountStatus status,
Set<Long> roleRefs,
AccountInfo accountInfo) {
var newInstance = new Account(null, username, email, mobilePhone, password, status, accountInfo, roleRefs,
0L, null, 0);
newInstance.addDomainEvent(new AccountCreated(newInstance));
return newInstance;
}
@Override
public Optional<Long> getId() {
return Optional.ofNullable(id);
}
public Set<Long> getRoleIds() {
return Set.copyOf(this.roleRefs);
}
Password getPassword() {
return password;
}
}

View File

@@ -0,0 +1,43 @@
package xyz.zhouxy.plusone.system.domain.model.account;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Objects;
import lombok.Getter;
import lombok.ToString;
import xyz.zhouxy.plusone.domain.IValueObject;
/**
* 账号详细信息
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
*/
@Getter
@ToString
public class AccountInfo implements IValueObject {
private final Nickname nickname;
private final URL avatar;
private final Sex sex;
private AccountInfo(Nickname nickname, URL avatar, Sex sex) {
this.nickname = nickname;
this.avatar = avatar;
this.sex = Objects.nonNull(sex) ? sex : Sex.UNSET;
}
public static AccountInfo of(Nickname nickname, URL avatar, Sex sex) {
return new AccountInfo(nickname, avatar, sex);
}
public static AccountInfo of(String nickname, String avatar, Sex sex) {
URL avatarURL;
try {
avatarURL = Objects.nonNull(avatar) ? new URL(avatar) : null;
} catch (MalformedURLException e) {
throw new IllegalArgumentException(e);
}
return new AccountInfo(Nickname.ofNullable(nickname), avatarURL, Objects.nonNull(sex) ? sex : Sex.UNSET);
}
}

View File

@@ -0,0 +1,29 @@
package xyz.zhouxy.plusone.system.domain.model.account;
import java.util.Collection;
import xyz.zhouxy.plusone.domain.IRepository;
/**
* AccountRepository
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
* @see Account
*/
public interface AccountRepository extends IRepository<Account, Long> {
Collection<Account> findByRoleId(Long roleId);
Account findByEmail(Email email);
Account findByMobilePhone(MobilePhone mobilePhone);
Account findByUsername(Username username);
boolean existsUsername(Username username);
boolean existsEmail(Email email);
boolean existsMobilePhone(MobilePhone email);
}

View File

@@ -0,0 +1,29 @@
package xyz.zhouxy.plusone.system.domain.model.account;
import lombok.Getter;
import xyz.zhouxy.plusone.domain.IValueObject;
import xyz.zhouxy.plusone.util.Enumeration;
import xyz.zhouxy.plusone.util.EnumerationValuesHolder;
/**
* 账号状态
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
*/
@Getter
public class AccountStatus extends Enumeration<AccountStatus> implements IValueObject {
private AccountStatus(int value, String name) {
super(value, name);
}
public static final AccountStatus AVAILABLE = new AccountStatus(0, "账号正常");
public static final AccountStatus LOCKED = new AccountStatus(1, "账号被锁定");
private static final EnumerationValuesHolder<AccountStatus> ENUMERATION_VALUES = new EnumerationValuesHolder<>(
new AccountStatus[] { AVAILABLE, LOCKED });
public static AccountStatus of(int value) {
return ENUMERATION_VALUES.get(value);
}
}

View File

@@ -0,0 +1,47 @@
package xyz.zhouxy.plusone.system.domain.model.account;
import java.util.Objects;
import cn.hutool.core.util.DesensitizedUtil;
import xyz.zhouxy.plusone.constant.RegexConsts;
/**
* 值对象:电子邮箱地址
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
*/
public class Email extends Principal {
public static final String REGEX = RegexConsts.EMAIL;
private Email(String email) {
super(REGEX);
if (email == null) {
throw new IllegalArgumentException("邮箱地址不能为空");
}
this.value = email;
if (!isValid()) {
throw new IllegalArgumentException("邮箱地址格式错误");
}
}
public static Email of(String email) {
return new Email(email);
}
public static Email ofNullable(String email) {
return Objects.nonNull(email) ? new Email(email) : null;
}
/**
* 脱敏后的数据
*/
public String safeValue() {
return DesensitizedUtil.email(this.value);
}
@Override
public String toString() {
return this.safeValue();
}
}

View File

@@ -0,0 +1,44 @@
package xyz.zhouxy.plusone.system.domain.model.account;
import java.util.Objects;
import cn.hutool.core.util.DesensitizedUtil;
import xyz.zhouxy.plusone.constant.RegexConsts;
/**
* 值对象:手机号码
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
*/
public class MobilePhone extends Principal {
public static final String REGEX = RegexConsts.MOBILE_PHONE;
private MobilePhone(String mobilePhone) {
super(REGEX);
if (mobilePhone == null) {
throw new IllegalArgumentException("手机号不能为空");
}
this.value = mobilePhone;
if (!isValid()) {
throw new IllegalArgumentException("手机号格式错误");
}
}
public static MobilePhone of(String mobilePhone) {
return new MobilePhone(mobilePhone);
}
public static MobilePhone ofNullable(String mobilePhone) {
return Objects.nonNull(mobilePhone) ? new MobilePhone(mobilePhone) : null;
}
public String safeValue() {
return DesensitizedUtil.mobilePhone(this.value);
}
@Override
public String toString() {
return this.safeValue();
}
}

View File

@@ -0,0 +1,35 @@
package xyz.zhouxy.plusone.system.domain.model.account;
import java.util.Objects;
import xyz.zhouxy.plusone.constant.RegexConsts;
import xyz.zhouxy.plusone.domain.ValidatableStringRecord;
/**
* 值对象:昵称
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
*/
public class Nickname extends ValidatableStringRecord {
public static final String REGEX = RegexConsts.NICKNAME;
private Nickname(String value) {
super(REGEX);
if (value == null) {
throw new IllegalArgumentException("昵称不能为空");
}
this.value = value;
if (!isValid()) {
throw new IllegalArgumentException("昵称格式错误");
}
}
public static Nickname of(String nickname) {
return new Nickname(nickname);
}
public static Nickname ofNullable(String nickname) {
return Objects.nonNull(nickname) ? new Nickname(nickname) : null;
}
}

View File

@@ -0,0 +1,90 @@
package xyz.zhouxy.plusone.system.domain.model.account;
import java.util.Objects;
import java.util.regex.Pattern;
import javax.annotation.Nonnull;
import org.springframework.util.Assert;
import xyz.zhouxy.plusone.constant.RegexConsts;
import xyz.zhouxy.plusone.domain.IValueObject;
import xyz.zhouxy.plusone.exception.PlusoneException;
import xyz.zhouxy.plusone.system.util.PasswordUtil;
/**
* 值对象:加盐加密的密码
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
*/
public class Password implements IValueObject {
private static final Pattern PATTERN = Pattern.compile(RegexConsts.PASSWORD);
private static final String DEFAULT_PASSWORD = "A1b2C3d4";
@Nonnull
private final String passwordVal;
@Nonnull
private final String saltVal;
private Password(String password) {
if (password == null) {
throw new IllegalArgumentException("密码不能为空");
}
if (!PATTERN.matcher(password).matches()) {
throw new IllegalArgumentException("密码格式不符合要求");
}
var salt = PasswordUtil.generateRandomSalt();
if (salt == null) {
throw new PlusoneException(9999999, "未知错误:生成随机盐失败");
}
this.saltVal = salt;
this.passwordVal = PasswordUtil.hashPassword(password, salt);
}
private Password(String password, String salt) {
if (password == null || salt == null) {
throw new IllegalArgumentException("password 和 salt 不能为空");
}
this.passwordVal = password;
this.saltVal = salt;
}
public static Password of(String password, String salt) {
return new Password(password, salt);
}
public static Password newPassword(String newPassword, String passwordConfirmation) {
Assert.isTrue(Objects.equals(newPassword, passwordConfirmation), "两次输入的密码不一致");
return newPassword(newPassword);
}
public static Password newPassword(String newPassword) {
return new Password(newPassword);
}
public boolean check(String password) {
if (password == null) {
throw new IllegalArgumentException("password 不能为空");
}
Assert.hasText(password, "密码不能为空");
return Objects.equals(this.passwordVal, PasswordUtil.hashPassword(password, this.saltVal));
}
public String value() {
return passwordVal;
}
public String getSalt() {
return saltVal;
}
public static Password newDefaultPassword() {
return newPassword(DEFAULT_PASSWORD);
}
@Override
public String toString() {
return "********";
}
}

View File

@@ -0,0 +1,14 @@
package xyz.zhouxy.plusone.system.domain.model.account;
import xyz.zhouxy.plusone.domain.ValidatableStringRecord;
/**
* 账号标识符
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
*/
public abstract class Principal extends ValidatableStringRecord {
protected Principal(String format) {
super(format);
}
}

View File

@@ -0,0 +1,30 @@
package xyz.zhouxy.plusone.system.domain.model.account;
import xyz.zhouxy.plusone.domain.IValueObject;
import xyz.zhouxy.plusone.util.Enumeration;
import xyz.zhouxy.plusone.util.EnumerationValuesHolder;
/**
* 值对象:性别
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
*/
public class Sex extends Enumeration<Sex> implements IValueObject {
public static final Sex UNSET = new Sex(0, "未设置");
public static final Sex MALE = new Sex(1, "男性");
public static final Sex FEMALE = new Sex(2, "女性");
private Sex(int value, String name) {
super(value, name);
}
private static EnumerationValuesHolder<Sex> values = new EnumerationValuesHolder<>(new Sex[] {
UNSET,
MALE,
FEMALE
});
public static Sex of(int value) {
return values.get(value);
}
}

View File

@@ -0,0 +1,28 @@
package xyz.zhouxy.plusone.system.domain.model.account;
import xyz.zhouxy.plusone.constant.RegexConsts;
/**
* 值对象:用户名
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
*/
public class Username extends Principal {
public static final String REGEX = RegexConsts.USERNAME;
private Username(String username) {
super(REGEX);
if (username == null) {
throw new IllegalArgumentException("用户名不能为空");
}
this.value = username;
if (!isValid()) {
throw new IllegalArgumentException("用户名格式错误");
}
}
public static Username of(String username) {
return new Username(username);
}
}

View File

@@ -0,0 +1,129 @@
package xyz.zhouxy.plusone.system.domain.model.dict;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import lombok.ToString;
import xyz.zhouxy.plusone.domain.AggregateRoot;
import xyz.zhouxy.plusone.domain.IWithLabel;
import xyz.zhouxy.plusone.domain.IWithVersion;
/**
* 聚合根:数据字典
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
*/
@ToString
public class Dict extends AggregateRoot<Long> implements IWithLabel, IWithVersion {
private Long id;
private String dictType;
private String dictLabel;
private Map<Integer, DictValue> values = new HashMap<>();
private long version;
// ==================== 领域逻辑 ====================
public void addValue(int key, String label) {
if (this.values.containsKey(key)) {
throw new IllegalArgumentException(String.format("字典 %s 已存在值:%d", dictType, key));
}
this.values.put(key, DictValue.of(key, label));
}
public void removeValue(int key) {
this.values.remove(key);
}
public void updateDict(String dictType, String dictLabel, Map<Integer, String> keyLabelMap) {
this.dictType = dictType;
this.dictLabel = dictLabel;
var valueKeys = this.values.keySet();
for (Integer key : valueKeys) {
if (!keyLabelMap.containsKey(key)) {
this.values.remove(key);
}
}
keyLabelMap.forEach((Integer key, String label) -> {
var temp = this.values.get(key);
if (temp != null) {
temp.label = label;
} else {
this.values.put(key, DictValue.of(key, label));
}
});
}
// ==================== 实例化 ====================
Dict(Long id, String dictType, String dictLabel, Set<DictValue> values, long version) {
this.id = id;
this.dictType = dictType;
this.dictLabel = dictLabel;
values.forEach(dictValue -> this.values.put(dictValue.key, dictValue));
this.version = version;
}
public static Dict newInstance(
String dictType,
String dictLabel) {
return new Dict(null, dictType, dictLabel, Collections.emptySet(), 0);
}
public static Dict newInstance(
String dictType,
String dictLabel,
Set<DictValue> values) {
return new Dict(null, dictType, dictLabel, values, 0);
}
public static Dict newInstance(
String dictType,
String dictLabel,
Map<Integer, String> keyLabelMap) {
var values = buildDictValues(keyLabelMap);
return new Dict(null, dictType, dictLabel, values, 0);
}
private static Set<DictValue> buildDictValues(Map<Integer, String> keyLabelMap) {
Set<DictValue> dictValues = new HashSet<>(keyLabelMap.size());
keyLabelMap.forEach((Integer key, String label) -> dictValues.add(DictValue.of(key, label)));
return dictValues;
}
// ==================== getters ====================
@Override
public Optional<Long> getId() {
return Optional.ofNullable(id);
}
public String getDictType() {
return dictType;
}
@Override
public String getLabel() {
return this.dictLabel;
}
public Set<DictValue> getValues() {
return this.values.values().stream().collect(Collectors.toSet());
}
@Override
public long getVersion() {
return this.version;
}
public int count() {
return this.values.size();
}
}

View File

@@ -0,0 +1,10 @@
package xyz.zhouxy.plusone.system.domain.model.dict;
import java.util.List;
import xyz.zhouxy.plusone.domain.IRepository;
public interface DictRepository extends IRepository<Dict, Long> {
List<Dict> findAll();
}

View File

@@ -0,0 +1,51 @@
package xyz.zhouxy.plusone.system.domain.model.dict;
import java.util.Objects;
import lombok.Getter;
import lombok.ToString;
import xyz.zhouxy.plusone.domain.IValueObject;
/**
* 字典值
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
*/
@Getter
@ToString
public class DictValue implements IValueObject {
final Integer key;
String label;
// ==================== 实例化 ====================
private DictValue(int key, String label) {
this.key = key;
this.label = label;
}
public static DictValue of(int key, String label) {
return new DictValue(key, label);
}
@Override
public int hashCode() {
return Objects.hash(key);
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
DictValue other = (DictValue) obj;
return Objects.equals(key, other.key);
}
}

View File

@@ -0,0 +1,54 @@
package xyz.zhouxy.plusone.system.domain.model.menu;
import java.util.Optional;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Getter;
import xyz.zhouxy.plusone.domain.Entity;
import xyz.zhouxy.plusone.domain.IWithLabel;
/**
* 行为。
* <p>
* 一个 Menu 代表对应一个资源Action 表示对该资源的行为,每个行为具有对应权限。
* </p>
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
*
* @see Menu
*/
public class Action extends Entity<Long> implements IWithLabel {
Long id;
String resource;
@Getter
String identifier;
@Getter
String label;
private Action(Long id, String resource, String identifier, String label) {
this.id = id;
this.resource = resource;
this.identifier = identifier;
this.label = label;
}
static Action of(Long id, String resource, String identifier, String label) {
return new Action(id, resource, identifier, label);
}
static Action newInstance(String resource, String identifier, String label) {
return new Action(null, resource, identifier, label);
}
@JsonProperty("value")
public String value() {
return resource + identifier;
}
@Override
public Optional<Long> getId() {
return Optional.ofNullable(id);
}
}

View File

@@ -0,0 +1,140 @@
package xyz.zhouxy.plusone.system.domain.model.menu;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import com.fasterxml.jackson.annotation.JsonValue;
import lombok.Getter;
import lombok.ToString;
import xyz.zhouxy.plusone.constant.EntityStatus;
import xyz.zhouxy.plusone.domain.AggregateRoot;
import xyz.zhouxy.plusone.domain.IWithOrderNumber;
import xyz.zhouxy.plusone.domain.IWithVersion;
/**
* 聚合根:菜单
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
*
* @see Action
* @see MenuConstructor
*/
@Getter
@ToString
public class Menu extends AggregateRoot<Long> implements IWithOrderNumber, IWithVersion {
MenuType type;
Long id;
Long parentId;
String name;
// 若 type 为 MENU_ITEM 且 path 以 http:// 或 https:// 开头则被识别为外链
String path;
String title;
String icon;
boolean hidden;
int orderNumber;
EntityStatus status;
String remarks;
// MENU_ITEM
String component;
Boolean cache;
String resource;
private List<Action> actions;
private @Getter long version;
public void updateMenuInfo(
MenuType type,
long parentId,
String name,
String path,
String title,
String icon,
boolean hidden,
int orderNumber,
EntityStatus status,
String remarks,
String component,
boolean cache,
String resource) {
this.type = type;
this.parentId = parentId;
this.path = path;
this.name = name;
this.title = title;
this.icon = icon;
this.hidden = hidden;
this.orderNumber = orderNumber;
this.status = status;
this.component = component;
this.resource = resource;
this.cache = cache;
this.remarks = remarks;
}
public Menu addAction(String action, String label) {
return addAction(Action.newInstance(this.resource, action, label));
}
public void removeAction(Long actionId) {
this.actions.removeIf(action -> Objects.equals(actionId, action.getId().orElseThrow()));
}
public void removeAction(String identifier) {
this.actions.removeIf(action -> Objects.equals(identifier, action.identifier));
}
@Override
public Optional<Long> getId() {
return Optional.ofNullable(id);
}
@Override
public int getOrderNumber() {
return this.orderNumber;
}
private Menu addAction(Action action) {
if (this.actions == null) {
this.actions = new ArrayList<>(8);
}
this.actions.add(action);
return this;
}
Menu(MenuType type, Long id, Long parentId, String name, String path, String title, String icon,
boolean hidden, int orderNumber, EntityStatus status, String remarks, String component, Boolean cache,
String resource, List<Action> actions, long version) {
this.type = type;
this.id = id;
this.parentId = parentId;
this.name = name;
this.path = path;
this.title = title;
this.icon = icon;
this.hidden = hidden;
this.orderNumber = orderNumber;
this.status = status;
this.remarks = remarks;
this.component = component;
this.cache = cache;
this.resource = resource;
this.actions = actions;
this.version = version;
}
public enum MenuType {
MENU_LIST, MENU_ITEM;
@JsonValue
public int value() {
return ordinal();
}
}
}

View File

@@ -0,0 +1,55 @@
package xyz.zhouxy.plusone.system.domain.model.menu;
import java.util.List;
import xyz.zhouxy.plusone.constant.EntityStatus;
import xyz.zhouxy.plusone.system.domain.model.menu.Menu.MenuType;
/**
* 菜单构造器。生成新的 MenuList 或 MenuItem
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
*/
public class MenuConstructor {
private MenuConstructor() {
throw new IllegalStateException("Utility class");
}
public static Menu newMenuItem(
long parentId,
String path,
String name,
String title,
String icon,
boolean hidden,
int orderNumber,
EntityStatus status,
String component,
String resource,
boolean cache,
String remarks) {
List<Action> actions = List.of(
Action.newInstance(resource, "-query", "查询"),
Action.newInstance(resource, "-details", "详情"),
Action.newInstance(resource, "-add", "新增"),
Action.newInstance(resource, "-update", "修改"),
Action.newInstance(resource, "-delete", "删除"));
return new Menu(MenuType.MENU_ITEM, null, parentId, name, path, title, icon, hidden, orderNumber, status,
remarks, component, cache, resource, actions, 0L);
}
public static Menu newMenuList(
long parentId,
String path,
String name,
String title,
String icon,
boolean hidden,
int orderNumber,
EntityStatus status,
String remarks) {
return new Menu(MenuType.MENU_LIST, null, parentId, name, path, title, icon, hidden, orderNumber, status,
remarks, null, null, null, null, 0L);
}
}

View File

@@ -0,0 +1,15 @@
package xyz.zhouxy.plusone.system.domain.model.menu;
import java.util.Collection;
import xyz.zhouxy.plusone.domain.IRepository;
public interface MenuRepository extends IRepository<Menu, Long> {
Collection<Menu> findByIdIn(Collection<Long> ids);
Collection<Menu> queryByRoleId(Long roleId);
Collection<Action> findPermissionsByIdIn(Collection<Long> permissionIds);
}

View File

@@ -0,0 +1,26 @@
package xyz.zhouxy.plusone.system.domain.model.menu;
import com.fasterxml.jackson.annotation.JsonValue;
import xyz.zhouxy.plusone.domain.IWithLabel;
public enum Target implements IWithLabel {
BLANK("_blank"),
PARENT("_parent"),
SELF("_self"),
TOP("_top"),
;
private final String label;
Target(String label) {
this.label = label;
}
@JsonValue
@Override
public String getLabel() {
return this.label;
}
}

View File

@@ -0,0 +1,47 @@
package xyz.zhouxy.plusone.system.domain.model.permission;
import java.util.Optional;
import lombok.Getter;
import xyz.zhouxy.plusone.domain.Entity;
import xyz.zhouxy.plusone.domain.IWithLabel;
import xyz.zhouxy.plusone.domain.IWithVersion;
/**
* 行为
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
*/
public class Action extends Entity<Long> implements IWithLabel, IWithVersion {
Long id;
String resource;
@Getter String identifier;
@Getter String label;
@Getter long version;
public Action(Long id, String resource, String identifier, String label, long version) {
this.id = id;
this.resource = resource;
this.identifier = identifier;
this.label = label;
this.version = version;
}
static Action newInstance(String resource, String identifier, String label) {
return new Action(null, resource, identifier, label, 0L);
}
static Action existingInstance(Long id, String resource, String action, String label, Long version) {
return new Action(id, resource, action, label, version);
}
public String value() {
return resource + identifier;
}
@Override
public Optional<Long> getId() {
return Optional.ofNullable(id);
}
}

View File

@@ -0,0 +1,69 @@
package xyz.zhouxy.plusone.system.domain.model.permission;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import lombok.Getter;
import xyz.zhouxy.plusone.domain.AggregateRoot;
import xyz.zhouxy.plusone.domain.IWithVersion;
/**
* 权限
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
*/
public class Permission extends AggregateRoot<Long> implements IWithVersion {
private Long id;
private @Getter String resource;
private List<Action> actions = new ArrayList<>(8);
private @Getter long version;
public Permission addAction(String action, String label) {
return addAction(Action.newInstance(resource, action, label));
}
public void removeAction(Long actionId) {
this.actions.removeIf(action -> Objects.equals(actionId, action.getId().orElseThrow()));
}
public void removeAction(String identifier) {
this.actions.removeIf(action -> Objects.equals(identifier, action.identifier));
}
// ==================== 实例化 ====================
public static Permission newInstance(String resource) {
return new Permission(
null, resource,
List.of(Action.newInstance(resource, ":add", "添加"),
Action.newInstance(resource, ":delete", "删除"),
Action.newInstance(resource, ":update", "更改"),
Action.newInstance(resource, ":query", "查询"),
Action.newInstance(resource, ":details", "详情")),
0);
}
Permission(Long id, String resource, List<Action> actions, long version) {
this.id = id;
this.resource = resource;
this.actions.addAll(actions);
this.version = version;
}
// ==================== private ====================
private Permission addAction(Action action) {
this.actions.add(action);
return this;
}
// ==================== getter ====================
@Override
public Optional<Long> getId() {
return Optional.ofNullable(id);
}
}

View File

@@ -0,0 +1,3 @@
# Description
后期考虑菜单项与 Permission 绑定,使之与 Action 解耦Permission 亦可以单独管理。

View File

@@ -0,0 +1,12 @@
package xyz.zhouxy.plusone.system.domain.model.role;
/**
* ActionRef
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
*/
public record ActionRef(Long actionId) {
public static ActionRef of(Long actionId) {
return new ActionRef(actionId);
}
}

View File

@@ -0,0 +1,12 @@
package xyz.zhouxy.plusone.system.domain.model.role;
/**
* MenuRef
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
*/
public record MenuRef(Long menuId) {
public static MenuRef of(Long menuId) {
return new MenuRef(menuId);
}
}

View File

@@ -0,0 +1,112 @@
package xyz.zhouxy.plusone.system.domain.model.role;
import java.util.HashSet;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import lombok.Getter;
import lombok.ToString;
import xyz.zhouxy.plusone.constant.EntityStatus;
import xyz.zhouxy.plusone.domain.AggregateRoot;
import xyz.zhouxy.plusone.domain.IWithVersion;
import xyz.zhouxy.plusone.system.domain.model.menu.Action;
import xyz.zhouxy.plusone.system.domain.model.menu.Menu;
/**
* 聚合根:角色
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
*/
@ToString
public class Role extends AggregateRoot<Long> implements IWithVersion {
private Long id;
private @Getter String name;
private @Getter String identifier;
private @Getter EntityStatus status;
private @Getter String remarks;
private final Set<MenuRef> menus = new HashSet<>();
private final Set<ActionRef> permissions = new HashSet<>();
private @Getter long version;
public void bindMenus(Set<MenuRef> menus) {
this.menus.clear();
this.menus.addAll(menus);
}
public void bindPermissions(Set<ActionRef> permissions) {
this.permissions.clear();
this.permissions.addAll(permissions);
}
public void update(
String name,
String identifier,
EntityStatus status,
String remarks,
Set<Menu> menus,
Set<Action> permissions) {
this.name = name;
this.identifier = identifier;
this.status = status;
this.remarks = remarks;
bindMenus(menus.stream().map(menu -> new MenuRef(menu.getId().orElseThrow())).collect(Collectors.toSet()));
bindPermissions(permissions
.stream()
.map(permission -> new ActionRef(permission.getId().orElseThrow()))
.collect(Collectors.toSet()));
}
// getters
@Override
public Optional<Long> getId() {
return Optional.ofNullable(id);
}
public Set<MenuRef> getMenus() {
return Set.copyOf(menus);
}
public Set<ActionRef> getPermissions() {
return Set.copyOf(permissions);
}
public static Role newInstance(
String name,
String identifier,
EntityStatus status,
String remarks,
Set<MenuRef> menus,
Set<ActionRef> permissions) {
return new Role(null, name, identifier, status, remarks, menus, permissions, 0L);
}
/**
* 构造方法
*
* @param id id
* @param name 角色名
* @param identifier 标识符
* @param status 状态
* @param remarks 备注
* @param menus 菜单
* @param permissions 权限
* @param version 版本号
*/
Role(Long id, String name, String identifier, EntityStatus status, String remarks,
Set<MenuRef> menus, Set<ActionRef> permissions, long version) {
this.id = id;
this.name = name;
this.identifier = identifier;
this.status = status;
this.remarks = remarks;
bindMenus(menus);
bindPermissions(permissions);
this.version = version;
}
}

View File

@@ -0,0 +1,11 @@
package xyz.zhouxy.plusone.system.domain.model.role;
import java.util.Collection;
import xyz.zhouxy.plusone.domain.IRepository;
public interface RoleRepository extends IRepository<Role, Long> {
Collection<Role> findByAccountId(Long accountId);
}

View File

@@ -0,0 +1,44 @@
package xyz.zhouxy.plusone.system.domain.service;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
import java.util.stream.Collectors;
import org.springframework.stereotype.Service;
import xyz.zhouxy.plusone.system.domain.model.menu.Menu;
import xyz.zhouxy.plusone.system.domain.model.menu.MenuRepository;
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
public class MenuService {
private final RoleRepository roleRepository;
private final MenuRepository menuRepository;
MenuService(RoleRepository roleRepository, MenuRepository menuRepository) {
this.roleRepository = roleRepository;
this.menuRepository = menuRepository;
}
/**
* 根据账号 id 查询菜单列表(不形成树结构)
* @param accountId
* @return
*/
public Collection<Menu> queryAllMenuListByAccountId(Long accountId) {
Collection<Role> roles = roleRepository.findByAccountId(accountId);
Set<MenuRef> menuRefs = new HashSet<>();
roles.forEach(role -> menuRefs.addAll(role.getMenus()));
return menuRepository.findByIdIn(menuRefs.stream().map(MenuRef::menuId).collect(Collectors.toSet()));
}
}

View File

@@ -0,0 +1,18 @@
package xyz.zhouxy.plusone.system.domain.model.account;
import static org.junit.jupiter.api.Assertions.assertTrue;
import org.junit.jupiter.api.Test;
import lombok.extern.slf4j.Slf4j;
@Slf4j
class PasswordTests {
@Test
void testNewDefaultPassword() {
var pwd = Password.newDefaultPassword();
log.debug("value -- {}; salt -- {}", pwd.value(), pwd.getSalt());
assertTrue(pwd.check("A1b2C3d4"), "默认密码校验失败");
}
}

View File

@@ -0,0 +1,23 @@
<?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-infrastructure</artifactId>
<dependencies>
<dependency>
<groupId>xyz.zhouxy</groupId>
<artifactId>plusone-basic-infrastructure</artifactId>
</dependency>
<dependency>
<groupId>xyz.zhouxy</groupId>
<artifactId>plusone-system-domain</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,218 @@
package xyz.zhouxy.plusone.system.domain.model.account;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.time.LocalDateTime;
import java.util.Collection;
import java.util.Objects;
import javax.annotation.Nonnull;
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.jdbc.core.namedparam.SqlParameterSource;
import org.springframework.stereotype.Repository;
import cn.hutool.core.util.IdUtil;
import xyz.zhouxy.plusone.jdbc.JdbcRepositorySupport;
import xyz.zhouxy.plusone.util.AssertResult;
/**
* AccountRepository 实现类
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
*/
@Repository
public class AccountRepositoryImpl extends JdbcRepositorySupport<Account, Long> implements AccountRepository {
private final AccountRoleRefDAO accountRoleDAO;
public AccountRepositoryImpl(@Nonnull NamedParameterJdbcTemplate namedParameterJdbcTemplate) {
super(namedParameterJdbcTemplate);
this.accountRoleDAO = new AccountRoleRefDAO(namedParameterJdbcTemplate);
}
@Override
protected final void doDelete(@Nonnull Account entity) {
int i = this.jdbc.update("""
UPDATE sys_account SET deleted = id, "version" = "version" + 1
WHERE id = :id AND deleted = 0 AND "version" = :version
""",
new MapSqlParameterSource()
.addValue("id", entity.getId().orElseThrow())
.addValue("version", entity.getVersion()));
AssertResult.update(i, 1);
}
@Override
protected final Account doFindById(@Nonnull Long id) {
return queryForObject("""
SELECT
id, email, mobile_phone, username, "password", salt, avatar, sex, nickname, status,
created_by, updated_by, "version"
FROM sys_account
WHERE id = :id AND deleted = 0
""",
new MapSqlParameterSource("id", id));
}
@Override
public Account findByEmail(Email email) {
return queryForObject("""
SELECT
id, email, mobile_phone, username, "password", salt, avatar, sex, nickname, status,
created_by, updated_by, "version"
FROM sys_account
WHERE email = :email AND deleted = 0
""",
new MapSqlParameterSource("email", email.value()));
}
@Override
public Account findByMobilePhone(MobilePhone mobilePhone) {
return queryForObject("""
SELECT
id, email, mobile_phone, username, "password", salt, avatar, sex, nickname, status,
created_by, updated_by, "version"
FROM sys_account
WHERE mobile_phone = :mobilePhone AND deleted = 0
""",
new MapSqlParameterSource("mobilePhone", mobilePhone.value()));
}
@Override
public Account findByUsername(Username username) {
return queryForObject("""
SELECT
id, email, mobile_phone, username, "password", salt, avatar, sex, nickname, status,
created_by, updated_by, "version"
FROM sys_account
WHERE username = :username AND deleted = 0
""",
new MapSqlParameterSource("username", username.value()));
}
@Override
public boolean exists(Long id) {
return queryExists("SELECT 1 FROM sys_account WHERE id = :id AND deleted = 0 LIMIT 1",
new MapSqlParameterSource("id", id));
}
@Override
public boolean existsUsername(Username username) {
return queryExists("SELECT 1 FROM sys_account WHERE username = :username AND deleted = 0 LIMIT 1",
new MapSqlParameterSource("username", username.value()));
}
@Override
public boolean existsEmail(Email email) {
return queryExists("SELECT 1 FROM sys_account WHERE email = :email AND deleted = 0 LIMIT 1",
new MapSqlParameterSource("email", email.value()));
}
@Override
public boolean existsMobilePhone(MobilePhone mobilePhone) {
return queryExists("SELECT 1 FROM sys_account WHERE mobile_phone = :mobile_phone AND deleted = 0 LIMIT 1",
new MapSqlParameterSource("mobile_phone", mobilePhone.value()));
}
@Override
public Collection<Account> findByRoleId(Long roleId) {
return queryForList("""
SELECT
a.id, a.email, a.mobile_phone, a.username, a."password", a.salt,
a.avatar, a.sex, a.nickname, a.status,
a.created_by, a.updated_by, a."version"
FROM sys_account a
LEFT JOIN sys_account_role ar ON a.id = ar.account_id
WHERE ar.role_id = :roleId AND a.deleted = 0
""",
new MapSqlParameterSource("roleId", roleId));
}
@Override
protected final Account doInsert(@Nonnull Account entity) {
String sql = """
INSERT INTO sys_account
(id, email, mobile_phone, username, "password", salt, avatar, sex, nickname, status, created_by, create_time)
VALUES
(:id, :email, :mobilePhone, :username, :password, :salt, :avatar, :sex, :nickname, :status, :createdBy, :createTime)
""";
long id = IdUtil.getSnowflakeNextId();
SqlParameterSource params = generateParamSource(id, entity);
int i = jdbc.update(sql, params);
AssertResult.update(i, 1);
this.accountRoleDAO.insertAccountRoleRefs(id, entity.getRoleIds());
return entity;
}
@Override
protected final Account doUpdate(@Nonnull Account entity) {
String sql = """
UPDATE sys_account
SET "email" = :email,
"mobile_phone" = :mobilePhone,
"username" = :username,
"password" = :password,
"salt" = :salt,
"avatar" = :avatar,
"sex" = :sex,
"nickname" = :nickname,
"status" = :status,
"updated_by" = :updatedBy,
"update_time" = :updateTime,
"version" = "version" + 1
WHERE id = :id AND deleted = 0 AND "version" = :version
""";
SqlParameterSource params = generateParamSource(entity);
int i = this.jdbc.update(sql, params);
AssertResult.update(i, 1);
this.accountRoleDAO.saveAccountRoleRefs(entity);
return entity;
}
@Override
protected final Account mapRow(ResultSet rs) throws SQLException {
long accountId = rs.getLong("id");
AccountInfo accountInfo = AccountInfo.of(
rs.getString("nickname"),
rs.getString("avatar"),
Sex.of(rs.getInt("sex")));
return new Account(
accountId,
rs.getString("username"),
rs.getString("email"),
rs.getString("mobile_phone"),
Password.of(rs.getString("password"), rs.getString("salt")),
AccountStatus.of(rs.getInt("status")),
accountInfo,
this.accountRoleDAO.selectRoleIdsByAccountId(accountId),
rs.getLong("created_by"),
rs.getLong("updated_by"),
rs.getLong("version"));
}
@Override
protected final SqlParameterSource generateParamSource(Long id, @Nonnull Account entity) {
LocalDateTime now = LocalDateTime.now();
AccountInfo accountInfo = entity.getAccountInfo();
return new MapSqlParameterSource()
.addValue("id", id)
.addValue("email", Objects.nonNull(entity.getEmail()) ? entity.getEmail().value() : null)
.addValue("mobilePhone",
Objects.nonNull(entity.getMobilePhone()) ? entity.getMobilePhone().value() : null)
.addValue("username", entity.getUsername().value())
.addValue("password", entity.getPassword().value())
.addValue("salt", entity.getPassword().getSalt())
.addValue("avatar", accountInfo.getAvatar().toString())
.addValue("sex", accountInfo.getSex().getValue())
.addValue("nickname",
Objects.nonNull(accountInfo.getNickname()) ? accountInfo.getNickname().value() : null)
.addValue("status", entity.getStatus().getValue())
.addValue("createdBy", entity.getCreatedBy())
.addValue("createTime", now)
.addValue("updatedBy", entity.getUpdatedBy())
.addValue("updateTime", now)
.addValue("version", entity.getVersion());
}
}

View File

@@ -0,0 +1,53 @@
package xyz.zhouxy.plusone.system.domain.model.account;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import xyz.zhouxy.plusone.util.AssertResult;
import xyz.zhouxy.plusone.util.NumberUtil;
class AccountRoleRefDAO {
private final NamedParameterJdbcTemplate jdbc;
AccountRoleRefDAO(NamedParameterJdbcTemplate jdbc) {
this.jdbc = jdbc;
}
Set<Long> selectRoleIdsByAccountId(Long accountId) {
List<Long> roleRefs = this.jdbc.queryForList("""
SELECT r.id FROM sys_role r RIGHT JOIN sys_account_role ar ON r.id = ar.role_id
WHERE r.deleted = 0 AND ar.account_id = :accountId;
""",
new MapSqlParameterSource("accountId", accountId),
Long.TYPE);
return new HashSet<>(roleRefs);
}
void clearAccountRoleRefs(Account entity) {
var param = new MapSqlParameterSource("accountId", entity.getId().orElseThrow());
this.jdbc.update("DELETE FROM sys_account_role WHERE account_id = :accountId", param);
}
void insertAccountRoleRefs(Long accountId, Set<Long> roleRefs) {
String sql = "INSERT INTO sys_account_role (account_id, role_id) VALUES (:accountId, :roleId)";
MapSqlParameterSource[] batchArgs = roleRefs
.stream()
.map((Long roleId) -> new MapSqlParameterSource()
.addValue("accountId", accountId)
.addValue("roleId", roleId))
.toArray(MapSqlParameterSource[]::new);
int[] i = this.jdbc.batchUpdate(sql, batchArgs);
AssertResult.update(roleRefs.size(), NumberUtil.sum(i));
}
void saveAccountRoleRefs(Account entity) {
Long accountId = entity.getId().orElseThrow();
Set<Long> roleRefs = entity.getRoleIds();
clearAccountRoleRefs(entity);
insertAccountRoleRefs(accountId, roleRefs);
}
}

View File

@@ -0,0 +1,118 @@
package xyz.zhouxy.plusone.system.domain.model.dict;
import static xyz.zhouxy.plusone.system.constant.AuthLogic.adminAuthLogic;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.time.LocalDateTime;
import java.util.List;
import javax.annotation.Nonnull;
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.jdbc.core.namedparam.SqlParameterSource;
import org.springframework.stereotype.Repository;
import cn.hutool.core.util.IdUtil;
import xyz.zhouxy.plusone.jdbc.JdbcRepositorySupport;
import xyz.zhouxy.plusone.util.AssertResult;
/**
* DictRepository 实现类
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
*/
@Repository
public class DictRepositoryImpl extends JdbcRepositorySupport<Dict, Long> implements DictRepository {
private final DictValueDAO dictValueDAO;
public DictRepositoryImpl(@Nonnull NamedParameterJdbcTemplate namedParameterJdbcTemplate) {
super(namedParameterJdbcTemplate);
this.dictValueDAO = new DictValueDAO(namedParameterJdbcTemplate);
}
@Override
public Dict doFindById(@Nonnull Long id) {
return queryForObject("SELECT id, dict_type, dict_label, \"version\" WHERE id = :id AND deleted = 0",
new MapSqlParameterSource("id", id));
}
@Override
protected final Dict doInsert(@Nonnull Dict entity) {
long id = IdUtil.getSnowflakeNextId();
int i = this.jdbc.update("""
INSERT INTO sys_dict_type (dict_type, dict_label, create_time, created_by)
VALUES (:dictType, :dictLabel, :createTime, :createdBy)
""",
generateParamSource(id, entity));
AssertResult.update(i, 1);
this.dictValueDAO.insertDictValues(id, entity);
return find(id);
}
@Override
protected final Dict doUpdate(@Nonnull Dict entity) {
int i = this.jdbc.update("""
UPDATE sys_dict_type
SET dict_type = :dictType,
dict_label = :dictLabel,
update_time = :updateTime,
updated_by = :updatedBy,
"version" = "version" + 1
WHERE id = :id AND deleted = 0 AND "version" = :version
""",
generateParamSource(entity));
AssertResult.update(i, 1);
this.dictValueDAO.updateDictValues(entity);
return find(entity.getId().orElseThrow());
}
@Override
protected final void doDelete(@Nonnull Dict entity) {
int i = this.jdbc.update("""
UPDATE sys_dict_type SET deleted = id, "version" = "version" + 1
WHERE id = :id AND deleted = 0 AND "version" = :version
""",
generateParamSource(entity));
AssertResult.update(i, 1);
}
@Override
public boolean exists(Long id) {
return queryExists("SELECT 1 FROM sys_dict_type WHERE id = :id AND deleted = 0 LIMIT 1",
new MapSqlParameterSource("id", id));
}
@Override
public List<Dict> findAll() {
return queryForList("SELECT id, dict_type, dict_label, \"version\" WHERE deleted = 0");
}
@Override
protected final Dict mapRow(ResultSet rs) throws SQLException {
long id = rs.getLong("id");
return new Dict(
id,
rs.getString("dict_type"),
rs.getString("dict_label"),
this.dictValueDAO.selectDictValuesByDictId(id),
rs.getLong("version"));
}
@Override
protected final SqlParameterSource generateParamSource(Long id, @Nonnull Dict entity) {
LocalDateTime now = LocalDateTime.now();
long loginId = adminAuthLogic.getLoginIdAsLong();
return new MapSqlParameterSource()
.addValue("dictType", entity.getDictType())
.addValue("dictLabel", entity.getLabel())
.addValue("createTime", now)
.addValue("createdBy", loginId)
.addValue("updateTime", now)
.addValue("updatedBy", loginId)
.addValue("id", id)
.addValue("version", entity.getVersion());
}
}

View File

@@ -0,0 +1,51 @@
package xyz.zhouxy.plusone.system.domain.model.dict;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.jdbc.core.namedparam.SqlParameterSource;
import org.springframework.util.CollectionUtils;
import xyz.zhouxy.plusone.util.AssertResult;
import xyz.zhouxy.plusone.util.NumberUtil;
class DictValueDAO {
private final NamedParameterJdbcTemplate jdbc;
DictValueDAO(NamedParameterJdbcTemplate jdbc) {
this.jdbc = jdbc;
}
void updateDictValues(Dict entity) {
MapSqlParameterSource deleteParam = new MapSqlParameterSource("dictType", entity.getId().orElseThrow());
this.jdbc.update("DELETE FROM sys_dict_value WHERE dict_type = :dictType", deleteParam);
int i = insertDictValues(entity.getId().orElseThrow(), entity);
AssertResult.update(i, entity.count());
}
int insertDictValues(Long dictId, Dict entity) {
if (Objects.isNull(dictId) || Objects.isNull(entity) || CollectionUtils.isEmpty(entity.getValues())) {
return 0;
}
int[] i = this.jdbc.batchUpdate(
"INSERT INTO sys_dict_value (dict_type, dict_key, label) VALUES (:dictType, :dictKey, :label)",
entity.getValues().stream()
.map(dictValue -> new MapSqlParameterSource()
.addValue("dictType", dictId)
.addValue("dictKey", dictValue.getKey())
.addValue("label", dictValue.getLabel()))
.toArray(SqlParameterSource[]::new));
return NumberUtil.sum(i);
}
Set<DictValue> selectDictValuesByDictId(long id) {
return this.jdbc.queryForStream("""
SELECT dict_key, label FROM sys_dict_value WHERE dict_type = :dictType
""", new MapSqlParameterSource("dictType", id),
(rs, rowNum) -> DictValue.of(rs.getInt("dict_key"), rs.getString("label")))
.collect(Collectors.toSet());
}
}

View File

@@ -0,0 +1,123 @@
package xyz.zhouxy.plusone.system.domain.model.menu;
import static xyz.zhouxy.plusone.system.constant.AuthLogic.adminAuthLogic;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.time.LocalDateTime;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import javax.annotation.Nonnull;
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.jdbc.core.namedparam.SqlParameterSource;
import cn.hutool.core.util.IdUtil;
import xyz.zhouxy.plusone.jdbc.JdbcEntityDaoSupport;
/**
* {@link Action} 的数据访问对象
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
* @date 2022-10-31 12:31:49
*/
class ActionDAO extends JdbcEntityDaoSupport<Action, Long> {
ActionDAO(@Nonnull NamedParameterJdbcTemplate jdbc) {
super(jdbc);
}
void saveActions(Long menuId, List<Action> actions) {
// 删除要删除的权限
Collection<Long> ids = actions.stream()
.filter(action -> action.getId().isPresent())
.map(action -> action.getId().orElseThrow())
.collect(Collectors.toSet());
if (!ids.isEmpty()) {
this.jdbc.update(
"UPDATE sys_action SET deleted = id WHERE resource = :resource AND id NOT IN (:ids) AND deleted = 0",
new MapSqlParameterSource()
.addValue("resource", menuId)
.addValue("ids", ids));
}
// 更新存在的数据
this.jdbc.batchUpdate("""
UPDATE sys_action
SET resource = :resource,
identifier = :identifier,
label = :label,
update_time = :updateTime,
updated_by = :updatedBy
WHERE id = :id AND deleted = 0
""",
actions.stream()
.filter(action -> action.getId().isPresent())
.map(action -> generateParamSource(menuId, action))
.toArray(MapSqlParameterSource[]::new));
// 插入新添加的数据
this.jdbc.batchUpdate("""
INSERT INTO sys_action
(id, resource, identifier, "label", create_time, created_by)
VALUES
(:id, :resource, :identifier, :label, :createTime, :createdBy)
""",
actions.stream()
.filter(action -> action.getId().isEmpty())
.map(action -> generateParamSource(menuId, IdUtil.getSnowflakeNextId(), action))
.toArray(MapSqlParameterSource[]::new));
}
List<Action> selectActionsByMenuId(long menuId) {
return queryForList("""
SELECT a.id, m.resource, a.identifier, a.label
FROM sys_action a
JOIN (SELECT id, resource FROM sys_menu WHERE id = :menuId AND deleted = 0) m ON a.resource = m.id
WHERE a.deleted = 0
""", new MapSqlParameterSource("menuId", menuId));
}
Collection<Action> selectActionsByIdIn(Collection<Long> actionIds) {
if (Objects.isNull(actionIds) || actionIds.isEmpty()) {
return Collections.emptyList();
}
return queryForList("""
SELECT a.id, m.resource, a.identifier, a.label
FROM sys_action a
LEFT JOIN sys_menu m ON a.resource = m.id
WHERE a.id IN (:actionIds) AND a.deleted = 0
""", new MapSqlParameterSource("actionIds", actionIds));
}
private SqlParameterSource generateParamSource(Long menuId, Action action) {
return generateParamSource(menuId, action.getId().orElseThrow(), action);
}
private SqlParameterSource generateParamSource(Long menuId, Long actionId, Action action) {
long loginId = adminAuthLogic.getLoginIdAsLong();
LocalDateTime now = LocalDateTime.now();
return new MapSqlParameterSource("id", actionId)
.addValue("resource", menuId)
.addValue("identifier", action.getIdentifier())
.addValue("label", action.getLabel())
.addValue("createTime", now)
.addValue("createdBy", loginId)
.addValue("updateTime", now)
.addValue("updatedBy", loginId);
}
@Override
protected Action mapRow(ResultSet rs) throws SQLException {
return Action.of(
rs.getLong("id"),
rs.getString("resource"),
rs.getString("identifier"),
rs.getString("label"));
}
}

View File

@@ -0,0 +1,200 @@
package xyz.zhouxy.plusone.system.domain.model.menu;
import static xyz.zhouxy.plusone.system.constant.AuthLogic.adminAuthLogic;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.time.LocalDateTime;
import java.util.Collection;
import java.util.Collections;
import java.util.Objects;
import javax.annotation.Nonnull;
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.stereotype.Repository;
import cn.hutool.core.util.IdUtil;
import xyz.zhouxy.plusone.constant.EntityStatus;
import xyz.zhouxy.plusone.jdbc.JdbcRepositorySupport;
import xyz.zhouxy.plusone.system.domain.model.menu.Menu.MenuType;
import xyz.zhouxy.plusone.util.AssertResult;
import xyz.zhouxy.plusone.util.EnumUtil;
/**
* MenuRepository 实现类
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
*/
@Repository
public class MenuRepositoryImpl extends JdbcRepositorySupport<Menu, Long> implements MenuRepository {
private final ActionDAO actionDAO;
MenuRepositoryImpl(@Nonnull NamedParameterJdbcTemplate namedParameterJdbcTemplate) {
super(namedParameterJdbcTemplate);
this.actionDAO = new ActionDAO(namedParameterJdbcTemplate);
}
@Override
protected final Menu doFindById(@Nonnull Long id) {
return queryForObject("""
SELECT
id, parent_id, "type", "name", "path", title, icon, hidden, order_number, status, remarks,
component, "cache", resource, "version"
FROM sys_menu
WHERE id = :id AND deleted = 0
""",
new MapSqlParameterSource("id", id));
}
@Override
protected final Menu doInsert(@Nonnull Menu entity) {
long id = IdUtil.getSnowflakeNextId();
String sql = """
INSERT INTO sys_menu (
id, parent_id, "type", name, "path", title, icon, hidden, order_number, status, remarks,
component, "cache", resource, create_time, created_by)
VALUES
(:id, :parentId, :type, :name, :path, :title, :icon, :hidden, :orderNumber, :status, :remarks,
:component, :cache, :resource, :createTime, :createdBy)
""";
MapSqlParameterSource paramSource = generateParamSource(id, entity);
int i = this.jdbc.update(sql, paramSource);
AssertResult.update(i, 1);
this.actionDAO.saveActions(id, entity.getActions());
return entity;
}
@Override
protected final Menu doUpdate(@Nonnull Menu entity) {
String sql = """
UPDATE sys_menu
SET "parent_id" = :parentId,
"type" = :type,
"name" = :name,
"path" = :path,
"title" = :title,
"icon" = :icon,
"hidden" = :hidden,
"order_number" = :orderNumber,
"status" = :status,
"remarks" = :remarks,
"component" = :component,
"cache" = :cache,
"resource" = :resource,
"update_time" = :updateTime,
"updated_by" = :updatedBy,
"version" = "version" + 1
WHERE id = :id AND deleted = 0 AND "version" = :version
""";
// 更新菜单
int i = this.jdbc.update(sql, generateParamSource(entity));
AssertResult.update(i, 1);
// 保存权限
Long id = entity.getId().orElseThrow();
this.actionDAO.saveActions(id, entity.getActions());
return entity;
}
@Override
protected final void doDelete(@Nonnull Menu entity) {
int i = this.jdbc.update("""
UPDATE sys_menu SET deleted = id, "version" = "version" + 1
WHERE id = :id AND deleted = 0 AND "version" = :version
""",
new MapSqlParameterSource("id", entity.getId().orElseThrow())
.addValue("version", entity.getVersion()));
AssertResult.update(i, 1);
}
@Override
public boolean exists(Long id) {
return queryExists("SELECT 1 FROM sys_menu WHERE id = :id AND deleted = 0 LIMIT 1",
new MapSqlParameterSource("id", id));
}
@Override
public Collection<Menu> findByIdIn(Collection<Long> ids) {
if (Objects.isNull(ids) || ids.isEmpty()) {
return Collections.emptyList();
}
return queryForList("""
SELECT
id, parent_id, "type", "name", "path", title, icon, hidden, order_number, status, remarks,
component, "cache", resource, "version"
FROM sys_menu
WHERE id IN (:ids) AND deleted = 0
""",
new MapSqlParameterSource("ids", ids));
}
@Override
public Collection<Action> findPermissionsByIdIn(Collection<Long> permissionIds) {
return this.actionDAO.selectActionsByIdIn(permissionIds);
}
@Override
public Collection<Menu> queryByRoleId(Long roleId) {
return queryForList("""
SELECT
m.id, m.parent_id, m."type", m."name", m."path", m.title, m.icon, m.hidden, m.order_number,
m.status, m.remarks, m.component, m."cache", m.resource, m."version"
FROM sys_menu AS m
LEFT JOIN sys_role_menu AS rm ON m.id = rm.menu_id
WHERE rm.role_id = :roleId AND r.deleted = 0
""",
new MapSqlParameterSource("roleId", roleId));
}
@Override
protected final Menu mapRow(ResultSet rs) throws SQLException {
long menuId = rs.getLong("id");
return new Menu(
EnumUtil.valueOf(MenuType.class, rs.getInt("type")),
menuId,
rs.getLong("parent_id"),
rs.getString("name"),
rs.getString("path"),
rs.getString("title"),
rs.getString("icon"),
rs.getBoolean("hidden"),
rs.getInt("order_number"),
EntityStatus.of(rs.getInt("status")),
rs.getString("remarks"),
rs.getString("component"),
rs.getBoolean("cache"),
rs.getString("resource"),
this.actionDAO.selectActionsByMenuId(menuId),
rs.getLong("version"));
}
@Override
protected final MapSqlParameterSource generateParamSource(Long id, @Nonnull Menu entity) {
LocalDateTime now = LocalDateTime.now();
long loginId = adminAuthLogic.getLoginIdAsLong();
return new MapSqlParameterSource()
.addValue("id", id)
.addValue("parentId", entity.getParentId())
.addValue("type", entity.getType().value())
.addValue("name", entity.getName())
.addValue("path", entity.getPath())
.addValue("title", entity.getTitle())
.addValue("icon", entity.getIcon())
.addValue("hidden", entity.isHidden())
.addValue("orderNumber", entity.getOrderNumber())
.addValue("status", entity.getStatus().getValue())
.addValue("remarks", entity.getRemarks())
.addValue("component", entity.getComponent())
.addValue("cache", entity.getCache())
.addValue("resource", entity.getResource())
.addValue("createTime", now)
.addValue("createdBy", loginId)
.addValue("updateTime", now)
.addValue("updatedBy", loginId)
.addValue("version", entity.getVersion());
}
}

Some files were not shown because too many files have changed in this diff Show More