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,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"), "默认密码校验失败");
}
}