可正常启动
This commit is contained in:
parent
2597946ef8
commit
9089d9bd46
@ -154,6 +154,19 @@
|
|||||||
<artifactId>hutool-all</artifactId>
|
<artifactId>hutool-all</artifactId>
|
||||||
<version>${hutool.version}</version>
|
<version>${hutool.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.retry</groupId>
|
||||||
|
<artifactId>spring-retry</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-aop</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.google.guava</groupId>
|
||||||
|
<artifactId>guava</artifactId>
|
||||||
|
<version>32.1.3-jre</version>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
<build>
|
<build>
|
||||||
|
|||||||
@ -4,7 +4,9 @@ import org.springframework.boot.SpringApplication;
|
|||||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
import org.springframework.cloud.openfeign.EnableFeignClients;
|
import org.springframework.cloud.openfeign.EnableFeignClients;
|
||||||
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
|
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
|
||||||
|
import org.springframework.retry.annotation.EnableRetry;
|
||||||
|
|
||||||
|
@EnableRetry
|
||||||
@SpringBootApplication
|
@SpringBootApplication
|
||||||
@EnableFeignClients
|
@EnableFeignClients
|
||||||
public class BackendApplication {
|
public class BackendApplication {
|
||||||
|
|||||||
@ -7,11 +7,13 @@ import com.qqchen.deploy.backend.framework.query.BaseQuery;
|
|||||||
import com.qqchen.deploy.backend.framework.dto.BaseRequest;
|
import com.qqchen.deploy.backend.framework.dto.BaseRequest;
|
||||||
import com.qqchen.deploy.backend.framework.api.Response;
|
import com.qqchen.deploy.backend.framework.api.Response;
|
||||||
import com.qqchen.deploy.backend.framework.service.IBaseService;
|
import com.qqchen.deploy.backend.framework.service.IBaseService;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
import org.springframework.data.domain.Page;
|
import org.springframework.data.domain.Page;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 通用REST控制器
|
* 通用REST控制器
|
||||||
@ -70,4 +72,17 @@ public abstract class BaseController<T extends Entity<ID>, ID extends Serializab
|
|||||||
List<T> entities = service.findAll(query);
|
List<T> entities = service.findAll(query);
|
||||||
return Response.success(converter.toResponseList(entities));
|
return Response.success(converter.toResponseList(entities));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PostMapping("/batch")
|
||||||
|
public CompletableFuture<Response<Void>> batchProcess(@RequestBody List<T> entities) {
|
||||||
|
return CompletableFuture.runAsync(() -> {
|
||||||
|
service.batchProcess(entities);
|
||||||
|
}).thenApply(v -> Response.success());
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/export")
|
||||||
|
public void export(HttpServletResponse response, BaseQuery query) {
|
||||||
|
List<T> data = service.findAll(query);
|
||||||
|
// 导出逻辑
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -6,6 +6,8 @@ import jakarta.persistence.GeneratedValue;
|
|||||||
import jakarta.persistence.GenerationType;
|
import jakarta.persistence.GenerationType;
|
||||||
import jakarta.persistence.Id;
|
import jakarta.persistence.Id;
|
||||||
import jakarta.persistence.MappedSuperclass;
|
import jakarta.persistence.MappedSuperclass;
|
||||||
|
import jakarta.persistence.OptimisticLockException;
|
||||||
|
import jakarta.persistence.PreUpdate;
|
||||||
import jakarta.persistence.Version;
|
import jakarta.persistence.Version;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.Setter;
|
import lombok.Setter;
|
||||||
@ -45,9 +47,22 @@ public abstract class Entity<ID extends Serializable> implements Serializable {
|
|||||||
private LocalDateTime updateTime;
|
private LocalDateTime updateTime;
|
||||||
|
|
||||||
@Version
|
@Version
|
||||||
private Integer version;
|
@Column(name = "version", nullable = false)
|
||||||
|
private Integer version = 0;
|
||||||
|
|
||||||
@Column(nullable = false)
|
@Column(nullable = false)
|
||||||
private Boolean deleted = false;
|
private Boolean deleted = false;
|
||||||
|
|
||||||
|
@PreUpdate
|
||||||
|
protected void onPreUpdate() {
|
||||||
|
if (this.version == null) {
|
||||||
|
this.version = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void checkVersion(Integer expectedVersion) {
|
||||||
|
if (expectedVersion != null && !expectedVersion.equals(this.version)) {
|
||||||
|
throw new OptimisticLockException("数据已被其他用户修改");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -17,6 +17,12 @@ public enum ResponseCode {
|
|||||||
TENANT_NOT_FOUND(1001, "tenant.not.found"),
|
TENANT_NOT_FOUND(1001, "tenant.not.found"),
|
||||||
DATA_NOT_FOUND(1002, "data.not.found"),
|
DATA_NOT_FOUND(1002, "data.not.found"),
|
||||||
|
|
||||||
|
// 系统异常 (1开头)
|
||||||
|
OPTIMISTIC_LOCK_ERROR(1003, "system.optimistic.lock.error"), // 乐观锁异常
|
||||||
|
PESSIMISTIC_LOCK_ERROR(1004, "system.pessimistic.lock.error"), // 悲观锁异常
|
||||||
|
CONCURRENT_UPDATE_ERROR(1005, "system.concurrent.update.error"), // 并发更新异常
|
||||||
|
RETRY_EXCEEDED_ERROR(1006, "system.retry.exceeded.error"), // 重试次数超限异常
|
||||||
|
|
||||||
// 用户相关错误码(2开头)
|
// 用户相关错误码(2开头)
|
||||||
USER_NOT_FOUND(2001, "user.not.found"),
|
USER_NOT_FOUND(2001, "user.not.found"),
|
||||||
USERNAME_EXISTS(2002, "user.username.exists"),
|
USERNAME_EXISTS(2002, "user.username.exists"),
|
||||||
|
|||||||
@ -1,22 +1,21 @@
|
|||||||
package com.qqchen.deploy.backend.framework.exception;
|
package com.qqchen.deploy.backend.framework.exception;
|
||||||
|
|
||||||
import com.qqchen.deploy.backend.framework.enums.ResponseCode;
|
import com.qqchen.deploy.backend.framework.enums.ResponseCode;
|
||||||
import com.qqchen.deploy.backend.framework.utils.MessageUtils;
|
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
|
|
||||||
@Getter
|
@Getter
|
||||||
public class BusinessException extends RuntimeException {
|
public class BusinessException extends RuntimeException {
|
||||||
|
|
||||||
private final ResponseCode errorCode;
|
private final ResponseCode errorCode;
|
||||||
|
private final Object[] args;
|
||||||
|
|
||||||
public BusinessException(ResponseCode errorCode) {
|
public BusinessException(ResponseCode errorCode) {
|
||||||
super(MessageUtils.getMessage(errorCode.getMessageKey()));
|
this(errorCode, null);
|
||||||
this.errorCode = errorCode;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public BusinessException(ResponseCode errorCode, Object[] args) {
|
||||||
public BusinessException(ResponseCode errorCode, Throwable cause) {
|
super();
|
||||||
super(MessageUtils.getMessage(errorCode.getMessageKey()), cause);
|
|
||||||
this.errorCode = errorCode;
|
this.errorCode = errorCode;
|
||||||
|
this.args = args;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -2,26 +2,59 @@ package com.qqchen.deploy.backend.framework.exception;
|
|||||||
|
|
||||||
import com.qqchen.deploy.backend.framework.api.Response;
|
import com.qqchen.deploy.backend.framework.api.Response;
|
||||||
import com.qqchen.deploy.backend.framework.enums.ResponseCode;
|
import com.qqchen.deploy.backend.framework.enums.ResponseCode;
|
||||||
|
import jakarta.persistence.OptimisticLockException;
|
||||||
|
import jakarta.persistence.PessimisticLockException;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.context.MessageSource;
|
||||||
|
import org.springframework.context.NoSuchMessageException;
|
||||||
|
import org.springframework.context.i18n.LocaleContextHolder;
|
||||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||||
|
|
||||||
|
import java.util.ConcurrentModificationException;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@RestControllerAdvice
|
@RestControllerAdvice
|
||||||
public class GlobalExceptionHandler {
|
public class GlobalExceptionHandler {
|
||||||
|
|
||||||
// @ExceptionHandler(AuthenticationException.class)
|
@Autowired
|
||||||
// public ApiResult<String> handleAuthenticationException(AuthenticationException e) {
|
private MessageSource messageSource;
|
||||||
// log.error("认证异常", e);
|
|
||||||
// if (e instanceof BadCredentialsException) {
|
private String getMessage(String key) {
|
||||||
// return ApiResult.error(401, "用户名或密码错误");
|
try {
|
||||||
// }
|
return messageSource.getMessage(key, null, LocaleContextHolder.getLocale());
|
||||||
// return ApiResult.error(401, "认证失败:" + e.getMessage());
|
} catch (NoSuchMessageException e) {
|
||||||
// }
|
return key;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@ExceptionHandler(BusinessException.class)
|
@ExceptionHandler(BusinessException.class)
|
||||||
public Response<?> handleApiException(BusinessException e) {
|
public Response<?> handleApiException(BusinessException e) {
|
||||||
return Response.error(e.getErrorCode());
|
String message = messageSource.getMessage(
|
||||||
|
e.getErrorCode().getMessageKey(),
|
||||||
|
e.getArgs(),
|
||||||
|
LocaleContextHolder.getLocale()
|
||||||
|
);
|
||||||
|
return Response.error(e.getErrorCode(), message);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ExceptionHandler(OptimisticLockException.class)
|
||||||
|
public Response<Void> handleOptimisticLockException(OptimisticLockException ex) {
|
||||||
|
log.warn("Optimistic lock exception", ex);
|
||||||
|
return Response.error(ResponseCode.OPTIMISTIC_LOCK_ERROR, getMessage("system.optimistic.lock.error"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@ExceptionHandler(PessimisticLockException.class)
|
||||||
|
public Response<Void> handlePessimisticLockException(PessimisticLockException ex) {
|
||||||
|
log.warn("Pessimistic lock exception", ex);
|
||||||
|
return Response.error(ResponseCode.PESSIMISTIC_LOCK_ERROR, getMessage("system.pessimistic.lock.error"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@ExceptionHandler(ConcurrentModificationException.class)
|
||||||
|
public Response<Void> handleConcurrentModificationException(ConcurrentModificationException ex) {
|
||||||
|
log.warn("Concurrent modification exception", ex);
|
||||||
|
return Response.error(ResponseCode.CONCURRENT_UPDATE_ERROR, getMessage("system.concurrent.update.error"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ExceptionHandler(Exception.class)
|
@ExceptionHandler(Exception.class)
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
package com.qqchen.deploy.backend.framework.repository;
|
package com.qqchen.deploy.backend.framework.repository;
|
||||||
|
|
||||||
import com.qqchen.deploy.backend.framework.domain.Entity;
|
import com.qqchen.deploy.backend.framework.domain.Entity;
|
||||||
|
import jakarta.persistence.LockModeType;
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
import org.springframework.data.querydsl.QuerydslPredicateExecutor;
|
import org.springframework.data.querydsl.QuerydslPredicateExecutor;
|
||||||
import org.springframework.data.repository.NoRepositoryBean;
|
import org.springframework.data.repository.NoRepositoryBean;
|
||||||
@ -8,6 +9,8 @@ import org.springframework.data.jpa.repository.Query;
|
|||||||
import org.springframework.data.jpa.repository.Modifying;
|
import org.springframework.data.jpa.repository.Modifying;
|
||||||
import org.springframework.data.domain.Pageable;
|
import org.springframework.data.domain.Pageable;
|
||||||
import org.springframework.data.domain.Sort;
|
import org.springframework.data.domain.Sort;
|
||||||
|
import org.springframework.data.repository.query.Param;
|
||||||
|
import org.springframework.data.jpa.repository.Lock;
|
||||||
|
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@ -121,4 +124,49 @@ public interface IBaseRepository<T extends Entity<ID>, ID extends Serializable>
|
|||||||
iterable.forEach(result::add);
|
iterable.forEach(result::add);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 批量插入优化
|
||||||
|
@Modifying
|
||||||
|
@Query(value = "INSERT INTO #{#entityName} (id, create_time, create_by) VALUES (:id, :createTime, :createBy)",
|
||||||
|
nativeQuery = true)
|
||||||
|
void batchInsert(
|
||||||
|
@Param("id") ID id,
|
||||||
|
@Param("createTime") LocalDateTime createTime,
|
||||||
|
@Param("createBy") String createBy
|
||||||
|
);
|
||||||
|
|
||||||
|
// 批量更新优化
|
||||||
|
@Modifying
|
||||||
|
@Query("UPDATE #{#entityName} e SET e.updateTime = :updateTime, e.updateBy = :updateBy WHERE e.id IN :ids")
|
||||||
|
void batchUpdate(
|
||||||
|
@Param("ids") Collection<ID> ids,
|
||||||
|
@Param("updateTime") LocalDateTime updateTime,
|
||||||
|
@Param("updateBy") String updateBy
|
||||||
|
);
|
||||||
|
|
||||||
|
// 批量逻辑删除优化
|
||||||
|
@Modifying
|
||||||
|
@Query("UPDATE #{#entityName} e SET e.deleted = true, e.updateTime = :updateTime, e.updateBy = :updateBy WHERE e.id IN :ids")
|
||||||
|
void batchLogicDelete(
|
||||||
|
@Param("ids") Collection<ID> ids,
|
||||||
|
@Param("updateTime") LocalDateTime updateTime,
|
||||||
|
@Param("updateBy") String updateBy
|
||||||
|
);
|
||||||
|
|
||||||
|
// 添加悲观锁查询
|
||||||
|
@Lock(LockModeType.PESSIMISTIC_WRITE)
|
||||||
|
@Query("SELECT e FROM #{#entityName} e WHERE e.id = :id AND e.deleted = false")
|
||||||
|
Optional<T> findByIdWithLock(@Param("id") ID id);
|
||||||
|
|
||||||
|
// 批量更新时添加版本控制
|
||||||
|
@Modifying
|
||||||
|
@Query("UPDATE #{#entityName} e SET e.updateTime = :updateTime, " +
|
||||||
|
"e.updateBy = :updateBy, e.version = e.version + 1 " +
|
||||||
|
"WHERE e.id IN :ids AND e.version = :version")
|
||||||
|
int batchUpdateWithVersion(
|
||||||
|
@Param("ids") Collection<ID> ids,
|
||||||
|
@Param("updateTime") LocalDateTime updateTime,
|
||||||
|
@Param("updateBy") String updateBy,
|
||||||
|
@Param("version") Integer version
|
||||||
|
);
|
||||||
}
|
}
|
||||||
@ -24,4 +24,8 @@ public interface IBaseService<T extends Entity<ID>, ID extends Serializable> {
|
|||||||
List<T> findAll(BaseQuery query);
|
List<T> findAll(BaseQuery query);
|
||||||
|
|
||||||
Page<T> page(BaseQuery query);
|
Page<T> page(BaseQuery query);
|
||||||
|
|
||||||
|
void batchProcess(List<T> entities);
|
||||||
|
|
||||||
|
T updateWithRetry(T entity);
|
||||||
}
|
}
|
||||||
@ -5,7 +5,9 @@ import java.time.LocalDateTime;
|
|||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import com.google.common.collect.Lists;
|
||||||
import com.qqchen.deploy.backend.framework.annotation.LogicDelete;
|
import com.qqchen.deploy.backend.framework.annotation.LogicDelete;
|
||||||
import com.qqchen.deploy.backend.framework.domain.Entity;
|
import com.qqchen.deploy.backend.framework.domain.Entity;
|
||||||
import com.qqchen.deploy.backend.framework.enums.QueryType;
|
import com.qqchen.deploy.backend.framework.enums.QueryType;
|
||||||
@ -16,28 +18,41 @@ import com.qqchen.deploy.backend.framework.repository.IBaseRepository;
|
|||||||
import com.qqchen.deploy.backend.framework.annotation.QueryField;
|
import com.qqchen.deploy.backend.framework.annotation.QueryField;
|
||||||
import com.qqchen.deploy.backend.framework.service.IBaseService;
|
import com.qqchen.deploy.backend.framework.service.IBaseService;
|
||||||
import com.qqchen.deploy.backend.framework.utils.EntityPathResolver;
|
import com.qqchen.deploy.backend.framework.utils.EntityPathResolver;
|
||||||
|
import com.qqchen.deploy.backend.framework.security.SecurityUtils;
|
||||||
import com.querydsl.core.BooleanBuilder;
|
import com.querydsl.core.BooleanBuilder;
|
||||||
import com.querydsl.core.types.EntityPath;
|
import com.querydsl.core.types.EntityPath;
|
||||||
import com.querydsl.core.types.Path;
|
import com.querydsl.core.types.Path;
|
||||||
import com.querydsl.core.types.Predicate;
|
import com.querydsl.core.types.Predicate;
|
||||||
import com.querydsl.core.types.dsl.*;
|
import com.querydsl.core.types.dsl.*;
|
||||||
|
import com.qqchen.deploy.backend.framework.exception.EntityNotFoundException;
|
||||||
|
import jakarta.persistence.OptimisticLockException;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.core.GenericTypeResolver;
|
import org.springframework.core.GenericTypeResolver;
|
||||||
import org.springframework.data.domain.Page;
|
import org.springframework.data.domain.Page;
|
||||||
import org.springframework.data.domain.PageRequest;
|
import org.springframework.data.domain.PageRequest;
|
||||||
import org.springframework.data.domain.Sort;
|
import org.springframework.data.domain.Sort;
|
||||||
|
import org.springframework.transaction.annotation.Propagation;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
import org.springframework.util.StringUtils;
|
import org.springframework.util.StringUtils;
|
||||||
|
import org.springframework.retry.annotation.Backoff;
|
||||||
|
import org.springframework.retry.annotation.Retryable;
|
||||||
|
import jakarta.persistence.EntityManager;
|
||||||
|
import jakarta.persistence.PersistenceContext;
|
||||||
|
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
import java.lang.reflect.Field;
|
import java.lang.reflect.Field;
|
||||||
|
|
||||||
@Transactional
|
@Transactional(readOnly = true)
|
||||||
|
@Slf4j
|
||||||
public abstract class BaseServiceImpl<T extends Entity<ID>, ID extends Number & Serializable> implements IBaseService<T, ID> {
|
public abstract class BaseServiceImpl<T extends Entity<ID>, ID extends Number & Serializable> implements IBaseService<T, ID> {
|
||||||
|
|
||||||
protected final IBaseRepository<T, ID> repository;
|
protected final IBaseRepository<T, ID> repository;
|
||||||
|
|
||||||
protected final EntityPath<T> entityPath;
|
protected final EntityPath<T> entityPath;
|
||||||
|
|
||||||
|
@PersistenceContext
|
||||||
|
protected EntityManager entityManager;
|
||||||
|
|
||||||
protected BaseServiceImpl(IBaseRepository<T, ID> repository) {
|
protected BaseServiceImpl(IBaseRepository<T, ID> repository) {
|
||||||
this.repository = repository;
|
this.repository = repository;
|
||||||
this.entityPath = getEntityPath();
|
this.entityPath = getEntityPath();
|
||||||
@ -234,7 +249,7 @@ public abstract class BaseServiceImpl<T extends Entity<ID>, ID extends Number &
|
|||||||
return new BigDecimal(strValue);
|
return new BigDecimal(strValue);
|
||||||
}
|
}
|
||||||
} catch (NumberFormatException e) {
|
} catch (NumberFormatException e) {
|
||||||
// 忽略转换错误
|
// 忽略转换误
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -260,8 +275,13 @@ public abstract class BaseServiceImpl<T extends Entity<ID>, ID extends Number &
|
|||||||
return repository.save(entity);
|
return repository.save(entity);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
@Override
|
@Override
|
||||||
public T update(T entity) {
|
public T update(T entity) {
|
||||||
|
// 先查询最新版本
|
||||||
|
T currentEntity = findById(entity.getId());
|
||||||
|
// 版本检查
|
||||||
|
currentEntity.checkVersion(entity.getVersion());
|
||||||
return repository.save(entity);
|
return repository.save(entity);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -273,7 +293,7 @@ public abstract class BaseServiceImpl<T extends Entity<ID>, ID extends Number &
|
|||||||
@Override
|
@Override
|
||||||
public T findById(ID id) {
|
public T findById(ID id) {
|
||||||
return repository.findById(id)
|
return repository.findById(id)
|
||||||
.orElseThrow(() -> new RuntimeException("Entity not found"));
|
.orElseThrow(() -> new EntityNotFoundException(id));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -312,4 +332,72 @@ public abstract class BaseServiceImpl<T extends Entity<ID>, ID extends Number &
|
|||||||
iterable.forEach(result::add);
|
iterable.forEach(result::add);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Transactional(propagation = Propagation.REQUIRES_NEW)
|
||||||
|
@Override
|
||||||
|
public void batchProcess(List<T> entities) {
|
||||||
|
LocalDateTime now = LocalDateTime.now();
|
||||||
|
String operator = SecurityUtils.getCurrentUsername();
|
||||||
|
|
||||||
|
Lists.partition(entities, 500).forEach(batch -> {
|
||||||
|
try {
|
||||||
|
// 加锁查询最新数据
|
||||||
|
List<T> currentEntities = batch.stream()
|
||||||
|
.map(e -> repository.findById(e.getId())
|
||||||
|
.orElseThrow(() -> new EntityNotFoundException(getEntityClass().getSimpleName(), e.getId())))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
// 版本检查
|
||||||
|
for (int i = 0; i < batch.size(); i++) {
|
||||||
|
currentEntities.get(i).checkVersion(batch.get(i).getVersion());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 批量更新
|
||||||
|
List<ID> ids = currentEntities.stream()
|
||||||
|
.map(Entity::getId)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
repository.batchUpdate(ids, now, operator);
|
||||||
|
repository.flush();
|
||||||
|
entityManager.clear();
|
||||||
|
} catch (OptimisticLockException e) {
|
||||||
|
// 记录失败的批次
|
||||||
|
log.error("Batch update failed for batch size: {}", batch.size(), e);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
@Retryable(
|
||||||
|
value = {OptimisticLockException.class},
|
||||||
|
maxAttempts = 3,
|
||||||
|
backoff = @Backoff(delay = 1000, multiplier = 2)
|
||||||
|
)
|
||||||
|
@Override
|
||||||
|
public T updateWithRetry(T entity) {
|
||||||
|
try {
|
||||||
|
return update(entity);
|
||||||
|
} catch (OptimisticLockException e) {
|
||||||
|
// 重试前先刷新实体
|
||||||
|
entityManager.refresh(entity);
|
||||||
|
throw e; // 抛出异常触发重试
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加悲观锁查询方法
|
||||||
|
@Transactional
|
||||||
|
public T findByIdWithLock(ID id) {
|
||||||
|
return repository.findByIdWithLock(id)
|
||||||
|
.orElseThrow(() -> new EntityNotFoundException(getEntityClass().getSimpleName(), id));
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
protected Class<T> getEntityClass() {
|
||||||
|
Class<?>[] genericTypes = GenericTypeResolver.resolveTypeArguments(getClass(), BaseServiceImpl.class);
|
||||||
|
if (genericTypes != null && genericTypes.length > 0) {
|
||||||
|
return (Class<T>) genericTypes[0];
|
||||||
|
}
|
||||||
|
throw new IllegalStateException("Could not resolve entity class");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -1,18 +1,29 @@
|
|||||||
# \u4E2D\u6587
|
# 通用响应
|
||||||
response.success=\u64CD\u4F5C\u6210\u529F
|
response.success=操作成功
|
||||||
response.error=\u7CFB\u7EDF\u9519\u8BEF
|
response.error=系统错误
|
||||||
response.invalid.param=\u65E0\u6548\u7684\u53C2\u6570
|
response.invalid.param=无效的参数
|
||||||
response.unauthorized=\u672A\u6388\u6743
|
response.unauthorized=未授权
|
||||||
response.forbidden=\u7981\u6B62\u8BBF\u95EE
|
response.forbidden=禁止访问
|
||||||
response.not.found=\u8D44\u6E90\u672A\u627E\u5230
|
response.not.found=资源未找到
|
||||||
response.conflict=\u8D44\u6E90\u51B2\u7A81
|
response.conflict=资源冲突
|
||||||
response.unauthorized.full=\u8BBF\u95EE\u6B64\u8D44\u6E90\u9700\u8981\u5B8C\u5168\u8EAB\u4EFD\u9A8C\u8BC1
|
response.unauthorized.full=访问此资源需要完全身份验证
|
||||||
|
|
||||||
|
# 业务错误
|
||||||
|
tenant.not.found=租户不存在
|
||||||
|
data.not.found=找不到ID为{0}的{1}
|
||||||
|
|
||||||
tenant.not.found=\u79DF\u6237\u4E0D\u5B58\u5728
|
# 用户相关
|
||||||
|
user.not.found=用户不存在
|
||||||
|
user.username.exists=用户名已存在
|
||||||
|
user.email.exists=邮箱已存在
|
||||||
|
|
||||||
user.not.found=\u7528\u6237\u4E0D\u5B58\u5728
|
# 系统异常消息
|
||||||
user.username.exists=\u7528\u6237\u540D\u5DF2\u5B58\u5728
|
system.optimistic.lock.error=数据已被其他用户修改,请刷新后重试
|
||||||
user.email.exists=\u90AE\u7BB1\u5DF2\u5B58\u5728
|
system.pessimistic.lock.error=数据正被其他用户操作,请稍后重试
|
||||||
|
system.concurrent.update.error=并发更新冲突,请重试
|
||||||
|
system.retry.exceeded.error=操作重试次数超限,请稍后再试
|
||||||
|
|
||||||
data.not.found=数据不存在
|
# Entity Not Found Messages
|
||||||
|
entity.not.found.id=找不到ID为{0}的实体
|
||||||
|
entity.not.found.message={0}
|
||||||
|
entity.not.found.name.id=找不到ID为{1}的{0}
|
||||||
@ -1,4 +1,4 @@
|
|||||||
# \u9ED8\u8BA4\u8BED\u8A00\uFF08\u82F1\u6587\uFF09
|
# Common Response
|
||||||
response.success=Success
|
response.success=Success
|
||||||
response.error=System Error
|
response.error=System Error
|
||||||
response.invalid.param=Invalid Parameter
|
response.invalid.param=Invalid Parameter
|
||||||
@ -8,11 +8,22 @@ response.not.found=Resource Not Found
|
|||||||
response.conflict=Resource Conflict
|
response.conflict=Resource Conflict
|
||||||
response.unauthorized.full=Full authentication is required to access this resource
|
response.unauthorized.full=Full authentication is required to access this resource
|
||||||
|
|
||||||
|
# Business Error
|
||||||
tenant.not.found=Tenant not found
|
tenant.not.found=Tenant not found
|
||||||
|
data.not.found={0} with id {1} not found
|
||||||
|
|
||||||
|
# User Related
|
||||||
user.not.found=User not found
|
user.not.found=User not found
|
||||||
user.username.exists=Username already exists
|
user.username.exists=Username already exists
|
||||||
user.email.exists=Email already exists
|
user.email.exists=Email already exists
|
||||||
|
|
||||||
data.not.found=Data not found
|
# System Exception Messages
|
||||||
|
system.optimistic.lock.error=Data has been modified by another user, please refresh and try again
|
||||||
|
system.pessimistic.lock.error=Data is being operated by another user, please try again later
|
||||||
|
system.concurrent.update.error=Concurrent update conflict, please try again
|
||||||
|
system.retry.exceeded.error=Operation retry limit exceeded, please try again later
|
||||||
|
|
||||||
|
# Entity Not Found Messages
|
||||||
|
entity.not.found.id=Entity with id {0} not found
|
||||||
|
entity.not.found.message={0}
|
||||||
|
entity.not.found.name.id={0} with id {1} not found
|
||||||
@ -1,16 +1,29 @@
|
|||||||
# \u4E2D\u6587
|
# 通用响应
|
||||||
response.success=\u64CD\u4F5C\u6210\u529F
|
response.success=操作成功
|
||||||
response.error=\u7CFB\u7EDF\u9519\u8BEF
|
response.error=系统错误
|
||||||
response.invalid.param=\u65E0\u6548\u7684\u53C2\u6570
|
response.invalid.param=无效的参数
|
||||||
response.unauthorized=\u672A\u6388\u6743
|
response.unauthorized=未授权
|
||||||
response.forbidden=\u7981\u6B62\u8BBF\u95EE
|
response.forbidden=禁止访问
|
||||||
response.not.found=\u8D44\u6E90\u672A\u627E\u5230
|
response.not.found=资源未找到
|
||||||
response.conflict=\u8D44\u6E90\u51B2\u7A81
|
response.conflict=资源冲突
|
||||||
response.unauthorized.full=\u8BBF\u95EE\u6B64\u8D44\u6E90\u9700\u8981\u5B8C\u5168\u8EAB\u4EFD\u9A8C\u8BC1
|
response.unauthorized.full=访问此资源需要完全身份验证
|
||||||
|
|
||||||
tenant.not.found=\u79DF\u6237\u4E0D\u5B58\u5728
|
# 业务错误
|
||||||
|
tenant.not.found=租户不存在
|
||||||
|
data.not.found=数据不存在
|
||||||
|
|
||||||
user.not.found=\u7528\u6237\u4E0D\u5B58\u5728
|
# 用户相关
|
||||||
user.username.exists=\u7528\u6237\u540D\u5DF2\u5B58\u5728
|
user.not.found=用户不存在
|
||||||
user.email.exists=\u90AE\u7BB1\u5DF2\u5B58\u5728
|
user.username.exists=用户名已存在
|
||||||
data.not.found=数据不存在
|
user.email.exists=邮箱已存在
|
||||||
|
|
||||||
|
# 系统异常消息
|
||||||
|
system.optimistic.lock.error=数据已被其他用户修改,请刷新后重试
|
||||||
|
system.pessimistic.lock.error=数据正被其他用户操作,请稍后重试
|
||||||
|
system.concurrent.update.error=并发更新冲突,请重试
|
||||||
|
system.retry.exceeded.error=操作重试次数超限,请稍后再试
|
||||||
|
|
||||||
|
# Entity Not Found Messages
|
||||||
|
entity.not.found.id=找不到ID为{0}的实体
|
||||||
|
entity.not.found.message={0}
|
||||||
|
entity.not.found.name.id=找不到ID为{1}的{0}
|
||||||
Loading…
Reference in New Issue
Block a user