From 9089d9bd46b85af256c2a35b5bfa577fe4223113 Mon Sep 17 00:00:00 2001 From: dengqichen Date: Thu, 28 Nov 2024 17:36:22 +0800 Subject: [PATCH] =?UTF-8?q?=E5=8F=AF=E6=AD=A3=E5=B8=B8=E5=90=AF=E5=8A=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/pom.xml | 13 +++ .../deploy/backend/BackendApplication.java | 2 + .../framework/controller/BaseController.java | 15 +++ .../backend/framework/domain/Entity.java | 17 +++- .../backend/framework/enums/ResponseCode.java | 6 ++ .../exception/BusinessException.java | 11 +-- .../exception/GlobalExceptionHandler.java | 51 ++++++++-- .../framework/repository/IBaseRepository.java | 48 ++++++++++ .../framework/service/IBaseService.java | 4 + .../service/impl/BaseServiceImpl.java | 94 ++++++++++++++++++- .../src/main/resources/messages.properties | 39 +++++--- .../src/main/resources/messages_en.properties | 17 +++- .../main/resources/messages_zh_CN.properties | 41 +++++--- 13 files changed, 308 insertions(+), 50 deletions(-) diff --git a/backend/pom.xml b/backend/pom.xml index 4f433a24..72dab6f0 100644 --- a/backend/pom.xml +++ b/backend/pom.xml @@ -154,6 +154,19 @@ hutool-all ${hutool.version} + + org.springframework.retry + spring-retry + + + org.springframework.boot + spring-boot-starter-aop + + + com.google.guava + guava + 32.1.3-jre + diff --git a/backend/src/main/java/com/qqchen/deploy/backend/BackendApplication.java b/backend/src/main/java/com/qqchen/deploy/backend/BackendApplication.java index 8914c48f..b824e3d5 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/BackendApplication.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/BackendApplication.java @@ -4,7 +4,9 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.openfeign.EnableFeignClients; import org.springframework.data.jpa.repository.config.EnableJpaRepositories; +import org.springframework.retry.annotation.EnableRetry; +@EnableRetry @SpringBootApplication @EnableFeignClients public class BackendApplication { diff --git a/backend/src/main/java/com/qqchen/deploy/backend/framework/controller/BaseController.java b/backend/src/main/java/com/qqchen/deploy/backend/framework/controller/BaseController.java index 38459e85..d4d25e34 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/framework/controller/BaseController.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/framework/controller/BaseController.java @@ -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.api.Response; import com.qqchen.deploy.backend.framework.service.IBaseService; +import jakarta.servlet.http.HttpServletResponse; import org.springframework.data.domain.Page; import org.springframework.web.bind.annotation.*; import java.io.Serializable; import java.util.List; +import java.util.concurrent.CompletableFuture; /** * 通用REST控制器 @@ -70,4 +72,17 @@ public abstract class BaseController, ID extends Serializab List entities = service.findAll(query); return Response.success(converter.toResponseList(entities)); } + + @PostMapping("/batch") + public CompletableFuture> batchProcess(@RequestBody List entities) { + return CompletableFuture.runAsync(() -> { + service.batchProcess(entities); + }).thenApply(v -> Response.success()); + } + + @GetMapping("/export") + public void export(HttpServletResponse response, BaseQuery query) { + List data = service.findAll(query); + // 导出逻辑 + } } \ No newline at end of file diff --git a/backend/src/main/java/com/qqchen/deploy/backend/framework/domain/Entity.java b/backend/src/main/java/com/qqchen/deploy/backend/framework/domain/Entity.java index 4b530107..44d45cde 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/framework/domain/Entity.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/framework/domain/Entity.java @@ -6,6 +6,8 @@ import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.MappedSuperclass; +import jakarta.persistence.OptimisticLockException; +import jakarta.persistence.PreUpdate; import jakarta.persistence.Version; import lombok.Getter; import lombok.Setter; @@ -45,9 +47,22 @@ public abstract class Entity implements Serializable { private LocalDateTime updateTime; @Version - private Integer version; + @Column(name = "version", nullable = false) + private Integer version = 0; @Column(nullable = 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("数据已被其他用户修改"); + } + } } \ No newline at end of file diff --git a/backend/src/main/java/com/qqchen/deploy/backend/framework/enums/ResponseCode.java b/backend/src/main/java/com/qqchen/deploy/backend/framework/enums/ResponseCode.java index acd8bdae..2dbab190 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/framework/enums/ResponseCode.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/framework/enums/ResponseCode.java @@ -17,6 +17,12 @@ public enum ResponseCode { TENANT_NOT_FOUND(1001, "tenant.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开头) USER_NOT_FOUND(2001, "user.not.found"), USERNAME_EXISTS(2002, "user.username.exists"), diff --git a/backend/src/main/java/com/qqchen/deploy/backend/framework/exception/BusinessException.java b/backend/src/main/java/com/qqchen/deploy/backend/framework/exception/BusinessException.java index 04b4cd86..f19430f0 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/framework/exception/BusinessException.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/framework/exception/BusinessException.java @@ -1,22 +1,21 @@ package com.qqchen.deploy.backend.framework.exception; import com.qqchen.deploy.backend.framework.enums.ResponseCode; -import com.qqchen.deploy.backend.framework.utils.MessageUtils; import lombok.Getter; @Getter public class BusinessException extends RuntimeException { private final ResponseCode errorCode; + private final Object[] args; public BusinessException(ResponseCode errorCode) { - super(MessageUtils.getMessage(errorCode.getMessageKey())); - this.errorCode = errorCode; + this(errorCode, null); } - - public BusinessException(ResponseCode errorCode, Throwable cause) { - super(MessageUtils.getMessage(errorCode.getMessageKey()), cause); + public BusinessException(ResponseCode errorCode, Object[] args) { + super(); this.errorCode = errorCode; + this.args = args; } } \ No newline at end of file diff --git a/backend/src/main/java/com/qqchen/deploy/backend/framework/exception/GlobalExceptionHandler.java b/backend/src/main/java/com/qqchen/deploy/backend/framework/exception/GlobalExceptionHandler.java index 2340df54..fbde0c38 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/framework/exception/GlobalExceptionHandler.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/framework/exception/GlobalExceptionHandler.java @@ -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.enums.ResponseCode; +import jakarta.persistence.OptimisticLockException; +import jakarta.persistence.PessimisticLockException; 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.RestControllerAdvice; +import java.util.ConcurrentModificationException; + @Slf4j @RestControllerAdvice public class GlobalExceptionHandler { -// @ExceptionHandler(AuthenticationException.class) -// public ApiResult handleAuthenticationException(AuthenticationException e) { -// log.error("认证异常", e); -// if (e instanceof BadCredentialsException) { -// return ApiResult.error(401, "用户名或密码错误"); -// } -// return ApiResult.error(401, "认证失败:" + e.getMessage()); -// } + @Autowired + private MessageSource messageSource; + + private String getMessage(String key) { + try { + return messageSource.getMessage(key, null, LocaleContextHolder.getLocale()); + } catch (NoSuchMessageException e) { + return key; + } + } @ExceptionHandler(BusinessException.class) 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 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 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 handleConcurrentModificationException(ConcurrentModificationException ex) { + log.warn("Concurrent modification exception", ex); + return Response.error(ResponseCode.CONCURRENT_UPDATE_ERROR, getMessage("system.concurrent.update.error")); } @ExceptionHandler(Exception.class) diff --git a/backend/src/main/java/com/qqchen/deploy/backend/framework/repository/IBaseRepository.java b/backend/src/main/java/com/qqchen/deploy/backend/framework/repository/IBaseRepository.java index cb334a58..a38423f7 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/framework/repository/IBaseRepository.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/framework/repository/IBaseRepository.java @@ -1,6 +1,7 @@ package com.qqchen.deploy.backend.framework.repository; import com.qqchen.deploy.backend.framework.domain.Entity; +import jakarta.persistence.LockModeType; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.querydsl.QuerydslPredicateExecutor; 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.domain.Pageable; 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.util.List; @@ -121,4 +124,49 @@ public interface IBaseRepository, ID extends Serializable> iterable.forEach(result::add); 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 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 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 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 ids, + @Param("updateTime") LocalDateTime updateTime, + @Param("updateBy") String updateBy, + @Param("version") Integer version + ); } \ No newline at end of file diff --git a/backend/src/main/java/com/qqchen/deploy/backend/framework/service/IBaseService.java b/backend/src/main/java/com/qqchen/deploy/backend/framework/service/IBaseService.java index 2f409c1e..ec3eee8f 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/framework/service/IBaseService.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/framework/service/IBaseService.java @@ -24,4 +24,8 @@ public interface IBaseService, ID extends Serializable> { List findAll(BaseQuery query); Page page(BaseQuery query); + + void batchProcess(List entities); + + T updateWithRetry(T entity); } \ No newline at end of file diff --git a/backend/src/main/java/com/qqchen/deploy/backend/framework/service/impl/BaseServiceImpl.java b/backend/src/main/java/com/qqchen/deploy/backend/framework/service/impl/BaseServiceImpl.java index 92972191..4ae573af 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/framework/service/impl/BaseServiceImpl.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/framework/service/impl/BaseServiceImpl.java @@ -5,7 +5,9 @@ import java.time.LocalDateTime; import java.util.ArrayList; import java.util.Arrays; 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.domain.Entity; 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.service.IBaseService; 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.types.EntityPath; import com.querydsl.core.types.Path; import com.querydsl.core.types.Predicate; 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.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Sort; +import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; 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.lang.reflect.Field; -@Transactional +@Transactional(readOnly = true) +@Slf4j public abstract class BaseServiceImpl, ID extends Number & Serializable> implements IBaseService { protected final IBaseRepository repository; protected final EntityPath entityPath; + @PersistenceContext + protected EntityManager entityManager; + protected BaseServiceImpl(IBaseRepository repository) { this.repository = repository; this.entityPath = getEntityPath(); @@ -234,7 +249,7 @@ public abstract class BaseServiceImpl, ID extends Number & return new BigDecimal(strValue); } } catch (NumberFormatException e) { - // 忽略转换错误 + // 忽略转换误 } return null; } @@ -260,8 +275,13 @@ public abstract class BaseServiceImpl, ID extends Number & return repository.save(entity); } + @Transactional @Override public T update(T entity) { + // 先查询最新版本 + T currentEntity = findById(entity.getId()); + // 版本检查 + currentEntity.checkVersion(entity.getVersion()); return repository.save(entity); } @@ -273,7 +293,7 @@ public abstract class BaseServiceImpl, ID extends Number & @Override public T findById(ID id) { return repository.findById(id) - .orElseThrow(() -> new RuntimeException("Entity not found")); + .orElseThrow(() -> new EntityNotFoundException(id)); } @Override @@ -312,4 +332,72 @@ public abstract class BaseServiceImpl, ID extends Number & iterable.forEach(result::add); return result; } + + @Transactional(propagation = Propagation.REQUIRES_NEW) + @Override + public void batchProcess(List entities) { + LocalDateTime now = LocalDateTime.now(); + String operator = SecurityUtils.getCurrentUsername(); + + Lists.partition(entities, 500).forEach(batch -> { + try { + // 加锁查询最新数据 + List 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 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 getEntityClass() { + Class[] genericTypes = GenericTypeResolver.resolveTypeArguments(getClass(), BaseServiceImpl.class); + if (genericTypes != null && genericTypes.length > 0) { + return (Class) genericTypes[0]; + } + throw new IllegalStateException("Could not resolve entity class"); + } } \ No newline at end of file diff --git a/backend/src/main/resources/messages.properties b/backend/src/main/resources/messages.properties index dc8b4376..a61c74c1 100644 --- a/backend/src/main/resources/messages.properties +++ b/backend/src/main/resources/messages.properties @@ -1,18 +1,29 @@ -# \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 -response.unauthorized.full=\u8BBF\u95EE\u6B64\u8D44\u6E90\u9700\u8981\u5B8C\u5168\u8EAB\u4EFD\u9A8C\u8BC1 +# 通用响应 +response.success=操作成功 +response.error=系统错误 +response.invalid.param=无效的参数 +response.unauthorized=未授权 +response.forbidden=禁止访问 +response.not.found=资源未找到 +response.conflict=资源冲突 +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 -user.email.exists=\u90AE\u7BB1\u5DF2\u5B58\u5728 +# 系统异常消息 +system.optimistic.lock.error=数据已被其他用户修改,请刷新后重试 +system.pessimistic.lock.error=数据正被其他用户操作,请稍后重试 +system.concurrent.update.error=并发更新冲突,请重试 +system.retry.exceeded.error=操作重试次数超限,请稍后再试 -data.not.found=数据不存在 \ No newline at end of file +# Entity Not Found Messages +entity.not.found.id=找不到ID为{0}的实体 +entity.not.found.message={0} +entity.not.found.name.id=找不到ID为{1}的{0} \ No newline at end of file diff --git a/backend/src/main/resources/messages_en.properties b/backend/src/main/resources/messages_en.properties index 414c139f..ff38898e 100644 --- a/backend/src/main/resources/messages_en.properties +++ b/backend/src/main/resources/messages_en.properties @@ -1,4 +1,4 @@ -# \u9ED8\u8BA4\u8BED\u8A00\uFF08\u82F1\u6587\uFF09 +# Common Response response.success=Success response.error=System Error response.invalid.param=Invalid Parameter @@ -8,11 +8,22 @@ response.not.found=Resource Not Found response.conflict=Resource Conflict response.unauthorized.full=Full authentication is required to access this resource - +# Business Error tenant.not.found=Tenant not found +data.not.found={0} with id {1} not found +# User Related user.not.found=User not found user.username.exists=Username already exists user.email.exists=Email already exists -data.not.found=Data not found \ No newline at end of file +# 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 \ No newline at end of file diff --git a/backend/src/main/resources/messages_zh_CN.properties b/backend/src/main/resources/messages_zh_CN.properties index da4c906b..6069cbab 100644 --- a/backend/src/main/resources/messages_zh_CN.properties +++ b/backend/src/main/resources/messages_zh_CN.properties @@ -1,16 +1,29 @@ -# \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 -response.unauthorized.full=\u8BBF\u95EE\u6B64\u8D44\u6E90\u9700\u8981\u5B8C\u5168\u8EAB\u4EFD\u9A8C\u8BC1 +# 通用响应 +response.success=操作成功 +response.error=系统错误 +response.invalid.param=无效的参数 +response.unauthorized=未授权 +response.forbidden=禁止访问 +response.not.found=资源未找到 +response.conflict=资源冲突 +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.email.exists=\u90AE\u7BB1\u5DF2\u5B58\u5728 -data.not.found=数据不存在 \ No newline at end of file +# 用户相关 +user.not.found=用户不存在 +user.username.exists=用户名已存在 +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} \ No newline at end of file