迁移实体类,并增加了领域模型(尝试使用event)

This commit is contained in:
dengqichen 2024-11-26 18:22:04 +08:00
parent ee1e20daff
commit b49a64724c
32 changed files with 966 additions and 69 deletions

View File

@ -3,7 +3,6 @@ package com.qqchen.deploy.backend.api;
import com.qqchen.deploy.backend.common.controller.BaseController;
import com.qqchen.deploy.backend.common.api.Response;
import com.qqchen.deploy.backend.common.enums.ResponseCode;
import com.qqchen.deploy.backend.common.utils.MessageUtils;
import com.qqchen.deploy.backend.converter.UserConverter;
import com.qqchen.deploy.backend.entity.User;
import com.qqchen.deploy.backend.dto.query.UserQuery;
@ -39,7 +38,7 @@ public class UserApiController extends BaseController<User, Long, UserQuery, Use
User user = converter.toEntity(request);
User savedUser = userService.register(user);
return Response.success(converter.toVO(savedUser));
return Response.success(converter.toResponse(savedUser));
}
@GetMapping("/page")

View File

@ -5,8 +5,15 @@ import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 标记实体是否支持逻辑删除
* 被标记的实体在删除时不会物理删除数据而是标记删除状态
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface SoftDelete {
boolean value() default true; // 默认启用软删除
public @interface LogicDelete {
/**
* 是否启用逻辑删除默认启用
*/
boolean value() default true;
}

View File

@ -28,7 +28,7 @@ public abstract class BaseController<T extends Entity<ID>, ID extends Serializab
public Response<V> create(@RequestBody R request) {
T entity = converter.toEntity(request);
T savedEntity = service.create(entity);
return Response.success(converter.toVO(savedEntity));
return Response.success(converter.toResponse(savedEntity));
}
@PutMapping("/{id}")
@ -36,7 +36,7 @@ public abstract class BaseController<T extends Entity<ID>, ID extends Serializab
T entity = service.findById(id);
converter.updateEntity(entity, request);
T updatedEntity = service.update(entity);
return Response.success(converter.toVO(updatedEntity));
return Response.success(converter.toResponse(updatedEntity));
}
@DeleteMapping("/{id}")
@ -48,18 +48,18 @@ public abstract class BaseController<T extends Entity<ID>, ID extends Serializab
@GetMapping("/{id}")
public Response<V> findById(@PathVariable ID id) {
T entity = service.findById(id);
return Response.success(converter.toVO(entity));
return Response.success(converter.toResponse(entity));
}
@GetMapping
public Response<List<V>> findAll() {
List<T> entities = service.findAll();
return Response.success(converter.toVOList(entities));
return Response.success(converter.toResponseList(entities));
}
@GetMapping("/page")
public Response<Page<V>> page(Q query) {
Page<T> page = service.page(query);
return Response.success(page.map(entity -> converter.toVO(entity)));
return Response.success(page.map(entity -> converter.toResponse(entity)));
}
}

View File

@ -12,16 +12,16 @@ public interface BaseConverter<D extends Entity<?>, R extends BaseRequest, V> {
D toEntity(R request);
// Domain -> VO
V toVO(D entity);
V toResponse(D entity);
// Domain List -> VO List
List<V> toVOList(List<D> entityList);
List<V> toResponseList(List<D> entityList);
// 更新实体
void updateEntity(@MappingTarget D entity, R request);
// 转换分页结果
default Page<V> toVOPage(Page<D> page) {
return page.map(this::toVO);
default Page<V> toResponsePage(Page<D> page) {
return page.map(this::toResponse);
}
}

View File

@ -1,10 +1,72 @@
package com.qqchen.deploy.backend.common.domain;
import java.io.Serializable;
import com.qqchen.deploy.backend.common.event.DomainEvent;
import org.springframework.data.domain.AfterDomainEventPublication;
import org.springframework.data.domain.DomainEvents;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
/**
* 聚合根基类
*/
public abstract class AggregateRoot<ID extends Serializable> extends Entity<ID> {
// 聚合根特定的行为可以在这里添加
private transient final List<DomainEvent> domainEvents = new ArrayList<>();
/**
* 注册领域事件
*/
protected void registerDomainEvent(DomainEvent event) {
if (event != null) {
domainEvents.add(event);
}
}
/**
* 获取并清除所有事件
*/
@DomainEvents
protected Collection<DomainEvent> domainEvents() {
List<DomainEvent> events = new ArrayList<>(this.domainEvents);
this.domainEvents.clear();
return events;
}
/**
* Spring Data JPA会在事件发布后调用此方法
*/
@AfterDomainEventPublication
protected void clearDomainEvents() {
this.domainEvents.clear();
}
/**
* 验证聚合根状态
* 子类可以重写此方法实现具体的验证逻辑
*/
protected void validateState() {
// 默认实现为空由子类根据需要重写
}
/**
* 在保存前验证状态
*/
public void validateBeforeSave() {
validateState();
}
/**
* 在更新前验证状态
*/
public void validateBeforeUpdate() {
validateState();
}
/**
* 在删除前验证状态
*/
public void validateBeforeDelete() {
// 子类可以重写此方法添加删除前的验证
}
}

View File

@ -0,0 +1,16 @@
package com.qqchen.deploy.backend.common.event;
import lombok.Getter;
import java.time.LocalDateTime;
@Getter
public abstract class DomainEvent {
private final String eventId;
private final LocalDateTime occurredOn;
protected DomainEvent() {
this.eventId = java.util.UUID.randomUUID().toString();
this.occurredOn = LocalDateTime.now();
}
}

View File

@ -5,7 +5,7 @@ import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.List;
import com.qqchen.deploy.backend.common.annotation.SoftDelete;
import com.qqchen.deploy.backend.common.annotation.LogicDelete;
import com.qqchen.deploy.backend.common.domain.Entity;
import com.qqchen.deploy.backend.common.enums.QueryType;
import com.qqchen.deploy.backend.common.query.BaseQuery;
@ -73,7 +73,7 @@ public abstract class BaseServiceImpl<T extends Entity<ID>, ID extends Serializa
Class<?>[] genericTypes = GenericTypeResolver.resolveTypeArguments(getClass(), BaseServiceImpl.class);
if (genericTypes != null && genericTypes.length > 0) {
Class<T> entityClass = (Class<T>) genericTypes[0];
SoftDelete softDelete = entityClass.getAnnotation(SoftDelete.class);
LogicDelete softDelete = entityClass.getAnnotation(LogicDelete.class);
if (softDelete != null && softDelete.value()) {
Path<?> deletedPath = EntityPathResolver.getPath(entityPath, "deleted");

View File

@ -19,7 +19,7 @@ public interface UserConverter extends BaseConverter<User, UserRequest, UserResp
@Override
@Mapping(target = "password", ignore = true)
UserResponse toVO(User user);
UserResponse toResponse(User user);
@Override
@Mapping(target = "id", ignore = true)

View File

@ -1,14 +0,0 @@
package com.qqchen.deploy.backend.dto.query;
import com.qqchen.deploy.backend.common.annotation.QueryField;
import com.qqchen.deploy.backend.common.enums.QueryType;
import lombok.Data;
@Data
public class UserAddressQuery {
@QueryField(type = QueryType.LIKE)
private String city;
@QueryField(type = QueryType.EQUAL)
private String postcode;
}

View File

@ -0,0 +1,49 @@
package com.qqchen.deploy.backend.entity;
import com.qqchen.deploy.backend.common.annotation.LogicDelete;
import com.qqchen.deploy.backend.common.domain.Entity;
import jakarta.persistence.Column;
import jakarta.persistence.Table;
import jakarta.persistence.Transient;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.ArrayList;
import java.util.List;
@Data
@EqualsAndHashCode(callSuper = true)
@jakarta.persistence.Entity
@Table(name = "sys_department")
@LogicDelete
public class Department extends Entity<Long> {
@NotBlank(message = "部门名称不能为空")
@Column(nullable = false)
private String name;
@NotBlank(message = "部门编码不能为空")
@Column(nullable = false, unique = true)
private String code;
private String description;
@Column(name = "parent_id")
private Long parentId;
@Column(nullable = false)
private Integer sort = 0;
@Column(nullable = false)
private Boolean enabled = true;
@Column(name = "leader_id")
private Long leaderId;
@Column(name = "leader_name")
private String leaderName;
@Transient // 不映射到数据库
private List<Department> children = new ArrayList<>();
}

View File

@ -0,0 +1,41 @@
package com.qqchen.deploy.backend.entity;
import com.qqchen.deploy.backend.common.annotation.LogicDelete;
import com.qqchen.deploy.backend.common.domain.Entity;
import jakarta.persistence.Column;
import jakarta.persistence.Table;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.time.LocalDateTime;
@Data
@EqualsAndHashCode(callSuper = true)
@jakarta.persistence.Entity
@Table(name = "sys_jenkins_build")
@LogicDelete
public class JenkinsBuild extends Entity<Long> {
@Column(name = "jenkins_id", nullable = false)
private Long jenkinsId;
@Column(name = "job_id", nullable = false)
private Long jobId;
@Column(name = "build_number", nullable = false)
private Integer buildNumber;
@Column(name = "build_url", nullable = false)
private String buildUrl;
@Column(name = "build_status", nullable = false)
private String buildStatus;
@Column(name = "start_time", nullable = false)
private LocalDateTime startTime;
private Long duration;
@Column(name = "trigger_cause", columnDefinition = "TEXT")
private String triggerCause;
}

View File

@ -0,0 +1,46 @@
package com.qqchen.deploy.backend.entity;
import com.qqchen.deploy.backend.common.annotation.LogicDelete;
import com.qqchen.deploy.backend.common.domain.Entity;
import jakarta.persistence.Column;
import jakarta.persistence.Table;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.time.LocalDateTime;
@Data
@EqualsAndHashCode(callSuper = true)
@jakarta.persistence.Entity
@Table(name = "sys_jenkins_config")
@LogicDelete
public class JenkinsConfig extends Entity<Long> {
@Column(nullable = false)
private String name;
@Column(nullable = false)
private String url;
@Column(nullable = false)
private String username;
@Column(nullable = false)
private String password;
private Integer sort;
private String remark;
@Column(name = "last_all_sync_time")
private LocalDateTime lastAllSyncTime;
@Column(name = "last_view_sync_time")
private LocalDateTime lastViewSyncTime;
@Column(name = "last_job_sync_time")
private LocalDateTime lastJobSyncTime;
@Column(name = "last_build_sync_time")
private LocalDateTime lastBuildSyncTime;
}

View File

@ -0,0 +1,49 @@
package com.qqchen.deploy.backend.entity;
import com.qqchen.deploy.backend.common.annotation.LogicDelete;
import com.qqchen.deploy.backend.common.domain.Entity;
import jakarta.persistence.Column;
import jakarta.persistence.FetchType;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.time.LocalDateTime;
@Data
@EqualsAndHashCode(callSuper = true)
@jakarta.persistence.Entity
@Table(name = "sys_jenkins_job")
@LogicDelete
public class JenkinsJob extends Entity<Long> {
@Column(name = "jenkins_id", nullable = false)
private Long jenkinsId;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "view_id")
private JenkinsView view;
@Column(name = "job_name", nullable = false)
private String jobName;
@Column(name = "job_url", nullable = false)
private String jobUrl;
private String description;
private Boolean buildable;
@Column(name = "last_build_number")
private Integer lastBuildNumber;
@Column(name = "last_build_time")
private LocalDateTime lastBuildTime;
@Column(name = "last_build_status")
private String lastBuildStatus;
}

View File

@ -0,0 +1,53 @@
package com.qqchen.deploy.backend.entity;
import com.qqchen.deploy.backend.common.annotation.LogicDelete;
import com.qqchen.deploy.backend.common.domain.Entity;
import jakarta.persistence.Column;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.Table;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.time.LocalDateTime;
@Data
@EqualsAndHashCode(callSuper = true)
@jakarta.persistence.Entity
@Table(name = "sys_jenkins_sync_history")
@LogicDelete
public class JenkinsSyncHistory extends Entity<Long> {
@Column(name = "jenkins_id", nullable = false)
private Long jenkinsId;
@Column(name = "sync_type", nullable = false)
@Enumerated(EnumType.STRING)
private SyncType syncType;
@Column(nullable = false)
@Enumerated(EnumType.STRING)
private SyncStatus status;
@Column(name = "start_time", nullable = false)
private LocalDateTime startTime;
@Column(name = "end_time")
private LocalDateTime endTime;
@Column(name = "error_message", columnDefinition = "TEXT")
private String errorMessage;
public enum SyncType {
ALL, // 全量同步
VIEW, // 同步视图
JOB, // 同步作业
BUILD // 同步构建记录
}
public enum SyncStatus {
SUCCESS, // 同步成功
FAILED, // 同步失败
RUNNING // 同步中
}
}

View File

@ -0,0 +1,28 @@
package com.qqchen.deploy.backend.entity;
import com.qqchen.deploy.backend.common.annotation.LogicDelete;
import com.qqchen.deploy.backend.common.domain.Entity;
import jakarta.persistence.Column;
import jakarta.persistence.Table;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode(callSuper = true)
@jakarta.persistence.Entity
@Table(name = "sys_jenkins_view")
@LogicDelete
public class JenkinsView extends Entity<Long> {
@Column(name = "jenkins_id", nullable = false)
private Long jenkinsId;
@Column(name = "view_name", nullable = false)
private String viewName;
@Column(name = "view_url", nullable = false)
private String viewUrl;
private String description;
}

View File

@ -0,0 +1,43 @@
package com.qqchen.deploy.backend.entity;
import com.qqchen.deploy.backend.common.annotation.LogicDelete;
import com.qqchen.deploy.backend.common.domain.Entity;
import jakarta.persistence.Column;
import jakarta.persistence.Table;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode(callSuper = true)
@jakarta.persistence.Entity
@Table(name = "sys_menu")
@LogicDelete
public class Menu extends Entity<Long> {
@Column(nullable = false)
private String name;
private String permission;
private String path;
private String component;
@Column(nullable = false)
private Integer type; // 0-目录 1-菜单 2-按钮
private String icon;
@Column(name = "parent_id")
private Long parentId;
@Column(nullable = false)
private Integer sort = 0;
private Boolean hidden = false;
@Column(nullable = false)
private Boolean enabled = true;
}

View File

@ -0,0 +1,56 @@
package com.qqchen.deploy.backend.entity;
import com.qqchen.deploy.backend.common.annotation.LogicDelete;
import com.qqchen.deploy.backend.common.domain.Entity;
import jakarta.persistence.Column;
import jakarta.persistence.Table;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.time.LocalDateTime;
@Data
@EqualsAndHashCode(callSuper = true)
@jakarta.persistence.Entity
@Table(name = "sys_repository_branch")
@LogicDelete
public class RepositoryBranch extends Entity<Long> {
@Column(name = "repository_id", nullable = false)
private Long repositoryId;
@Column(name = "project_id", nullable = false)
private Long projectId;
@Column(nullable = false)
private String name;
@Column(name = "commit_id")
private String commitId;
@Column(name = "commit_message", columnDefinition = "TEXT")
private String commitMessage;
@Column(name = "commit_author")
private String commitAuthor;
@Column(name = "commit_date")
private LocalDateTime commitDate;
private Boolean protected_ = false;
@Column(name = "developers_can_push")
private Boolean developersCanPush = true;
@Column(name = "developers_can_merge")
private Boolean developersCanMerge = true;
@Column(name = "can_push")
private Boolean canPush = true;
@Column(name = "default_branch")
private Boolean defaultBranch = false;
@Column(name = "web_url")
private String webUrl;
}

View File

@ -0,0 +1,44 @@
package com.qqchen.deploy.backend.entity;
import com.qqchen.deploy.backend.common.annotation.LogicDelete;
import com.qqchen.deploy.backend.common.domain.Entity;
import jakarta.persistence.Column;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.Table;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.time.LocalDateTime;
@Data
@EqualsAndHashCode(callSuper = true)
@jakarta.persistence.Entity
@Table(name = "sys_repository_config")
@LogicDelete
public class RepositoryConfig extends Entity<Long> {
@Column(nullable = false)
private String name;
@Column(nullable = false)
private String url;
@Column(name = "access_token", nullable = false)
private String accessToken;
@Column(name = "api_version")
private String apiVersion;
private Integer sort;
private String remark;
@Column(name = "last_sync_time")
private LocalDateTime lastSyncTime;
@Column(name = "last_sync_status")
@Enumerated(EnumType.STRING)
private RepositorySyncHistory.SyncStatus lastSyncStatus;
}

View File

@ -0,0 +1,47 @@
package com.qqchen.deploy.backend.entity;
import com.qqchen.deploy.backend.common.annotation.LogicDelete;
import com.qqchen.deploy.backend.common.domain.Entity;
import jakarta.persistence.Column;
import jakarta.persistence.Table;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode(callSuper = true)
@jakarta.persistence.Entity
@Table(name = "sys_repository_group")
@LogicDelete
public class RepositoryGroup extends Entity<Long> {
@Column(name = "repository_id", nullable = false)
private Long repositoryId;
@Column(name = "group_id", nullable = false)
private Long groupId;
@Column(nullable = false)
private String name;
@Column(nullable = false)
private String path;
private String description;
private String visibility;
@Column(name = "parent_id")
private Long parentId;
@Column(name = "web_url")
private String webUrl;
@Column(name = "avatar_url")
private String avatarUrl;
@Column(nullable = false)
private Boolean enabled = true;
private Integer sort = 0;
}

View File

@ -0,0 +1,57 @@
package com.qqchen.deploy.backend.entity;
import com.qqchen.deploy.backend.common.annotation.LogicDelete;
import com.qqchen.deploy.backend.common.domain.Entity;
import jakarta.persistence.Column;
import jakarta.persistence.Table;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.time.LocalDateTime;
@Data
@EqualsAndHashCode(callSuper = true)
@jakarta.persistence.Entity
@Table(name = "sys_repository_project")
@LogicDelete
public class RepositoryProject extends Entity<Long> {
@Column(name = "repository_id", nullable = false)
private Long repositoryId;
@Column(name = "project_id", nullable = false)
private Long projectId;
@Column(nullable = false)
private String name;
@Column(nullable = false)
private String path;
private String description;
private String visibility;
@Column(name = "group_id")
private Long groupId;
@Column(name = "default_branch")
private String defaultBranch;
@Column(name = "web_url")
private String webUrl;
@Column(name = "ssh_url")
private String sshUrl;
@Column(name = "http_url")
private String httpUrl;
@Column(name = "last_activity_at")
private LocalDateTime lastActivityAt;
@Column(nullable = false)
private Boolean enabled = true;
private Integer sort = 0;
}

View File

@ -0,0 +1,48 @@
package com.qqchen.deploy.backend.entity;
import com.qqchen.deploy.backend.common.annotation.LogicDelete;
import com.qqchen.deploy.backend.common.domain.Entity;
import jakarta.persistence.Column;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.Table;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.time.LocalDateTime;
@Data
@EqualsAndHashCode(callSuper = true)
@jakarta.persistence.Entity
@Table(name = "sys_repository_sync_history")
@LogicDelete
public class RepositorySyncHistory extends Entity<Long> {
@Column(name = "repository_id", nullable = false)
private Long repositoryId;
@Column(name = "sync_type", nullable = false)
@Enumerated(EnumType.STRING)
private SyncType syncType;
@Column(nullable = false)
@Enumerated(EnumType.STRING)
private SyncStatus status;
@Column(name = "error_message", columnDefinition = "TEXT")
private String errorMessage;
@Column(name = "start_time", nullable = false)
private LocalDateTime startTime;
@Column(name = "end_time")
private LocalDateTime endTime;
public enum SyncType {
GROUP, PROJECT, BRANCH
}
public enum SyncStatus {
SUCCESS, FAILED, RUNNING
}
}

View File

@ -0,0 +1,31 @@
package com.qqchen.deploy.backend.entity;
import com.qqchen.deploy.backend.common.annotation.LogicDelete;
import com.qqchen.deploy.backend.common.domain.Entity;
import jakarta.persistence.Column;
import jakarta.persistence.Table;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode(callSuper = true)
@jakarta.persistence.Entity
@Table(name = "sys_role")
@LogicDelete
public class Role extends Entity<Long> {
@NotBlank(message = "角色名称不能为空")
@Column(nullable = false)
private String name;
@NotBlank(message = "角色编码不能为空")
@Column(nullable = false, unique = true)
private String code;
private String description;
@Column(nullable = false)
private Integer sort = 0;
}

View File

@ -0,0 +1,24 @@
package com.qqchen.deploy.backend.entity;
import com.qqchen.deploy.backend.common.annotation.LogicDelete;
import com.qqchen.deploy.backend.common.domain.Entity;
import jakarta.persistence.Column;
import jakarta.persistence.Table;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode(callSuper = true)
@jakarta.persistence.Entity
@Table(name = "sys_role_menu")
@LogicDelete
public class RoleMenu extends Entity<Long> {
@Column(name = "role_id", nullable = false)
private Long roleId;
@Column(name = "menu_id", nullable = false)
private Long menuId;
}

View File

@ -0,0 +1,39 @@
package com.qqchen.deploy.backend.entity;
import com.qqchen.deploy.backend.common.annotation.LogicDelete;
import com.qqchen.deploy.backend.common.domain.Entity;
import jakarta.persistence.Column;
import jakarta.persistence.Table;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode(callSuper = true)
@jakarta.persistence.Entity
@Table(name = "sys_tenant")
@LogicDelete
public class Tenant extends Entity<Long> {
@NotBlank(message = "租户名称不能为空")
@Column(nullable = false)
private String name;
@NotBlank(message = "租户编码不能为空")
@Column(nullable = false, unique = true)
private String code;
@Column(name = "contact_name")
private String contactName;
@Column(name = "contact_phone")
private String contactPhone;
private String email;
private String address;
@Column(nullable = false)
private Boolean enabled = true;
}

View File

@ -1,31 +1,143 @@
package com.qqchen.deploy.backend.entity;
import com.qqchen.deploy.backend.common.annotation.SoftDelete;
import com.qqchen.deploy.backend.common.domain.Entity;
import com.qqchen.deploy.backend.common.annotation.LogicDelete;
import com.qqchen.deploy.backend.common.domain.AggregateRoot;
import com.qqchen.deploy.backend.event.UserRoleChangedEvent;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Column;
import jakarta.persistence.OneToMany;
import jakarta.persistence.Table;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
@Data
@EqualsAndHashCode(callSuper = true)
@jakarta.persistence.Entity
@Table(name = "t_user")
@SoftDelete
public class User extends Entity<Long> {
@Table(name = "sys_user")
@LogicDelete
public class User extends AggregateRoot<Long> {
@Column(unique = true, nullable = false)
private String username;
@Column(unique = true, nullable = false)
private String email;
@Column(nullable = false)
private String password;
@Column(length = 50)
private String nickname;
private String email;
private String phone;
@Column(nullable = false)
private Boolean enabled = true;
}
@Column(name = "dept_id")
private Long deptId;
@Column(name = "dept_name")
private String deptName;
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true)
private Set<UserRole> userRoles = new HashSet<>();
public void addRole(Role role) {
UserRole userRole = new UserRole(this, role);
userRoles.add(userRole);
registerDomainEvent(new UserRoleChangedEvent(this.getId(), role.getId(), "ADD"));
validateState();
}
public void removeRole(Role role) {
boolean removed = userRoles.removeIf(ur -> ur.getRole().equals(role));
if (removed) {
registerDomainEvent(new UserRoleChangedEvent(this.getId(), role.getId(), "REMOVE"));
validateState();
}
}
public void changePassword(String oldPassword, String newPassword) {
if (!this.password.equals(oldPassword)) {
throw new IllegalArgumentException("Old password is incorrect");
}
this.password = newPassword;
registerDomainEvent(new PasswordChangedEvent(this.getId()));
validateState();
}
public void setEnabled(boolean enabled) {
if (this.enabled != enabled) {
this.enabled = enabled;
registerDomainEvent(new UserStatusChangedEvent(this.getId(), enabled));
validateState();
}
}
public void updateBasicInfo(String nickname, String phone, String email) {
boolean changed = false;
if (!Objects.equals(this.nickname, nickname)) {
this.nickname = nickname;
changed = true;
}
if (!Objects.equals(this.phone, phone)) {
this.phone = phone;
changed = true;
}
if (!Objects.equals(this.email, email)) {
this.email = email;
changed = true;
}
if (changed) {
registerDomainEvent(new UserUpdatedEvent(this.getId()));
validateState();
}
}
public boolean hasRole(String roleCode) {
return userRoles.stream()
.map(UserRole::getRole)
.anyMatch(role -> role.getCode().equals(roleCode));
}
public Set<String> getRoleCodes() {
return userRoles.stream()
.map(UserRole::getRole)
.map(Role::getCode)
.collect(Collectors.toSet());
}
@Override
protected void validateState() {
if (username == null || username.trim().isEmpty()) {
throw new IllegalStateException("Username cannot be empty");
}
if (email == null || !email.matches("^[A-Za-z0-9+_.-]+@(.+)$")) {
throw new IllegalStateException("Invalid email format");
}
if (password == null || password.length() < 6) {
throw new IllegalStateException("Password must be at least 6 characters");
}
if (phone != null && !phone.matches("^\\d{11}$")) {
throw new IllegalStateException("Invalid phone number format");
}
}
@Override
public void validateBeforeDelete() {
if (!userRoles.isEmpty()) {
throw new IllegalStateException("Cannot delete user with assigned roles");
}
}
}

View File

@ -0,0 +1,22 @@
package com.qqchen.deploy.backend.entity;
import com.qqchen.deploy.backend.common.annotation.LogicDelete;
import com.qqchen.deploy.backend.common.domain.Entity;
import jakarta.persistence.Column;
import jakarta.persistence.Table;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode(callSuper = true)
@jakarta.persistence.Entity
@Table(name = "sys_user_role")
@LogicDelete
public class UserRole extends Entity<Long> {
@Column(name = "user_id", nullable = false)
private Long userId;
@Column(name = "role_id", nullable = false)
private Long roleId;
}

View File

@ -0,0 +1,22 @@
package com.qqchen.deploy.backend.event;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.transaction.event.TransactionPhase;
import org.springframework.transaction.event.TransactionalEventListener;
@Slf4j
@Component
public class UserEventListener {
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void handleUserRoleChanged(UserRoleChangedEvent event) {
log.info("User role changed - userId: {}, roleId: {}, action: {}",
event.getUserId(),
event.getRoleId(),
event.getAction());
// 这里可以添加后续处理逻辑
// 例如发送通知更新缓存等
}
}

View File

@ -0,0 +1,17 @@
package com.qqchen.deploy.backend.event;
import com.qqchen.deploy.backend.common.event.DomainEvent;
import lombok.Getter;
@Getter
public class UserRoleChangedEvent extends DomainEvent {
private final Long userId;
private final Long roleId;
private final String action; // "ADD" or "REMOVE"
public UserRoleChangedEvent(Long userId, Long roleId, String action) {
super();
this.userId = userId;
this.roleId = roleId;
this.action = action;
}
}

View File

@ -0,0 +1,12 @@
# \u4E2D\u6587
response.success=\u64CD\u4F5C\u6210\u529F
response.error=\u7CFB\u7EDF\u9519\u8BEF
response.invalid.param=\u65E0\u6548\u7684\u53C2\u6570
response.unauthorized=\u672A\u6388\u6743
response.forbidden=\u7981\u6B62\u8BBF\u95EE
response.not.found=\u8D44\u6E90\u672A\u627E\u5230
response.conflict=\u8D44\u6E90\u51B2\u7A81
user.not.found=\u7528\u6237\u4E0D\u5B58\u5728
user.username.exists=\u7528\u6237\u540D\u5DF2\u5B58\u5728
user.email.exists=\u90AE\u7BB1\u5DF2\u5B58\u5728

View File

@ -1,12 +0,0 @@
# 中文
response.success=操作成功
response.error=系统错误
response.invalid.param=无效的参数
response.unauthorized=未授权
response.forbidden=禁止访问
response.not.found=资源未找到
response.conflict=资源冲突
user.not.found=用户不存在
user.username.exists=用户名已存在
user.email.exists=邮箱已存在

View File

@ -0,0 +1,12 @@
# \u4E2D\u6587
response.success=\u64CD\u4F5C\u6210\u529F
response.error=\u7CFB\u7EDF\u9519\u8BEF
response.invalid.param=\u65E0\u6548\u7684\u53C2\u6570
response.unauthorized=\u672A\u6388\u6743
response.forbidden=\u7981\u6B62\u8BBF\u95EE
response.not.found=\u8D44\u6E90\u672A\u627E\u5230
response.conflict=\u8D44\u6E90\u51B2\u7A81
user.not.found=\u7528\u6237\u4E0D\u5B58\u5728
user.username.exists=\u7528\u6237\u540D\u5DF2\u5B58\u5728
user.email.exists=\u90AE\u7BB1\u5DF2\u5B58\u5728

View File

@ -1,13 +0,0 @@
package com.qqchen.deploy.common;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class BackendApplicationTests {
@Test
void contextLoads() {
}
}