可正常启动

This commit is contained in:
dengqichen 2024-11-28 17:36:22 +08:00
parent 2597946ef8
commit 9089d9bd46
13 changed files with 308 additions and 50 deletions

View File

@ -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>

View File

@ -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 {

View File

@ -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);
// 导出逻辑
}
} }

View File

@ -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("数据已被其他用户修改");
}
}
} }

View File

@ -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"),

View File

@ -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;
} }
} }

View File

@ -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)

View File

@ -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
);
} }

View File

@ -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);
} }

View File

@ -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");
}
} }

View File

@ -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}

View File

@ -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

View File

@ -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=租户不存在
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=数据不存在 data.not.found=数据不存在
# 用户相关
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}