文档概述
本文档系统对比 Go 与 Java(Spring Boot)在分层架构中的设计差异,涵盖参数校验、身份认证、权限控制、缓存策略、日志系统、异常处理、事务管理等核心模块。
一、层级对照表
| 层级 | Go 命名 | Java 命名 | 职责 |
|---|---|---|---|
| 入口层 | handler / controller | Controller | 接收请求、参数校验、调用服务 |
| 业务层 | service / usecase | Service | 业务逻辑、事务编排 |
| 数据层 | repository / store | Mapper / DAO / Repository | 数据持久化 |
| 实体层 | entity / model / domain | Entity / PO / DO | 数据结构定义 |
| 传输层 | dto / request / response | DTO / VO / BO | 数据传输对象 |
| 中间件层 | middleware | Filter / Interceptor / Aspect | 认证、鉴权、日志、限流 |
| 配置层 | config | Configuration / Properties | 配置管理 |
二、企业级目录结构对比
Go 典型结构
code
project/
├── cmd/
│ └── server/
│ └── main.go # 程序入口
├── internal/
│ ├── handler/ # HTTP 处理器
│ │ ├── user_handler.go
│ │ ├── order_handler.go
│ │ └── middleware/ # 中间件
│ │ ├── auth.go # 认证中间件
│ │ ├── permission.go # 权限中间件
│ │ ├── logger.go # 日志中间件
│ │ ├── ratelimit.go # 限流中间件
│ │ └── recovery.go # 异常恢复
│ ├── service/ # 业务逻辑
│ │ ├── user_service.go
│ │ └── order_service.go
│ ├── repository/ # 数据访问
│ │ ├── user_repository.go
│ │ ├── order_repository.go
│ │ └── cache/ # 缓存实现
│ │ └── user_cache.go
│ ├── entity/ # 实体定义
│ │ ├── user.go
│ │ └── order.go
│ ├── dto/ # 传输对象
│ │ ├── request/
│ │ │ ├── user_request.go
│ │ │ └── order_request.go
│ │ └── response/
│ │ ├── user_response.go
│ │ └── common.go # 统一响应结构
│ └── errors/ # 错误定义
│ └── errors.go
├── pkg/ # 可复用包
│ ├── auth/ # JWT 工具
│ │ └── jwt.go
│ ├── cache/ # 缓存抽象
│ │ └── redis.go
│ ├── logger/ # 日志工具
│ │ └── logger.go
│ ├── validator/ # 校验器
│ │ └── validator.go
│ └── utils/
│ └── crypto.go
├── config/ # 配置
│ ├── config.go
│ └── config.yaml
├── migrations/ # 数据库迁移
└── go.mod
Java 典型结构(Spring Boot)
code
project/
├── src/main/java/com/example/
│ ├── Application.java # 程序入口
│ ├── controller/ # 控制器
│ │ ├── UserController.java
│ │ └── OrderController.java
│ ├── service/ # 服务接口
│ │ ├── UserService.java
│ │ └── OrderService.java
│ ├── service/impl/ # 服务实现
│ │ ├── UserServiceImpl.java
│ │ └── OrderServiceImpl.java
│ ├── mapper/ # MyBatis Mapper
│ │ ├── UserMapper.java
│ │ └── OrderMapper.java
│ ├── entity/ # 实体类
│ │ ├── User.java
│ │ └── Order.java
│ ├── dto/ # 传输对象
│ │ ├── request/
│ │ │ ├── CreateUserRequest.java
│ │ │ └── UpdateUserRequest.java
│ │ └── response/
│ │ ├── UserResponse.java
│ │ └── CommonResponse.java
│ ├── config/ # 配置类
│ │ ├── SecurityConfig.java # 安全配置
│ │ ├── RedisConfig.java # Redis 配置
│ │ └── WebMvcConfig.java # MVC 配置
│ ├── filter/ # 过滤器
│ │ └── JwtAuthenticationFilter.java
│ ├── interceptor/ # 拦截器
│ │ ├── AuthInterceptor.java
│ │ └── PermissionInterceptor.java
│ ├── aspect/ # 切面
│ │ ├── LogAspect.java
│ │ └── PermissionAspect.java
│ ├── exception/ # 异常处理
│ │ ├── BusinessException.java
│ │ └── GlobalExceptionHandler.java
│ ├── enums/ # 枚举
│ │ └── ErrorCode.java
│ └── util/ # 工具类
│ ├── JwtUtil.java
│ └── RedisUtil.java
├── src/main/resources/
│ ├── mapper/ # MyBatis XML
│ │ └── UserMapper.xml
│ ├── application.yml
│ └── application-dev.yml
└── pom.xml
三、核心架构流程图
3.1 请求处理流程图
3.2 完整请求时序图
Go 请求时序图
Java 请求时序图
3.3 认证授权流程图
3.4 权限校验流程图
3.5 缓存处理流程图
3.6 异常处理流程图
3.7 事务处理流程图
3.8 完整系统架构图
3.9 数据流转图
3.10 并发处理模型对比
四、参数校验
Go 实现
go
// pkg/validator/validator.go
package validator
import (
"regexp"
"github.com/go-playground/validator/v10"
)
var validate *validator.Validate
func init() {
validate = validator.New()
// 注册自定义校验器
validate.RegisterValidation("mobile", validateMobile)
validate.RegisterValidation("idcard", validateIDCard)
}
func validateMobile(fl validator.FieldLevel) bool {
mobile := fl.Field().String()
matched, _ := regexp.MatchString(`^1[3-9]\d{9}$`, mobile)
return matched
}
func validateIDCard(fl validator.FieldLevel) bool {
idcard := fl.Field().String()
matched, _ := regexp.MatchString(`^\d{17}[\dXx]$`, idcard)
return matched
}
func Validate(s interface{}) error {
return validate.Struct(s)
}
// 获取第一个校验错误的友好提示
func GetValidationError(err error) string {
if errs, ok := err.(validator.ValidationErrors); ok {
for _, e := range errs {
return formatFieldError(e)
}
}
return err.Error()
}
func formatFieldError(e validator.FieldError) string {
fieldMessages := map[string]map[string]string{
"Username": {
"required": "用户名不能为空",
"min": "用户名长度不能少于3个字符",
"max": "用户名长度不能超过32个字符",
},
"Mobile": {
"required": "手机号不能为空",
"mobile": "手机号格式不正确",
},
"Email": {
"required": "邮箱不能为空",
"email": "邮箱格式不正确",
},
}
if messages, ok := fieldMessages[e.Field()]; ok {
if msg, ok := messages[e.Tag()]; ok {
return msg
}
}
return e.Error()
}
go
// internal/dto/request/user_request.go
package request
type CreateUserRequest struct {
Username string `json:"username" binding:"required,min=3,max=32"`
Password string `json:"password" binding:"required,min=8,max=64"`
Email string `json:"email" binding:"required,email"`
Mobile string `json:"mobile" binding:"required,mobile"`
IDCard string `json:"id_card" binding:"omitempty,idcard"`
Age int `json:"age" binding:"omitempty,gte=0,lte=150"`
RoleID int64 `json:"role_id" binding:"required,gt=0"`
}
type UpdateUserRequest struct {
Username *string `json:"username" binding:"omitempty,min=3,max=32"`
Email *string `json:"email" binding:"omitempty,email"`
Mobile *string `json:"mobile" binding:"omitempty,mobile"`
Status *int `json:"status" binding:"omitempty,oneof=0 1 2"`
}
type ListUserRequest struct {
Page int `form:"page" binding:"omitempty,gte=1"`
PageSize int `form:"page_size" binding:"omitempty,gte=1,lte=100"`
Keyword string `form:"keyword" binding:"omitempty,max=64"`
Status *int `form:"status" binding:"omitempty,oneof=0 1 2"`
OrderBy string `form:"order_by" binding:"omitempty,oneof=created_at updated_at"`
Order string `form:"order" binding:"omitempty,oneof=asc desc"`
}
func (r *ListUserRequest) SetDefaults() {
if r.Page <= 0 {
r.Page = 1
}
if r.PageSize <= 0 {
r.PageSize = 20
}
if r.OrderBy == "" {
r.OrderBy = "created_at"
}
if r.Order == "" {
r.Order = "desc"
}
}
go
// internal/handler/user_handler.go
func (h *UserHandler) Create(c *gin.Context) {
var req request.CreateUserRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, validator.GetValidationError(err))
return
}
// 业务层额外校验(如唯一性)
user, err := h.userService.Create(c.Request.Context(), &req)
if err != nil {
response.Error(c, err)
return
}
response.Success(c, dto.ToUserResponse(user))
}
Java 实现
java
// 自定义校验注解
// validator/Mobile.java
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = MobileValidator.class)
public @interface Mobile {
String message() default "手机号格式不正确";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
// validator/MobileValidator.java
public class MobileValidator implements ConstraintValidator<Mobile, String> {
private static final Pattern MOBILE_PATTERN = Pattern.compile("^1[3-9]\\d{9}$");
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
if (value == null || value.isEmpty()) {
return true; // 由 @NotBlank 处理空值
}
return MOBILE_PATTERN.matcher(value).matches();
}
}
// dto/request/CreateUserRequest.java
@Data
public class CreateUserRequest {
@NotBlank(message = "用户名不能为空")
@Size(min = 3, max = 32, message = "用户名长度需在3-32个字符之间")
private String username;
@NotBlank(message = "密码不能为空")
@Size(min = 8, max = 64, message = "密码长度需在8-64个字符之间")
private String password;
@NotBlank(message = "邮箱不能为空")
@Email(message = "邮箱格式不正确")
private String email;
@NotBlank(message = "手机号不能为空")
@Mobile
private String mobile;
@Pattern(regexp = "^\\d{17}[\\dXx]$", message = "身份证号格式不正确")
private String idCard;
@Min(value = 0, message = "年龄不能小于0")
@Max(value = 150, message = "年龄不能大于150")
private Integer age;
@NotNull(message = "角色ID不能为空")
@Positive(message = "角色ID必须为正数")
private Long roleId;
}
// dto/request/UpdateUserRequest.java
@Data
public class UpdateUserRequest {
@Size(min = 3, max = 32, message = "用户名长度需在3-32个字符之间")
private String username;
@Email(message = "邮箱格式不正确")
private String email;
@Mobile
private String mobile;
@ValidStatus // 自定义校验:值必须在 0, 1, 2 中
private Integer status;
}
// dto/request/ListUserRequest.java
@Data
public class ListUserRequest {
@Min(value = 1, message = "页码最小为1")
private Integer page = 1;
@Min(value = 1, message = "每页条数最小为1")
@Max(value = 100, message = "每页条数最大为100")
private Integer pageSize = 20;
@Size(max = 64, message = "关键词最大64个字符")
private String keyword;
private Integer status;
@Pattern(regexp = "^(created_at|updated_at)$", message = "排序字段不合法")
private String orderBy = "created_at";
@Pattern(regexp = "^(asc|desc)$", message = "排序方向不合法")
private String order = "desc";
}
// controller/UserController.java
@RestController
@RequestMapping("/api/v1/users")
@Validated
public class UserController {
@Autowired
private UserService userService;
@PostMapping
public CommonResponse<UserResponse> create(
@Valid @RequestBody CreateUserRequest request) {
User user = userService.create(request);
return CommonResponse.success(UserResponse.from(user));
}
@GetMapping
public CommonResponse<PageResult<UserResponse>> list(
@Valid ListUserRequest request) {
return CommonResponse.success(userService.list(request));
}
}
校验对比表
| 方面 | Go | Java |
|---|---|---|
| 校验库 | go-playground/validator | Hibernate Validator (JSR-380) |
| 校验位置 | Handler 层显式调用 | @Valid 注解自动触发 |
| 自定义校验 | RegisterValidation 函数 | @Constraint 注解 + Validator 类 |
| 错误消息 | 手动映射 | message 属性声明 |
| 分组校验 | 通过标签组合 | groups 分组 |
五、身份认证
Go 实现(JWT)
go
// pkg/auth/jwt.go
package auth
import (
"errors"
"time"
"github.com/golang-jwt/jwt/v5"
)
var (
ErrTokenExpired = errors.New("token已过期")
ErrTokenInvalid = errors.New("token无效")
ErrTokenMalformed = errors.New("token格式错误")
)
type Claims struct {
UserID int64 `json:"user_id"`
Username string `json:"username"`
RoleID int64 `json:"role_id"`
jwt.RegisteredClaims
}
type JWTManager struct {
secretKey []byte
accessExpiry time.Duration
refreshExpiry time.Duration
issuer string
}
func NewJWTManager(secret string, accessExpiry, refreshExpiry time.Duration) *JWTManager {
return &JWTManager{
secretKey: []byte(secret),
accessExpiry: accessExpiry,
refreshExpiry: refreshExpiry,
issuer: "myapp",
}
}
type TokenPair struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
ExpiresIn int64 `json:"expires_in"`
}
func (m *JWTManager) GenerateTokenPair(userID int64, username string, roleID int64) (*TokenPair, error) {
now := time.Now()
// Access Token
accessClaims := Claims{
UserID: userID,
Username: username,
RoleID: roleID,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(now.Add(m.accessExpiry)),
IssuedAt: jwt.NewNumericDate(now),
NotBefore: jwt.NewNumericDate(now),
Issuer: m.issuer,
Subject: "access",
},
}
accessToken := jwt.NewWithClaims(jwt.SigningMethodHS256, accessClaims)
accessTokenString, err := accessToken.SignedString(m.secretKey)
if err != nil {
return nil, err
}
// Refresh Token
refreshClaims := Claims{
UserID: userID,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(now.Add(m.refreshExpiry)),
IssuedAt: jwt.NewNumericDate(now),
Subject: "refresh",
},
}
refreshToken := jwt.NewWithClaims(jwt.SigningMethodHS256, refreshClaims)
refreshTokenString, err := refreshToken.SignedString(m.secretKey)
if err != nil {
return nil, err
}
return &TokenPair{
AccessToken: accessTokenString,
RefreshToken: refreshTokenString,
ExpiresIn: int64(m.accessExpiry.Seconds()),
}, nil
}
func (m *JWTManager) ParseToken(tokenString string) (*Claims, error) {
token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) {
return m.secretKey, nil
})
if err != nil {
if errors.Is(err, jwt.ErrTokenExpired) {
return nil, ErrTokenExpired
}
if errors.Is(err, jwt.ErrTokenMalformed) {
return nil, ErrTokenMalformed
}
return nil, ErrTokenInvalid
}
if claims, ok := token.Claims.(*Claims); ok && token.Valid {
return claims, nil
}
return nil, ErrTokenInvalid
}
go
// internal/handler/middleware/auth.go
package middleware
import (
"strings"
"github.com/gin-gonic/gin"
)
type contextKey string
const (
UserIDKey contextKey = "user_id"
UsernameKey contextKey = "username"
RoleIDKey contextKey = "role_id"
)
func AuthMiddleware(jwtManager *auth.JWTManager, cache cache.TokenCache) gin.HandlerFunc {
return func(c *gin.Context) {
authHeader := c.GetHeader("Authorization")
if authHeader == "" {
response.Unauthorized(c, "缺少认证信息")
c.Abort()
return
}
parts := strings.SplitN(authHeader, " ", 2)
if len(parts) != 2 || parts[0] != "Bearer" {
response.Unauthorized(c, "认证格式错误")
c.Abort()
return
}
tokenString := parts[1]
// 检查 Token 是否在黑名单中(用于登出)
if cache.IsBlacklisted(c.Request.Context(), tokenString) {
response.Unauthorized(c, "Token已失效")
c.Abort()
return
}
claims, err := jwtManager.ParseToken(tokenString)
if err != nil {
switch err {
case auth.ErrTokenExpired:
response.Unauthorized(c, "Token已过期")
default:
response.Unauthorized(c, "Token无效")
}
c.Abort()
return
}
// 存入上下文
c.Set(string(UserIDKey), claims.UserID)
c.Set(string(UsernameKey), claims.Username)
c.Set(string(RoleIDKey), claims.RoleID)
c.Next()
}
}
// 获取当前用户ID
func GetCurrentUserID(c *gin.Context) int64 {
if v, exists := c.Get(string(UserIDKey)); exists {
return v.(int64)
}
return 0
}
// 获取当前用户角色
func GetCurrentRoleID(c *gin.Context) int64 {
if v, exists := c.Get(string(RoleIDKey)); exists {
return v.(int64)
}
return 0
}
Java 实现(Spring Security + JWT)
java
// util/JwtUtil.java
@Component
public class JwtUtil {
@Value("${jwt.secret}")
private String secret;
@Value("${jwt.access-expiry:7200}")
private long accessExpiry; // 秒
@Value("${jwt.refresh-expiry:604800}")
private long refreshExpiry; // 秒
@Data
@AllArgsConstructor
public static class TokenPair {
private String accessToken;
private String refreshToken;
private Long expiresIn;
}
public TokenPair generateTokenPair(Long userId, String username, Long roleId) {
Date now = new Date();
String accessToken = Jwts.builder()
.setSubject(userId.toString())
.claim("username", username)
.claim("role_id", roleId)
.claim("type", "access")
.setIssuedAt(now)
.setExpiration(new Date(now.getTime() + accessExpiry * 1000))
.signWith(Keys.hmacShaKeyFor(secret.getBytes()), SignatureAlgorithm.HS256)
.compact();
String refreshToken = Jwts.builder()
.setSubject(userId.toString())
.claim("type", "refresh")
.setIssuedAt(now)
.setExpiration(new Date(now.getTime() + refreshExpiry * 1000))
.signWith(Keys.hmacShaKeyFor(secret.getBytes()), SignatureAlgorithm.HS256)
.compact();
return new TokenPair(accessToken, refreshToken, accessExpiry);
}
public Claims parseToken(String token) {
return Jwts.parserBuilder()
.setSigningKey(Keys.hmacShaKeyFor(secret.getBytes()))
.build()
.parseClaimsJws(token)
.getBody();
}
public Long getUserIdFromToken(String token) {
return Long.parseLong(parseToken(token).getSubject());
}
public boolean isTokenExpired(String token) {
try {
return parseToken(token).getExpiration().before(new Date());
} catch (ExpiredJwtException e) {
return true;
}
}
}
// filter/JwtAuthenticationFilter.java
@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtUtil jwtUtil;
private final UserDetailsService userDetailsService;
private final RedisTemplate<String, Object> redisTemplate;
private static final String TOKEN_BLACKLIST_PREFIX = "token:blacklist:";
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
String authHeader = request.getHeader("Authorization");
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
filterChain.doFilter(request, response);
return;
}
String token = authHeader.substring(7);
try {
// 检查黑名单
if (Boolean.TRUE.equals(redisTemplate.hasKey(TOKEN_BLACKLIST_PREFIX + token))) {
sendErrorResponse(response, HttpStatus.UNAUTHORIZED, "Token已失效");
return;
}
Long userId = jwtUtil.getUserIdFromToken(token);
if (userId != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = userDetailsService.loadUserByUsername(userId.toString());
if (!jwtUtil.isTokenExpired(token)) {
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
} catch (ExpiredJwtException e) {
sendErrorResponse(response, HttpStatus.UNAUTHORIZED, "Token已过期");
return;
} catch (JwtException e) {
sendErrorResponse(response, HttpStatus.UNAUTHORIZED, "Token无效");
return;
}
filterChain.doFilter(request, response);
}
private void sendErrorResponse(HttpServletResponse response, HttpStatus status, String message)
throws IOException {
response.setStatus(status.value());
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(
String.format("{\"code\":%d,\"message\":\"%s\"}", status.value(), message));
}
}
// config/SecurityConfig.java
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
private final JwtAuthenticationFilter jwtAuthenticationFilter;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(AbstractHttpConfigurer::disable)
.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/v1/auth/**").permitAll()
.requestMatchers("/api/v1/public/**").permitAll()
.requestMatchers("/actuator/health").permitAll()
.anyRequest().authenticated())
.addFilterBefore(jwtAuthenticationFilter,
UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
认证对比表
| 方面 | Go | Java |
|---|---|---|
| JWT 库 | golang-jwt/jwt | jjwt / spring-security-jwt |
| 认证实现 | 中间件(Middleware) | Filter + Spring Security |
| Token 存储 | Context(gin.Context) | SecurityContext |
| 黑名单 | Redis 手动实现 | Redis 手动实现 |
| 密码加密 | bcrypt 手动调用 | PasswordEncoder Bean |
六、权限控制
Go 实现(RBAC)
go
// internal/service/permission_service.go
package service
type PermissionService interface {
GetRolePermissions(ctx context.Context, roleID int64) ([]string, error)
HasPermission(ctx context.Context, roleID int64, permCode string) (bool, error)
CheckPermissions(ctx context.Context, roleID int64, permCodes ...string) (bool, error)
}
type permissionService struct {
roleRepo repository.RoleRepository
permissionRepo repository.PermissionRepository
cache cache.Cache
}
func NewPermissionService(
roleRepo repository.RoleRepository,
permRepo repository.PermissionRepository,
cache cache.Cache,
) PermissionService {
return &permissionService{
roleRepo: roleRepo,
permissionRepo: permRepo,
cache: cache,
}
}
func (s *permissionService) GetRolePermissions(ctx context.Context, roleID int64) ([]string, error) {
// 先查缓存
cacheKey := fmt.Sprintf("role:permissions:%d", roleID)
var permissions []string
if err := s.cache.Get(ctx, cacheKey, &permissions); err == nil {
return permissions, nil
}
// 查数据库
permissions, err := s.permissionRepo.FindCodesByRoleID(ctx, roleID)
if err != nil {
return nil, err
}
// 写入缓存,过期时间 30 分钟
_ = s.cache.Set(ctx, cacheKey, permissions, 30*time.Minute)
return permissions, nil
}
func (s *permissionService) HasPermission(ctx context.Context, roleID int64, permCode string) (bool, error) {
permissions, err := s.GetRolePermissions(ctx, roleID)
if err != nil {
return false, err
}
for _, p := range permissions {
if p == permCode || p == "*" { // * 表示超级权限
return true, nil
}
// 支持通配符匹配:user:* 匹配 user:create, user:read 等
if strings.HasSuffix(p, ":*") {
prefix := strings.TrimSuffix(p, "*")
if strings.HasPrefix(permCode, prefix) {
return true, nil
}
}
}
return false, nil
}
func (s *permissionService) CheckPermissions(ctx context.Context, roleID int64, permCodes ...string) (bool, error) {
for _, code := range permCodes {
has, err := s.HasPermission(ctx, roleID, code)
if err != nil {
return false, err
}
if !has {
return false, nil
}
}
return true, nil
}
go
// internal/handler/middleware/permission.go
package middleware
import (
"github.com/gin-gonic/gin"
)
// RequirePermission 创建权限校验中间件
func RequirePermission(permService service.PermissionService, permissions ...string) gin.HandlerFunc {
return func(c *gin.Context) {
roleID := GetCurrentRoleID(c)
if roleID == 0 {
response.Forbidden(c, "未获取到角色信息")
c.Abort()
return
}
has, err := permService.CheckPermissions(c.Request.Context(), roleID, permissions...)
if err != nil {
response.InternalError(c, "权限校验失败")
c.Abort()
return
}
if !has {
response.Forbidden(c, "权限不足")
c.Abort()
return
}
c.Next()
}
}
// RequireAnyPermission 满足任一权限即可
func RequireAnyPermission(permService service.PermissionService, permissions ...string) gin.HandlerFunc {
return func(c *gin.Context) {
roleID := GetCurrentRoleID(c)
if roleID == 0 {
response.Forbidden(c, "未获取到角色信息")
c.Abort()
return
}
for _, perm := range permissions {
has, err := permService.HasPermission(c.Request.Context(), roleID, perm)
if err != nil {
continue
}
if has {
c.Next()
return
}
}
response.Forbidden(c, "权限不足")
c.Abort()
}
}
// RequireRole 角色校验
func RequireRole(roles ...string) gin.HandlerFunc {
roleSet := make(map[string]struct{})
for _, r := range roles {
roleSet[r] = struct{}{}
}
return func(c *gin.Context) {
roleCode := GetCurrentRoleCode(c)
if _, ok := roleSet[roleCode]; !ok {
response.Forbidden(c, "角色权限不足")
c.Abort()
return
}
c.Next()
}
}
go
// 路由注册示例
// cmd/server/router.go
func SetupRouter(
userHandler *handler.UserHandler,
orderHandler *handler.OrderHandler,
permService service.PermissionService,
jwtManager *auth.JWTManager,
tokenCache cache.TokenCache,
) *gin.Engine {
r := gin.New()
// 全局中间件
r.Use(middleware.Recovery())
r.Use(middleware.Logger())
r.Use(middleware.RateLimit(100, time.Minute))
r.Use(middleware.CORS())
// 公开路由
public := r.Group("/api/v1")
{
public.POST("/auth/login", userHandler.Login)
public.POST("/auth/register", userHandler.Register)
public.POST("/auth/refresh", userHandler.RefreshToken)
}
// 需要认证的路由
authorized := r.Group("/api/v1")
authorized.Use(middleware.AuthMiddleware(jwtManager, tokenCache))
{
// 用户模块
users := authorized.Group("/users")
{
users.GET("",
middleware.RequirePermission(permService, "user:list"),
userHandler.List)
users.GET("/:id",
middleware.RequirePermission(permService, "user:read"),
userHandler.GetByID)
users.POST("",
middleware.RequirePermission(permService, "user:create"),
userHandler.Create)
users.PUT("/:id",
middleware.RequirePermission(permService, "user:update"),
userHandler.Update)
users.DELETE("/:id",
middleware.RequirePermission(permService, "user:delete"),
userHandler.Delete)
}
// 订单模块
orders := authorized.Group("/orders")
{
orders.GET("",
middleware.RequireAnyPermission(permService, "order:list", "order:*"),
orderHandler.List)
orders.POST("/:id/cancel",
middleware.RequirePermission(permService, "order:cancel"),
orderHandler.Cancel)
}
// 管理员专用
admin := authorized.Group("/admin")
admin.Use(middleware.RequireRole("admin", "super_admin"))
{
admin.GET("/dashboard", userHandler.Dashboard)
admin.POST("/users/:id/ban", userHandler.BanUser)
}
}
return r
}
Java 实现(Spring Security + 自定义注解)
java
// annotation/RequirePermission.java
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface RequirePermission {
String[] value(); // 所需权限码
Logical logical() default Logical.AND; // 权限间逻辑关系
}
// annotation/RequireRole.java
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface RequireRole {
String[] value();
}
// enums/Logical.java
public enum Logical {
AND, // 所有权限都需要
OR // 满足任一即可
}
// service/PermissionService.java
public interface PermissionService {
List<String> getRolePermissions(Long roleId);
boolean hasPermission(Long roleId, String permCode);
boolean checkPermissions(Long roleId, List<String> permCodes, Logical logical);
}
// service/impl/PermissionServiceImpl.java
@Service
@RequiredArgsConstructor
public class PermissionServiceImpl implements PermissionService {
private final RoleMapper roleMapper;
private final PermissionMapper permissionMapper;
private final RedisTemplate<String, Object> redisTemplate;
private static final String ROLE_PERM_KEY = "role:permissions:";
private static final long CACHE_EXPIRE = 30; // 分钟
@Override
@SuppressWarnings("unchecked")
public List<String> getRolePermissions(Long roleId) {
String cacheKey = ROLE_PERM_KEY + roleId;
// 查缓存
Object cached = redisTemplate.opsForValue().get(cacheKey);
if (cached != null) {
return (List<String>) cached;
}
// 查数据库
List<String> permissions = permissionMapper.selectCodesByRoleId(roleId);
// 写缓存
redisTemplate.opsForValue().set(cacheKey, permissions, CACHE_EXPIRE, TimeUnit.MINUTES);
return permissions;
}
@Override
public boolean hasPermission(Long roleId, String permCode) {
List<String> permissions = getRolePermissions(roleId);
for (String p : permissions) {
if (p.equals(permCode) || p.equals("*")) {
return true;
}
// 通配符支持
if (p.endsWith(":*")) {
String prefix = p.substring(0, p.length() - 1);
if (permCode.startsWith(prefix)) {
return true;
}
}
}
return false;
}
@Override
public boolean checkPermissions(Long roleId, List<String> permCodes, Logical logical) {
if (logical == Logical.AND) {
return permCodes.stream().allMatch(code -> hasPermission(roleId, code));
} else {
return permCodes.stream().anyMatch(code -> hasPermission(roleId, code));
}
}
}
// aspect/PermissionAspect.java
@Aspect
@Component
@RequiredArgsConstructor
@Order(2) // 在认证之后执行
public class PermissionAspect {
private final PermissionService permissionService;
@Before("@annotation(requirePermission)")
public void checkPermission(JoinPoint joinPoint, RequirePermission requirePermission) {
Long roleId = getCurrentRoleId();
if (roleId == null) {
throw new ForbiddenException("未获取到角色信息");
}
List<String> required = Arrays.asList(requirePermission.value());
boolean hasPermission = permissionService.checkPermissions(
roleId, required, requirePermission.logical());
if (!hasPermission) {
throw new ForbiddenException("权限不足");
}
}
@Before("@annotation(requireRole)")
public void checkRole(JoinPoint joinPoint, RequireRole requireRole) {
String currentRole = getCurrentRoleCode();
if (currentRole == null) {
throw new ForbiddenException("未获取到角色信息");
}
Set<String> allowedRoles = Set.of(requireRole.value());
if (!allowedRoles.contains(currentRole)) {
throw new ForbiddenException("角色权限不足");
}
}
private Long getCurrentRoleId() {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth != null && auth.getPrincipal() instanceof CustomUserDetails) {
return ((CustomUserDetails) auth.getPrincipal()).getRoleId();
}
return null;
}
private String getCurrentRoleCode() {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth != null && auth.getPrincipal() instanceof CustomUserDetails) {
return ((CustomUserDetails) auth.getPrincipal()).getRoleCode();
}
return null;
}
}
// controller/UserController.java
@RestController
@RequestMapping("/api/v1/users")
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
@GetMapping
@RequirePermission("user:list")
public CommonResponse<PageResult<UserResponse>> list(@Valid ListUserRequest request) {
return CommonResponse.success(userService.list(request));
}
@GetMapping("/{id}")
@RequirePermission("user:read")
public CommonResponse<UserResponse> getById(@PathVariable Long id) {
return CommonResponse.success(userService.getById(id));
}
@PostMapping
@RequirePermission("user:create")
public CommonResponse<UserResponse> create(@Valid @RequestBody CreateUserRequest request) {
return CommonResponse.success(userService.create(request));
}
@PutMapping("/{id}")
@RequirePermission("user:update")
public CommonResponse<Void> update(
@PathVariable Long id,
@Valid @RequestBody UpdateUserRequest request) {
userService.update(id, request);
return CommonResponse.success();
}
@DeleteMapping("/{id}")
@RequirePermission("user:delete")
public CommonResponse<Void> delete(@PathVariable Long id) {
userService.delete(id);
return CommonResponse.success();
}
// 需要多个权限(AND 逻辑)
@PostMapping("/{id}/reset-password")
@RequirePermission(value = {"user:update", "user:sensitive"}, logical = Logical.AND)
public CommonResponse<Void> resetPassword(@PathVariable Long id) {
userService.resetPassword(id);
return CommonResponse.success();
}
// 满足任一权限即可(OR 逻辑)
@GetMapping("/{id}/detail")
@RequirePermission(value = {"user:read", "user:admin"}, logical = Logical.OR)
public CommonResponse<UserDetailResponse> getDetail(@PathVariable Long id) {
return CommonResponse.success(userService.getDetail(id));
}
}
// controller/AdminController.java
@RestController
@RequestMapping("/api/v1/admin")
@RequireRole({"admin", "super_admin"}) // 类级别角色限制
public class AdminController {
@GetMapping("/dashboard")
public CommonResponse<DashboardVO> dashboard() {
// ...
}
@PostMapping("/users/{id}/ban")
@RequirePermission("user:ban") // 方法级别额外权限
public CommonResponse<Void> banUser(@PathVariable Long id) {
// ...
}
}
权限对比表
| 方面 | Go | Java |
|---|---|---|
| 权限检查 | 中间件函数 | AOP 切面 + 注解 |
| 权限声明 | 路由注册时指定 | 方法/类注解 |
| 权限缓存 | 手动实现 | 手动实现 |
| 通配符 | 手动解析 | 手动解析 |
| 逻辑组合 | 不同中间件函数 | 注解属性 |
七、缓存策略
Go 实现
go
// pkg/cache/cache.go
package cache
import (
"context"
"encoding/json"
"time"
"github.com/redis/go-redis/v9"
)
type Cache interface {
Get(ctx context.Context, key string, dest interface{}) error
Set(ctx context.Context, key string, value interface{}, expiration time.Duration) error
Delete(ctx context.Context, keys ...string) error
Exists(ctx context.Context, key string) (bool, error)
SetNX(ctx context.Context, key string, value interface{}, expiration time.Duration) (bool, error)
Incr(ctx context.Context, key string) (int64, error)
Expire(ctx context.Context, key string, expiration time.Duration) error
}
type redisCache struct {
client *redis.Client
}
func NewRedisCache(client *redis.Client) Cache {
return &redisCache{client: client}
}
func (c *redisCache) Get(ctx context.Context, key string, dest interface{}) error {
data, err := c.client.Get(ctx, key).Bytes()
if err != nil {
if err == redis.Nil {
return ErrCacheMiss
}
return err
}
return json.Unmarshal(data, dest)
}
func (c *redisCache) Set(ctx context.Context, key string, value interface{}, expiration time.Duration) error {
data, err := json.Marshal(value)
if err != nil {
return err
}
return c.client.Set(ctx, key, data, expiration).Err()
}
func (c *redisCache) Delete(ctx context.Context, keys ...string) error {
return c.client.Del(ctx, keys...).Err()
}
var ErrCacheMiss = errors.New("cache miss")
go
// internal/repository/cache/user_cache.go
package cache
import (
"context"
"fmt"
"time"
)
type UserCache interface {
GetUser(ctx context.Context, id int64) (*entity.User, error)
SetUser(ctx context.Context, user *entity.User) error
DeleteUser(ctx context.Context, id int64) error
GetUserByEmail(ctx context.Context, email string) (*entity.User, error)
SetUserByEmail(ctx context.Context, user *entity.User) error
}
type userCache struct {
cache cache.Cache
expiration time.Duration
}
func NewUserCache(c cache.Cache) UserCache {
return &userCache{
cache: c,
expiration: 30 * time.Minute,
}
}
func (c *userCache) userKey(id int64) string {
return fmt.Sprintf("user:id:%d", id)
}
func (c *userCache) userEmailKey(email string) string {
return fmt.Sprintf("user:email:%s", email)
}
func (c *userCache) GetUser(ctx context.Context, id int64) (*entity.User, error) {
var user entity.User
err := c.cache.Get(ctx, c.userKey(id), &user)
if err != nil {
return nil, err
}
return &user, nil
}
func (c *userCache) SetUser(ctx context.Context, user *entity.User) error {
return c.cache.Set(ctx, c.userKey(user.ID), user, c.expiration)
}
func (c *userCache) DeleteUser(ctx context.Context, id int64) error {
return c.cache.Delete(ctx, c.userKey(id))
}
go
// internal/service/user_service.go - 带缓存的服务层
package service
type userService struct {
userRepo repository.UserRepository
userCache cache.UserCache
logger *zap.Logger
}
func (s *userService) GetByID(ctx context.Context, id int64) (*entity.User, error) {
// 1. 查缓存
user, err := s.userCache.GetUser(ctx, id)
if err == nil {
s.logger.Debug("cache hit", zap.Int64("user_id", id))
return user, nil
}
if err != cache.ErrCacheMiss {
s.logger.Warn("cache error", zap.Error(err))
}
// 2. 查数据库
user, err = s.userRepo.FindByID(ctx, id)
if err != nil {
return nil, err
}
if user == nil {
return nil, ErrUserNotFound
}
// 3. 写入缓存(异步)
go func() {
if err := s.userCache.SetUser(context.Background(), user); err != nil {
s.logger.Warn("failed to set cache", zap.Error(err))
}
}()
return user, nil
}
func (s *userService) Update(ctx context.Context, id int64, req *dto.UpdateUserRequest) error {
// 1. 更新数据库
if err := s.userRepo.Update(ctx, id, req); err != nil {
return err
}
// 2. 删除缓存(保证一致性)
if err := s.userCache.DeleteUser(ctx, id); err != nil {
s.logger.Warn("failed to delete cache", zap.Error(err))
}
return nil
}
func (s *userService) Delete(ctx context.Context, id int64) error {
// 1. 删除数据库
if err := s.userRepo.Delete(ctx, id); err != nil {
return err
}
// 2. 删除缓存
if err := s.userCache.DeleteUser(ctx, id); err != nil {
s.logger.Warn("failed to delete cache", zap.Error(err))
}
return nil
}
go
// 防缓存穿透 - 布隆过滤器 + 空值缓存
// internal/service/user_service.go
func (s *userService) GetByIDWithProtection(ctx context.Context, id int64) (*entity.User, error) {
// 1. 布隆过滤器检查(可选)
if s.bloomFilter != nil && !s.bloomFilter.Test([]byte(fmt.Sprintf("user:%d", id))) {
return nil, ErrUserNotFound
}
// 2. 查缓存
user, err := s.userCache.GetUser(ctx, id)
if err == nil {
// 检查是否为空值缓存
if user.ID == 0 {
return nil, ErrUserNotFound
}
return user, nil
}
// 3. 分布式锁防止缓存击穿
lockKey := fmt.Sprintf("lock:user:%d", id)
locked, err := s.cache.SetNX(ctx, lockKey, "1", 5*time.Second)
if err != nil {
return nil, err
}
if !locked {
// 未获取到锁,等待后重试
time.Sleep(100 * time.Millisecond)
return s.GetByIDWithProtection(ctx, id)
}
defer s.cache.Delete(ctx, lockKey)
// 4. 再次检查缓存(双重检查)
user, err = s.userCache.GetUser(ctx, id)
if err == nil {
return user, nil
}
// 5. 查数据库
user, err = s.userRepo.FindByID(ctx, id)
if err != nil {
return nil, err
}
if user == nil {
// 缓存空值,防止缓存穿透
_ = s.userCache.SetUser(ctx, &entity.User{})
return nil, ErrUserNotFound
}
// 6. 写入缓存
_ = s.userCache.SetUser(ctx, user)
return user, nil
}
Java 实现(Spring Cache + Redis)
java
// config/RedisConfig.java
@Configuration
@EnableCaching
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
// Key 序列化
template.setKeySerializer(new StringRedisSerializer());
template.setHashKeySerializer(new StringRedisSerializer());
// Value 序列化
Jackson2JsonRedisSerializer<Object> jsonSerializer =
new Jackson2JsonRedisSerializer<>(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.activateDefaultTyping(
LaissezFaireSubTypeValidator.instance,
ObjectMapper.DefaultTyping.NON_FINAL);
jsonSerializer.setObjectMapper(om);
template.setValueSerializer(jsonSerializer);
template.setHashValueSerializer(jsonSerializer);
template.afterPropertiesSet();
return template;
}
@Bean
public CacheManager cacheManager(RedisConnectionFactory factory) {
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(30))
.serializeKeysWith(
RedisSerializationContext.SerializationPair.fromSerializer(
new StringRedisSerializer()))
.serializeValuesWith(
RedisSerializationContext.SerializationPair.fromSerializer(
new GenericJackson2JsonRedisSerializer()))
.disableCachingNullValues();
// 不同缓存空间配置不同过期时间
Map<String, RedisCacheConfiguration> configMap = new HashMap<>();
configMap.put("user", config.entryTtl(Duration.ofMinutes(30)));
configMap.put("order", config.entryTtl(Duration.ofMinutes(10)));
configMap.put("permission", config.entryTtl(Duration.ofHours(1)));
return RedisCacheManager.builder(factory)
.cacheDefaults(config)
.withInitialCacheConfigurations(configMap)
.build();
}
}
// service/impl/UserServiceImpl.java
@Service
@RequiredArgsConstructor
@Slf4j
public class UserServiceImpl implements UserService {
private final UserMapper userMapper;
private final RedisTemplate<String, Object> redisTemplate;
private static final String USER_CACHE_KEY = "user:id:";
private static final long CACHE_EXPIRE = 30; // 分钟
@Override
@Cacheable(value = "user", key = "#id", unless = "#result == null")
public User getById(Long id) {
log.debug("Cache miss, querying database for user: {}", id);
return userMapper.selectById(id);
}
@Override
@CachePut(value = "user", key = "#result.id")
@Transactional
public User create(CreateUserRequest request) {
User user = new User();
BeanUtils.copyProperties(request, user);
user.setPassword(passwordEncoder.encode(request.getPassword()));
user.setCreatedAt(LocalDateTime.now());
userMapper.insert(user);
return user;
}
@Override
@CacheEvict(value = "user", key = "#id")
@Transactional
public void update(Long id, UpdateUserRequest request) {
User user = userMapper.selectById(id);
if (user == null) {
throw new NotFoundException("用户不存在");
}
if (request.getUsername() != null) {
user.setUsername(request.getUsername());
}
if (request.getEmail() != null) {
user.setEmail(request.getEmail());
}
user.setUpdatedAt(LocalDateTime.now());
userMapper.updateById(user);
}
@Override
@CacheEvict(value = "user", key = "#id")
@Transactional
public void delete(Long id) {
userMapper.deleteById(id);
}
// 批量删除缓存
@Override
@CacheEvict(value = "user", allEntries = true)
@Transactional
public void batchDelete(List<Long> ids) {
userMapper.deleteBatchIds(ids);
}
// 手动缓存控制 - 带穿透保护
@Override
public User getByIdWithProtection(Long id) {
String cacheKey = USER_CACHE_KEY + id;
// 1. 查缓存
Object cached = redisTemplate.opsForValue().get(cacheKey);
if (cached != null) {
if (cached instanceof NullValue) {
return null; // 空值缓存
}
return (User) cached;
}
// 2. 分布式锁
String lockKey = "lock:user:" + id;
Boolean locked = redisTemplate.opsForValue()
.setIfAbsent(lockKey, "1", Duration.ofSeconds(5));
if (Boolean.FALSE.equals(locked)) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return getByIdWithProtection(id);
}
try {
// 3. 双重检查
cached = redisTemplate.opsForValue().get(cacheKey);
if (cached != null) {
return cached instanceof NullValue ? null : (User) cached;
}
// 4. 查数据库
User user = userMapper.selectById(id);
// 5. 写缓存
if (user != null) {
redisTemplate.opsForValue().set(cacheKey, user,
CACHE_EXPIRE, TimeUnit.MINUTES);
} else {
// 缓存空值,防止穿透
redisTemplate.opsForValue().set(cacheKey, NullValue.INSTANCE,
5, TimeUnit.MINUTES);
}
return user;
} finally {
redisTemplate.delete(lockKey);
}
}
// 空值标记
private static class NullValue implements Serializable {
static final NullValue INSTANCE = new NullValue();
private static final long serialVersionUID = 1L;
}
}
// 自定义缓存注解 - 支持更复杂的缓存策略
// annotation/CacheableWithLock.java
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface CacheableWithLock {
String cacheName();
String key();
long expire() default 30; // 分钟
long lockTimeout() default 5; // 秒
boolean cacheNull() default true;
}
// aspect/CacheAspect.java
@Aspect
@Component
@RequiredArgsConstructor
@Slf4j
public class CacheAspect {
private final RedisTemplate<String, Object> redisTemplate;
private final SpelExpressionParser parser = new SpelExpressionParser();
@Around("@annotation(cacheableWithLock)")
public Object around(ProceedingJoinPoint pjp, CacheableWithLock cacheableWithLock)
throws Throwable {
String cacheKey = buildCacheKey(pjp, cacheableWithLock);
// 1. 查缓存
Object cached = redisTemplate.opsForValue().get(cacheKey);
if (cached != null) {
if (cached instanceof NullValue) {
return null;
}
log.debug("Cache hit: {}", cacheKey);
return cached;
}
// 2. 加锁
String lockKey = "lock:" + cacheKey;
Boolean locked = redisTemplate.opsForValue()
.setIfAbsent(lockKey, "1",
Duration.ofSeconds(cacheableWithLock.lockTimeout()));
if (Boolean.FALSE.equals(locked)) {
Thread.sleep(100);
return around(pjp, cacheableWithLock); // 重试
}
try {
// 3. 双重检查
cached = redisTemplate.opsForValue().get(cacheKey);
if (cached != null) {
return cached instanceof NullValue ? null : cached;
}
// 4. 执行方法
Object result = pjp.proceed();
// 5. 写缓存
if (result != null) {
redisTemplate.opsForValue().set(cacheKey, result,
Duration.ofMinutes(cacheableWithLock.expire()));
} else if (cacheableWithLock.cacheNull()) {
redisTemplate.opsForValue().set(cacheKey, NullValue.INSTANCE,
Duration.ofMinutes(5));
}
return result;
} finally {
redisTemplate.delete(lockKey);
}
}
private String buildCacheKey(ProceedingJoinPoint pjp, CacheableWithLock annotation) {
MethodSignature signature = (MethodSignature) pjp.getSignature();
EvaluationContext context = new StandardEvaluationContext();
String[] paramNames = signature.getParameterNames();
Object[] args = pjp.getArgs();
for (int i = 0; i < paramNames.length; i++) {
context.setVariable(paramNames[i], args[i]);
}
String keyExpression = annotation.key();
String key = parser.parseExpression(keyExpression)
.getValue(context, String.class);
return annotation.cacheName() + ":" + key;
}
private static class NullValue implements Serializable {
static final NullValue INSTANCE = new NullValue();
}
}
缓存对比表
| 方面 | Go | Java |
|---|---|---|
| 缓存框架 | go-redis + 手动封装 | Spring Cache + Redis |
| 缓存注解 | 无(手动实现) | @Cacheable/@CachePut/@CacheEvict |
| 序列化 | json.Marshal | Jackson/GenericJackson2Json |
| 穿透保护 | 手动实现空值缓存 + 分布式锁 | 手动实现或自定义切面 |
| 缓存预热 | 启动时手动加载 | @PostConstruct / ApplicationRunner |
| 多级缓存 | 手动实现本地 + Redis | Caffeine + Redis 组合 |
八、日志系统
Go 实现(Zap)
go
// pkg/logger/logger.go
package logger
import (
"os"
"time"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"gopkg.in/natefinch/lumberjack.v2"
)
var Logger *zap.Logger
type Config struct {
Level string
Filename string
MaxSize int // MB
MaxBackups int
MaxAge int // days
Compress bool
Console bool
}
func Init(cfg *Config) error {
level := parseLevel(cfg.Level)
// 编码器配置
encoderConfig := zapcore.EncoderConfig{
TimeKey: "timestamp",
LevelKey: "level",
NameKey: "logger",
CallerKey: "caller",
FunctionKey: zapcore.OmitKey,
MessageKey: "message",
StacktraceKey: "stacktrace",
LineEnding: zapcore.DefaultLineEnding,
EncodeLevel: zapcore.LowercaseLevelEncoder,
EncodeTime: zapcore.ISO8601TimeEncoder,
EncodeDuration: zapcore.SecondsDurationEncoder,
EncodeCaller: zapcore.ShortCallerEncoder,
}
var cores []zapcore.Core
// 文件输出
if cfg.Filename != "" {
fileWriter := &lumberjack.Logger{
Filename: cfg.Filename,
MaxSize: cfg.MaxSize,
MaxBackups: cfg.MaxBackups,
MaxAge: cfg.MaxAge,
Compress: cfg.Compress,
}
fileCore := zapcore.NewCore(
zapcore.NewJSONEncoder(encoderConfig),
zapcore.AddSync(fileWriter),
level,
)
cores = append(cores, fileCore)
}
// 控制台输出
if cfg.Console {
consoleEncoder := zapcore.NewConsoleEncoder(encoderConfig)
consoleCore := zapcore.NewCore(
consoleEncoder,
zapcore.AddSync(os.Stdout),
level,
)
cores = append(cores, consoleCore)
}
core := zapcore.NewTee(cores...)
Logger = zap.New(core, zap.AddCaller(), zap.AddCallerSkip(1))
return nil
}
func parseLevel(level string) zapcore.Level {
switch level {
case "debug":
return zapcore.DebugLevel
case "info":
return zapcore.InfoLevel
case "warn":
return zapcore.WarnLevel
case "error":
return zapcore.ErrorLevel
default:
return zapcore.InfoLevel
}
}
// 便捷方法
func Debug(msg string, fields ...zap.Field) { Logger.Debug(msg, fields...) }
func Info(msg string, fields ...zap.Field) { Logger.Info(msg, fields...) }
func Warn(msg string, fields ...zap.Field) { Logger.Warn(msg, fields...) }
func Error(msg string, fields ...zap.Field) { Logger.Error(msg, fields...) }
func With(fields ...zap.Field) *zap.Logger {
return Logger.With(fields...)
}
go
// internal/handler/middleware/logger.go
package middleware
import (
"bytes"
"io"
"time"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
)
type responseWriter struct {
gin.ResponseWriter
body *bytes.Buffer
}
func (w *responseWriter) Write(b []byte) (int, error) {
w.body.Write(b)
return w.ResponseWriter.Write(b)
}
func Logger(log *zap.Logger) gin.HandlerFunc {
return func(c *gin.Context) {
start := time.Now()
path := c.Request.URL.Path
query := c.Request.URL.RawQuery
// 读取请求体
var requestBody []byte
if c.Request.Body != nil {
requestBody, _ = io.ReadAll(c.Request.Body)
c.Request.Body = io.NopCloser(bytes.NewBuffer(requestBody))
}
// 包装 ResponseWriter 以捕获响应
blw := &responseWriter{
ResponseWriter: c.Writer,
body: bytes.NewBuffer(nil),
}
c.Writer = blw
// 生成请求 ID
requestID := c.GetHeader("X-Request-ID")
if requestID == "" {
requestID = generateRequestID()
}
c.Set("request_id", requestID)
c.Header("X-Request-ID", requestID)
// 执行请求
c.Next()
// 计算耗时
latency := time.Since(start)
// 构建日志字段
fields := []zap.Field{
zap.String("request_id", requestID),
zap.String("method", c.Request.Method),
zap.String("path", path),
zap.String("query", query),
zap.Int("status", c.Writer.Status()),
zap.Duration("latency", latency),
zap.String("client_ip", c.ClientIP()),
zap.String("user_agent", c.Request.UserAgent()),
}
// 添加用户信息(如果已认证)
if userID, exists := c.Get("user_id"); exists {
fields = append(fields, zap.Any("user_id", userID))
}
// 记录请求体(敏感信息脱敏)
if len(requestBody) > 0 && len(requestBody) < 10240 {
sanitized := sanitizeBody(requestBody)
fields = append(fields, zap.String("request_body", sanitized))
}
// 错误请求记录响应体
if c.Writer.Status() >= 400 {
fields = append(fields, zap.String("response_body", blw.body.String()))
}
// 根据状态码选择日志级别
switch {
case c.Writer.Status() >= 500:
log.Error("Server error", fields...)
case c.Writer.Status() >= 400:
log.Warn("Client error", fields...)
default:
log.Info("Request completed", fields...)
}
}
}
// 敏感字段脱敏
func sanitizeBody(body []byte) string {
var data map[string]interface{}
if err := json.Unmarshal(body, &data); err != nil {
return "[non-json body]"
}
sensitiveFields := []string{"password", "token", "secret", "card_number"}
for _, field := range sensitiveFields {
if _, ok := data[field]; ok {
data[field] = "***"
}
}
sanitized, _ := json.Marshal(data)
return string(sanitized)
}
func generateRequestID() string {
return fmt.Sprintf("%d-%s", time.Now().UnixNano(), randomString(8))
}
go
// 操作审计日志
// internal/service/audit_service.go
package service
type AuditLog struct {
ID int64
UserID int64
Username string
Action string // create, update, delete, login, logout
Resource string // user, order, product
ResourceID string
OldValue string // JSON
NewValue string // JSON
IP string
UserAgent string
RequestID string
CreatedAt time.Time
}
type AuditService interface {
Log(ctx context.Context, log *AuditLog) error
Query(ctx context.Context, query *AuditQuery) ([]*AuditLog, int64, error)
}
type auditService struct {
repo repository.AuditRepository
logger *zap.Logger
}
func (s *auditService) Log(ctx context.Context, log *AuditLog) error {
log.CreatedAt = time.Now()
// 异步写入数据库
go func() {
if err := s.repo.Create(context.Background(), log); err != nil {
s.logger.Error("failed to save audit log",
zap.Error(err),
zap.Any("audit", log))
}
}()
// 同步写入日志文件(确保不丢失)
s.logger.Info("audit",
zap.Int64("user_id", log.UserID),
zap.String("action", log.Action),
zap.String("resource", log.Resource),
zap.String("resource_id", log.ResourceID),
zap.String("request_id", log.RequestID))
return nil
}
Java 实现(Logback + AOP)
java
// resources/logback-spring.xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<springProperty scope="context" name="APP_NAME" source="spring.application.name"/>
<!-- 控制台输出 -->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<!-- JSON 格式文件输出(用于 ELK) -->
<appender name="FILE_JSON" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>logs/${APP_NAME}.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>logs/${APP_NAME}.%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern>
<maxFileSize>100MB</maxFileSize>
<maxHistory>30</maxHistory>
<totalSizeCap>10GB</totalSizeCap>
</rollingPolicy>
<encoder class="net.logstash.logback.encoder.LogstashEncoder">
<includeMdcKeyName>request_id</includeMdcKeyName>
<includeMdcKeyName>user_id</includeMdcKeyName>
</encoder>
</appender>
<!-- 审计日志单独文件 -->
<appender name="AUDIT_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>logs/audit.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>logs/audit.%d{yyyy-MM-dd}.log.gz</fileNamePattern>
<maxHistory>90</maxHistory>
</rollingPolicy>
<encoder class="net.logstash.logback.encoder.LogstashEncoder"/>
</appender>
<!-- 审计日志 Logger -->
<logger name="AUDIT" level="INFO" additivity="false">
<appender-ref ref="AUDIT_FILE"/>
</logger>
<root level="INFO">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="FILE_JSON"/>
</root>
</configuration>
java
// filter/RequestLoggingFilter.java
@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
@Slf4j
public class RequestLoggingFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
long startTime = System.currentTimeMillis();
// 生成 Request ID
String requestId = request.getHeader("X-Request-ID");
if (requestId == null || requestId.isEmpty()) {
requestId = UUID.randomUUID().toString();
}
// 放入 MDC(日志上下文)
MDC.put("request_id", requestId);
response.setHeader("X-Request-ID", requestId);
// 包装请求以支持多次读取
ContentCachingRequestWrapper wrappedRequest =
new ContentCachingRequestWrapper(request);
ContentCachingResponseWrapper wrappedResponse =
new ContentCachingResponseWrapper(response);
try {
filterChain.doFilter(wrappedRequest, wrappedResponse);
} finally {
long duration = System.currentTimeMillis() - startTime;
logRequest(wrappedRequest, wrappedResponse, duration);
wrappedResponse.copyBodyToResponse();
MDC.clear();
}
}
private void logRequest(ContentCachingRequestWrapper request,
ContentCachingResponseWrapper response,
long duration) {
int status = response.getStatus();
String method = request.getMethod();
String uri = request.getRequestURI();
String query = request.getQueryString();
String clientIp = getClientIp(request);
// 构建日志消息
Map<String, Object> logData = new LinkedHashMap<>();
logData.put("method", method);
logData.put("uri", uri);
logData.put("query", query);
logData.put("status", status);
logData.put("duration_ms", duration);
logData.put("client_ip", clientIp);
logData.put("user_agent", request.getHeader("User-Agent"));
// 请求体(敏感信息脱敏)
String requestBody = getRequestBody(request);
if (requestBody != null && !requestBody.isEmpty()) {
logData.put("request_body", sanitize(requestBody));
}
// 错误响应记录响应体
if (status >= 400) {
String responseBody = getResponseBody(response);
logData.put("response_body", responseBody);
}
if (status >= 500) {
log.error("Request completed: {}", logData);
} else if (status >= 400) {
log.warn("Request completed: {}", logData);
} else {
log.info("Request completed: {}", logData);
}
}
private String sanitize(String body) {
try {
ObjectMapper mapper = new ObjectMapper();
Map<String, Object> data = mapper.readValue(body, Map.class);
List<String> sensitiveFields = Arrays.asList(
"password", "token", "secret", "card_number", "cvv");
for (String field : sensitiveFields) {
if (data.containsKey(field)) {
data.put(field, "***");
}
}
return mapper.writeValueAsString(data);
} catch (Exception e) {
return body;
}
}
private String getClientIp(HttpServletRequest request) {
String ip = request.getHeader("X-Forwarded-For");
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("X-Real-IP");
}
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
return ip.split(",")[0].trim();
}
}
java
// aspect/AuditLogAspect.java
@Aspect
@Component
@RequiredArgsConstructor
@Slf4j
public class AuditLogAspect {
private final AuditLogService auditLogService;
private final ObjectMapper objectMapper;
private static final Logger AUDIT_LOGGER = LoggerFactory.getLogger("AUDIT");
@Around("@annotation(auditLog)")
public Object around(ProceedingJoinPoint pjp, AuditLog auditLog) throws Throwable {
// 获取旧值(更新/删除操作)
String oldValue = null;
if (auditLog.action() == AuditAction.UPDATE ||
auditLog.action() == AuditAction.DELETE) {
oldValue = getOldValue(pjp, auditLog);
}
// 执行方法
Object result = pjp.proceed();
// 记录审计日志
try {
AuditLogEntity entity = buildAuditLog(pjp, auditLog, oldValue, result);
// 异步保存到数据库
auditLogService.saveAsync(entity);
// 同步写入日志文件
AUDIT_LOGGER.info(objectMapper.writeValueAsString(entity));
} catch (Exception e) {
log.error("Failed to save audit log", e);
}
return result;
}
private AuditLogEntity buildAuditLog(ProceedingJoinPoint pjp,
AuditLog annotation,
String oldValue,
Object result) {
HttpServletRequest request = getCurrentRequest();
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
AuditLogEntity entity = new AuditLogEntity();
entity.setAction(annotation.action().name());
entity.setResource(annotation.resource());
entity.setResourceId(extractResourceId(pjp, annotation));
entity.setOldValue(oldValue);
entity.setNewValue(result != null ? toJson(result) : null);
entity.setRequestId(MDC.get("request_id"));
entity.setCreatedAt(LocalDateTime.now());
if (auth != null && auth.getPrincipal() instanceof CustomUserDetails) {
CustomUserDetails user = (CustomUserDetails) auth.getPrincipal();
entity.setUserId(user.getId());
entity.setUsername(user.getUsername());
}
if (request != null) {
entity.setIp(getClientIp(request));
entity.setUserAgent(request.getHeader("User-Agent"));
}
return entity;
}
}
// annotation/AuditLog.java
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AuditLog {
AuditAction action();
String resource();
String resourceIdExpr() default ""; // SpEL 表达式
}
// enums/AuditAction.java
public enum AuditAction {
CREATE, UPDATE, DELETE, LOGIN, LOGOUT, EXPORT, IMPORT
}
// 使用示例
@Service
public class UserServiceImpl implements UserService {
@Override
@AuditLog(action = AuditAction.CREATE, resource = "user")
@Transactional
public User create(CreateUserRequest request) {
// ...
}
@Override
@AuditLog(action = AuditAction.UPDATE, resource = "user", resourceIdExpr = "#id")
@Transactional
public void update(Long id, UpdateUserRequest request) {
// ...
}
@Override
@AuditLog(action = AuditAction.DELETE, resource = "user", resourceIdExpr = "#id")
@Transactional
public void delete(Long id) {
// ...
}
}
日志对比表
| 方面 | Go | Java |
|---|---|---|
| 日志库 | Zap / Zerolog | Logback / Log4j2 |
| 结构化日志 | 原生支持 | Logstash Encoder |
| 日志上下文 | Context 传递 | MDC(Mapped Diagnostic Context) |
| 请求日志 | 中间件实现 | Filter / Interceptor |
| 审计日志 | 手动调用服务 | AOP + 注解 |
| 日志轮转 | lumberjack | Logback RollingPolicy |
| 敏感信息 | 手动脱敏 | 手动脱敏 |
九、异常处理与统一响应
Go 实现
go
// internal/errors/errors.go
package errors
import (
"fmt"
"net/http"
)
type ErrorCode int
const (
CodeSuccess ErrorCode = 0
CodeBadRequest ErrorCode = 400
CodeUnauthorized ErrorCode = 401
CodeForbidden ErrorCode = 403
CodeNotFound ErrorCode = 404
CodeConflict ErrorCode = 409
CodeTooManyRequests ErrorCode = 429
CodeInternalError ErrorCode = 500
CodeServiceUnavailable ErrorCode = 503
// 业务错误码 (10000+)
CodeUserNotFound ErrorCode = 10001
CodeUserExists ErrorCode = 10002
CodePasswordWrong ErrorCode = 10003
CodeTokenExpired ErrorCode = 10004
CodeTokenInvalid ErrorCode = 10005
CodePermissionDenied ErrorCode = 10006
CodeOrderNotFound ErrorCode = 20001
CodeOrderCancelled ErrorCode = 20002
CodeInsufficientStock ErrorCode = 20003
)
var codeMessages = map[ErrorCode]string{
CodeSuccess: "成功",
CodeBadRequest: "请求参数错误",
CodeUnauthorized: "未授权",
CodeForbidden: "禁止访问",
CodeNotFound: "资源不存在",
CodeConflict: "资源冲突",
CodeTooManyRequests: "请求过于频繁",
CodeInternalError: "服务器内部错误",
CodeServiceUnavailable: "服务暂不可用",
CodeUserNotFound: "用户不存在",
CodeUserExists: "用户已存在",
CodePasswordWrong: "密码错误",
CodeTokenExpired: "Token已过期",
CodeTokenInvalid: "Token无效",
CodePermissionDenied: "权限不足",
CodeOrderNotFound: "订单不存在",
CodeOrderCancelled: "订单已取消",
CodeInsufficientStock: "库存不足",
}
type AppError struct {
Code ErrorCode
Message string
Err error // 原始错误(不对外暴露)
Data interface{}
}
func (e *AppError) Error() string {
if e.Err != nil {
return fmt.Sprintf("[%d] %s: %v", e.Code, e.Message, e.Err)
}
return fmt.Sprintf("[%d] %s", e.Code, e.Message)
}
func (e *AppError) Unwrap() error {
return e.Err
}
func New(code ErrorCode) *AppError {
return &AppError{
Code: code,
Message: codeMessages[code],
}
}
func NewWithMessage(code ErrorCode, message string) *AppError {
return &AppError{
Code: code,
Message: message,
}
}
func Wrap(code ErrorCode, err error) *AppError {
return &AppError{
Code: code,
Message: codeMessages[code],
Err: err,
}
}
func WrapWithMessage(code ErrorCode, message string, err error) *AppError {
return &AppError{
Code: code,
Message: message,
Err: err,
}
}
// 判断错误类型
func IsAppError(err error) bool {
_, ok := err.(*AppError)
return ok
}
func GetCode(err error) ErrorCode {
if appErr, ok := err.(*AppError); ok {
return appErr.Code
}
return CodeInternalError
}
func GetHTTPStatus(code ErrorCode) int {
switch {
case code == CodeSuccess:
return http.StatusOK
case code == CodeBadRequest:
return http.StatusBadRequest
case code == CodeUnauthorized, code == CodeTokenExpired, code == CodeTokenInvalid:
return http.StatusUnauthorized
case code == CodeForbidden, code == CodePermissionDenied:
return http.StatusForbidden
case code == CodeNotFound, code == CodeUserNotFound, code == CodeOrderNotFound:
return http.StatusNotFound
case code == CodeConflict, code == CodeUserExists:
return http.StatusConflict
case code == CodeTooManyRequests:
return http.StatusTooManyRequests
case code >= 10000:
return http.StatusBadRequest // 业务错误返回 400
default:
return http.StatusInternalServerError
}
}
// 预定义错误
var (
ErrUserNotFound = New(CodeUserNotFound)
ErrUserExists = New(CodeUserExists)
ErrPasswordWrong = New(CodePasswordWrong)
ErrTokenExpired = New(CodeTokenExpired)
ErrTokenInvalid = New(CodeTokenInvalid)
ErrPermissionDenied = New(CodePermissionDenied)
ErrOrderNotFound = New(CodeOrderNotFound)
ErrInsufficientStock = New(CodeInsufficientStock)
)
go
// internal/dto/response/response.go
package response
type PageData struct {
List interface{} `json:"list"`
Total int64 `json:"total"`
Page int `json:"page"`
PageSize int `json:"page_size"`
Pages int `json:"pages"`
}
func NewPageData(list interface{}, total int64, page, pageSize int) *PageData {
pages := int(total) / pageSize
if int(total)%pageSize > 0 {
pages++
}
return &PageData{
List: list,
Total: total,
Page: page,
PageSize: pageSize,
Pages: pages,
}
}
func Success(c *gin.Context, data interface{}) {
requestID, _ := c.Get("request_id")
c.JSON(200, Response{
Code: 0,
Message: "success",
Data: data,
RequestID: requestID.(string),
})
}
func SuccessWithMessage(c *gin.Context, message string, data interface{}) {
requestID, _ := c.Get("request_id")
c.JSON(200, Response{
Code: 0,
Message: message,
Data: data,
RequestID: requestID.(string),
})
}
func Error(c *gin.Context, err error) {
requestID, _ := c.Get("request_id")
var appErr *errors.AppError
if e, ok := err.(*errors.AppError); ok {
appErr = e
} else {
appErr = errors.Wrap(errors.CodeInternalError, err)
}
httpStatus := errors.GetHTTPStatus(appErr.Code)
c.JSON(httpStatus, Response{
Code: int(appErr.Code),
Message: appErr.Message,
RequestID: requestID.(string),
})
}
func BadRequest(c *gin.Context, message string) {
requestID, _ := c.Get("request_id")
c.JSON(400, Response{
Code: int(errors.CodeBadRequest),
Message: message,
RequestID: requestID.(string),
})
}
func Unauthorized(c *gin.Context, message string) {
requestID, _ := c.Get("request_id")
c.JSON(401, Response{
Code: int(errors.CodeUnauthorized),
Message: message,
RequestID: requestID.(string),
})
}
func Forbidden(c *gin.Context, message string) {
requestID, _ := c.Get("request_id")
c.JSON(403, Response{
Code: int(errors.CodeForbidden),
Message: message,
RequestID: requestID.(string),
})
}
func NotFound(c *gin.Context, message string) {
requestID, _ := c.Get("request_id")
c.JSON(404, Response{
Code: int(errors.CodeNotFound),
Message: message,
RequestID: requestID.(string),
})
}
func InternalError(c *gin.Context, message string) {
requestID, _ := c.Get("request_id")
c.JSON(500, Response{
Code: int(errors.CodeInternalError),
Message: message,
RequestID: requestID.(string),
})
}
go
// internal/handler/middleware/recovery.go
package middleware
import (
"fmt"
"runtime/debug"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
)
func Recovery(logger *zap.Logger) gin.HandlerFunc {
return func(c *gin.Context) {
defer func() {
if r := recover(); r != nil {
// 记录堆栈
stack := string(debug.Stack())
requestID, _ := c.Get("request_id")
logger.Error("Panic recovered",
zap.Any("error", r),
zap.String("stack", stack),
zap.String("request_id", requestID.(string)),
zap.String("path", c.Request.URL.Path),
zap.String("method", c.Request.Method),
)
response.InternalError(c, "服务器内部错误")
c.Abort()
}
}()
c.Next()
}
}
go
// 使用示例 - internal/service/user_service.go
func (s *userService) GetByID(ctx context.Context, id int64) (*entity.User, error) {
user, err := s.userRepo.FindByID(ctx, id)
if err != nil {
return nil, errors.Wrap(errors.CodeInternalError, err)
}
if user == nil {
return nil, errors.ErrUserNotFound
}
return user, nil
}
func (s *userService) Create(ctx context.Context, req *dto.CreateUserRequest) (*entity.User, error) {
// 检查用户是否存在
existing, err := s.userRepo.FindByEmail(ctx, req.Email)
if err != nil {
return nil, errors.Wrap(errors.CodeInternalError, err)
}
if existing != nil {
return nil, errors.ErrUserExists
}
// 业务规则校验
if req.Age < 18 {
return nil, errors.NewWithMessage(errors.CodeBadRequest, "用户年龄不能小于18岁")
}
user := &entity.User{
Username: req.Username,
Email: req.Email,
Password: hashPassword(req.Password),
}
if err := s.userRepo.Create(ctx, user); err != nil {
return nil, errors.Wrap(errors.CodeInternalError, err)
}
return user, nil
}
// internal/handler/user_handler.go
func (h *UserHandler) GetByID(c *gin.Context) {
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
response.BadRequest(c, "无效的用户ID")
return
}
user, err := h.userService.GetByID(c.Request.Context(), id)
if err != nil {
response.Error(c, err)
return
}
response.Success(c, dto.ToUserResponse(user))
}
Java 实现
java
// enums/ErrorCode.java
@Getter
@AllArgsConstructor
public enum ErrorCode {
// 通用错误
SUCCESS(0, "成功"),
BAD_REQUEST(400, "请求参数错误"),
UNAUTHORIZED(401, "未授权"),
FORBIDDEN(403, "禁止访问"),
NOT_FOUND(404, "资源不存在"),
CONFLICT(409, "资源冲突"),
TOO_MANY_REQUESTS(429, "请求过于频繁"),
INTERNAL_ERROR(500, "服务器内部错误"),
SERVICE_UNAVAILABLE(503, "服务暂不可用"),
// 用户模块 (10000+)
USER_NOT_FOUND(10001, "用户不存在"),
USER_EXISTS(10002, "用户已存在"),
PASSWORD_WRONG(10003, "密码错误"),
TOKEN_EXPIRED(10004, "Token已过期"),
TOKEN_INVALID(10005, "Token无效"),
PERMISSION_DENIED(10006, "权限不足"),
// 订单模块 (20000+)
ORDER_NOT_FOUND(20001, "订单不存在"),
ORDER_CANCELLED(20002, "订单已取消"),
INSUFFICIENT_STOCK(20003, "库存不足"),
ORDER_STATUS_ERROR(20004, "订单状态不允许此操作");
private final int code;
private final String message;
public HttpStatus toHttpStatus() {
if (code == 0) return HttpStatus.OK;
if (code == 400 || code >= 10000) return HttpStatus.BAD_REQUEST;
if (code == 401) return HttpStatus.UNAUTHORIZED;
if (code == 403) return HttpStatus.FORBIDDEN;
if (code == 404) return HttpStatus.NOT_FOUND;
if (code == 409) return HttpStatus.CONFLICT;
if (code == 429) return HttpStatus.TOO_MANY_REQUESTS;
return HttpStatus.INTERNAL_SERVER_ERROR;
}
}
// exception/BusinessException.java
@Getter
public class BusinessException extends RuntimeException {
private final ErrorCode errorCode;
private final Object data;
public BusinessException(ErrorCode errorCode) {
super(errorCode.getMessage());
this.errorCode = errorCode;
this.data = null;
}
public BusinessException(ErrorCode errorCode, String message) {
super(message);
this.errorCode = errorCode;
this.data = null;
}
public BusinessException(ErrorCode errorCode, Throwable cause) {
super(errorCode.getMessage(), cause);
this.errorCode = errorCode;
this.data = null;
}
public BusinessException(ErrorCode errorCode, String message, Object data) {
super(message);
this.errorCode = errorCode;
this.data = data;
}
}
// exception/NotFoundException.java
public class NotFoundException extends BusinessException {
public NotFoundException(String message) {
super(ErrorCode.NOT_FOUND, message);
}
}
// exception/ForbiddenException.java
public class ForbiddenException extends BusinessException {
public ForbiddenException(String message) {
super(ErrorCode.FORBIDDEN, message);
}
}
java
// dto/response/CommonResponse.java
@Data
@NoArgsConstructor
@AllArgsConstructor
public class CommonResponse<T> {
private int code;
private String message;
private T data;
private String requestId;
private long timestamp;
public CommonResponse(int code, String message) {
this.code = code;
this.message = message;
this.timestamp = System.currentTimeMillis();
this.requestId = MDC.get("request_id");
}
public CommonResponse(int code, String message, T data) {
this.code = code;
this.message = message;
this.data = data;
this.timestamp = System.currentTimeMillis();
this.requestId = MDC.get("request_id");
}
public static <T> CommonResponse<T> success() {
return new CommonResponse<>(0, "success", null);
}
public static <T> CommonResponse<T> success(T data) {
return new CommonResponse<>(0, "success", data);
}
public static <T> CommonResponse<T> success(String message, T data) {
return new CommonResponse<>(0, message, data);
}
public static <T> CommonResponse<T> error(ErrorCode errorCode) {
return new CommonResponse<>(errorCode.getCode(), errorCode.getMessage());
}
public static <T> CommonResponse<T> error(ErrorCode errorCode, String message) {
return new CommonResponse<>(errorCode.getCode(), message);
}
public static <T> CommonResponse<T> error(int code, String message) {
return new CommonResponse<>(code, message);
}
}
// dto/response/PageResult.java
@Data
@NoArgsConstructor
@AllArgsConstructor
public class PageResult<T> {
private List<T> list;
private long total;
private int page;
private int pageSize;
private int pages;
public static <T> PageResult<T> of(List<T> list, long total, int page, int pageSize) {
PageResult<T> result = new PageResult<>();
result.setList(list);
result.setTotal(total);
result.setPage(page);
result.setPageSize(pageSize);
result.setPages((int) Math.ceil((double) total / pageSize));
return result;
}
public static <T> PageResult<T> of(IPage<T> page) {
return of(page.getRecords(), page.getTotal(),
(int) page.getCurrent(), (int) page.getSize());
}
}
java
// exception/GlobalExceptionHandler.java
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
/**
* 业务异常
*/
@ExceptionHandler(BusinessException.class)
public ResponseEntity<CommonResponse<Object>> handleBusinessException(
BusinessException e, HttpServletRequest request) {
log.warn("Business exception: {} - {}", e.getErrorCode(), e.getMessage());
CommonResponse<Object> response = CommonResponse.error(
e.getErrorCode().getCode(), e.getMessage());
if (e.getData() != null) {
response.setData(e.getData());
}
return ResponseEntity
.status(e.getErrorCode().toHttpStatus())
.body(response);
}
/**
* 参数校验异常 - @Valid 校验失败
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<CommonResponse<Map<String, String>>> handleValidationException(
MethodArgumentNotValidException e) {
Map<String, String> errors = new HashMap<>();
e.getBindingResult().getFieldErrors().forEach(error ->
errors.put(error.getField(), error.getDefaultMessage()));
// 取第一个错误作为主要消息
String message = e.getBindingResult().getFieldErrors().stream()
.findFirst()
.map(FieldError::getDefaultMessage)
.orElse("参数校验失败");
log.warn("Validation failed: {}", errors);
CommonResponse<Map<String, String>> response =
CommonResponse.error(ErrorCode.BAD_REQUEST, message);
response.setData(errors);
return ResponseEntity.badRequest().body(response);
}
/**
* 参数校验异常 - @Validated 校验失败(方法参数)
*/
@ExceptionHandler(ConstraintViolationException.class)
public ResponseEntity<CommonResponse<Void>> handleConstraintViolation(
ConstraintViolationException e) {
String message = e.getConstraintViolations().stream()
.findFirst()
.map(ConstraintViolation::getMessage)
.orElse("参数校验失败");
return ResponseEntity.badRequest()
.body(CommonResponse.error(ErrorCode.BAD_REQUEST, message));
}
/**
* 参数绑定异常
*/
@ExceptionHandler(BindException.class)
public ResponseEntity<CommonResponse<Void>> handleBindException(BindException e) {
String message = e.getBindingResult().getFieldErrors().stream()
.findFirst()
.map(FieldError::getDefaultMessage)
.orElse("参数绑定失败");
return ResponseEntity.badRequest()
.body(CommonResponse.error(ErrorCode.BAD_REQUEST, message));
}
/**
* 请求方法不支持
*/
@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
public ResponseEntity<CommonResponse<Void>> handleMethodNotSupported(
HttpRequestMethodNotSupportedException e) {
return ResponseEntity.status(HttpStatus.METHOD_NOT_ALLOWED)
.body(CommonResponse.error(405, "请求方法不支持: " + e.getMethod()));
}
/**
* 请求体缺失
*/
@ExceptionHandler(HttpMessageNotReadableException.class)
public ResponseEntity<CommonResponse<Void>> handleMessageNotReadable(
HttpMessageNotReadableException e) {
return ResponseEntity.badRequest()
.body(CommonResponse.error(ErrorCode.BAD_REQUEST, "请求体格式错误"));
}
/**
* JWT 相关异常
*/
@ExceptionHandler(ExpiredJwtException.class)
public ResponseEntity<CommonResponse<Void>> handleExpiredJwt(ExpiredJwtException e) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
.body(CommonResponse.error(ErrorCode.TOKEN_EXPIRED));
}
@ExceptionHandler(JwtException.class)
public ResponseEntity<CommonResponse<Void>> handleJwtException(JwtException e) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
.body(CommonResponse.error(ErrorCode.TOKEN_INVALID));
}
/**
* 访问拒绝
*/
@ExceptionHandler(AccessDeniedException.class)
public ResponseEntity<CommonResponse<Void>> handleAccessDenied(AccessDeniedException e) {
return ResponseEntity.status(HttpStatus.FORBIDDEN)
.body(CommonResponse.error(ErrorCode.FORBIDDEN));
}
/**
* 限流异常
*/
@ExceptionHandler(RateLimitExceededException.class)
public ResponseEntity<CommonResponse<Void>> handleRateLimit(RateLimitExceededException e) {
return ResponseEntity.status(HttpStatus.TOO_MANY_REQUESTS)
.body(CommonResponse.error(ErrorCode.TOO_MANY_REQUESTS));
}
/**
* 兜底异常处理
*/
@ExceptionHandler(Exception.class)
public ResponseEntity<CommonResponse<Void>> handleException(
Exception e, HttpServletRequest request) {
log.error("Unhandled exception: {} - {}",
request.getRequestURI(), e.getMessage(), e);
// 生产环境不暴露具体错误信息
String message = "服务器内部错误";
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(CommonResponse.error(ErrorCode.INTERNAL_ERROR, message));
}
}
java
// 使用示例 - service/impl/UserServiceImpl.java
@Service
@RequiredArgsConstructor
@Slf4j
public class UserServiceImpl implements UserService {
private final UserMapper userMapper;
private final PasswordEncoder passwordEncoder;
@Override
public User getById(Long id) {
User user = userMapper.selectById(id);
if (user == null) {
throw new BusinessException(ErrorCode.USER_NOT_FOUND);
}
return user;
}
@Override
@Transactional
public User create(CreateUserRequest request) {
// 检查用户是否存在
User existing = userMapper.selectByEmail(request.getEmail());
if (existing != null) {
throw new BusinessException(ErrorCode.USER_EXISTS);
}
// 业务规则校验
if (request.getAge() != null && request.getAge() < 18) {
throw new BusinessException(ErrorCode.BAD_REQUEST, "用户年龄不能小于18岁");
}
User user = new User();
BeanUtils.copyProperties(request, user);
user.setPassword(passwordEncoder.encode(request.getPassword()));
user.setCreatedAt(LocalDateTime.now());
userMapper.insert(user);
return user;
}
@Override
@Transactional
public void updateStatus(Long id, Integer status) {
User user = userMapper.selectById(id);
if (user == null) {
throw new BusinessException(ErrorCode.USER_NOT_FOUND);
}
// 状态流转校验
if (user.getStatus().equals(2) && status.equals(1)) {
throw new BusinessException(ErrorCode.BAD_REQUEST, "已禁用的用户不能直接激活");
}
user.setStatus(status);
user.setUpdatedAt(LocalDateTime.now());
userMapper.updateById(user);
}
}
// controller/UserController.java
@RestController
@RequestMapping("/api/v1/users")
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
@GetMapping("/{id}")
public CommonResponse<UserResponse> getById(@PathVariable Long id) {
User user = userService.getById(id);
return CommonResponse.success(UserResponse.from(user));
}
@PostMapping
public CommonResponse<UserResponse> create(
@Valid @RequestBody CreateUserRequest request) {
User user = userService.create(request);
return CommonResponse.success(UserResponse.from(user));
}
@GetMapping
public CommonResponse<PageResult<UserResponse>> list(@Valid ListUserRequest request) {
PageResult<UserResponse> result = userService.list(request);
return CommonResponse.success(result);
}
}
异常处理对比表
| 方面 | Go | Java |
|---|---|---|
| 错误类型 | 自定义 error 结构体 | 自定义 Exception 类 |
| 错误传播 | 返回值传递 | throw 抛出 |
| 全局处理 | Recovery 中间件 | @RestControllerAdvice |
| 错误码 | 常量定义 | 枚举定义 |
| 堆栈跟踪 | debug.Stack() | Exception.getStackTrace() |
| 错误包装 | errors.Wrap | new Exception(msg, cause) |
十、事务管理
Go 实现
go
// pkg/database/transaction.go
package database
import (
"context"
"database/sql"
)
type TxKey struct{}
// 从 Context 获取事务
func GetTx(ctx context.Context) *sql.Tx {
if tx, ok := ctx.Value(TxKey{}).(*sql.Tx); ok {
return tx
}
return nil
}
// 事务管理器
type TxManager interface {
WithTransaction(ctx context.Context, fn func(ctx context.Context) error) error
}
type txManager struct {
db *sql.DB
}
func NewTxManager(db *sql.DB) TxManager {
return &txManager{db: db}
}
func (m *txManager) WithTransaction(ctx context.Context, fn func(ctx context.Context) error) error {
// 检查是否已在事务中
if GetTx(ctx) != nil {
return fn(ctx)
}
tx, err := m.db.BeginTx(ctx, nil)
if err != nil {
return err
}
// 将事务放入 Context
txCtx := context.WithValue(ctx, TxKey{}, tx)
defer func() {
if r := recover(); r != nil {
_ = tx.Rollback()
panic(r)
}
}()
if err := fn(txCtx); err != nil {
if rbErr := tx.Rollback(); rbErr != nil {
return fmt.Errorf("rollback error: %v (original: %v)", rbErr, err)
}
return err
}
return tx.Commit()
}
// GORM 版本
type GormTxManager struct {
db *gorm.DB
}
func NewGormTxManager(db *gorm.DB) *GormTxManager {
return &GormTxManager{db: db}
}
func (m *GormTxManager) WithTransaction(ctx context.Context, fn func(ctx context.Context) error) error {
return m.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
txCtx := context.WithValue(ctx, TxKey{}, tx)
return fn(txCtx)
})
}
go
// internal/repository/user_repository.go
type userRepository struct {
db *gorm.DB
}
// 自动获取事务或普通连接
func (r *userRepository) getDB(ctx context.Context) *gorm.DB {
if tx, ok := ctx.Value(database.TxKey{}).(*gorm.DB); ok {
return tx
}
return r.db.WithContext(ctx)
}
func (r *userRepository) Create(ctx context.Context, user *entity.User) error {
return r.getDB(ctx).Create(user).Error
}
func (r *userRepository) Update(ctx context.Context, user *entity.User) error {
return r.getDB(ctx).Save(user).Error
}
func (r *userRepository) Delete(ctx context.Context, id int64) error {
return r.getDB(ctx).Delete(&entity.User{}, id).Error
}
go
// internal/service/order_service.go
type orderService struct {
txManager database.TxManager
orderRepo repository.OrderRepository
productRepo repository.ProductRepository
userRepo repository.UserRepository
logger *zap.Logger
}
func (s *orderService) CreateOrder(ctx context.Context, req *dto.CreateOrderRequest) (*entity.Order, error) {
var order *entity.Order
err := s.txManager.WithTransaction(ctx, func(txCtx context.Context) error {
// 1. 检查用户
user, err := s.userRepo.FindByID(txCtx, req.UserID)
if err != nil {
return err
}
if user == nil {
return errors.ErrUserNotFound
}
// 2. 检查库存并扣减
product, err := s.productRepo.FindByIDForUpdate(txCtx, req.ProductID)
if err != nil {
return err
}
if product == nil {
return errors.New(errors.CodeNotFound, "商品不存在")
}
if product.Stock < req.Quantity {
return errors.ErrInsufficientStock
}
product.Stock -= req.Quantity
if err := s.productRepo.Update(txCtx, product); err != nil {
return err
}
// 3. 创建订单
order = &entity.Order{
UserID: req.UserID,
ProductID: req.ProductID,
Quantity: req.Quantity,
TotalPrice: product.Price * float64(req.Quantity),
Status: entity.OrderStatusPending,
CreatedAt: time.Now(),
}
if err := s.orderRepo.Create(txCtx, order); err != nil {
return err
}
// 4. 创建订单明细
detail := &entity.OrderDetail{
OrderID: order.ID,
ProductID: req.ProductID,
Quantity: req.Quantity,
Price: product.Price,
}
if err := s.orderRepo.CreateDetail(txCtx, detail); err != nil {
return err
}
return nil
})
if err != nil {
s.logger.Error("create order failed",
zap.Error(err),
zap.Int64("user_id", req.UserID))
return nil, err
}
return order, nil
}
// 带 Saga 补偿的分布式事务示例
func (s *orderService) CreateOrderWithSaga(ctx context.Context, req *dto.CreateOrderRequest) (*entity.Order, error) {
var order *entity.Order
var compensations []func() error
// 定义补偿函数
defer func() {
if r := recover(); r != nil {
s.executeCompensations(compensations)
panic(r)
}
}()
// Step 1: 扣减库存
if err := s.productRepo.DeductStock(ctx, req.ProductID, req.Quantity); err != nil {
return nil, err
}
compensations = append(compensations, func() error {
return s.productRepo.AddStock(ctx, req.ProductID, req.Quantity)
})
// Step 2: 创建订单
order = &entity.Order{
UserID: req.UserID,
ProductID: req.ProductID,
Quantity: req.Quantity,
Status: entity.OrderStatusPending,
}
if err := s.orderRepo.Create(ctx, order); err != nil {
s.executeCompensations(compensations)
return nil, err
}
compensations = append(compensations, func() error {
return s.orderRepo.Delete(ctx, order.ID)
})
// Step 3: 调用支付服务(外部服务)
if err := s.paymentClient.CreatePayment(ctx, order.ID, order.TotalPrice); err != nil {
s.executeCompensations(compensations)
return nil, err
}
return order, nil
}
func (s *orderService) executeCompensations(compensations []func() error) {
// 逆序执行补偿
for i := len(compensations) - 1; i >= 0; i-- {
if err := compensations[i](); err != nil {
s.logger.Error("compensation failed", zap.Error(err))
}
}
}
Java 实现
java
// service/impl/OrderServiceImpl.java
@Service
@RequiredArgsConstructor
@Slf4j
public class OrderServiceImpl implements OrderService {
private final OrderMapper orderMapper;
private final ProductMapper productMapper;
private final UserMapper userMapper;
private final OrderDetailMapper orderDetailMapper;
@Override
@Transactional(rollbackFor = Exception.class)
public Order createOrder(CreateOrderRequest request) {
// 1. 检查用户
User user = userMapper.selectById(request.getUserId());
if (user == null) {
throw new BusinessException(ErrorCode.USER_NOT_FOUND);
}
// 2. 检查库存并扣减(悲观锁)
Product product = productMapper.selectByIdForUpdate(request.getProductId());
if (product == null) {
throw new BusinessException(ErrorCode.NOT_FOUND, "商品不存在");
}
if (product.getStock() < request.getQuantity()) {
throw new BusinessException(ErrorCode.INSUFFICIENT_STOCK);
}
product.setStock(product.getStock() - request.getQuantity());
product.setUpdatedAt(LocalDateTime.now());
productMapper.updateById(product);
// 3. 创建订单
Order order = new Order();
order.setUserId(request.getUserId());
order.setProductId(request.getProductId());
order.setQuantity(request.getQuantity());
order.setTotalPrice(product.getPrice().multiply(BigDecimal.valueOf(request.getQuantity())));
order.setStatus(OrderStatus.PENDING.getCode());
order.setCreatedAt(LocalDateTime.now());
orderMapper.insert(order);
// 4. 创建订单明细
OrderDetail detail = new OrderDetail();
detail.setOrderId(order.getId());
detail.setProductId(request.getProductId());
detail.setQuantity(request.getQuantity());
detail.setPrice(product.getPrice());
orderDetailMapper.insert(detail);
return order;
}
/**
* 带传播行为的嵌套事务
*/
@Override
@Transactional(rollbackFor = Exception.class)
public Order createOrderWithNotification(CreateOrderRequest request) {
// 创建订单(主事务)
Order order = createOrder(request);
// 发送通知(独立事务,失败不影响主事务)
try {
notificationService.sendOrderCreatedNotification(order);
} catch (Exception e) {
log.warn("Failed to send notification for order: {}", order.getId(), e);
}
return order;
}
/**
* 取消订单(状态校验 + 库存回滚)
*/
@Override
@Transactional(rollbackFor = Exception.class)
public void cancelOrder(Long orderId, String reason) {
Order order = orderMapper.selectByIdForUpdate(orderId);
if (order == null) {
throw new BusinessException(ErrorCode.ORDER_NOT_FOUND);
}
// 状态校验
if (!OrderStatus.canCancel(order.getStatus())) {
throw new BusinessException(ErrorCode.ORDER_STATUS_ERROR,
"当前订单状态不允许取消");
}
// 更新订单状态
order.setStatus(OrderStatus.CANCELLED.getCode());
order.setCancelReason(reason);
order.setCancelledAt(LocalDateTime.now());
order.setUpdatedAt(LocalDateTime.now());
orderMapper.updateById(order);
// 恢复库存
productMapper.addStock(order.getProductId(), order.getQuantity());
log.info("Order cancelled: orderId={}, reason={}", orderId, reason);
}
}
// 通知服务 - 使用 REQUIRES_NEW 传播行为
@Service
@RequiredArgsConstructor
@Slf4j
public class NotificationServiceImpl implements NotificationService {
private final NotificationMapper notificationMapper;
private final EmailClient emailClient;
/**
* 独立事务:即使失败也不影响主事务
*/
@Override
@Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
public void sendOrderCreatedNotification(Order order) {
// 记录通知
Notification notification = new Notification();
notification.setUserId(order.getUserId());
notification.setType("ORDER_CREATED");
notification.setContent("您的订单 " + order.getId() + " 已创建成功");
notification.setCreatedAt(LocalDateTime.now());
notificationMapper.insert(notification);
// 发送邮件(可能失败)
emailClient.sendOrderConfirmation(order);
}
/**
* 异步发送,不影响主流程
*/
@Override
@Async
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void sendOrderCreatedNotificationAsync(Order order) {
sendOrderCreatedNotification(order);
}
}
java
// 编程式事务管理
@Service
@RequiredArgsConstructor
@Slf4j
public class OrderServiceWithProgrammaticTx implements OrderService {
private final TransactionTemplate transactionTemplate;
private final PlatformTransactionManager transactionManager;
private final OrderMapper orderMapper;
private final ProductMapper productMapper;
/**
* 使用 TransactionTemplate
*/
public Order createOrderWithTemplate(CreateOrderRequest request) {
return transactionTemplate.execute(status -> {
try {
// 业务逻辑
Product product = productMapper.selectByIdForUpdate(request.getProductId());
if (product.getStock() < request.getQuantity()) {
throw new BusinessException(ErrorCode.INSUFFICIENT_STOCK);
}
product.setStock(product.getStock() - request.getQuantity());
productMapper.updateById(product);
Order order = new Order();
order.setProductId(request.getProductId());
order.setQuantity(request.getQuantity());
order.setStatus(OrderStatus.PENDING.getCode());
orderMapper.insert(order);
return order;
} catch (Exception e) {
status.setRollbackOnly();
throw e;
}
});
}
/**
* 手动控制事务
*/
public Order createOrderManual(CreateOrderRequest request) {
DefaultTransactionDefinition def = new DefaultTransactionDefinition();
def.setIsolationLevel(TransactionDefinition.ISOLATION_READ_COMMITTED);
def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
def.setTimeout(30);
TransactionStatus status = transactionManager.getTransaction(def);
try {
// 业务逻辑
Product product = productMapper.selectByIdForUpdate(request.getProductId());
if (product.getStock() < request.getQuantity()) {
throw new BusinessException(ErrorCode.INSUFFICIENT_STOCK);
}
product.setStock(product.getStock() - request.getQuantity());
productMapper.updateById(product);
Order order = new Order();
order.setProductId(request.getProductId());
order.setQuantity(request.getQuantity());
orderMapper.insert(order);
transactionManager.commit(status);
return order;
} catch (Exception e) {
transactionManager.rollback(status);
throw e;
}
}
/**
* 分段提交(大批量处理)
*/
@Override
public void batchUpdateOrderStatus(List<Long> orderIds, Integer newStatus) {
int batchSize = 100;
for (int i = 0; i < orderIds.size(); i += batchSize) {
int end = Math.min(i + batchSize, orderIds.size());
List<Long> batch = orderIds.subList(i, end);
transactionTemplate.execute(status -> {
orderMapper.batchUpdateStatus(batch, newStatus, LocalDateTime.now());
return null;
});
log.info("Batch updated orders: {}-{}", i, end);
}
}
}
java
// 分布式事务 - Seata AT 模式示例
@Service
@RequiredArgsConstructor
@Slf4j
public class DistributedOrderService {
private final OrderMapper orderMapper;
private final ProductFeignClient productClient;
private final PaymentFeignClient paymentClient;
/**
* Seata 分布式事务
*/
@GlobalTransactional(name = "create-order", rollbackFor = Exception.class)
public Order createDistributedOrder(CreateOrderRequest request) {
// 1. 扣减库存(库存服务)
productClient.deductStock(request.getProductId(), request.getQuantity());
// 2. 创建订单(订单服务 - 本地)
Order order = new Order();
order.setUserId(request.getUserId());
order.setProductId(request.getProductId());
order.setQuantity(request.getQuantity());
order.setStatus(OrderStatus.PENDING.getCode());
orderMapper.insert(order);
// 3. 创建支付单(支付服务)
paymentClient.createPayment(order.getId(), order.getTotalPrice());
return order;
}
/**
* TCC 模式示例
*/
@GlobalTransactional
public Order createOrderWithTcc(CreateOrderRequest request) {
// Try: 预留资源
productClient.tryDeductStock(request.getProductId(), request.getQuantity());
Order order = new Order();
order.setStatus(OrderStatus.TRYING.getCode());
orderMapper.insert(order);
paymentClient.tryCreatePayment(order.getId(), order.getTotalPrice());
// Confirm 和 Cancel 由 Seata 框架自动调用
return order;
}
}
// Saga 模式 - 手动补偿
@Service
@RequiredArgsConstructor
@Slf4j
public class SagaOrderService {
private final OrderMapper orderMapper;
private final ProductClient productClient;
private final PaymentClient paymentClient;
public Order createOrderWithSaga(CreateOrderRequest request) {
List<Runnable> compensations = new ArrayList<>();
try {
// Step 1: 扣减库存
productClient.deductStock(request.getProductId(), request.getQuantity());
compensations.add(() ->
productClient.addStock(request.getProductId(), request.getQuantity()));
// Step 2: 创建订单
Order order = new Order();
order.setProductId(request.getProductId());
order.setQuantity(request.getQuantity());
order.setStatus(OrderStatus.PENDING.getCode());
orderMapper.insert(order);
compensations.add(() -> orderMapper.deleteById(order.getId()));
// Step 3: 创建支付
paymentClient.createPayment(order.getId(), order.getTotalPrice());
compensations.add(() -> paymentClient.cancelPayment(order.getId()));
return order;
} catch (Exception e) {
// 执行补偿(逆序)
log.error("Saga failed, executing compensations", e);
Collections.reverse(compensations);
for (Runnable compensation : compensations) {
try {
compensation.run();
} catch (Exception compEx) {
log.error("Compensation failed", compEx);
// 记录补偿失败,人工处理
}
}
throw new BusinessException(ErrorCode.INTERNAL_ERROR, "订单创建失败");
}
}
}
事务对比表
| 方面 | Go | Java |
|---|---|---|
| 事务声明 | 手动 Begin/Commit/Rollback | @Transactional 注解 |
| 事务传播 | Context 传递手动处理 | propagation 属性 |
| 隔离级别 | BeginTx 参数 | isolation 属性 |
| 只读事务 | 需手动实现 | readOnly = true |
| 超时控制 | Context WithTimeout | timeout 属性 |
| 嵌套事务 | 需手动保存点 | NESTED 传播行为 |
| 分布式事务 | 手动 Saga/消息队列 | Seata/手动 Saga |
十一、限流与熔断
Go 实现
go
// pkg/ratelimit/ratelimit.go
package ratelimit
import (
"context"
"time"
"github.com/redis/go-redis/v9"
"golang.org/x/time/rate"
)
// 本地限流器(单机)
type LocalRateLimiter struct {
limiter *rate.Limiter
}
func NewLocalRateLimiter(r rate.Limit, b int) *LocalRateLimiter {
return &LocalRateLimiter{
limiter: rate.NewLimiter(r, b),
}
}
func (l *LocalRateLimiter) Allow() bool {
return l.limiter.Allow()
}
func (l *LocalRateLimiter) Wait(ctx context.Context) error {
return l.limiter.Wait(ctx)
}
// 分布式限流器(基于 Redis)
type RedisRateLimiter struct {
client *redis.Client
keyPrefix string
limit int
window time.Duration
}
func NewRedisRateLimiter(client *redis.Client, keyPrefix string, limit int, window time.Duration) *RedisRateLimiter {
return &RedisRateLimiter{
client: client,
keyPrefix: keyPrefix,
limit: limit,
window: window,
}
}
// 滑动窗口限流
func (l *RedisRateLimiter) Allow(ctx context.Context, key string) (bool, error) {
now := time.Now().UnixMilli()
windowStart := now - l.window.Milliseconds()
fullKey := l.keyPrefix + key
pipe := l.client.Pipeline()
// 移除窗口外的请求
pipe.ZRemRangeByScore(ctx, fullKey, "0", fmt.Sprintf("%d", windowStart))
// 获取当前窗口内的请求数
countCmd := pipe.ZCard(ctx, fullKey)
// 添加当前请求
pipe.ZAdd(ctx, fullKey, redis.Z{Score: float64(now), Member: now})
// 设置过期时间
pipe.Expire(ctx, fullKey, l.window)
_, err := pipe.Exec(ctx)
if err != nil {
return false, err
}
count := countCmd.Val()
return count < int64(l.limit), nil
}
// 令牌桶限流(Redis 实现)
const tokenBucketScript = `
local key = KEYS[1]
local capacity = tonumber(ARGV[1])
local rate = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
local requested = tonumber(ARGV[4])
local bucket = redis.call('HMGET', key, 'tokens', 'last_time')
local tokens = tonumber(bucket[1]) or capacity
local last_time = tonumber(bucket[2]) or now
local elapsed = now - last_time
local new_tokens = math.min(capacity, tokens + elapsed * rate / 1000)
if new_tokens >= requested then
new_tokens = new_tokens - requested
redis.call('HMSET', key, 'tokens', new_tokens, 'last_time', now)
redis.call('EXPIRE', key, math.ceil(capacity / rate) + 1)
return 1
else
return 0
end
`
type TokenBucketLimiter struct {
client *redis.Client
script *redis.Script
capacity int
rate int // tokens per second
}
func NewTokenBucketLimiter(client *redis.Client, capacity, rate int) *TokenBucketLimiter {
return &TokenBucketLimiter{
client: client,
script: redis.NewScript(tokenBucketScript),
capacity: capacity,
rate: rate,
}
}
func (l *TokenBucketLimiter) Allow(ctx context.Context, key string) (bool, error) {
result, err := l.script.Run(ctx, l.client, []string{key},
l.capacity, l.rate, time.Now().UnixMilli(), 1).Int()
if err != nil {
return false, err
}
return result == 1, nil
}
go
// internal/handler/middleware/ratelimit.go
package middleware
import (
"github.com/gin-gonic/gin"
)
// IP 限流中间件
func RateLimitByIP(limiter *ratelimit.RedisRateLimiter) gin.HandlerFunc {
return func(c *gin.Context) {
ip := c.ClientIP()
allowed, err := limiter.Allow(c.Request.Context(), "ip:"+ip)
if err != nil {
// 限流器故障时放行(降级策略)
c.Next()
return
}
if !allowed {
response.TooManyRequests(c, "请求过于频繁,请稍后再试")
c.Abort()
return
}
c.Next()
}
}
// 用户限流中间件
func RateLimitByUser(limiter *ratelimit.RedisRateLimiter) gin.HandlerFunc {
return func(c *gin.Context) {
userID := GetCurrentUserID(c)
if userID == 0 {
c.Next()
return
}
allowed, err := limiter.Allow(c.Request.Context(), fmt.Sprintf("user:%d", userID))
if err != nil {
c.Next()
return
}
if !allowed {
response.TooManyRequests(c, "请求过于频繁,请稍后再试")
c.Abort()
return
}
c.Next()
}
}
// API 限流中间件(按接口)
func RateLimitByAPI(limiter *ratelimit.TokenBucketLimiter) gin.HandlerFunc {
return func(c *gin.Context) {
key := fmt.Sprintf("api:%s:%s", c.Request.Method, c.FullPath())
allowed, err := limiter.Allow(c.Request.Context(), key)
if err != nil {
c.Next()
return
}
if !allowed {
c.Header("Retry-After", "1")
response.TooManyRequests(c, "接口请求过于频繁")
c.Abort()
return
}
c.Next()
}
}
go
// pkg/circuitbreaker/circuitbreaker.go
package circuitbreaker
import (
"context"
"errors"
"sync"
"time"
)
var (
ErrCircuitOpen = errors.New("circuit breaker is open")
ErrTooManyRequests = errors.New("too many requests")
)
type State int
const (
StateClosed State = iota
StateOpen
StateHalfOpen
)
type CircuitBreaker struct {
name string
maxFailures int
timeout time.Duration
halfOpenMax int
mu sync.RWMutex
state State
failures int
successes int
lastFailure time.Time
halfOpenCount int
}
type Config struct {
Name string
MaxFailures int // 触发熔断的失败次数
Timeout time.Duration // 熔断持续时间
HalfOpenMax int // 半开状态允许的请求数
}
func New(cfg Config) *CircuitBreaker {
return &CircuitBreaker{
name: cfg.Name,
maxFailures: cfg.MaxFailures,
timeout: cfg.Timeout,
halfOpenMax: cfg.HalfOpenMax,
state: StateClosed,
}
}
func (cb *CircuitBreaker) Execute(ctx context.Context, fn func() error) error {
if err := cb.beforeRequest(); err != nil {
return err
}
err := fn()
cb.afterRequest(err)
return err
}
func (cb *CircuitBreaker) beforeRequest() error {
cb.mu.Lock()
defer cb.mu.Unlock()
switch cb.state {
case StateClosed:
return nil
case StateOpen:
if time.Since(cb.lastFailure) > cb.timeout {
cb.state = StateHalfOpen
cb.halfOpenCount = 0
return nil
}
return ErrCircuitOpen
case StateHalfOpen:
if cb.halfOpenCount >= cb.halfOpenMax {
return ErrTooManyRequests
}
cb.halfOpenCount++
return nil
}
return nil
}
func (cb *CircuitBreaker) afterRequest(err error) {
cb.mu.Lock()
defer cb.mu.Unlock()
if err != nil {
cb.onFailure()
} else {
cb.onSuccess()
}
}
func (cb *CircuitBreaker) onFailure() {
cb.failures++
cb.lastFailure = time.Now()
switch cb.state {
case StateClosed:
if cb.failures >= cb.maxFailures {
cb.state = StateOpen
}
case StateHalfOpen:
cb.state = StateOpen
}
}
func (cb *CircuitBreaker) onSuccess() {
switch cb.state {
case StateClosed:
cb.failures = 0
case StateHalfOpen:
cb.successes++
if cb.successes >= cb.halfOpenMax {
cb.state = StateClosed
cb.failures = 0
cb.successes = 0
}
}
}
func (cb *CircuitBreaker) State() State {
cb.mu.RLock()
defer cb.mu.RUnlock()
return cb.state
}
// 使用示例
type PaymentClient struct {
cb *CircuitBreaker
httpCli *http.Client
}
func (c *PaymentClient) CreatePayment(ctx context.Context, orderID int64, amount float64) error {
return c.cb.Execute(ctx, func() error {
resp, err := c.httpCli.Post(...)
if err != nil {
return err
}
if resp.StatusCode >= 500 {
return errors.New("payment service error")
}
return nil
})
}
Java 实现(Resilience4j)
java
// config/Resilience4jConfig.java
@Configuration
public class Resilience4jConfig {
@Bean
public RateLimiterConfig rateLimiterConfig() {
return RateLimiterConfig.custom()
.limitRefreshPeriod(Duration.ofSeconds(1))
.limitForPeriod(100) // 每秒 100 个请求
.timeoutDuration(Duration.ofMillis(500))
.build();
}
@Bean
public RateLimiterRegistry rateLimiterRegistry(RateLimiterConfig config) {
return RateLimiterRegistry.of(config);
}
@Bean
public CircuitBreakerConfig circuitBreakerConfig() {
return CircuitBreakerConfig.custom()
.failureRateThreshold(50) // 失败率阈值 50%
.waitDurationInOpenState(Duration.ofSeconds(30)) // 熔断持续时间
.permittedNumberOfCallsInHalfOpenState(10) // 半开状态请求数
.slidingWindowSize(100) // 滑动窗口大小
.slidingWindowType(CircuitBreakerConfig.SlidingWindowType.COUNT_BASED)
.build();
}
@Bean
public CircuitBreakerRegistry circuitBreakerRegistry(CircuitBreakerConfig config) {
return CircuitBreakerRegistry.of(config);
}
@Bean
public RetryConfig retryConfig() {
return RetryConfig.custom()
.maxAttempts(3)
.waitDuration(Duration.ofMillis(500))
.retryExceptions(IOException.class, TimeoutException.class)
.ignoreExceptions(BusinessException.class)
.build();
}
@Bean
public RetryRegistry retryRegistry(RetryConfig config) {
return RetryRegistry.of(config);
}
}
// application.yml 配置方式
/*
resilience4j:
ratelimiter:
instances:
default:
limitForPeriod: 100
limitRefreshPeriod: 1s
timeoutDuration: 500ms
userApi:
limitForPeriod: 50
limitRefreshPeriod: 1s
circuitbreaker:
instances:
paymentService:
failureRateThreshold: 50
waitDurationInOpenState: 30s
permittedNumberOfCallsInHalfOpenState: 10
slidingWindowSize: 100
retry:
instances:
paymentService:
maxAttempts: 3
waitDuration: 500ms
*/
java
// service/impl/PaymentServiceImpl.java
@Service
@RequiredArgsConstructor
@Slf4j
public class PaymentServiceImpl implements PaymentService {
private final PaymentClient paymentClient;
private final CircuitBreakerRegistry circuitBreakerRegistry;
private final RetryRegistry retryRegistry;
private final RateLimiterRegistry rateLimiterRegistry;
@Override
@CircuitBreaker(name = "paymentService", fallbackMethod = "createPaymentFallback")
@Retry(name = "paymentService")
@RateLimiter(name = "paymentService")
public PaymentResult createPayment(Long orderId, BigDecimal amount) {
log.info("Creating payment for order: {}", orderId);
return paymentClient.create(orderId, amount);
}
// 熔断降级方法
public PaymentResult createPaymentFallback(Long orderId, BigDecimal amount, Throwable t) {
log.warn("Payment service fallback triggered for order: {}, error: {}",
orderId, t.getMessage());
// 返回降级响应
PaymentResult result = new PaymentResult();
result.setSuccess(false);
result.setMessage("支付服务暂时不可用,请稍后重试");
result.setPending(true);
return result;
}
// 编程式使用
public PaymentResult createPaymentProgrammatic(Long orderId, BigDecimal amount) {
CircuitBreaker circuitBreaker = circuitBreakerRegistry.circuitBreaker("paymentService");
Retry retry = retryRegistry.retry("paymentService");
RateLimiter rateLimiter = rateLimiterRegistry.rateLimiter("paymentService");
Supplier<PaymentResult> supplier = () -> paymentClient.create(orderId, amount);
Supplier<PaymentResult> decoratedSupplier = Decorators.ofSupplier(supplier)
.withCircuitBreaker(circuitBreaker)
.withRetry(retry)
.withRateLimiter(rateLimiter)
.withFallback(List.of(CallNotPermittedException.class, RequestNotPermitted.class),
t -> createPaymentFallback(orderId, amount, t))
.decorate();
return decoratedSupplier.get();
}
}
java
// 自定义 Redis 分布式限流
// ratelimit/RedisRateLimiter.java
@Component
@RequiredArgsConstructor
public class RedisRateLimiter {
private final StringRedisTemplate redisTemplate;
private static final String SLIDING_WINDOW_SCRIPT = """
local key = KEYS[1]
local window = tonumber(ARGV[1])
local limit = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
redis.call('ZREMRANGEBYSCORE', key, 0, now - window)
local count = redis.call('ZCARD', key)
if count < limit then
redis.call('ZADD', key, now, now .. '-' .. math.random())
redis.call('EXPIRE', key, math.ceil(window / 1000))
return 1
else
return 0
end
""";
private final DefaultRedisScript<Long> slidingWindowScript;
@PostConstruct
public void init() {
slidingWindowScript = new DefaultRedisScript<>();
slidingWindowScript.setScriptText(SLIDING_WINDOW_SCRIPT);
slidingWindowScript.setResultType(Long.class);
}
/**
* 滑动窗口限流
* @param key 限流 key
* @param windowMs 窗口大小(毫秒)
* @param limit 限制次数
* @return 是否允许
*/
public boolean isAllowed(String key, long windowMs, int limit) {
Long result = redisTemplate.execute(
slidingWindowScript,
List.of(key),
String.valueOf(windowMs),
String.valueOf(limit),
String.valueOf(System.currentTimeMillis())
);
return result != null && result == 1L;
}
/**
* IP 限流
*/
public boolean isAllowedByIp(String ip, int limit, Duration window) {
return isAllowed("ratelimit:ip:" + ip, window.toMillis(), limit);
}
/**
* 用户限流
*/
public boolean isAllowedByUser(Long userId, int limit, Duration window) {
return isAllowed("ratelimit:user:" + userId, window.toMillis(), limit);
}
/**
* 接口限流
*/
public boolean isAllowedByApi(String method, String path, int limit, Duration window) {
return isAllowed("ratelimit:api:" + method + ":" + path, window.toMillis(), limit);
}
}
// interceptor/RateLimitInterceptor.java
@Component
@RequiredArgsConstructor
public class RateLimitInterceptor implements HandlerInterceptor {
private final RedisRateLimiter rateLimiter;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
Object handler) throws Exception {
String ip = getClientIp(request);
// IP 限流:每分钟 100 次
if (!rateLimiter.isAllowedByIp(ip, 100, Duration.ofMinutes(1))) {
sendRateLimitResponse(response);
return false;
}
// 用户限流(如果已登录)
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth != null && auth.getPrincipal() instanceof CustomUserDetails) {
Long userId = ((CustomUserDetails) auth.getPrincipal()).getId();
if (!rateLimiter.isAllowedByUser(userId, 200, Duration.ofMinutes(1))) {
sendRateLimitResponse(response);
return false;
}
}
return true;
}
private void sendRateLimitResponse(HttpServletResponse response) throws IOException {
response.setStatus(HttpStatus.TOO_MANY_REQUESTS.value());
response.setContentType("application/json;charset=UTF-8");
response.setHeader("Retry-After", "60");
response.getWriter().write(
"{\"code\":429,\"message\":\"请求过于频繁,请稍后再试\"}");
}
private String getClientIp(HttpServletRequest request) {
String ip = request.getHeader("X-Forwarded-For");
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("X-Real-IP");
}
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
return ip.split(",")[0].trim();
}
}
限流熔断对比表
| 方面 | Go | Java |
|---|---|---|
| 本地限流 | golang.org/x/time/rate | Guava RateLimiter / Bucket4j |
| 分布式限流 | Redis Lua 脚本手动实现 | Redis Lua / Redisson |
| 熔断器 | 手动实现 / sony/gobreaker | Resilience4j / Sentinel |
| 配置方式 | 代码配置 | 注解 + YAML 配置 |
| 降级处理 | 回调函数 | fallbackMethod |
| 监控指标 | Prometheus 手动埋点 | Actuator 自动暴露 |
十二、完整分层代码对比
12.1 Handler / Controller 层
Go 实现
go
// internal/handler/user_handler.go
package handler
import (
"strconv"
"github.com/gin-gonic/gin"
"myapp/internal/dto/request"
"myapp/internal/dto/response"
"myapp/internal/service"
"myapp/pkg/validator"
)
type UserHandler struct {
userService service.UserService
}
func NewUserHandler(userService service.UserService) *UserHandler {
return &UserHandler{userService: userService}
}
// Create 创建用户
// @Summary 创建用户
// @Tags 用户管理
// @Accept json
// @Produce json
// @Param request body request.CreateUserRequest true "创建用户请求"
// @Success 200 {object} response.Response{data=response.UserResponse}
// @Failure 400 {object} response.Response
// @Router /api/v1/users [post]
func (h *UserHandler) Create(c *gin.Context) {
var req request.CreateUserRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, validator.GetValidationError(err))
return
}
user, err := h.userService.Create(c.Request.Context(), &req)
if err != nil {
response.Error(c, err)
return
}
response.Success(c, response.ToUserResponse(user))
}
// GetByID 根据ID获取用户
func (h *UserHandler) GetByID(c *gin.Context) {
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
response.BadRequest(c, "无效的用户ID")
return
}
user, err := h.userService.GetByID(c.Request.Context(), id)
if err != nil {
response.Error(c, err)
return
}
response.Success(c, response.ToUserResponse(user))
}
// List 获取用户列表
func (h *UserHandler) List(c *gin.Context) {
var req request.ListUserRequest
if err := c.ShouldBindQuery(&req); err != nil {
response.BadRequest(c, validator.GetValidationError(err))
return
}
req.SetDefaults()
users, total, err := h.userService.List(c.Request.Context(), &req)
if err != nil {
response.Error(c, err)
return
}
list := make([]*response.UserResponse, len(users))
for i, u := range users {
list[i] = response.ToUserResponse(u)
}
response.Success(c, response.NewPageData(list, total, req.Page, req.PageSize))
}
// Update 更新用户
func (h *UserHandler) Update(c *gin.Context) {
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
response.BadRequest(c, "无效的用户ID")
return
}
var req request.UpdateUserRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, validator.GetValidationError(err))
return
}
if err := h.userService.Update(c.Request.Context(), id, &req); err != nil {
response.Error(c, err)
return
}
response.SuccessWithMessage(c, "更新成功", nil)
}
// Delete 删除用户
func (h *UserHandler) Delete(c *gin.Context) {
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
response.BadRequest(c, "无效的用户ID")
return
}
if err := h.userService.Delete(c.Request.Context(), id); err != nil {
response.Error(c, err)
return
}
response.SuccessWithMessage(c, "删除成功", nil)
}
// Login 用户登录
func (h *UserHandler) Login(c *gin.Context) {
var req request.LoginRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, validator.GetValidationError(err))
return
}
tokenPair, err := h.userService.Login(c.Request.Context(), &req)
if err != nil {
response.Error(c, err)
return
}
response.Success(c, tokenPair)
}
// GetCurrentUser 获取当前登录用户
func (h *UserHandler) GetCurrentUser(c *gin.Context) {
userID := middleware.GetCurrentUserID(c)
if userID == 0 {
response.Unauthorized(c, "未登录")
return
}
user, err := h.userService.GetByID(c.Request.Context(), userID)
if err != nil {
response.Error(c, err)
return
}
response.Success(c, response.ToUserDetailResponse(user))
}
Java 实现
java
// controller/UserController.java
package com.example.controller;
import com.example.dto.request.*;
import com.example.dto.response.*;
import com.example.service.UserService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
@RestController
@RequestMapping("/api/v1/users")
@RequiredArgsConstructor
@Tag(name = "用户管理")
@Validated
public class UserController {
private final UserService userService;
@PostMapping
@Operation(summary = "创建用户")
@RequirePermission("user:create")
public CommonResponse<UserResponse> create(
@Valid @RequestBody CreateUserRequest request) {
return CommonResponse.success(userService.create(request));
}
@GetMapping("/{id}")
@Operation(summary = "根据ID获取用户")
@RequirePermission("user:read")
public CommonResponse<UserResponse> getById(@PathVariable Long id) {
return CommonResponse.success(userService.getById(id));
}
@GetMapping
@Operation(summary = "获取用户列表")
@RequirePermission("user:list")
public CommonResponse<PageResult<UserResponse>> list(@Valid ListUserRequest request) {
return CommonResponse.success(userService.list(request));
}
@PutMapping("/{id}")
@Operation(summary = "更新用户")
@RequirePermission("user:update")
public CommonResponse<Void> update(
@PathVariable Long id,
@Valid @RequestBody UpdateUserRequest request) {
userService.update(id, request);
return CommonResponse.success("更新成功");
}
@DeleteMapping("/{id}")
@Operation(summary = "删除用户")
@RequirePermission("user:delete")
public CommonResponse<Void> delete(@PathVariable Long id) {
userService.delete(id);
return CommonResponse.success("删除成功");
}
@PostMapping("/login")
@Operation(summary = "用户登录")
public CommonResponse<TokenPairResponse> login(
@Valid @RequestBody LoginRequest request) {
return CommonResponse.success(userService.login(request));
}
@GetMapping("/me")
@Operation(summary = "获取当前登录用户")
public CommonResponse<UserDetailResponse> getCurrentUser() {
Long userId = SecurityUtils.getCurrentUserId();
return CommonResponse.success(userService.getDetail(userId));
}
@PutMapping("/me/password")
@Operation(summary = "修改密码")
public CommonResponse<Void> changePassword(
@Valid @RequestBody ChangePasswordRequest request) {
Long userId = SecurityUtils.getCurrentUserId();
userService.changePassword(userId, request);
return CommonResponse.success("密码修改成功");
}
@PostMapping("/{id}/disable")
@Operation(summary = "禁用用户")
@RequirePermission("user:disable")
@AuditLog(action = AuditAction.UPDATE, resource = "user", resourceIdExpr = "#id")
public CommonResponse<Void> disable(@PathVariable Long id) {
userService.updateStatus(id, UserStatus.DISABLED.getCode());
return CommonResponse.success("用户已禁用");
}
}
Handler/Controller 对比表
| 方面 | Go | Java |
|---|---|---|
| 路由绑定 | 构造函数注入 + 手动注册 | @RequestMapping 注解 |
| 参数校验 | ShouldBindJSON + 手动调用 | @Valid 自动触发 |
| 路径参数 | c.Param("id") + 手动转换 | @PathVariable 自动转换 |
| 查询参数 | c.ShouldBindQuery | 自动绑定到对象 |
| 响应封装 | 调用 response.Success/Error | 返回 CommonResponse |
| API 文档 | swaggo 注释生成 | SpringDoc 注解 |
| 权限控制 | 中间件链 | @RequirePermission 注解 |
12.2 Service 层
Go 实现
go
// internal/service/user_service.go
package service
import (
"context"
"time"
"myapp/internal/dto/request"
"myapp/internal/entity"
"myapp/internal/errors"
"myapp/internal/repository"
"myapp/internal/repository/cache"
"myapp/pkg/auth"
"myapp/pkg/database"
"go.uber.org/zap"
"golang.org/x/crypto/bcrypt"
)
type UserService interface {
Create(ctx context.Context, req *request.CreateUserRequest) (*entity.User, error)
GetByID(ctx context.Context, id int64) (*entity.User, error)
List(ctx context.Context, req *request.ListUserRequest) ([]*entity.User, int64, error)
Update(ctx context.Context, id int64, req *request.UpdateUserRequest) error
Delete(ctx context.Context, id int64) error
Login(ctx context.Context, req *request.LoginRequest) (*auth.TokenPair, error)
ChangePassword(ctx context.Context, userID int64, req *request.ChangePasswordRequest) error
}
type userService struct {
userRepo repository.UserRepository
userCache cache.UserCache
txManager database.TxManager
jwtManager *auth.JWTManager
logger *zap.Logger
}
func NewUserService(
userRepo repository.UserRepository,
userCache cache.UserCache,
txManager database.TxManager,
jwtManager *auth.JWTManager,
logger *zap.Logger,
) UserService {
return &userService{
userRepo: userRepo,
userCache: userCache,
txManager: txManager,
jwtManager: jwtManager,
logger: logger,
}
}
func (s *userService) Create(ctx context.Context, req *request.CreateUserRequest) (*entity.User, error) {
// 检查邮箱是否已存在
existing, err := s.userRepo.FindByEmail(ctx, req.Email)
if err != nil {
return nil, errors.Wrap(errors.CodeInternalError, err)
}
if existing != nil {
return nil, errors.ErrUserExists
}
// 检查用户名是否已存在
existing, err = s.userRepo.FindByUsername(ctx, req.Username)
if err != nil {
return nil, errors.Wrap(errors.CodeInternalError, err)
}
if existing != nil {
return nil, errors.NewWithMessage(errors.CodeConflict, "用户名已存在")
}
// 密码加密
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
if err != nil {
return nil, errors.Wrap(errors.CodeInternalError, err)
}
user := &entity.User{
Username: req.Username,
Email: req.Email,
Mobile: req.Mobile,
Password: string(hashedPassword),
RoleID: req.RoleID,
Status: entity.UserStatusActive,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
if err := s.userRepo.Create(ctx, user); err != nil {
return nil, errors.Wrap(errors.CodeInternalError, err)
}
s.logger.Info("user created",
zap.Int64("user_id", user.ID),
zap.String("username", user.Username))
return user, nil
}
func (s *userService) GetByID(ctx context.Context, id int64) (*entity.User, error) {
// 先查缓存
user, err := s.userCache.GetUser(ctx, id)
if err == nil {
return user, nil
}
// 查数据库
user, err = s.userRepo.FindByID(ctx, id)
if err != nil {
return nil, errors.Wrap(errors.CodeInternalError, err)
}
if user == nil {
return nil, errors.ErrUserNotFound
}
// 异步写入缓存
go func() {
if err := s.userCache.SetUser(context.Background(), user); err != nil {
s.logger.Warn("failed to cache user", zap.Error(err))
}
}()
return user, nil
}
func (s *userService) List(ctx context.Context, req *request.ListUserRequest) ([]*entity.User, int64, error) {
users, total, err := s.userRepo.FindByCondition(ctx, &repository.UserQuery{
Keyword: req.Keyword,
Status: req.Status,
Page: req.Page,
PageSize: req.PageSize,
OrderBy: req.OrderBy,
Order: req.Order,
})
if err != nil {
return nil, 0, errors.Wrap(errors.CodeInternalError, err)
}
return users, total, nil
}
func (s *userService) Update(ctx context.Context, id int64, req *request.UpdateUserRequest) error {
user, err := s.userRepo.FindByID(ctx, id)
if err != nil {
return errors.Wrap(errors.CodeInternalError, err)
}
if user == nil {
return errors.ErrUserNotFound
}
// 检查用户名唯一性
if req.Username != nil && *req.Username != user.Username {
existing, err := s.userRepo.FindByUsername(ctx, *req.Username)
if err != nil {
return errors.Wrap(errors.CodeInternalError, err)
}
if existing != nil {
return errors.NewWithMessage(errors.CodeConflict, "用户名已存在")
}
user.Username = *req.Username
}
if req.Email != nil {
user.Email = *req.Email
}
if req.Mobile != nil {
user.Mobile = *req.Mobile
}
if req.Status != nil {
user.Status = *req.Status
}
user.UpdatedAt = time.Now()
if err := s.userRepo.Update(ctx, user); err != nil {
return errors.Wrap(errors.CodeInternalError, err)
}
// 删除缓存
_ = s.userCache.DeleteUser(ctx, id)
return nil
}
func (s *userService) Delete(ctx context.Context, id int64) error {
user, err := s.userRepo.FindByID(ctx, id)
if err != nil {
return errors.Wrap(errors.CodeInternalError, err)
}
if user == nil {
return errors.ErrUserNotFound
}
// 软删除
if err := s.userRepo.Delete(ctx, id); err != nil {
return errors.Wrap(errors.CodeInternalError, err)
}
// 删除缓存
_ = s.userCache.DeleteUser(ctx, id)
s.logger.Info("user deleted", zap.Int64("user_id", id))
return nil
}
func (s *userService) Login(ctx context.Context, req *request.LoginRequest) (*auth.TokenPair, error) {
user, err := s.userRepo.FindByUsername(ctx, req.Username)
if err != nil {
return nil, errors.Wrap(errors.CodeInternalError, err)
}
if user == nil {
return nil, errors.ErrUserNotFound
}
// 验证密码
if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(req.Password)); err != nil {
return nil, errors.ErrPasswordWrong
}
// 检查用户状态
if user.Status != entity.UserStatusActive {
return nil, errors.NewWithMessage(errors.CodeForbidden, "用户已被禁用")
}
// 生成 Token
tokenPair, err := s.jwtManager.GenerateTokenPair(user.ID, user.Username, user.RoleID)
if err != nil {
return nil, errors.Wrap(errors.CodeInternalError, err)
}
// 更新最后登录时间
go func() {
_ = s.userRepo.UpdateLastLogin(context.Background(), user.ID, time.Now())
}()
s.logger.Info("user logged in",
zap.Int64("user_id", user.ID),
zap.String("username", user.Username))
return tokenPair, nil
}
func (s *userService) ChangePassword(ctx context.Context, userID int64, req *request.ChangePasswordRequest) error {
user, err := s.userRepo.FindByID(ctx, userID)
if err != nil {
return errors.Wrap(errors.CodeInternalError, err)
}
if user == nil {
return errors.ErrUserNotFound
}
// 验证旧密码
if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(req.OldPassword)); err != nil {
return errors.NewWithMessage(errors.CodeBadRequest, "原密码错误")
}
// 加密新密码
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.NewPassword), bcrypt.DefaultCost)
if err != nil {
return errors.Wrap(errors.CodeInternalError, err)
}
user.Password = string(hashedPassword)
user.UpdatedAt = time.Now()
if err := s.userRepo.Update(ctx, user); err != nil {
return errors.Wrap(errors.CodeInternalError, err)
}
return nil
}
Java 实现
java
// service/UserService.java
package com.example.service;
import com.example.dto.request.*;
import com.example.dto.response.*;
import com.example.entity.User;
public interface UserService {
UserResponse create(CreateUserRequest request);
UserResponse getById(Long id);
UserDetailResponse getDetail(Long id);
PageResult<UserResponse> list(ListUserRequest request);
void update(Long id, UpdateUserRequest request);
void delete(Long id);
TokenPairResponse login(LoginRequest request);
void changePassword(Long userId, ChangePasswordRequest request);
void updateStatus(Long id, Integer status);
}
// service/impl/UserServiceImpl.java
package com.example.service.impl;
import com.example.dto.request.*;
import com.example.dto.response.*;
import com.example.entity.User;
import com.example.enums.ErrorCode;
import com.example.enums.UserStatus;
import com.example.exception.BusinessException;
import com.example.mapper.UserMapper;
import com.example.service.UserService;
import com.example.util.JwtUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.annotation.*;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import java.time.LocalDateTime;
@Service
@RequiredArgsConstructor
@Slf4j
public class UserServiceImpl implements UserService {
private final UserMapper userMapper;
private final PasswordEncoder passwordEncoder;
private final JwtUtil jwtUtil;
@Override
@Transactional(rollbackFor = Exception.class)
public UserResponse create(CreateUserRequest request) {
// 检查邮箱是否已存在
if (userMapper.selectByEmail(request.getEmail()) != null) {
throw new BusinessException(ErrorCode.USER_EXISTS, "邮箱已被使用");
}
// 检查用户名是否已存在
if (userMapper.selectByUsername(request.getUsername()) != null) {
throw new BusinessException(ErrorCode.CONFLICT, "用户名已存在");
}
User user = new User();
user.setUsername(request.getUsername());
user.setEmail(request.getEmail());
user.setMobile(request.getMobile());
user.setPassword(passwordEncoder.encode(request.getPassword()));
user.setRoleId(request.getRoleId());
user.setStatus(UserStatus.ACTIVE.getCode());
user.setCreatedAt(LocalDateTime.now());
user.setUpdatedAt(LocalDateTime.now());
userMapper.insert(user);
log.info("User created: userId={}, username={}", user.getId(), user.getUsername());
return UserResponse.from(user);
}
@Override
@Cacheable(value = "user", key = "#id", unless = "#result == null")
public UserResponse getById(Long id) {
User user = userMapper.selectById(id);
if (user == null) {
throw new BusinessException(ErrorCode.USER_NOT_FOUND);
}
return UserResponse.from(user);
}
@Override
@Cacheable(value = "user:detail", key = "#id", unless = "#result == null")
public UserDetailResponse getDetail(Long id) {
User user = userMapper.selectWithRoleById(id);
if (user == null) {
throw new BusinessException(ErrorCode.USER_NOT_FOUND);
}
return UserDetailResponse.from(user);
}
@Override
public PageResult<UserResponse> list(ListUserRequest request) {
Page<User> page = new Page<>(request.getPage(), request.getPageSize());
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
// 关键词搜索
if (StringUtils.hasText(request.getKeyword())) {
wrapper.and(w -> w
.like(User::getUsername, request.getKeyword())
.or()
.like(User::getEmail, request.getKeyword())
.or()
.like(User::getMobile, request.getKeyword())
);
}
// 状态过滤
if (request.getStatus() != null) {
wrapper.eq(User::getStatus, request.getStatus());
}
// 排序
if ("created_at".equals(request.getOrderBy())) {
wrapper.orderBy(true, "asc".equals(request.getOrder()), User::getCreatedAt);
} else if ("updated_at".equals(request.getOrderBy())) {
wrapper.orderBy(true, "asc".equals(request.getOrder()), User::getUpdatedAt);
}
Page<User> result = userMapper.selectPage(page, wrapper);
return PageResult.of(
result.getRecords().stream().map(UserResponse::from).toList(),
result.getTotal(),
request.getPage(),
request.getPageSize()
);
}
@Override
@Transactional(rollbackFor = Exception.class)
@CacheEvict(value = {"user", "user:detail"}, key = "#id")
public void update(Long id, UpdateUserRequest request) {
User user = userMapper.selectById(id);
if (user == null) {
throw new BusinessException(ErrorCode.USER_NOT_FOUND);
}
// 检查用户名唯一性
if (StringUtils.hasText(request.getUsername())
&& !request.getUsername().equals(user.getUsername())) {
if (userMapper.selectByUsername(request.getUsername()) != null) {
throw new BusinessException(ErrorCode.CONFLICT, "用户名已存在");
}
user.setUsername(request.getUsername());
}
if (StringUtils.hasText(request.getEmail())) {
user.setEmail(request.getEmail());
}
if (StringUtils.hasText(request.getMobile())) {
user.setMobile(request.getMobile());
}
if (request.getStatus() != null) {
user.setStatus(request.getStatus());
}
user.setUpdatedAt(LocalDateTime.now());
userMapper.updateById(user);
log.info("User updated: userId={}", id);
}
@Override
@Transactional(rollbackFor = Exception.class)
@Caching(evict = {
@CacheEvict(value = "user", key = "#id"),
@CacheEvict(value = "user:detail", key = "#id")
})
public void delete(Long id) {
User user = userMapper.selectById(id);
if (user == null) {
throw new BusinessException(ErrorCode.USER_NOT_FOUND);
}
// 软删除
userMapper.deleteById(id);
log.info("User deleted: userId={}", id);
}
@Override
public TokenPairResponse login(LoginRequest request) {
User user = userMapper.selectByUsername(request.getUsername());
if (user == null) {
throw new BusinessException(ErrorCode.USER_NOT_FOUND);
}
// 验证密码
if (!passwordEncoder.matches(request.getPassword(), user.getPassword())) {
throw new BusinessException(ErrorCode.PASSWORD_WRONG);
}
// 检查用户状态
if (!UserStatus.ACTIVE.getCode().equals(user.getStatus())) {
throw new BusinessException(ErrorCode.FORBIDDEN, "用户已被禁用");
}
// 生成 Token
JwtUtil.TokenPair tokenPair = jwtUtil.generateTokenPair(
user.getId(), user.getUsername(), user.getRoleId());
// 异步更新最后登录时间
updateLastLoginAsync(user.getId());
log.info("User logged in: userId={}, username={}", user.getId(), user.getUsername());
return TokenPairResponse.from(tokenPair);
}
@Async
protected void updateLastLoginAsync(Long userId) {
try {
userMapper.updateLastLogin(userId, LocalDateTime.now());
} catch (Exception e) {
log.warn("Failed to update last login time: userId={}", userId, e);
}
}
@Override
@Transactional(rollbackFor = Exception.class)
public void changePassword(Long userId, ChangePasswordRequest request) {
User user = userMapper.selectById(userId);
if (user == null) {
throw new BusinessException(ErrorCode.USER_NOT_FOUND);
}
// 验证旧密码
if (!passwordEncoder.matches(request.getOldPassword(), user.getPassword())) {
throw new BusinessException(ErrorCode.BAD_REQUEST, "原密码错误");
}
// 更新密码
user.setPassword(passwordEncoder.encode(request.getNewPassword()));
user.setUpdatedAt(LocalDateTime.now());
userMapper.updateById(user);
log.info("User password changed: userId={}", userId);
}
@Override
@Transactional(rollbackFor = Exception.class)
@CacheEvict(value = {"user", "user:detail"}, key = "#id")
public void updateStatus(Long id, Integer status) {
User user = userMapper.selectById(id);
if (user == null) {
throw new BusinessException(ErrorCode.USER_NOT_FOUND);
}
// 状态流转校验
if (UserStatus.DELETED.getCode().equals(user.getStatus())) {
throw new BusinessException(ErrorCode.BAD_REQUEST, "已删除的用户不能修改状态");
}
user.setStatus(status);
user.setUpdatedAt(LocalDateTime.now());
userMapper.updateById(user);
log.info("User status updated: userId={}, status={}", id, status);
}
}
Service 层对比表
| 方面 | Go | Java |
|---|---|---|
| 接口定义 | interface 类型 | interface 关键字 |
| 依赖注入 | 构造函数手动注入 | @Autowired / @RequiredArgsConstructor |
| 事务管理 | txManager.WithTransaction | @Transactional 注解 |
| 缓存操作 | 手动调用 cache 方法 | @Cacheable/@CacheEvict 注解 |
| 密码加密 | bcrypt 包手动调用 | PasswordEncoder Bean |
| 日志记录 | zap.Logger 字段日志 | @Slf4j + log.info() |
| 异步处理 | go func() 协程 | @Async 注解 |
| 错误返回 | return nil, error | throw Exception |
12.3 Repository / Mapper 层
Go 实现
go
// internal/repository/user_repository.go
package repository
import (
"context"
"time"
"myapp/internal/entity"
"gorm.io/gorm"
)
type UserQuery struct {
Keyword string
Status *int
RoleID *int64
Page int
PageSize int
OrderBy string
Order string
}
type UserRepository interface {
Create(ctx context.Context, user *entity.User) error
Update(ctx context.Context, user *entity.User) error
Delete(ctx context.Context, id int64) error
FindByID(ctx context.Context, id int64) (*entity.User, error)
FindByEmail(ctx context.Context, email string) (*entity.User, error)
FindByUsername(ctx context.Context, username string) (*entity.User, error)
FindByCondition(ctx context.Context, query *UserQuery) ([]*entity.User, int64, error)
UpdateLastLogin(ctx context.Context, id int64, loginTime time.Time) error
BatchUpdateStatus(ctx context.Context, ids []int64, status int) error
}
type userRepository struct {
db *gorm.DB
}
func NewUserRepository(db *gorm.DB) UserRepository {
return &userRepository{db: db}
}
// getDB 获取数据库连接(支持事务)
func (r *userRepository) getDB(ctx context.Context) *gorm.DB {
if tx, ok := ctx.Value(database.TxKey{}).(*gorm.DB); ok {
return tx
}
return r.db.WithContext(ctx)
}
func (r *userRepository) Create(ctx context.Context, user *entity.User) error {
return r.getDB(ctx).Create(user).Error
}
func (r *userRepository) Update(ctx context.Context, user *entity.User) error {
return r.getDB(ctx).Save(user).Error
}
func (r *userRepository) Delete(ctx context.Context, id int64) error {
// 软删除
return r.getDB(ctx).Delete(&entity.User{}, id).Error
}
func (r *userRepository) FindByID(ctx context.Context, id int64) (*entity.User, error) {
var user entity.User
err := r.getDB(ctx).First(&user, id).Error
if err != nil {
if err == gorm.ErrRecordNotFound {
return nil, nil
}
return nil, err
}
return &user, nil
}
func (r *userRepository) FindByEmail(ctx context.Context, email string) (*entity.User, error) {
var user entity.User
err := r.getDB(ctx).Where("email = ?", email).First(&user).Error
if err != nil {
if err == gorm.ErrRecordNotFound {
return nil, nil
}
return nil, err
}
return &user, nil
}
func (r *userRepository) FindByUsername(ctx context.Context, username string) (*entity.User, error) {
var user entity.User
err := r.getDB(ctx).Where("username = ?", username).First(&user).Error
if err != nil {
if err == gorm.ErrRecordNotFound {
return nil, nil
}
return nil, err
}
return &user, nil
}
func (r *userRepository) FindByCondition(ctx context.Context, query *UserQuery) ([]*entity.User, int64, error) {
db := r.getDB(ctx).Model(&entity.User{})
// 关键词搜索
if query.Keyword != "" {
keyword := "%" + query.Keyword + "%"
db = db.Where("username LIKE ? OR email LIKE ? OR mobile LIKE ?",
keyword, keyword, keyword)
}
// 状态过滤
if query.Status != nil {
db = db.Where("status = ?", *query.Status)
}
// 角色过滤
if query.RoleID != nil {
db = db.Where("role_id = ?", *query.RoleID)
}
// 统计总数
var total int64
if err := db.Count(&total).Error; err != nil {
return nil, 0, err
}
// 排序
orderClause := "created_at DESC"
if query.OrderBy != "" {
order := "DESC"
if query.Order == "asc" {
order = "ASC"
}
orderClause = query.OrderBy + " " + order
}
db = db.Order(orderClause)
// 分页
offset := (query.Page - 1) * query.PageSize
db = db.Offset(offset).Limit(query.PageSize)
var users []*entity.User
if err := db.Find(&users).Error; err != nil {
return nil, 0, err
}
return users, total, nil
}
func (r *userRepository) UpdateLastLogin(ctx context.Context, id int64, loginTime time.Time) error {
return r.getDB(ctx).Model(&entity.User{}).
Where("id = ?", id).
Update("last_login_at", loginTime).Error
}
func (r *userRepository) BatchUpdateStatus(ctx context.Context, ids []int64, status int) error {
return r.getDB(ctx).Model(&entity.User{}).
Where("id IN ?", ids).
Updates(map[string]interface{}{
"status": status,
"updated_at": time.Now(),
}).Error
}
// 使用原生 SQL 的复杂查询示例
func (r *userRepository) FindWithRoleByID(ctx context.Context, id int64) (*entity.UserWithRole, error) {
var result entity.UserWithRole
err := r.getDB(ctx).Raw(`
SELECT u.*, r.name as role_name, r.code as role_code
FROM users u
LEFT JOIN roles r ON u.role_id = r.id
WHERE u.id = ? AND u.deleted_at IS NULL
`, id).Scan(&result).Error
if err != nil {
return nil, err
}
if result.ID == 0 {
return nil, nil
}
return &result, nil
}
// 悲观锁查询
func (r *userRepository) FindByIDForUpdate(ctx context.Context, id int64) (*entity.User, error) {
var user entity.User
err := r.getDB(ctx).
Clauses(clause.Locking{Strength: "UPDATE"}).
First(&user, id).Error
if err != nil {
if err == gorm.ErrRecordNotFound {
return nil, nil
}
return nil, err
}
return &user, nil
}
Java 实现(MyBatis-Plus)
java
// mapper/UserMapper.java
package com.example.mapper;
import com.example.entity.User;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.*;
import java.time.LocalDateTime;
import java.util.List;
@Mapper
public interface UserMapper extends BaseMapper<User> {
@Select("SELECT * FROM users WHERE email = #{email} AND deleted_at IS NULL")
User selectByEmail(@Param("email") String email);
@Select("SELECT * FROM users WHERE username = #{username} AND deleted_at IS NULL")
User selectByUsername(@Param("username") String username);
@Update("UPDATE users SET last_login_at = #{loginTime} WHERE id = #{id}")
void updateLastLogin(@Param("id") Long id, @Param("loginTime") LocalDateTime loginTime);
@Update("<script>" +
"UPDATE users SET status = #{status}, updated_at = NOW() " +
"WHERE id IN " +
"<foreach collection='ids' item='id' open='(' separator=',' close=')'>" +
"#{id}" +
"</foreach>" +
"</script>")
void batchUpdateStatus(@Param("ids") List<Long> ids, @Param("status") Integer status);
// 关联查询
@Select("SELECT u.*, r.name as role_name, r.code as role_code " +
"FROM users u " +
"LEFT JOIN roles r ON u.role_id = r.id " +
"WHERE u.id = #{id} AND u.deleted_at IS NULL")
@Results({
@Result(property = "id", column = "id"),
@Result(property = "username", column = "username"),
@Result(property = "email", column = "email"),
@Result(property = "role.name", column = "role_name"),
@Result(property = "role.code", column = "role_code")
})
User selectWithRoleById(@Param("id") Long id);
// 悲观锁查询
@Select("SELECT * FROM users WHERE id = #{id} FOR UPDATE")
User selectByIdForUpdate(@Param("id") Long id);
// 复杂统计查询
@Select("SELECT DATE(created_at) as date, COUNT(*) as count " +
"FROM users " +
"WHERE created_at >= #{startDate} AND created_at < #{endDate} " +
"GROUP BY DATE(created_at) " +
"ORDER BY date")
List<UserDailyStats> selectDailyStats(@Param("startDate") LocalDateTime startDate,
@Param("endDate") LocalDateTime endDate);
}
java
// mapper/UserMapper.xml(复杂查询用 XML)
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.mapper.UserMapper">
<resultMap id="UserWithRoleMap" type="com.example.entity.User">
<id property="id" column="id"/>
<result property="username" column="username"/>
<result property="email" column="email"/>
<result property="mobile" column="mobile"/>
<result property="status" column="status"/>
<result property="createdAt" column="created_at"/>
<association property="role" javaType="com.example.entity.Role">
<id property="id" column="role_id"/>
<result property="name" column="role_name"/>
<result property="code" column="role_code"/>
</association>
</resultMap>
<select id="selectUsersWithRole" resultMap="UserWithRoleMap">
SELECT
u.id, u.username, u.email, u.mobile, u.status, u.created_at,
r.id as role_id, r.name as role_name, r.code as role_code
FROM users u
LEFT JOIN roles r ON u.role_id = r.id
WHERE u.deleted_at IS NULL
<if test="query.keyword != null and query.keyword != ''">
AND (u.username LIKE CONCAT('%', #{query.keyword}, '%')
OR u.email LIKE CONCAT('%', #{query.keyword}, '%')
OR u.mobile LIKE CONCAT('%', #{query.keyword}, '%'))
</if>
<if test="query.status != null">
AND u.status = #{query.status}
</if>
<if test="query.roleId != null">
AND u.role_id = #{query.roleId}
</if>
ORDER BY
<choose>
<when test="query.orderBy == 'created_at'">
u.created_at
</when>
<when test="query.orderBy == 'updated_at'">
u.updated_at
</when>
<otherwise>
u.created_at
</otherwise>
</choose>
<if test="query.order == 'asc'">ASC</if>
<if test="query.order != 'asc'">DESC</if>
LIMIT #{query.pageSize} OFFSET #{query.offset}
</select>
<select id="countUsersWithCondition" resultType="long">
SELECT COUNT(*)
FROM users u
WHERE u.deleted_at IS NULL
<if test="query.keyword != null and query.keyword != ''">
AND (u.username LIKE CONCAT('%', #{query.keyword}, '%')
OR u.email LIKE CONCAT('%', #{query.keyword}, '%'))
</if>
<if test="query.status != null">
AND u.status = #{query.status}
</if>
</select>
</mapper>
java
// Java JPA Repository 实现(对比)
// repository/UserRepository.java
package com.example.repository;
import com.example.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.data.jpa.repository.Lock;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import javax.persistence.LockModeType;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;
public interface UserRepository extends JpaRepository<User, Long>,
JpaSpecificationExecutor<User> {
Optional<User> findByEmail(String email);
Optional<User> findByUsername(String username);
@Modifying
@Query("UPDATE User u SET u.lastLoginAt = :loginTime WHERE u.id = :id")
void updateLastLogin(@Param("id") Long id, @Param("loginTime") LocalDateTime loginTime);
@Modifying
@Query("UPDATE User u SET u.status = :status, u.updatedAt = CURRENT_TIMESTAMP WHERE u.id IN :ids")
void batchUpdateStatus(@Param("ids") List<Long> ids, @Param("status") Integer status);
// 悲观锁
@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("SELECT u FROM User u WHERE u.id = :id")
Optional<User> findByIdForUpdate(@Param("id") Long id);
// 自定义查询
@Query("SELECT u FROM User u LEFT JOIN FETCH u.role WHERE u.id = :id")
Optional<User> findWithRoleById(@Param("id") Long id);
// 统计查询
@Query(value = "SELECT DATE(created_at) as date, COUNT(*) as count " +
"FROM users WHERE created_at >= :start AND created_at < :end " +
"GROUP BY DATE(created_at)", nativeQuery = true)
List<Object[]> findDailyStats(@Param("start") LocalDateTime start,
@Param("end") LocalDateTime end);
// 存在性检查
boolean existsByEmail(String email);
boolean existsByUsername(String username);
// 软删除
@Modifying
@Query("UPDATE User u SET u.deletedAt = CURRENT_TIMESTAMP WHERE u.id = :id")
void softDelete(@Param("id") Long id);
}
Repository/Mapper 层对比表
| 方面 | Go (GORM) | Java (MyBatis) | Java (JPA) |
|---|---|---|---|
| 定义方式 | interface + struct 实现 | interface + 注解/XML | interface 继承 JpaRepository |
| 简单查询 | db.First/Find | @Select 注解 | 方法名派生查询 |
| 复杂查询 | 链式调用 / Raw SQL | XML 动态 SQL | @Query / Specification |
| 分页 | Offset/Limit | PageHelper / 手动 | Pageable 参数 |
| 事务支持 | Context 传递 tx | 自动参与事务 | 自动参与事务 |
| 悲观锁 | clause.Locking | FOR UPDATE | @Lock 注解 |
| 批量操作 | Where IN + Updates | foreach 标签 | @Modifying + @Query |
| 关联查询 | Preload / Joins / Raw | resultMap / 嵌套查询 | FETCH JOIN |
12.4 Entity / Model 层
Go 实现
go
// internal/entity/user.go
package entity
import (
"time"
"gorm.io/gorm"
)
// 用户状态
const (
UserStatusActive = 1
UserStatusDisabled = 2
UserStatusDeleted = 3
)
type User struct {
ID int64 `gorm:"primaryKey;autoIncrement" json:"id"`
Username string `gorm:"size:32;uniqueIndex;not null" json:"username"`
Email string `gorm:"size:128;uniqueIndex;not null" json:"email"`
Mobile string `gorm:"size:20;index" json:"mobile"`
Password string `gorm:"size:128;not null" json:"-"` // json:"-" 不序列化
RoleID int64 `gorm:"index;not null" json:"role_id"`
Status int `gorm:"default:1;not null" json:"status"`
Avatar string `gorm:"size:256" json:"avatar"`
LastLoginAt *time.Time `json:"last_login_at"`
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` // 软删除
// 关联(不映射到数据库)
Role *Role `gorm:"foreignKey:RoleID" json:"role,omitempty"`
}
func (User) TableName() string {
return "users"
}
// IsActive 判断用户是否激活
func (u *User) IsActive() bool {
return u.Status == UserStatusActive
}
// 带角色信息的用户(用于关联查询结果)
type UserWithRole struct {
User
RoleName string `json:"role_name"`
RoleCode string `json:"role_code"`
}
// internal/entity/role.go
type Role struct {
ID int64 `gorm:"primaryKey" json:"id"`
Name string `gorm:"size:32;not null" json:"name"`
Code string `gorm:"size:32;uniqueIndex;not null" json:"code"`
Description string `gorm:"size:256" json:"description"`
Status int `gorm:"default:1" json:"status"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
Permissions []Permission `gorm:"many2many:role_permissions" json:"permissions,omitempty"`
}
// internal/entity/permission.go
type Permission struct {
ID int64 `gorm:"primaryKey" json:"id"`
Name string `gorm:"size:64;not null" json:"name"`
Code string `gorm:"size:64;uniqueIndex;not null" json:"code"`
Resource string `gorm:"size:32" json:"resource"`
Action string `gorm:"size:32" json:"action"`
CreatedAt time.Time `json:"created_at"`
}
// internal/entity/order.go
type Order struct {
ID int64 `gorm:"primaryKey" json:"id"`
OrderNo string `gorm:"size:32;uniqueIndex;not null" json:"order_no"`
UserID int64 `gorm:"index;not null" json:"user_id"`
ProductID int64 `gorm:"index;not null" json:"product_id"`
Quantity int `gorm:"not null" json:"quantity"`
TotalPrice float64 `gorm:"type:decimal(10,2);not null" json:"total_price"`
Status int `gorm:"default:1;not null" json:"status"`
PaymentTime *time.Time `json:"payment_time"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
User *User `gorm:"foreignKey:UserID" json:"user,omitempty"`
Product *Product `gorm:"foreignKey:ProductID" json:"product,omitempty"`
}
// 订单状态
const (
OrderStatusPending = 1 // 待支付
OrderStatusPaid = 2 // 已支付
OrderStatusShipped = 3 // 已发货
OrderStatusCompleted = 4 // 已完成
OrderStatusCancelled = 5 // 已取消
)
// CanCancel 判断订单是否可取消
func (o *Order) CanCancel() bool {
return o.Status == OrderStatusPending || o.Status == OrderStatusPaid
}
Java 实现
java
// entity/User.java
package com.example.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.time.LocalDateTime;
@Data
@TableName("users")
public class User {
@TableId(type = IdType.AUTO)
private Long id;
private String username;
private String email;
private String mobile;
private String password;
private Long roleId;
private Integer status;
private String avatar;
private LocalDateTime lastLoginAt;
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createdAt;
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updatedAt;
@TableLogic // 逻辑删除
private LocalDateTime deletedAt;
// 非数据库字段
@TableField(exist = false)
private Role role;
public boolean isActive() {
return UserStatus.ACTIVE.getCode().equals(this.status);
}
}
// entity/User.java(JPA 版本)
package com.example.entity;
import lombok.Data;
import org.hibernate.annotations.SQLDelete;
import org.hibernate.annotations.Where;
import javax.persistence.*;
import java.time.LocalDateTime;
@Data
@Entity
@Table(name = "users", indexes = {
@Index(name = "idx_email", columnList = "email"),
@Index(name = "idx_username", columnList = "username"),
@Index(name = "idx_role_id", columnList = "role_id")
})
@SQLDelete(sql = "UPDATE users SET deleted_at = NOW() WHERE id = ?")
@Where(clause = "deleted_at IS NULL")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(length = 32, nullable = false, unique = true)
private String username;
@Column(length = 128, nullable = false, unique = true)
private String email;
@Column(length = 20)
private String mobile;
@Column(length = 128, nullable = false)
private String password;
@Column(name = "role_id", nullable = false)
private Long roleId;
@Column(nullable = false)
private Integer status = 1;
@Column(length = 256)
private String avatar;
@Column(name = "last_login_at")
private LocalDateTime lastLoginAt;
@Column(name = "created_at", updatable = false)
private LocalDateTime createdAt;
@Column(name = "updated_at")
private LocalDateTime updatedAt;
@Column(name = "deleted_at")
private LocalDateTime deletedAt;
// 关联关系
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "role_id", insertable = false, updatable = false)
private Role role;
@PrePersist
protected void onCreate() {
createdAt = LocalDateTime.now();
updatedAt = LocalDateTime.now();
}
@PreUpdate
protected void onUpdate() {
updatedAt = LocalDateTime.now();
}
public boolean isActive() {
return UserStatus.ACTIVE.getCode().equals(this.status);
}
}
// enums/UserStatus.java
package com.example.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public enum UserStatus {
ACTIVE(1, "正常"),
DISABLED(2, "禁用"),
DELETED(3, "删除");
private final Integer code;
private final String description;
public static UserStatus fromCode(Integer code) {
for (UserStatus status : values()) {
if (status.code.equals(code)) {
return status;
}
}
return null;
}
}
// entity/Role.java
@Data
@Entity
@Table(name = "roles")
public class Role {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(length = 32, nullable = false)
private String name;
@Column(length = 32, nullable = false, unique = true)
private String code;
@Column(length = 256)
private String description;
private Integer status = 1;
@ManyToMany(fetch = FetchType.LAZY)
@JoinTable(
name = "role_permissions",
joinColumns = @JoinColumn(name = "role_id"),
inverseJoinColumns = @JoinColumn(name = "permission_id")
)
private List<Permission> permissions;
}
// entity/Order.java
@Data
@Entity
@Table(name = "orders")
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "order_no", length = 32, nullable = false, unique = true)
private String orderNo;
@Column(name = "user_id", nullable = false)
private Long userId;
@Column(name = "product_id", nullable = false)
private Long productId;
@Column(nullable = false)
private Integer quantity;
@Column(name = "total_price", precision = 10, scale = 2, nullable = false)
private BigDecimal totalPrice;
@Column(nullable = false)
private Integer status = 1;
@Column(name = "payment_time")
private LocalDateTime paymentTime;
@Column(name = "created_at")
private LocalDateTime createdAt;
@Column(name = "updated_at")
private LocalDateTime updatedAt;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id", insertable = false, updatable = false)
private User user;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "product_id", insertable = false, updatable = false)
private Product product;
public boolean canCancel() {
return OrderStatus.PENDING.getCode().equals(status)
|| OrderStatus.PAID.getCode().equals(status);
}
}
Entity 层对比表
| 方面 | Go (GORM) | Java (MyBatis-Plus) | Java (JPA) |
|---|---|---|---|
| 主键定义 | gorm:"primaryKey" | @TableId | @Id + @GeneratedValue |
| 自增 | autoIncrement | IdType.AUTO | GenerationType.IDENTITY |
| 表名 | TableName() 方法 | @TableName | @Table(name=) |
| 字段映射 | 自动驼峰转下划线 | 自动转换 | @Column(name=) |
| 唯一索引 | uniqueIndex | 数据库层定义 | unique = true |
| 软删除 | gorm.DeletedAt | @TableLogic | @SQLDelete + @Where |
| 自动时间 | autoCreateTime | @TableField(fill=) | @PrePersist |
| 关联关系 | gorm:"foreignKey" | @TableField(exist=false) | @ManyToOne 等 |
| 忽略字段 | gorm:"-" | @TableField(exist=false) | @Transient |
| JSON 忽略 | json:"-" | @JsonIgnore | @JsonIgnore |
12.5 DTO 层
Go 实现
go
// internal/dto/request/user_request.go
package request
// CreateUserRequest 创建用户请求
type CreateUserRequest struct {
Username string `json:"username" binding:"required,min=3,max=32"`
Email string `json:"email" binding:"required,email"`
Mobile string `json:"mobile" binding:"required,mobile"`
Password string `json:"password" binding:"required,min=8,max=64"`
RoleID int64 `json:"role_id" binding:"required,gt=0"`
}
// UpdateUserRequest 更新用户请求
type UpdateUserRequest struct {
Username *string `json:"username" binding:"omitempty,min=3,max=32"`
Email *string `json:"email" binding:"omitempty,email"`
Mobile *string `json:"mobile" binding:"omitempty,mobile"`
Status *int `json:"status" binding:"omitempty,oneof=1 2"`
Avatar *string `json:"avatar" binding:"omitempty,url"`
}
// ListUserRequest 用户列表请求
type ListUserRequest struct {
Page int `form:"page" binding:"omitempty,gte=1"`
PageSize int `form:"page_size" binding:"omitempty,gte=1,lte=100"`
Keyword string `form:"keyword" binding:"omitempty,max=64"`
Status *int `form:"status" binding:"omitempty,oneof=1 2 3"`
RoleID *int64 `form:"role_id" binding:"omitempty,gt=0"`
OrderBy string `form:"order_by" binding:"omitempty,oneof=created_at updated_at"`
Order string `form:"order" binding:"omitempty,oneof=asc desc"`
}
func (r *ListUserRequest) SetDefaults() {
if r.Page <= 0 {
r.Page = 1
}
if r.PageSize <= 0 {
r.PageSize = 20
}
if r.OrderBy == "" {
r.OrderBy = "created_at"
}
if r.Order == "" {
r.Order = "desc"
}
}
func (r *ListUserRequest) Offset() int {
return (r.Page - 1) * r.PageSize
}
// LoginRequest 登录请求
type LoginRequest struct {
Username string `json:"username" binding:"required"`
Password string `json:"password" binding:"required"`
}
// ChangePasswordRequest 修改密码请求
type ChangePasswordRequest struct {
OldPassword string `json:"old_password" binding:"required"`
NewPassword string `json:"new_password" binding:"required,min=8,max=64,nefield=OldPassword"`
}
// internal/dto/response/user_response.go
package response
import (
"myapp/internal/entity"
"time"
)
// UserResponse 用户响应
type UserResponse struct {
ID int64 `json:"id"`
Username string `json:"username"`
Email string `json:"email"`
Mobile string `json:"mobile"`
RoleID int64 `json:"role_id"`
Status int `json:"status"`
Avatar string `json:"avatar"`
CreatedAt time.Time `json:"created_at"`
}
// ToUserResponse 转换实体到响应
func ToUserResponse(user *entity.User) *UserResponse {
if user == nil {
return nil
}
return &UserResponse{
ID: user.ID,
Username: user.Username,
Email: user.Email,
Mobile: maskMobile(user.Mobile),
RoleID: user.RoleID,
Status: user.Status,
Avatar: user.Avatar,
CreatedAt: user.CreatedAt,
}
}
// UserDetailResponse 用户详情响应
type UserDetailResponse struct {
ID int64 `json:"id"`
Username string `json:"username"`
Email string `json:"email"`
Mobile string `json:"mobile"`
RoleID int64 `json:"role_id"`
RoleName string `json:"role_name"`
Status int `json:"status"`
StatusText string `json:"status_text"`
Avatar string `json:"avatar"`
LastLoginAt *time.Time `json:"last_login_at"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
func ToUserDetailResponse(user *entity.User) *UserDetailResponse {
if user == nil {
return nil
}
resp := &UserDetailResponse{
ID: user.ID,
Username: user.Username,
Email: user.Email,
Mobile: maskMobile(user.Mobile),
RoleID: user.RoleID,
Status: user.Status,
StatusText: getStatusText(user.Status),
Avatar: user.Avatar,
LastLoginAt: user.LastLoginAt,
CreatedAt: user.CreatedAt,
UpdatedAt: user.UpdatedAt,
}
if user.Role != nil {
resp.RoleName = user.Role.Name
}
return resp
}
// 手机号脱敏
func maskMobile(mobile string) string {
if len(mobile) != 11 {
return mobile
}
return mobile[:3] + "****" + mobile[7:]
}
func getStatusText(status int) string {
switch status {
case 1:
return "正常"
case 2:
return "禁用"
case 3:
return "删除"
default:
return "未知"
}
}
// TokenPairResponse Token 响应
type TokenPairResponse struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
ExpiresIn int64 `json:"expires_in"`
TokenType string `json:"token_type"`
}
func ToTokenPairResponse(pair *auth.TokenPair) *TokenPairResponse {
return &TokenPairResponse{
AccessToken: pair.AccessToken,
RefreshToken: pair.RefreshToken,
ExpiresIn: pair.ExpiresIn,
TokenType: "Bearer",
}
}
// internal/dto/response/common.go
package response
// Response 统一响应结构
type Response struct {
Code int `json:"code"`
Message string `json:"message"`
Data interface{} `json:"data,omitempty"`
RequestID string `json:"request_id,omitempty"`
Timestamp int64 `json:"timestamp"`
}
// PageData 分页数据
type PageData struct {
List interface{} `json:"list"`
Total int64 `json:"total"`
Page int `json:"page"`
PageSize int `json:"page_size"`
Pages int `json:"pages"`
}
func NewPageData(list interface{}, total int64, page, pageSize int) *PageData {
pages := int(total) / pageSize
if int(total)%pageSize > 0 {
pages++
}
return &PageData{
List: list,
Total: total,
Page: page,
PageSize: pageSize,
Pages: pages,
}
}
Java 实现
java
// dto/request/CreateUserRequest.java
package com.example.dto.request;
import com.example.validator.Mobile;
import lombok.Data;
import javax.validation.constraints.*;
@Data
public class CreateUserRequest {
@NotBlank(message = "用户名不能为空")
@Size(min = 3, max = 32, message = "用户名长度需在3-32个字符之间")
private String username;
@NotBlank(message = "邮箱不能为空")
@Email(message = "邮箱格式不正确")
private String email;
@NotBlank(message = "手机号不能为空")
@Mobile
private String mobile;
@NotBlank(message = "密码不能为空")
@Size(min = 8, max = 64, message = "密码长度需在8-64个字符之间")
private String password;
@NotNull(message = "角色ID不能为空")
@Positive(message = "角色ID必须为正数")
private Long roleId;
}
// dto/request/UpdateUserRequest.java
@Data
public class UpdateUserRequest {
@Size(min = 3, max = 32, message = "用户名长度需在3-32个字符之间")
private String username;
@Email(message = "邮箱格式不正确")
private String email;
@Mobile
private String mobile;
@Min(value = 1)
@Max(value = 2)
private Integer status;
@Pattern(regexp = "^https?://.*", message = "头像必须是有效的URL")
private String avatar;
}
// dto/request/ListUserRequest.java
@Data
public class ListUserRequest {
@Min(value = 1, message = "页码最小为1")
private Integer page = 1;
@Min(value = 1, message = "每页条数最小为1")
@Max(value = 100, message = "每页条数最大为100")
private Integer pageSize = 20;
@Size(max = 64, message = "关键词最大64个字符")
private String keyword;
private Integer status;
private Long roleId;
@Pattern(regexp = "^(created_at|updated_at)$", message = "排序字段不合法")
private String orderBy = "created_at";
@Pattern(regexp = "^(asc|desc)$", message = "排序方向不合法")
private String order = "desc";
public int getOffset() {
return (page - 1) * pageSize;
}
}
// dto/request/LoginRequest.java
@Data
public class LoginRequest {
@NotBlank(message = "用户名不能为空")
private String username;
@NotBlank(message = "密码不能为空")
private String password;
}
// dto/request/ChangePasswordRequest.java
@Data
public class ChangePasswordRequest {
@NotBlank(message = "原密码不能为空")
private String oldPassword;
@NotBlank(message = "新密码不能为空")
@Size(min = 8, max = 64, message = "密码长度需在8-64个字符之间")
private String newPassword;
@AssertTrue(message = "新密码不能与原密码相同")
private boolean isPasswordDifferent() {
return newPassword == null || !newPassword.equals(oldPassword);
}
}
java
// dto/response/UserResponse.java
package com.example.dto.response;
import com.example.entity.User;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import java.time.LocalDateTime;
@Data
public class UserResponse {
private Long id;
private String username;
private String email;
private String mobile;
private Long roleId;
private Integer status;
private String avatar;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createdAt;
public static UserResponse from(User user) {
if (user == null) {
return null;
}
UserResponse response = new UserResponse();
response.setId(user.getId());
response.setUsername(user.getUsername());
response.setEmail(user.getEmail());
response.setMobile(maskMobile(user.getMobile()));
response.setRoleId(user.getRoleId());
response.setStatus(user.getStatus());
response.setAvatar(user.getAvatar());
response.setCreatedAt(user.getCreatedAt());
return response;
}
private static String maskMobile(String mobile) {
if (mobile == null || mobile.length() != 11) {
return mobile;
}
return mobile.substring(0, 3) + "****" + mobile.substring(7);
}
}
// dto/response/UserDetailResponse.java
@Data
public class UserDetailResponse {
private Long id;
private String username;
private String email;
private String mobile;
private Long roleId;
private String roleName;
private Integer status;
private String statusText;
private String avatar;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime lastLoginAt;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createdAt;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime updatedAt;
public static UserDetailResponse from(User user) {
if (user == null) {
return null;
}
UserDetailResponse response = new UserDetailResponse();
response.setId(user.getId());
response.setUsername(user.getUsername());
response.setEmail(user.getEmail());
response.setMobile(maskMobile(user.getMobile()));
response.setRoleId(user.getRoleId());
response.setStatus(user.getStatus());
response.setStatusText(UserStatus.fromCode(user.getStatus()).getDescription());
response.setAvatar(user.getAvatar());
response.setLastLoginAt(user.getLastLoginAt());
response.setCreatedAt(user.getCreatedAt());
response.setUpdatedAt(user.getUpdatedAt());
if (user.getRole() != null) {
response.setRoleName(user.getRole().getName());
}
return response;
}
}
// dto/response/TokenPairResponse.java
@Data
@AllArgsConstructor
@NoArgsConstructor
public class TokenPairResponse {
private String accessToken;
private String refreshToken;
private Long expiresIn;
private String tokenType = "Bearer";
public static TokenPairResponse from(JwtUtil.TokenPair pair) {
return new TokenPairResponse(
pair.getAccessToken(),
pair.getRefreshToken(),
pair.getExpiresIn(),
"Bearer"
);
}
}
// dto/response/CommonResponse.java
@Data
@NoArgsConstructor
public class CommonResponse<T> {
private int code;
private String message;
private T data;
private String requestId;
private long timestamp;
public CommonResponse(int code, String message, T data) {
this.code = code;
this.message = message;
this.data = data;
this.timestamp = System.currentTimeMillis();
this.requestId = MDC.get("request_id");
}
public static <T> CommonResponse<T> success() {
return new CommonResponse<>(0, "success", null);
}
public static <T> CommonResponse<T> success(T data) {
return new CommonResponse<>(0, "success", data);
}
public static <T> CommonResponse<T> success(String message) {
return new CommonResponse<>(0, message, null);
}
public static <T> CommonResponse<T> error(ErrorCode errorCode) {
return new CommonResponse<>(errorCode.getCode(), errorCode.getMessage(), null);
}
public static <T> CommonResponse<T> error(ErrorCode errorCode, String message) {
return new CommonResponse<>(errorCode.getCode(), message, null);
}
}
// dto/response/PageResult.java
@Data
@NoArgsConstructor
@AllArgsConstructor
public class PageResult<T> {
private List<T> list;
private long total;
private int page;
private int pageSize;
private int pages;
public static <T> PageResult<T> of(List<T> list, long total, int page, int pageSize) {
PageResult<T> result = new PageResult<>();
result.setList(list);
result.setTotal(total);
result.setPage(page);
result.setPageSize(pageSize);
result.setPages((int) Math.ceil((double) total / pageSize));
return result;
}
public static <T, E> PageResult<T> of(IPage<E> page, Function<E, T> converter) {
List<T> list = page.getRecords().stream()
.map(converter)
.collect(Collectors.toList());
return of(list, page.getTotal(), (int) page.getCurrent(), (int) page.getSize());
}
}
DTO 层对比表
| 方面 | Go | Java |
|---|---|---|
| 请求绑定 | struct tags (binding) | @Valid + JSR-380 注解 |
| 可选字段 | 指针类型 (*string) | 包装类型 (String) |
| 默认值 | SetDefaults() 方法 | 字段初始化 |
| 响应转换 | ToXxxResponse 函数 | static from() 方法 |
| 时间格式 | json tag 或序列化器 | @JsonFormat |
| 数据脱敏 | 手动处理 | 手动处理 |
| 分页封装 | PageData 结构体 | PageResult 泛型类 |
| 统一响应 | Response 结构体 | CommonResponse 泛型类 |
十三、关键特性总结对比
13.1 核心差异总表
13.2 设计理念对比
| 方面 | Go | Java |
|---|---|---|
| 哲学 | 显式优于隐式 | 约定优于配置 |
| 依赖注入 | 构造函数手动组装 | IoC 容器自动注入 |
| AOP 实现 | 中间件 + 装饰器模式 | 动态代理 + 注解 |
| 错误处理 | 返回值 (value, error) | 异常抛出 try-catch |
| 并发模型 | Goroutine + Channel | Thread + CompletableFuture |
| 配置管理 | Viper + 结构体 | @Value + Properties |
| 代码生成 | 较少依赖 | Lombok / MapStruct |
| 框架侵入 | 低侵入 | 较高侵入 |
13.3 性能与资源对比
| 指标 | Go | Java (Spring Boot) |
|---|---|---|
| 启动时间 | ~100ms | ~3-10s |
| 内存占用 | ~10-30MB | ~200-500MB |
| 编译产物 | 单二进制文件 | JAR + JRE |
| 冷启动 | 快 | 慢(适合预热) |
| GC 停顿 | 低延迟 | G1/ZGC 可调优 |
| 并发处理 | Goroutine 轻量 | 线程池管理 |
13.4 开发效率对比
| 方面 | Go | Java |
|---|---|---|
| 学习曲线 | 语法简单,生态需学习 | 语法复杂,生态成熟 |
| 代码量 | 相对多(显式代码) | 相对少(注解魔法) |
| IDE 支持 | GoLand / VSCode | IntelliJ IDEA |
| 调试体验 | Delve 调试器 | 成熟的 Debug 工具 |
| 重构支持 | 一般 | 强大 |
| 文档生成 | swaggo | SpringDoc/Swagger |
13.5 适用场景建议
十四、附录:完整项目结构参考
Go 项目结构
code
myapp/
├── cmd/
│ └── server/
│ └── main.go # 程序入口
├── internal/
│ ├── handler/ # HTTP 处理器
│ │ ├── user_handler.go
│ │ ├── order_handler.go
│ │ └── middleware/
│ │ ├── auth.go
│ │ ├── permission.go
│ │ ├── logger.go
│ │ ├── ratelimit.go
│ │ ├── recovery.go
│ │ └── cors.go
│ ├── service/ # 业务逻辑
│ │ ├── user_service.go
│ │ ├── order_service.go
│ │ └── permission_service.go
│ ├── repository/ # 数据访问
│ │ ├── user_repository.go
│ │ ├── order_repository.go
│ │ └── cache/
│ │ └── user_cache.go
│ ├── entity/ # 实体定义
│ │ ├── user.go
│ │ ├── order.go
│ │ └── role.go
│ ├── dto/
│ │ ├── request/
│ │ │ └── user_request.go
│ │ └── response/
│ │ ├── user_response.go
│ │ └── common.go
│ └── errors/
│ └── errors.go
├── pkg/
│ ├── auth/
│ │ └── jwt.go
│ ├── cache/
│ │ └── redis.go
│ ├── logger/
│ │ └── logger.go
│ ├── validator/
│ │ └── validator.go
│ ├── database/
│ │ └── transaction.go
│ └── ratelimit/
│ └── ratelimit.go
├── config/
│ ├── config.go
│ └── config.yaml
├── docs/ # Swagger 文档
├── migrations/ # 数据库迁移
├── scripts/ # 构建脚本
├── Dockerfile
├── docker-compose.yaml
├── Makefile
└── go.mod
Java 项目结构
code
myapp/
├── src/main/java/com/example/
│ ├── Application.java
│ ├── controller/
│ │ ├── UserController.java
│ │ └── OrderController.java
│ ├── service/
│ │ ├── UserService.java
│ │ ├── OrderService.java
│ │ └── impl/
│ │ ├── UserServiceImpl.java
│ │ └── OrderServiceImpl.java
│ ├── mapper/ # MyBatis Mapper
│ │ ├── UserMapper.java
│ │ └── OrderMapper.java
│ ├── entity/
│ │ ├── User.java
│ │ ├── Order.java
│ │ └── Role.java
│ ├── dto/
│ │ ├── request/
│ │ │ ├── CreateUserRequest.java
│ │ │ └── ListUserRequest.java
│ │ └── response/
│ │ ├── UserResponse.java
│ │ ├── CommonResponse.java
│ │ └── PageResult.java
│ ├── config/
│ │ ├── SecurityConfig.java
│ │ ├── RedisConfig.java
│ │ ├── WebMvcConfig.java
│ │ └── Resilience4jConfig.java
│ ├── filter/
│ │ └── JwtAuthenticationFilter.java
│ ├── interceptor/
│ │ ├── AuthInterceptor.java
│ │ └── RateLimitInterceptor.java
│ ├── aspect/
│ │ ├── LogAspect.java
│ │ ├── PermissionAspect.java
│ │ └── AuditLogAspect.java
│ ├── exception/
│ │ ├── BusinessException.java
│ │ └── GlobalExceptionHandler.java
│ ├── enums/
│ │ ├── ErrorCode.java
│ │ └── UserStatus.java
│ ├── annotation/
│ │ ├── RequirePermission.java
│ │ └── AuditLog.java
│ ├── validator/
│ │ ├── Mobile.java
│ │ └── MobileValidator.java
│ └── util/
│ ├── JwtUtil.java
│ └── SecurityUtils.java
├── src/main/resources/
│ ├── mapper/
│ │ └── UserMapper.xml
│ ├── application.yml
│ ├── application-dev.yml
│ ├── application-prod.yml
│ └── logback-spring.xml
├── src/test/
├── Dockerfile
├── docker-compose.yaml
└── pom.xml
结语
本文档从实际企业项目出发,系统对比了 Go 与 Java 在分层架构各层的实现差异。两种语言/框架各有优势:
- Go:简洁、高性能、低资源占用,适合云原生和高并发场景
- Java:生态丰富、工具链成熟、企业级特性完善,适合大型复杂业务系统
选择时应综合考虑团队技术栈、项目规模、性能需求和长期维护成本。核心的架构思想(分层、解耦、抽象)是通用的,具体实现可根据语言特性灵活调整。