Spring Boot 集成 MongoDB

Spring Boot 集成 MongoDB 使用文档

一、环境准备与项目初始化

1.1 创建 Spring Boot 项目

使用 Spring Initializr 创建项目,选择以下依赖:

  • Spring Web
  • Spring Data MongoDB
  • Lombok (可选但推荐)
  • Validation

或使用 Maven 依赖配置:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
         https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.14</version>
        <relativePath/>
    </parent>
    
    <groupId>com.example</groupId>
    <artifactId>mongodb-demo</artifactId>
    <version>1.0.0</version>
    <name>mongodb-demo</name>
    
    <properties>
        <java.version>11</java.version>
        <mongo.driver.version>4.7.2</mongo.driver.version>
    </properties>
    
    <dependencies>
        <!-- Spring Boot Starter -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        
        <!-- Spring Data MongoDB -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-mongodb</artifactId>
        </dependency>
        
        <!-- MongoDB Reactive Support (可选) -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-mongodb-reactive</artifactId>
        </dependency>
        
        <!-- Lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        
        <!-- Validation -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
        </dependency>
        
        <!-- Test -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        
        <!-- DevTools -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
    </dependencies>
    
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

1.2 配置文件

application.yml 配置:

server:
  port: 8080
  servlet:
    context-path: /api

spring:
  application:
    name: mongodb-demo
  
  # MongoDB 配置
  data:
    mongodb:
      # 单机配置
      host: localhost
      port: 27017
      database: mydatabase
      username: ${MONGO_USERNAME:admin}
      password: ${MONGO_PASSWORD:admin123}
      authentication-database: admin
      
      # 连接池配置
      auto-index-creation: true
      
      # 副本集配置(可选)
      # replica-set: rs0
      # uri: mongodb://user:pass@host1:27017,host2:27017,host3:27017/database?replicaSet=rs0
      
      # SSL 配置(可选)
      # ssl:
      #   enabled: false
      #   invalid-hostname-allowed: false

# 自定义配置
mongodb:
  connection:
    max-pool-size: 100
    min-pool-size: 10
    max-wait-time: 120000
    connect-timeout: 10000
    socket-timeout: 0  # 0表示永不超时

application.properties 配置:

# MongoDB 配置
spring.data.mongodb.host=localhost
spring.data.mongodb.port=27017
spring.data.mongodb.database=mydatabase
spring.data.mongodb.username=admin
spring.data.mongodb.password=admin123
spring.data.mongodb.authentication-database=admin

# 连接选项
spring.data.mongodb.auto-index-creation=true

# 连接池配置
spring.data.mongodb.uri=mongodb://admin:admin123@localhost:27017/mydatabase?authSource=admin

二、实体类设计

2.1 基本实体类

package com.example.mongodbdemo.entity;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.index.CompoundIndex;
import org.springframework.data.mongodb.core.index.CompoundIndexes;
import org.springframework.data.mongodb.core.index.Indexed;
import org.springframework.data.mongodb.core.mapping.Document;
import org.springframework.data.mongodb.core.mapping.Field;

import javax.validation.constraints.Email;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import java.time.LocalDateTime;
import java.util.List;

/**
 * 用户实体类
 * @Document - 指定MongoDB集合名称
 * @CompoundIndex - 复合索引
 */
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Document(collection = "users")  // 指定集合名称,默认为类名小写
@CompoundIndexes({
    @CompoundIndex(name = "email_status_idx", def = "{'email': 1, 'status': 1}"),
    @CompoundIndex(name = "name_city_idx", def = "{'lastName': 1, 'city': 1}")
})
public class User {
    
    @Id  // 主键标识
    private String id;
    
    @NotBlank(message = "First name is required")
    @Size(min = 2, max = 50, message = "First name must be between 2 and 50 characters")
    @Field("first_name")  // 指定字段名,默认使用驼峰转下划线
    private String firstName;
    
    @NotBlank(message = "Last name is required")
    @Size(min = 2, max = 50)
    @Field("last_name")
    private String lastName;
    
    @NotBlank(message = "Email is required")
    @Email(message = "Invalid email format")
    @Indexed(unique = true, background = true)  // 唯一索引,后台创建
    private String email;
    
    @NotNull(message = "Age is required")
    private Integer age;
    
    @Field("phone_number")
    private String phoneNumber;
    
    private String gender;
    
    @Field("birth_date")
    private LocalDateTime birthDate;
    
    @Field("registration_date")
    private LocalDateTime registrationDate;
    
    private String city;
    private String country;
    
    @Indexed  // 单字段索引
    private String status;  // ACTIVE, INACTIVE, DELETED
    
    private List<String> roles;  // 数组类型
    
    @Field("preferences")
    private Preferences preferences;  // 嵌套文档
    
    @Field("addresses")
    private List<Address> addresses;  // 嵌套文档数组
    
    // 审计字段
    @Field("created_at")
    private LocalDateTime createdAt;
    
    @Field("updated_at")
    private LocalDateTime updatedAt;
    
    @Field("created_by")
    private String createdBy;
    
    @Field("updated_by")
    private String updatedBy;
    
    // 版本控制(乐观锁)
    @Version
    private Long version;
    
    /**
     * 内嵌文档:用户偏好设置
     */
    @Data
    @Builder
    @NoArgsConstructor
    @AllArgsConstructor
    public static class Preferences {
        @Field("theme")
        private String theme;  // light, dark
        
        @Field("language")
        private String language;
        
        @Field("notification_enabled")
        private Boolean notificationEnabled;
        
        @Field("email_notifications")
        private Boolean emailNotifications;
        
        @Field("sms_notifications")
        private Boolean smsNotifications;
    }
    
    /**
     * 内嵌文档:地址信息
     */
    @Data
    @Builder
    @NoArgsConstructor
    @AllArgsConstructor
    public static class Address {
        @Field("type")
        private String type;  // HOME, WORK
        
        @Field("street")
        private String street;
        
        @Field("city")
        private String city;
        
        @Field("state")
        private String state;
        
        @Field("postal_code")
        private String postalCode;
        
        @Field("country")
        private String country;
        
        @Field("is_default")
        private Boolean isDefault;
    }
}

2.2 产品实体类(展示更多MongoDB特性)

package com.example.mongodbdemo.entity;

import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.SuperBuilder;
import org.springframework.data.mongodb.core.mapping.Document;
import org.springframework.data.mongodb.core.mapping.Field;

import javax.validation.constraints.DecimalMin;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import java.math.BigDecimal;
import java.util.List;
import java.util.Map;

/**
 * 产品实体类(展示继承、多态)
 */
@Data
@SuperBuilder
@EqualsAndHashCode(callSuper = true)
@Document(collection = "products")
public class Product extends BaseEntity {
    
    @NotBlank(message = "Product name is required")
    @Field("product_name")
    private String productName;
    
    @Field("sku")
    @NotBlank(message = "SKU is required")
    private String sku;
    
    @Field("description")
    private String description;
    
    @NotNull(message = "Price is required")
    @DecimalMin(value = "0.0", inclusive = false, message = "Price must be greater than 0")
    @Field("price")
    private BigDecimal price;
    
    @Field("cost_price")
    private BigDecimal costPrice;
    
    @Field("quantity")
    private Integer quantity;
    
    @Field("category")
    private String category;
    
    @Field("tags")
    private List<String> tags;
    
    @Field("attributes")
    private Map<String, Object> attributes;  // 动态字段
    
    @Field("specifications")
    private List<Specification> specifications;
    
    @Field("variants")
    private List<ProductVariant> variants;
    
    @Field("rating")
    private Double rating;
    
    @Field("review_count")
    private Integer reviewCount;
    
    @Field("images")
    private List<Image> images;
    
    @Field("is_active")
    private Boolean isActive;
    
    @Field("is_featured")
    private Boolean isFeatured;
    
    @Field("meta_data")
    private MetaData metaData;
    
    /**
     * 规格
     */
    @Data
    @SuperBuilder
    public static class Specification {
        @Field("key")
        private String key;
        
        @Field("value")
        private String value;
        
        @Field("unit")
        private String unit;
    }
    
    /**
     * 产品变体
     */
    @Data
    @SuperBuilder
    public static class ProductVariant {
        @Field("variant_id")
        private String variantId;
        
        @Field("variant_name")
        private String variantName;
        
        @Field("price")
        private BigDecimal price;
        
        @Field("sku")
        private String sku;
        
        @Field("quantity")
        private Integer quantity;
        
        @Field("attributes")
        private Map<String, String> attributes;
    }
    
    /**
     * 图片
     */
    @Data
    @SuperBuilder
    public static class Image {
        @Field("url")
        private String url;
        
        @Field("alt_text")
        private String altText;
        
        @Field("is_primary")
        private Boolean isPrimary;
        
        @Field("order")
        private Integer order;
    }
    
    /**
     * 元数据
     */
    @Data
    @SuperBuilder
    public static class MetaData {
        @Field("seo_title")
        private String seoTitle;
        
        @Field("seo_description")
        private String seoDescription;
        
        @Field("keywords")
        private List<String> keywords;
        
        @Field("og_image")
        private String ogImage;
    }
}

/**
 * 基础实体类(抽象类)
 */
@Data
@SuperBuilder
abstract class BaseEntity {
    @org.springframework.data.annotation.Id
    private String id;
    
    @Field("created_at")
    private java.time.LocalDateTime createdAt;
    
    @Field("updated_at")
    private java.time.LocalDateTime updatedAt;
    
    @Field("created_by")
    private String createdBy;
    
    @Field("updated_by")
    private String updatedBy;
    
    @Field("is_deleted")
    private Boolean isDeleted;
}

三、Repository 层

3.1 基础 Repository

package com.example.mongodbdemo.repository;

import com.example.mongodbdemo.entity.User;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.mongodb.repository.MongoRepository;
import org.springframework.data.mongodb.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;

import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;

/**
 * 用户Repository
 * 继承 MongoRepository 获得基本的CRUD操作
 */
@Repository
public interface UserRepository extends MongoRepository<User, String> {
    
    // ========== 查询方法(根据方法名自动生成查询) ==========
    
    // 等值查询
    Optional<User> findByEmail(String email);
    
    List<User> findByFirstName(String firstName);
    
    List<User> findByLastName(String lastName);
    
    // 条件查询
    List<User> findByAgeGreaterThan(Integer age);
    
    List<User> findByAgeLessThan(Integer age);
    
    List<User> findByAgeBetween(Integer start, Integer end);
    
    List<User> findByAgeIn(List<Integer> ages);
    
    // 多条件查询
    List<User> findByFirstNameAndLastName(String firstName, String lastName);
    
    List<User> findByFirstNameOrLastName(String firstName, String lastName);
    
    // 模糊查询
    List<User> findByFirstNameLike(String pattern);
    
    List<User> findByFirstNameContaining(String keyword);
    
    List<User> findByFirstNameStartingWith(String prefix);
    
    List<User> findByFirstNameEndingWith(String suffix);
    
    // 忽略大小写
    List<User> findByFirstNameIgnoreCase(String firstName);
    
    // 嵌套文档查询
    List<User> findByPreferencesTheme(String theme);
    
    List<User> findByPreferencesNotificationEnabledTrue();
    
    // 数组查询
    List<User> findByRolesContaining(String role);
    
    // 排序和分页
    List<User> findByCityOrderByAgeDesc(String city);
    
    Page<User> findByStatus(String status, Pageable pageable);
    
    // 计数
    Long countByCity(String city);
    
    Long countByStatus(String status);
    
    // 删除
    void deleteByEmail(String email);
    
    Long deleteByStatus(String status);
    
    // 是否存在
    Boolean existsByEmail(String email);
    
    // ========== 自定义查询(使用 @Query 注解) ==========
    
    /**
     * 使用原生MongoDB查询语法
     * ?0, ?1 表示方法参数位置
     */
    @Query("{ 'age' : { $gt: ?0, $lt: ?1 } }")
    List<User> findUsersByAgeRange(Integer minAge, Integer maxAge);
    
    /**
     * 使用 SpEL 表达式
     */
    @Query("{ 'email' : :#{#email} }")
    Optional<User> findUserByEmail(@Param("email") String email);
    
    /**
     * 复杂查询:多条件 + 排序 + 字段投影
     */
    @Query(value = "{ 'city': ?0, 'status': 'ACTIVE', 'age': { $gte: ?1 } }", 
           sort = "{ 'registration_date': -1 }",
           fields = "{ 'firstName': 1, 'lastName': 1, 'email': 1, 'age': 1 }")
    List<User> findActiveUsersInCity(String city, Integer minAge);
    
    /**
     * 正则表达式查询
     */
    @Query("{ 'email': { $regex: ?0, $options: 'i' } }")
    List<User> findUsersByEmailPattern(String pattern);
    
    /**
     * 数组查询
     */
    @Query("{ 'roles': { $all: ?0 } }")
    List<User> findUsersWithAllRoles(List<String> roles);
    
    /**
     * 数组大小查询
     */
    @Query("{ 'roles': { $size: ?0 } }")
    List<User> findUsersWithRoleCount(Integer count);
    
    /**
     * 日期范围查询
     */
    @Query("{ 'registration_date': { $gte: ?0, $lte: ?1 } }")
    List<User> findUsersRegisteredBetween(LocalDateTime start, LocalDateTime end);
    
    /**
     * 嵌套文档查询
     */
    @Query("{ 'addresses.city': ?0, 'addresses.is_default': true }")
    List<User> findUsersWithDefaultAddressInCity(String city);
    
    /**
     * 聚合查询:分组统计
     */
    @Query(value = "{}", fields = "{ 'city': 1 }")
    List<User> findAllCities();
    
    // ========== 自定义查询方法(使用 @Aggregation 注解) ==========
    
    /**
     * 聚合查询:按城市分组统计用户数
     */
    @Aggregation(pipeline = {
        "{ $match: { status: 'ACTIVE' } }",
        "{ $group: { _id: '$city', count: { $sum: 1 }, avgAge: { $avg: '$age' } } }",
        "{ $sort: { count: -1 } }",
        "{ $limit: 10 }"
    })
    List<CityStats> countUsersByCity();
    
    /**
     * 聚合结果映射类
     */
    interface CityStats {
        String get_id();  // MongoDB分组后的_id字段
        Long getCount();
        Double getAvgAge();
    }
    
    /**
     * 聚合查询:用户年龄分布
     */
    @Aggregation(pipeline = {
        "{ $bucket: { " +
            "groupBy: '$age', " +
            "boundaries: [18, 25, 35, 45, 55, 65], " +
            "default: '65+', " +
            "output: { " +
                "count: { $sum: 1 }, " +
                "users: { $push: { name: { $concat: ['$firstName', ' ', '$lastName'] }, email: '$email' } } " +
            "}" +
        "}}"
    })
    List<AgeDistribution> getUserAgeDistribution();
    
    interface AgeDistribution {
        String get_id();
        Long getCount();
        List<UserInfo> getUsers();
        
        interface UserInfo {
            String getName();
            String getEmail();
        }
    }
}

3.2 自定义 Repository 实现

package com.example.mongodbdemo.repository.impl;

import com.example.mongodbdemo.entity.User;
import com.example.mongodbdemo.repository.custom.UserCustomRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.aggregation.*;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.core.query.Update;
import org.springframework.stereotype.Repository;

import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;

/**
 * 自定义Repository实现
 */
@Repository
@RequiredArgsConstructor
public class UserRepositoryImpl implements UserCustomRepository {
    
    private final MongoTemplate mongoTemplate;
    
    @Override
    public Page<User> searchUsers(Map<String, Object> criteria, Pageable pageable) {
        Query query = new Query();
        
        // 动态构建查询条件
        if (criteria.containsKey("firstName")) {
            query.addCriteria(Criteria.where("first_name").regex(criteria.get("firstName").toString(), "i"));
        }
        
        if (criteria.containsKey("lastName")) {
            query.addCriteria(Criteria.where("last_name").regex(criteria.get("lastName").toString(), "i"));
        }
        
        if (criteria.containsKey("email")) {
            query.addCriteria(Criteria.where("email").regex(criteria.get("email").toString(), "i"));
        }
        
        if (criteria.containsKey("ageFrom") && criteria.containsKey("ageTo")) {
            query.addCriteria(Criteria.where("age")
                    .gte(Integer.parseInt(criteria.get("ageFrom").toString()))
                    .lte(Integer.parseInt(criteria.get("ageTo").toString())));
        }
        
        if (criteria.containsKey("city")) {
            query.addCriteria(Criteria.where("city").is(criteria.get("city")));
        }
        
        if (criteria.containsKey("status")) {
            query.addCriteria(Criteria.where("status").is(criteria.get("status")));
        }
        
        // 分页和排序
        query.with(pageable);
        
        // 获取总数
        long total = mongoTemplate.count(query, User.class);
        
        // 获取数据
        List<User> users = mongoTemplate.find(query, User.class);
        
        return new PageImpl<>(users, pageable, total);
    }
    
    @Override
    public void updateUserStatus(String userId, String status) {
        Query query = new Query(Criteria.where("id").is(userId));
        Update update = new Update()
                .set("status", status)
                .set("updated_at", LocalDateTime.now());
        
        mongoTemplate.updateFirst(query, update, User.class);
    }
    
    @Override
    public void updateUserEmail(String userId, String newEmail) {
        Query query = new Query(Criteria.where("id").is(userId));
        Update update = new Update()
                .set("email", newEmail)
                .set("updated_at", LocalDateTime.now());
        
        mongoTemplate.updateFirst(query, update, User.class);
    }
    
    @Override
    public void bulkUpdateUserStatus(List<String> userIds, String status) {
        Query query = new Query(Criteria.where("id").in(userIds));
        Update update = new Update()
                .set("status", status)
                .set("updated_at", LocalDateTime.now());
        
        mongoTemplate.updateMulti(query, update, User.class);
    }
    
    @Override
    public List<Map> getUserStatistics() {
        // 使用聚合框架进行复杂统计
        Aggregation aggregation = Aggregation.newAggregation(
            Aggregation.match(Criteria.where("status").is("ACTIVE")),
            Aggregation.group("city")
                .count().as("userCount")
                .avg("age").as("avgAge")
                .sum("age").as("totalAge"),
            Aggregation.project("userCount", "avgAge", "totalAge")
                .and("_id").as("city"),
            Aggregation.sort(Sort.Direction.DESC, "userCount")
        );
        
        AggregationResults<Map> results = mongoTemplate
                .aggregate(aggregation, "users", Map.class);
        
        return results.getMappedResults();
    }
    
    @Override
    public List<User> findUsersByCustomQuery(String firstNamePattern, Integer minAge, String city) {
        Criteria criteria = new Criteria();
        List<Criteria> criteriaList = new java.util.ArrayList<>();
        
        if (firstNamePattern != null && !firstNamePattern.isEmpty()) {
            criteriaList.add(Criteria.where("first_name").regex(firstNamePattern, "i"));
        }
        
        if (minAge != null) {
            criteriaList.add(Criteria.where("age").gte(minAge));
        }
        
        if (city != null && !city.isEmpty()) {
            criteriaList.add(Criteria.where("city").is(city));
        }
        
        if (!criteriaList.isEmpty()) {
            criteria.andOperator(criteriaList.toArray(new Criteria[0]));
        }
        
        Query query = new Query(criteria);
        query.with(Sort.by(Sort.Direction.DESC, "registration_date"));
        
        return mongoTemplate.find(query, User.class);
    }
    
    @Override
    public List<String> findAllDistinctCities() {
        return mongoTemplate.findDistinct(
            new Query(), "city", User.class, String.class
        );
    }
    
    @Override
    public Long countUsersByCriteria(Map<String, Object> criteria) {
        Query query = new Query();
        
        criteria.forEach((key, value) -> {
            if (value != null) {
                switch (key) {
                    case "ageFrom":
                        query.addCriteria(Criteria.where("age").gte(value));
                        break;
                    case "ageTo":
                        query.addCriteria(Criteria.where("age").lte(value));
                        break;
                    case "city":
                        query.addCriteria(Criteria.where("city").is(value));
                        break;
                    case "status":
                        query.addCriteria(Criteria.where("status").is(value));
                        break;
                }
            }
        });
        
        return mongoTemplate.count(query, User.class);
    }
}

/**
 * 自定义Repository接口
 */
package com.example.mongodbdemo.repository.custom;

import com.example.mongodbdemo.entity.User;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;

import java.util.List;
import java.util.Map;

public interface UserCustomRepository {
    Page<User> searchUsers(Map<String, Object> criteria, Pageable pageable);
    void updateUserStatus(String userId, String status);
    void updateUserEmail(String userId, String newEmail);
    void bulkUpdateUserStatus(List<String> userIds, String status);
    List<Map> getUserStatistics();
    List<User> findUsersByCustomQuery(String firstNamePattern, Integer minAge, String city);
    List<String> findAllDistinctCities();
    Long countUsersByCriteria(Map<String, Object> criteria);
}

3.3 扩展 Repository 接口

package com.example.mongodbdemo.repository;

import com.example.mongodbdemo.entity.User;
import com.example.mongodbdemo.repository.custom.UserCustomRepository;
import org.springframework.data.mongodb.repository.MongoRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface UserRepository extends MongoRepository<User, String>, UserCustomRepository {
    // 继承自定义接口
}

四、Service 层

4.1 基础 Service

package com.example.mongodbdemo.service;

import com.example.mongodbdemo.entity.User;
import com.example.mongodbdemo.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;

import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
import java.util.Optional;

/**
 * 用户服务层
 */
@Service
@RequiredArgsConstructor
@Slf4j
public class UserService {
    
    private final UserRepository userRepository;
    
    /**
     * 创建用户
     */
    @Transactional
    public User createUser(User user) {
        // 验证邮箱是否已存在
        if (userRepository.existsByEmail(user.getEmail())) {
            throw new RuntimeException("Email already exists: " + user.getEmail());
        }
        
        // 设置审计字段
        user.setId(null);  // 确保ID由MongoDB生成
        user.setCreatedAt(LocalDateTime.now());
        user.setUpdatedAt(LocalDateTime.now());
        user.setStatus("ACTIVE");
        
        // 保存用户
        User savedUser = userRepository.save(user);
        log.info("Created user with ID: {}", savedUser.getId());
        
        return savedUser;
    }
    
    /**
     * 批量创建用户
     */
    @Transactional
    public List<User> createUsers(List<User> users) {
        // 验证邮箱唯一性
        List<String> emails = users.stream()
                .map(User::getEmail)
                .toList();
        
        List<User> existingUsers = userRepository.findByEmailIn(emails);
        if (!existingUsers.isEmpty()) {
            throw new RuntimeException("Some emails already exist");
        }
        
        // 设置审计字段
        LocalDateTime now = LocalDateTime.now();
        users.forEach(user -> {
            user.setId(null);
            user.setCreatedAt(now);
            user.setUpdatedAt(now);
            user.setStatus("ACTIVE");
        });
        
        return userRepository.saveAll(users);
    }
    
    /**
     * 根据ID获取用户
     */
    public Optional<User> getUserById(String id) {
        return userRepository.findById(id);
    }
    
    /**
     * 根据邮箱获取用户
     */
    public Optional<User> getUserByEmail(String email) {
        return userRepository.findByEmail(email);
    }
    
    /**
     * 获取所有用户
     */
    public List<User> getAllUsers() {
        return userRepository.findAll();
    }
    
    /**
     * 分页获取用户
     */
    public Page<User> getUsers(Pageable pageable) {
        return userRepository.findAll(pageable);
    }
    
    /**
     * 更新用户
     */
    @Transactional
    public User updateUser(String id, User userUpdates) {
        User existingUser = userRepository.findById(id)
                .orElseThrow(() -> new RuntimeException("User not found with id: " + id));
        
        // 更新允许修改的字段
        if (StringUtils.hasText(userUpdates.getFirstName())) {
            existingUser.setFirstName(userUpdates.getFirstName());
        }
        
        if (StringUtils.hasText(userUpdates.getLastName())) {
            existingUser.setLastName(userUpdates.getLastName());
        }
        
        if (userUpdates.getAge() != null) {
            existingUser.setAge(userUpdates.getAge());
        }
        
        if (StringUtils.hasText(userUpdates.getPhoneNumber())) {
            existingUser.setPhoneNumber(userUpdates.getPhoneNumber());
        }
        
        if (StringUtils.hasText(userUpdates.getCity())) {
            existingUser.setCity(userUpdates.getCity());
        }
        
        if (StringUtils.hasText(userUpdates.getCountry())) {
            existingUser.setCountry(userUpdates.getCountry());
        }
        
        // 更新审计字段
        existingUser.setUpdatedAt(LocalDateTime.now());
        
        return userRepository.save(existingUser);
    }
    
    /**
     * 部分更新用户
     */
    @Transactional
    public void partialUpdateUser(String id, Map<String, Object> updates) {
        User existingUser = userRepository.findById(id)
                .orElseThrow(() -> new RuntimeException("User not found with id: " + id));
        
        updates.forEach((key, value) -> {
            switch (key) {
                case "firstName":
                    existingUser.setFirstName((String) value);
                    break;
                case "lastName":
                    existingUser.setLastName((String) value);
                    break;
                case "age":
                    existingUser.setAge((Integer) value);
                    break;
                case "phoneNumber":
                    existingUser.setPhoneNumber((String) value);
                    break;
                case "city":
                    existingUser.setCity((String) value);
                    break;
                case "country":
                    existingUser.setCountry((String) value);
                    break;
                case "status":
                    existingUser.setStatus((String) value);
                    break;
            }
        });
        
        existingUser.setUpdatedAt(LocalDateTime.now());
        userRepository.save(existingUser);
    }
    
    /**
     * 删除用户
     */
    @Transactional
    public void deleteUser(String id) {
        if (!userRepository.existsById(id)) {
            throw new RuntimeException("User not found with id: " + id);
        }
        
        userRepository.deleteById(id);
        log.info("Deleted user with ID: {}", id);
    }
    
    /**
     * 软删除用户
     */
    @Transactional
    public void softDeleteUser(String id) {
        userRepository.updateUserStatus(id, "DELETED");
    }
    
    /**
     * 搜索用户
     */
    public Page<User> searchUsers(Map<String, Object> criteria, Pageable pageable) {
        return userRepository.searchUsers(criteria, pageable);
    }
    
    /**
     * 根据条件查询用户
     */
    public List<User> findUsersByCriteria(String firstNamePattern, Integer minAge, String city) {
        return userRepository.findUsersByCustomQuery(firstNamePattern, minAge, city);
    }
    
    /**
     * 获取用户统计
     */
    public List<Map> getUserStatistics() {
        return userRepository.getUserStatistics();
    }
    
    /**
     * 获取所有城市
     */
    public List<String> getAllCities() {
        return userRepository.findAllDistinctCities();
    }
    
    /**
     * 更新用户状态
     */
    @Transactional
    public void updateUserStatus(String id, String status) {
        if (!List.of("ACTIVE", "INACTIVE", "SUSPENDED", "DELETED").contains(status)) {
            throw new RuntimeException("Invalid status: " + status);
        }
        
        userRepository.updateUserStatus(id, status);
    }
    
    /**
     * 批量更新用户状态
     */
    @Transactional
    public void bulkUpdateUserStatus(List<String> ids, String status) {
        if (!List.of("ACTIVE", "INACTIVE", "SUSPENDED", "DELETED").contains(status)) {
            throw new RuntimeException("Invalid status: " + status);
        }
        
        userRepository.bulkUpdateUserStatus(ids, status);
    }
    
    /**
     * 根据条件统计用户数
     */
    public Long countUsers(Map<String, Object> criteria) {
        return userRepository.countUsersByCriteria(criteria);
    }
    
    /**
     * 验证用户是否存在
     */
    public boolean userExists(String id) {
        return userRepository.existsById(id);
    }
    
    /**
     * 验证邮箱是否已存在
     */
    public boolean emailExists(String email) {
        return userRepository.existsByEmail(email);
    }
}

4.2 高级 Service(包含事务和异常处理)

package com.example.mongodbdemo.service;

import com.example.mongodbdemo.entity.User;
import com.example.mongodbdemo.exception.ResourceNotFoundException;
import com.example.mongodbdemo.exception.ValidationException;
import com.example.mongodbdemo.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.core.query.Update;
import org.springframework.retry.annotation.Backoff;
import org.springframework.retry.annotation.Retryable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
import java.util.Optional;

/**
 * 高级用户服务(包含重试机制、事务管理等)
 */
@Service
@RequiredArgsConstructor
@Slf4j
public class AdvancedUserService {
    
    private final UserRepository userRepository;
    private final MongoTemplate mongoTemplate;
    
    /**
     * 创建用户(带重试机制)
     */
    @Retryable(
        value = {DuplicateKeyException.class},
        maxAttempts = 3,
        backoff = @Backoff(delay = 1000, multiplier = 2)
    )
    @Transactional
    public User createUserWithRetry(User user) {
        try {
            user.setId(null);
            user.setCreatedAt(LocalDateTime.now());
            user.setUpdatedAt(LocalDateTime.now());
            
            return userRepository.save(user);
        } catch (DuplicateKeyException e) {
            log.error("Duplicate key error when creating user", e);
            throw new ValidationException("Email already exists");
        }
    }
    
    /**
     * 使用 MongoTemplate 执行复杂操作
     */
    @Transactional
    public void updateUserWithMongoTemplate(String userId, Map<String, Object> updates) {
        Query query = new Query(Criteria.where("id").is(userId));
        User existingUser = mongoTemplate.findOne(query, User.class);
        
        if (existingUser == null) {
            throw new ResourceNotFoundException("User not found");
        }
        
        Update update = new Update();
        updates.forEach((key, value) -> {
            switch (key) {
                case "firstName":
                    update.set("first_name", value);
                    break;
                case "lastName":
                    update.set("last_name", value);
                    break;
                case "age":
                    update.set("age", value);
                    break;
                case "email":
                    // 验证邮箱是否唯一
                    if (!existingUser.getEmail().equals(value)) {
                        checkEmailUniqueness((String) value);
                        update.set("email", value);
                    }
                    break;
            }
        });
        
        update.set("updated_at", LocalDateTime.now());
        mongoTemplate.updateFirst(query, update, User.class);
    }
    
    /**
     * 使用事务执行多个操作
     */
    @Transactional
    public void executeInTransaction(List<Runnable> operations) {
        operations.forEach(Runnable::run);
    }
    
    /**
     * 批量操作
     */
    @Transactional
    public void batchInsert(List<User> users) {
        users.forEach(user -> {
            user.setId(null);
            user.setCreatedAt(LocalDateTime.now());
            user.setUpdatedAt(LocalDateTime.now());
        });
        
        mongoTemplate.insertAll(users);
    }
    
    /**
     * 使用聚合查询
     */
    public Map<String, Object> getUserAnalytics() {
        List<Map> cityStats = userRepository.getUserStatistics();
        List<UserRepository.CityStats> ageStats = userRepository.countUsersByCity();
        
        return Map.of(
            "cityStatistics", cityStats,
            "ageStatistics", ageStats,
            "totalUsers", userRepository.count(),
            "activeUsers", userRepository.countByStatus("ACTIVE")
        );
    }
    
    /**
     * 地理空间查询
     */
    public List<User> findUsersNearLocation(double longitude, double latitude, double maxDistance) {
        Query query = new Query(Criteria.where("location")
                .nearSphere(new org.springframework.data.geo.Point(longitude, latitude))
                .maxDistance(maxDistance));
        
        return mongoTemplate.find(query, User.class);
    }
    
    /**
     * 文本搜索
     */
    public List<User> searchUsersByText(String searchText) {
        Query query = new Query(Criteria.where("$text")
                .matching(new org.springframework.data.mongodb.core.query.TextCriteria()
                        .matching(searchText)));
        query.limit(50);
        
        return mongoTemplate.find(query, User.class);
    }
    
    private void checkEmailUniqueness(String email) {
        Query query = new Query(Criteria.where("email").is(email));
        long count = mongoTemplate.count(query, User.class);
        if (count > 0) {
            throw new ValidationException("Email already exists");
        }
    }
}

五、Controller 层

5.1 RESTful API 控制器

package com.example.mongodbdemo.controller;

import com.example.mongodbdemo.dto.*;
import com.example.mongodbdemo.entity.User;
import com.example.mongodbdemo.service.UserService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.web.PageableDefault;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

import javax.validation.Valid;
import javax.validation.constraints.Min;
import java.util.List;
import java.util.Map;

/**
 * 用户管理API
 */
@RestController
@RequestMapping("/api/v1/users")
@RequiredArgsConstructor
@Slf4j
@Validated
@Tag(name = "用户管理", description = "用户管理相关API")
public class UserController {
    
    private final UserService userService;
    
    /**
     * 创建用户
     */
    @PostMapping
    @Operation(summary = "创建用户", description = "创建新用户")
    public ResponseEntity<ApiResponse<User>> createUser(
            @Valid @RequestBody CreateUserRequest request) {
        log.info("Creating user with email: {}", request.getEmail());
        
        User user = convertToEntity(request);
        User createdUser = userService.createUser(user);
        
        ApiResponse<User> response = ApiResponse.success(
            createdUser,
            "用户创建成功"
        );
        
        return ResponseEntity.status(HttpStatus.CREATED).body(response);
    }
    
    /**
     * 批量创建用户
     */
    @PostMapping("/batch")
    @Operation(summary = "批量创建用户")
    public ResponseEntity<ApiResponse<List<User>>> createUsers(
            @Valid @RequestBody List<CreateUserRequest> requests) {
        log.info("Creating {} users", requests.size());
        
        List<User> users = requests.stream()
                .map(this::convertToEntity)
                .toList();
        
        List<User> createdUsers = userService.createUsers(users);
        
        ApiResponse<List<User>> response = ApiResponse.success(
            createdUsers,
            String.format("成功创建 %d 个用户", createdUsers.size())
        );
        
        return ResponseEntity.status(HttpStatus.CREATED).body(response);
    }
    
    /**
     * 获取用户详情
     */
    @GetMapping("/{id}")
    @Operation(summary = "获取用户详情", description = "根据ID获取用户信息")
    public ResponseEntity<ApiResponse<User>> getUserById(
            @PathVariable @Parameter(description = "用户ID", required = true) String id) {
        log.info("Getting user by ID: {}", id);
        
        return userService.getUserById(id)
                .map(user -> ResponseEntity.ok(ApiResponse.success(user, "获取成功")))
                .orElse(ResponseEntity.status(HttpStatus.NOT_FOUND)
                        .body(ApiResponse.error("用户不存在")));
    }
    
    /**
     * 根据邮箱获取用户
     */
    @GetMapping("/email/{email}")
    @Operation(summary = "根据邮箱获取用户")
    public ResponseEntity<ApiResponse<User>> getUserByEmail(
            @PathVariable String email) {
        log.info("Getting user by email: {}", email);
        
        return userService.getUserByEmail(email)
                .map(user -> ResponseEntity.ok(ApiResponse.success(user, "获取成功")))
                .orElse(ResponseEntity.status(HttpStatus.NOT_FOUND)
                        .body(ApiResponse.error("用户不存在")));
    }
    
    /**
     * 获取所有用户
     */
    @GetMapping
    @Operation(summary = "获取用户列表", description = "获取所有用户,支持分页和排序")
    public ResponseEntity<ApiResponse<Page<User>>> getAllUsers(
            @PageableDefault(size = 20, sort = "createdAt") Pageable pageable) {
        log.info("Getting all users with pageable: {}", pageable);
        
        Page<User> users = userService.getUsers(pageable);
        
        ApiResponse<Page<User>> response = ApiResponse.success(
            users,
            String.format("获取到 %d 个用户", users.getTotalElements())
        );
        
        return ResponseEntity.ok(response);
    }
    
    /**
     * 搜索用户
     */
    @GetMapping("/search")
    @Operation(summary = "搜索用户", description = "根据条件搜索用户")
    public ResponseEntity<ApiResponse<Page<User>>> searchUsers(
            @RequestParam(required = false) String firstName,
            @RequestParam(required = false) String lastName,
            @RequestParam(required = false) String email,
            @RequestParam(required = false) Integer ageFrom,
            @RequestParam(required = false) Integer ageTo,
            @RequestParam(required = false) String city,
            @RequestParam(required = false) String status,
            @PageableDefault(size = 20, sort = "createdAt") Pageable pageable) {
        
        log.info("Searching users with criteria");
        
        Map<String, Object> criteria = new java.util.HashMap<>();
        if (firstName != null) criteria.put("firstName", firstName);
        if (lastName != null) criteria.put("lastName", lastName);
        if (email != null) criteria.put("email", email);
        if (ageFrom != null) criteria.put("ageFrom", ageFrom);
        if (ageTo != null) criteria.put("ageTo", ageTo);
        if (city != null) criteria.put("city", city);
        if (status != null) criteria.put("status", status);
        
        Page<User> users = userService.searchUsers(criteria, pageable);
        
        ApiResponse<Page<User>> response = ApiResponse.success(
            users,
            String.format("搜索到 %d 个用户", users.getTotalElements())
        );
        
        return ResponseEntity.ok(response);
    }
    
    /**
     * 更新用户
     */
    @PutMapping("/{id}")
    @Operation(summary = "更新用户", description = "更新用户信息")
    public ResponseEntity<ApiResponse<User>> updateUser(
            @PathVariable String id,
            @Valid @RequestBody UpdateUserRequest request) {
        log.info("Updating user with ID: {}", id);
        
        User userUpdates = convertToEntity(request);
        User updatedUser = userService.updateUser(id, userUpdates);
        
        ApiResponse<User> response = ApiResponse.success(
            updatedUser,
            "用户更新成功"
        );
        
        return ResponseEntity.ok(response);
    }
    
    /**
     * 部分更新用户
     */
    @PatchMapping("/{id}")
    @Operation(summary = "部分更新用户", description = "更新用户部分字段")
    public ResponseEntity<ApiResponse<Void>> partialUpdateUser(
            @PathVariable String id,
            @RequestBody Map<String, Object> updates) {
        log.info("Partial updating user with ID: {}", id);
        
        userService.partialUpdateUser(id, updates);
        
        return ResponseEntity.ok(ApiResponse.success("用户更新成功"));
    }
    
    /**
     * 删除用户
     */
    @DeleteMapping("/{id}")
    @Operation(summary = "删除用户", description = "根据ID删除用户")
    public ResponseEntity<ApiResponse<Void>> deleteUser(
            @PathVariable String id) {
        log.info("Deleting user with ID: {}", id);
        
        userService.deleteUser(id);
        
        return ResponseEntity.ok(ApiResponse.success("用户删除成功"));
    }
    
    /**
     * 软删除用户
     */
    @DeleteMapping("/{id}/soft")
    @Operation(summary = "软删除用户")
    public ResponseEntity<ApiResponse<Void>> softDeleteUser(
            @PathVariable String id) {
        log.info("Soft deleting user with ID: {}", id);
        
        userService.softDeleteUser(id);
        
        return ResponseEntity.ok(ApiResponse.success("用户已标记为删除"));
    }
    
    /**
     * 获取用户统计信息
     */
    @GetMapping("/statistics")
    @Operation(summary = "获取用户统计信息")
    public ResponseEntity<ApiResponse<List<Map>>> getUserStatistics() {
        log.info("Getting user statistics");
        
        List<Map> statistics = userService.getUserStatistics();
        
        ApiResponse<List<Map>> response = ApiResponse.success(
            statistics,
            "获取统计信息成功"
        );
        
        return ResponseEntity.ok(response);
    }
    
    /**
     * 获取所有城市
     */
    @GetMapping("/cities")
    @Operation(summary = "获取所有城市")
    public ResponseEntity<ApiResponse<List<String>>> getAllCities() {
        log.info("Getting all cities");
        
        List<String> cities = userService.getAllCities();
        
        ApiResponse<List<String>> response = ApiResponse.success(
            cities,
            String.format("获取到 %d 个城市", cities.size())
        );
        
        return ResponseEntity.ok(response);
    }
    
    /**
     * 验证邮箱是否可用
     */
    @GetMapping("/check-email")
    @Operation(summary = "验证邮箱是否可用")
    public ResponseEntity<ApiResponse<Boolean>> checkEmailAvailability(
            @RequestParam String email) {
        log.info("Checking email availability: {}", email);
        
        boolean available = !userService.emailExists(email);
        
        String message = available ? "邮箱可用" : "邮箱已被使用";
        
        return ResponseEntity.ok(ApiResponse.success(available, message));
    }
    
    /**
     * 根据条件查询用户
     */
    @GetMapping("/query")
    @Operation(summary = "根据条件查询用户")
    public ResponseEntity<ApiResponse<List<User>>> queryUsers(
            @RequestParam(required = false) String firstName,
            @RequestParam(required = false) @Min(0) Integer minAge,
            @RequestParam(required = false) String city) {
        log.info("Querying users with criteria");
        
        List<User> users = userService.findUsersByCriteria(firstName, minAge, city);
        
        ApiResponse<List<User>> response = ApiResponse.success(
            users,
            String.format("查询到 %d 个用户", users.size())
        );
        
        return ResponseEntity.ok(response);
    }
    
    /**
     * 更新用户状态
     */
    @PutMapping("/{id}/status")
    @Operation(summary = "更新用户状态")
    public ResponseEntity<ApiResponse<Void>> updateUserStatus(
            @PathVariable String id,
            @RequestParam String status) {
        log.info("Updating user status, ID: {}, status: {}", id, status);
        
        userService.updateUserStatus(id, status);
        
        return ResponseEntity.ok(ApiResponse.success("用户状态更新成功"));
    }
    
    /**
     * 批量更新用户状态
     */
    @PutMapping("/batch-status")
    @Operation(summary = "批量更新用户状态")
    public ResponseEntity<ApiResponse<Void>> bulkUpdateUserStatus(
            @RequestParam List<String> ids,
            @RequestParam String status) {
        log.info("Bulk updating user status for {} users", ids.size());
        
        userService.bulkUpdateUserStatus(ids, status);
        
        return ResponseEntity.ok(ApiResponse.success("批量更新成功"));
    }
    
    /**
     * 根据条件统计用户数
     */
    @GetMapping("/count")
    @Operation(summary = "根据条件统计用户数")
    public ResponseEntity<ApiResponse<Long>> countUsers(
            @RequestParam(required = false) Integer ageFrom,
            @RequestParam(required = false) Integer ageTo,
            @RequestParam(required = false) String city,
            @RequestParam(required = false) String status) {
        log.info("Counting users with criteria");
        
        Map<String, Object> criteria = new java.util.HashMap<>();
        if (ageFrom != null) criteria.put("ageFrom", ageFrom);
        if (ageTo != null) criteria.put("ageTo", ageTo);
        if (city != null) criteria.put("city", city);
        if (status != null) criteria.put("status", status);
        
        Long count = userService.countUsers(criteria);
        
        return ResponseEntity.ok(ApiResponse.success(count, "统计完成"));
    }
    
    /**
     * 转换请求为实体
     */
    private User convertToEntity(CreateUserRequest request) {
        return User.builder()
                .firstName(request.getFirstName())
                .lastName(request.getLastName())
                .email(request.getEmail())
                .age(request.getAge())
                .phoneNumber(request.getPhoneNumber())
                .gender(request.getGender())
                .birthDate(request.getBirthDate())
                .city(request.getCity())
                .country(request.getCountry())
                .roles(request.getRoles())
                .preferences(request.getPreferences())
                .addresses(request.getAddresses())
                .build();
    }
    
    private User convertToEntity(UpdateUserRequest request) {
        return User.builder()
                .firstName(request.getFirstName())
                .lastName(request.getLastName())
                .age(request.getAge())
                .phoneNumber(request.getPhoneNumber())
                .city(request.getCity())
                .country(request.getCountry())
                .build();
    }
}

六、DTO 和请求/响应对象

6.1 请求对象

package com.example.mongodbdemo.dto;

import com.example.mongodbdemo.entity.User;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;

import javax.validation.constraints.*;
import java.time.LocalDateTime;
import java.util.List;

/**
 * 创建用户请求
 */
@Data
@Schema(description = "创建用户请求")
public class CreateUserRequest {
    
    @NotBlank(message = "First name is required")
    @Size(min = 2, max = 50, message = "First name must be between 2 and 50 characters")
    @Schema(description = "First name", example = "John", required = true)
    private String firstName;
    
    @NotBlank(message = "Last name is required")
    @Size(min = 2, max = 50, message = "Last name must be between 2 and 50 characters")
    @Schema(description = "Last name", example = "Doe", required = true)
    private String lastName;
    
    @NotBlank(message = "Email is required")
    @Email(message = "Invalid email format")
    @Schema(description = "Email address", example = "john.doe@example.com", required = true)
    private String email;
    
    @NotNull(message = "Age is required")
    @Min(value = 0, message = "Age must be positive")
    @Max(value = 150, message = "Age must be less than 150")
    @Schema(description = "Age", example = "25", required = true)
    private Integer age;
    
    @Pattern(regexp = "^[0-9\\-+()\\s]*$", message = "Invalid phone number format")
    @Schema(description = "Phone number", example = "+1-234-567-8900")
    private String phoneNumber;
    
    @Pattern(regexp = "^(MALE|FEMALE|OTHER)$", message = "Invalid gender")
    @Schema(description = "Gender", example = "MALE")
    private String gender;
    
    @Schema(description = "Birth date")
    private LocalDateTime birthDate;
    
    @Schema(description = "City", example = "New York")
    private String city;
    
    @Schema(description = "Country", example = "USA")
    private String country;
    
    @Schema(description = "User roles")
    private List<String> roles;
    
    @Schema(description = "User preferences")
    private User.Preferences preferences;
    
    @Schema(description = "User addresses")
    private List<User.Address> addresses;
}

/**
 * 更新用户请求
 */
@Data
@Schema(description = "更新用户请求")
public class UpdateUserRequest {
    
    @Size(min = 2, max = 50, message = "First name must be between 2 and 50 characters")
    @Schema(description = "First name", example = "John")
    private String firstName;
    
    @Size(min = 2, max = 50, message = "Last name must be between 2 and 50 characters")
    @Schema(description = "Last name", example = "Doe")
    private String lastName;
    
    @Min(value = 0, message = "Age must be positive")
    @Max(value = 150, message = "Age must be less than 150")
    @Schema(description = "Age", example = "26")
    private Integer age;
    
    @Pattern(regexp = "^[0-9\\-+()\\s]*$", message = "Invalid phone number format")
    @Schema(description = "Phone number", example = "+1-234-567-8901")
    private String phoneNumber;
    
    @Schema(description = "City", example = "Los Angeles")
    private String city;
    
    @Schema(description = "Country", example = "USA")
    private String country;
}

/**
 * 搜索用户请求
 */
@Data
@Schema(description = "搜索用户请求")
public class SearchUserRequest {
    
    @Schema(description = "First name", example = "John")
    private String firstName;
    
    @Schema(description = "Last name", example = "Doe")
    private String lastName;
    
    @Email(message = "Invalid email format")
    @Schema(description = "Email", example = "john@example.com")
    private String email;
    
    @Schema(description = "Minimum age", example = "18")
    private Integer minAge;
    
    @Schema(description = "Maximum age", example = "60")
    private Integer maxAge;
    
    @Schema(description = "City", example = "New York")
    private String city;
    
    @Schema(description = "Status", example = "ACTIVE")
    private String status;
    
    @Schema(description = "Page number", example = "0")
    private Integer page;
    
    @Schema(description = "Page size", example = "20")
    private Integer size;
    
    @Schema(description = "Sort field", example = "createdAt")
    private String sort;
    
    @Schema(description = "Sort direction", example = "DESC")
    private String direction;
}

6.2 响应对象

package com.example.mongodbdemo.dto;

import com.fasterxml.jackson.annotation.JsonInclude;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.time.LocalDateTime;

/**
 * 统一API响应
 */
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@JsonInclude(JsonInclude.Include.NON_NULL)
@Schema(description = "API响应")
public class ApiResponse<T> {
    
    @Schema(description = "是否成功", example = "true")
    private boolean success;
    
    @Schema(description = "响应消息", example = "操作成功")
    private String message;
    
    @Schema(description = "响应数据")
    private T data;
    
    @Schema(description = "错误代码")
    private String errorCode;
    
    @Schema(description = "时间戳", example = "2024-01-15T10:30:00")
    private LocalDateTime timestamp;
    
    /**
     * 成功响应
     */
    public static <T> ApiResponse<T> success(T data, String message) {
        return ApiResponse.<T>builder()
                .success(true)
                .message(message)
                .data(data)
                .timestamp(LocalDateTime.now())
                .build();
    }
    
    public static <T> ApiResponse<T> success(T data) {
        return success(data, "操作成功");
    }
    
    public static ApiResponse<Void> success(String message) {
        return success(null, message);
    }
    
    /**
     * 失败响应
     */
    public static <T> ApiResponse<T> error(String message, String errorCode) {
        return ApiResponse.<T>builder()
                .success(false)
                .message(message)
                .errorCode(errorCode)
                .timestamp(LocalDateTime.now())
                .build();
    }
    
    public static <T> ApiResponse<T> error(String message) {
        return error(message, null);
    }
}

/**
 * 用户响应DTO(展示如何控制返回字段)
 */
@Data
@Builder
@Schema(description = "用户响应")
public class UserResponse {
    
    @Schema(description = "用户ID", example = "507f1f77bcf86cd799439011")
    private String id;
    
    @Schema(description = "First name", example = "John")
    private String firstName;
    
    @Schema(description = "Last name", example = "Doe")
    private String lastName;
    
    @Schema(description = "Email", example = "john.doe@example.com")
    private String email;
    
    @Schema(description = "Age", example = "25")
    private Integer age;
    
    @Schema(description = "City", example = "New York")
    private String city;
    
    @Schema(description = "Country", example = "USA")
    private String country;
    
    @Schema(description = "Status", example = "ACTIVE")
    private String status;
    
    @Schema(description = "创建时间")
    private LocalDateTime createdAt;
    
    /**
     * 从实体转换
     */
    public static UserResponse fromEntity(com.example.mongodbdemo.entity.User user) {
        return UserResponse.builder()
                .id(user.getId())
                .firstName(user.getFirstName())
                .lastName(user.getLastName())
                .email(user.getEmail())
                .age(user.getAge())
                .city(user.getCity())
                .country(user.getCountry())
                .status(user.getStatus())
                .createdAt(user.getCreatedAt())
                .build();
    }
}

七、配置类

7.1 MongoDB 配置

package com.example.mongodbdemo.config;

import com.mongodb.ConnectionString;
import com.mongodb.MongoClientSettings;
import com.mongodb.client.MongoClient;
import com.mongodb.client.MongoClients;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.mongodb.MongoDatabaseFactory;
import org.springframework.data.mongodb.MongoTransactionManager;
import org.springframework.data.mongodb.config.AbstractMongoClientConfiguration;
import org.springframework.data.mongodb.core.convert.DefaultMongoTypeMapper;
import org.springframework.data.mongodb.core.convert.MappingMongoConverter;
import org.springframework.data.mongodb.core.convert.MongoCustomConversions;
import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
import org.springframework.data.mongodb.repository.config.EnableMongoRepositories;

import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Arrays;
import java.util.Date;

/**
 * MongoDB配置类
 */
@Configuration
@EnableMongoRepositories(basePackages = "com.example.mongodbdemo.repository")
@Slf4j
public class MongoConfig extends AbstractMongoClientConfiguration {
    
    @Value("${spring.data.mongodb.host:localhost}")
    private String host;
    
    @Value("${spring.data.mongodb.port:27017}")
    private int port;
    
    @Value("${spring.data.mongodb.database:mydatabase}")
    private String database;
    
    @Value("${spring.data.mongodb.username:}")
    private String username;
    
    @Value("${spring.data.mongodb.password:}")
    private String password;
    
    @Value("${spring.data.mongodb.authentication-database:admin}")
    private String authenticationDatabase;
    
    @Override
    protected String getDatabaseName() {
        return database;
    }
    
    @Override
    public MongoClient mongoClient() {
        log.info("Connecting to MongoDB: {}:{}, database: {}", host, port, database);
        
        String connectionString;
        if (username.isEmpty() || password.isEmpty()) {
            // 无认证连接
            connectionString = String.format("mongodb://%s:%d/%s", host, port, database);
        } else {
            // 带认证连接
            connectionString = String.format(
                "mongodb://%s:%s@%s:%d/%s?authSource=%s",
                username, password, host, port, database, authenticationDatabase
            );
        }
        
        MongoClientSettings settings = MongoClientSettings.builder()
                .applyConnectionString(new ConnectionString(connectionString))
                .applyToConnectionPoolSettings(builder -> builder
                        .maxSize(100)
                        .minSize(10)
                        .maxWaitTimeMillis(120000))
                .applyToSocketSettings(builder -> builder
                        .connectTimeout(10000, java.util.concurrent.TimeUnit.MILLISECONDS))
                .build();
        
        return MongoClients.create(settings);
    }
    
    /**
     * 配置事务管理器(MongoDB 4.0+ 支持事务)
     */
    @Bean
    public MongoTransactionManager transactionManager(MongoDatabaseFactory dbFactory) {
        return new MongoTransactionManager(dbFactory);
    }
    
    /**
     * 移除 _class 字段
     */
    @Bean
    @Override
    public MappingMongoConverter mappingMongoConverter(
            MongoDatabaseFactory databaseFactory,
            MongoCustomConversions customConversions,
            MongoMappingContext mappingContext) {
        
        MappingMongoConverter converter = super.mappingMongoConverter(
                databaseFactory, customConversions, mappingContext);
        
        // 移除 _class 字段
        converter.setTypeMapper(new DefaultMongoTypeMapper(null));
        
        return converter;
    }
    
    /**
     * 自定义类型转换
     */
    @Bean
    @Override
    public MongoCustomConversions customConversions() {
        return new MongoCustomConversions(Arrays.asList(
            // LocalDateTime 转 Date
            new org.springframework.core.convert.converter.Converter<LocalDateTime, Date>() {
                @Override
                public Date convert(LocalDateTime source) {
                    return Date.from(source.atZone(ZoneId.systemDefault()).toInstant());
                }
            },
            // Date 转 LocalDateTime
            new org.springframework.core.convert.converter.Converter<Date, LocalDateTime>() {
                @Override
                public LocalDateTime convert(Date source) {
                    return source.toInstant()
                            .atZone(ZoneId.systemDefault())
                            .toLocalDateTime();
                }
            }
        ));
    }
    
    /**
     * 启用索引自动创建
     */
    @Override
    protected boolean autoIndexCreation() {
        return true;
    }
}

7.2 审计配置

package com.example.mongodbdemo.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.domain.AuditorAware;
import org.springframework.data.mongodb.config.EnableMongoAuditing;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;

import java.util.Optional;

/**
 * MongoDB审计配置
 */
@Configuration
@EnableMongoAuditing  // 启用审计功能
public class MongoAuditConfig {
    
    /**
     * 审计员信息提供者
     */
    @Bean
    public AuditorAware<String> auditorProvider() {
        return () -> {
            // 从Spring Security获取当前用户
            Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
            if (authentication == null || !authentication.isAuthenticated()) {
                return Optional.of("system");
            }
            return Optional.ofNullable(authentication.getName());
        };
    }
}

7.3 序列化配置

package com.example.mongodbdemo.config;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;

import java.text.SimpleDateFormat;

@Configuration
public class JacksonConfig {
    
    @Bean
    public ObjectMapper objectMapper(Jackson2ObjectMapperBuilder builder) {
        ObjectMapper objectMapper = builder.createXmlMapper(false).build();
        
        // 注册Java 8时间模块
        objectMapper.registerModule(new JavaTimeModule());
        
        // 禁用日期转时间戳
        objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
        
        // 设置日期格式
        objectMapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
        
        // 忽略未知属性
        objectMapper.configure(com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
        
        return objectMapper;
    }
}

八、异常处理

8.1 自定义异常

package com.example.mongodbdemo.exception;

/**
 * 资源不存在异常
 */
public class ResourceNotFoundException extends RuntimeException {
    public ResourceNotFoundException(String message) {
        super(message);
    }
    
    public ResourceNotFoundException(String message, Throwable cause) {
        super(message, cause);
    }
}

/**
 * 验证异常
 */
public class ValidationException extends RuntimeException {
    public ValidationException(String message) {
        super(message);
    }
    
    public ValidationException(String message, Throwable cause) {
        super(message, cause);
    }
}

/**
 * 业务异常
 */
public class BusinessException extends RuntimeException {
    private String errorCode;
    
    public BusinessException(String message) {
        super(message);
    }
    
    public BusinessException(String errorCode, String message) {
        super(message);
        this.errorCode = errorCode;
    }
    
    public String getErrorCode() {
        return errorCode;
    }
}

8.2 全局异常处理器

package com.example.mongodbdemo.handler;

import com.example.mongodbdemo.dto.ApiResponse;
import com.example.mongodbdemo.exception.*;
import lombok.extern.slf4j.Slf4j;
import org.springframework.dao.DataAccessException;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.context.request.WebRequest;

import javax.validation.ConstraintViolationException;
import java.util.HashMap;
import java.util.Map;

/**
 * 全局异常处理器
 */
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
    
    /**
     * 处理资源不存在异常
     */
    @ExceptionHandler(ResourceNotFoundException.class)
    public ResponseEntity<ApiResponse<Void>> handleResourceNotFoundException(
            ResourceNotFoundException ex, WebRequest request) {
        
        log.error("Resource not found: {}", ex.getMessage());
        
        ApiResponse<Void> response = ApiResponse.error(
            ex.getMessage(),
            "RESOURCE_NOT_FOUND"
        );
        
        return ResponseEntity.status(HttpStatus.NOT_FOUND).body(response);
    }
    
    /**
     * 处理验证异常
     */
    @ExceptionHandler(ValidationException.class)
    public ResponseEntity<ApiResponse<Void>> handleValidationException(
            ValidationException ex, WebRequest request) {
        
        log.error("Validation error: {}", ex.getMessage());
        
        ApiResponse<Void> response = ApiResponse.error(
            ex.getMessage(),
            "VALIDATION_ERROR"
        );
        
        return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(response);
    }
    
    /**
     * 处理业务异常
     */
    @ExceptionHandler(BusinessException.class)
    public ResponseEntity<ApiResponse<Void>> handleBusinessException(
            BusinessException ex, WebRequest request) {
        
        log.error("Business error: {}", ex.getMessage());
        
        ApiResponse<Void> response = ApiResponse.error(
            ex.getMessage(),
            ex.getErrorCode()
        );
        
        return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(response);
    }
    
    /**
     * 处理参数验证异常
     */
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<ApiResponse<Map<String, String>>> handleValidationExceptions(
            MethodArgumentNotValidException ex) {
        
        Map<String, String> errors = new HashMap<>();
        ex.getBindingResult().getAllErrors().forEach(error -> {
            String fieldName = ((FieldError) error).getField();
            String errorMessage = error.getDefaultMessage();
            errors.put(fieldName, errorMessage);
        });
        
        ApiResponse<Map<String, String>> response = ApiResponse.error(
            "参数验证失败",
            "VALIDATION_FAILED"
        );
        response.setData(errors);
        
        return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(response);
    }
    
    /**
     * 处理约束违反异常
     */
    @ExceptionHandler(ConstraintViolationException.class)
    public ResponseEntity<ApiResponse<Map<String, String>>> handleConstraintViolationException(
            ConstraintViolationException ex) {
        
        Map<String, String> errors = new HashMap<>();
        ex.getConstraintViolations().forEach(violation -> {
            String fieldName = violation.getPropertyPath().toString();
            String errorMessage = violation.getMessage();
            errors.put(fieldName, errorMessage);
        });
        
        ApiResponse<Map<String, String>> response = ApiResponse.error(
            "参数约束违反",
            "CONSTRAINT_VIOLATION"
        );
        response.setData(errors);
        
        return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(response);
    }
    
    /**
     * 处理唯一键冲突异常
     */
    @ExceptionHandler(DuplicateKeyException.class)
    public ResponseEntity<ApiResponse<Void>> handleDuplicateKeyException(
            DuplicateKeyException ex) {
        
        log.error("Duplicate key error: {}", ex.getMessage());
        
        ApiResponse<Void> response = ApiResponse.error(
            "数据已存在,请勿重复添加",
            "DUPLICATE_KEY"
        );
        
        return ResponseEntity.status(HttpStatus.CONFLICT).body(response);
    }
    
    /**
     * 处理数据访问异常
     */
    @ExceptionHandler(DataAccessException.class)
    public ResponseEntity<ApiResponse<Void>> handleDataAccessException(
            DataAccessException ex) {
        
        log.error("Data access error: {}", ex.getMessage(), ex);
        
        ApiResponse<Void> response = ApiResponse.error(
            "数据访问异常,请稍后重试",
            "DATA_ACCESS_ERROR"
        );
        
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response);
    }
    
    /**
     * 处理其他所有异常
     */
    @ExceptionHandler(Exception.class)
    public ResponseEntity<ApiResponse<Void>> handleGlobalException(
            Exception ex, WebRequest request) {
        
        log.error("Unexpected error: {}", ex.getMessage(), ex);
        
        ApiResponse<Void> response = ApiResponse.error(
            "服务器内部错误,请稍后重试",
            "INTERNAL_SERVER_ERROR"
        );
        
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response);
    }
}

九、测试

9.1 单元测试

package com.example.mongodbdemo.service;

import com.example.mongodbdemo.entity.User;
import com.example.mongodbdemo.repository.UserRepository;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import java.time.LocalDateTime;
import java.util.Optional;

import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;

@ExtendWith(MockitoExtension.class)
class UserServiceTest {
    
    @Mock
    private UserRepository userRepository;
    
    @InjectMocks
    private UserService userService;
    
    private User testUser;
    
    @BeforeEach
    void setUp() {
        testUser = User.builder()
                .id("123")
                .firstName("John")
                .lastName("Doe")
                .email("john.doe@example.com")
                .age(25)
                .city("New York")
                .status("ACTIVE")
                .createdAt(LocalDateTime.now())
                .updatedAt(LocalDateTime.now())
                .build();
    }
    
    @Test
    void testCreateUser_Success() {
        // Arrange
        when(userRepository.existsByEmail(any())).thenReturn(false);
        when(userRepository.save(any(User.class))).thenReturn(testUser);
        
        // Act
        User createdUser = userService.createUser(testUser);
        
        // Assert
        assertNotNull(createdUser);
        assertEquals("John", createdUser.getFirstName());
        assertEquals("Doe", createdUser.getLastName());
        assertEquals("john.doe@example.com", createdUser.getEmail());
        
        verify(userRepository).existsByEmail("john.doe@example.com");
        verify(userRepository).save(any(User.class));
    }
    
    @Test
    void testCreateUser_EmailExists() {
        // Arrange
        when(userRepository.existsByEmail(any())).thenReturn(true);
        
        // Act & Assert
        assertThrows(RuntimeException.class, () -> {
            userService.createUser(testUser);
        });
        
        verify(userRepository).existsByEmail("john.doe@example.com");
        verify(userRepository, never()).save(any(User.class));
    }
    
    @Test
    void testGetUserById_Found() {
        // Arrange
        when(userRepository.findById("123")).thenReturn(Optional.of(testUser));
        
        // Act
        Optional<User> result = userService.getUserById("123");
        
        // Assert
        assertTrue(result.isPresent());
        assertEquals("John", result.get().getFirstName());
        
        verify(userRepository).findById("123");
    }
    
    @Test
    void testGetUserById_NotFound() {
        // Arrange
        when(userRepository.findById("999")).thenReturn(Optional.empty());
        
        // Act
        Optional<User> result = userService.getUserById("999");
        
        // Assert
        assertFalse(result.isPresent());
        
        verify(userRepository).findById("999");
    }
    
    @Test
    void testUpdateUser_Success() {
        // Arrange
        User updatedUser = User.builder()
                .firstName("Jane")
                .lastName("Smith")
                .age(26)
                .build();
        
        when(userRepository.findById("123")).thenReturn(Optional.of(testUser));
        when(userRepository.save(any(User.class))).thenAnswer(invocation -> {
            User user = invocation.getArgument(0);
            user.setFirstName("Jane");
            user.setLastName("Smith");
            user.setAge(26);
            return user;
        });
        
        // Act
        User result = userService.updateUser("123", updatedUser);
        
        // Assert
        assertEquals("Jane", result.getFirstName());
        assertEquals("Smith", result.getLastName());
        assertEquals(26, result.getAge());
        assertNotNull(result.getUpdatedAt());
        
        verify(userRepository).findById("123");
        verify(userRepository).save(any(User.class));
    }
    
    @Test
    void testDeleteUser_Success() {
        // Arrange
        when(userRepository.existsById("123")).thenReturn(true);
        doNothing().when(userRepository).deleteById("123");
        
        // Act
        userService.deleteUser("123");
        
        // Assert
        verify(userRepository).existsById("123");
        verify(userRepository).deleteById("123");
    }
    
    @Test
    void testDeleteUser_NotFound() {
        // Arrange
        when(userRepository.existsById("999")).thenReturn(false);
        
        // Act & Assert
        assertThrows(RuntimeException.class, () -> {
            userService.deleteUser("999");
        });
        
        verify(userRepository).existsById("999");
        verify(userRepository, never()).deleteById(any());
    }
}

9.2 集成测试

package com.example.mongodbdemo.integration;

import com.example.mongodbdemo.entity.User;
import com.example.mongodbdemo.repository.UserRepository;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.data.mongo.DataMongoTest;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.test.context.ActiveProfiles;

import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;

import static org.junit.jupiter.api.Assertions.*;

@DataMongoTest
@ActiveProfiles("test")
class UserRepositoryIntegrationTest {
    
    @Autowired
    private UserRepository userRepository;
    
    private User user1;
    private User user2;
    private User user3;
    
    @BeforeEach
    void setUp() {
        // 清理数据
        userRepository.deleteAll();
        
        // 准备测试数据
        user1 = User.builder()
                .firstName("John")
                .lastName("Doe")
                .email("john.doe@example.com")
                .age(25)
                .city("New York")
                .status("ACTIVE")
                .registrationDate(LocalDateTime.now())
                .createdAt(LocalDateTime.now())
                .updatedAt(LocalDateTime.now())
                .build();
        
        user2 = User.builder()
                .firstName("Jane")
                .lastName("Smith")
                .email("jane.smith@example.com")
                .age(30)
                .city("Los Angeles")
                .status("ACTIVE")
                .registrationDate(LocalDateTime.now().minusDays(1))
                .createdAt(LocalDateTime.now())
                .updatedAt(LocalDateTime.now())
                .build();
        
        user3 = User.builder()
                .firstName("Bob")
                .lastName("Johnson")
                .email("bob.johnson@example.com")
                .age(35)
                .city("New York")
                .status("INACTIVE")
                .registrationDate(LocalDateTime.now().minusDays(2))
                .createdAt(LocalDateTime.now())
                .updatedAt(LocalDateTime.now())
                .build();
        
        userRepository.saveAll(Arrays.asList(user1, user2, user3));
    }
    
    @AfterEach
    void tearDown() {
        userRepository.deleteAll();
    }
    
    @Test
    void testSaveAndFindById() {
        // Arrange
        User newUser = User.builder()
                .firstName("Alice")
                .lastName("Brown")
                .email("alice.brown@example.com")
                .age(28)
                .city("Chicago")
                .status("ACTIVE")
                .build();
        
        // Act
        User savedUser = userRepository.save(newUser);
        Optional<User> foundUser = userRepository.findById(savedUser.getId());
        
        // Assert
        assertTrue(foundUser.isPresent());
        assertEquals("Alice", foundUser.get().getFirstName());
        assertEquals("Brown", foundUser.get().getLastName());
        assertEquals("alice.brown@example.com", foundUser.get().getEmail());
    }
    
    @Test
    void testFindByEmail() {
        // Act
        Optional<User> result = userRepository.findByEmail("john.doe@example.com");
        
        // Assert
        assertTrue(result.isPresent());
        assertEquals("John", result.get().getFirstName());
        assertEquals("Doe", result.get().getLastName());
    }
    
    @Test
    void testFindByCity() {
        // Act
        List<User> users = userRepository.findByCity("New York");
        
        // Assert
        assertEquals(2, users.size());
        assertTrue(users.stream().anyMatch(u -> u.getFirstName().equals("John")));
        assertTrue(users.stream().anyMatch(u -> u.getFirstName().equals("Bob")));
    }
    
    @Test
    void testFindByAgeGreaterThan() {
        // Act
        List<User> users = userRepository.findByAgeGreaterThan(28);
        
        // Assert
        assertEquals(2, users.size());
        assertTrue(users.stream().anyMatch(u -> u.getFirstName().equals("Jane")));
        assertTrue(users.stream().anyMatch(u -> u.getFirstName().equals("Bob")));
    }
    
    @Test
    void testFindByStatus() {
        // Act
        Pageable pageable = PageRequest.of(0, 10);
        Page<User> activeUsers = userRepository.findByStatus("ACTIVE", pageable);
        
        // Assert
        assertEquals(2, activeUsers.getTotalElements());
        assertEquals(2, activeUsers.getContent().size());
    }
    
    @Test
    void testExistsByEmail() {
        // Act
        boolean exists = userRepository.existsByEmail("john.doe@example.com");
        boolean notExists = userRepository.existsByEmail("nonexistent@example.com");
        
        // Assert
        assertTrue(exists);
        assertFalse(notExists);
    }
    
    @Test
    void testCountByCity() {
        // Act
        Long count = userRepository.countByCity("New York");
        
        // Assert
        assertEquals(2, count);
    }
    
    @Test
    void testFindUsersByAgeRange() {
        // Act
        List<User> users = userRepository.findUsersByAgeRange(25, 35);
        
        // Assert
        assertEquals(2, users.size());
        assertTrue(users.stream().anyMatch(u -> u.getFirstName().equals("John")));
        assertTrue(users.stream().anyMatch(u -> u.getFirstName().equals("Jane")));
    }
    
    @Test
    void testDeleteByEmail() {
        // Act
        userRepository.deleteByEmail("john.doe@example.com");
        
        // Assert
        Optional<User> deletedUser = userRepository.findByEmail("john.doe@example.com");
        assertFalse(deletedUser.isPresent());
    }
}

十、应用启动和配置

10.1 主启动类

package com.example.mongodbdemo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.data.mongodb.config.EnableMongoAuditing;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.annotation.EnableScheduling;

@SpringBootApplication
@EnableMongoAuditing  // 启用审计功能
@EnableAsync          // 启用异步支持
@EnableScheduling     // 启用定时任务
@EnableConfigurationProperties  // 启用配置属性
public class MongodbDemoApplication {
    
    public static void main(String[] args) {
        SpringApplication.run(MongodbDemoApplication.class, args);
    }
}

10.2 初始化数据

package com.example.mongodbdemo.init;

import com.example.mongodbdemo.entity.User;
import com.example.mongodbdemo.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.CommandLineRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;

import java.time.LocalDateTime;
import java.util.Arrays;

/**
 * 初始化数据(仅用于开发环境)
 */
@Configuration
@RequiredArgsConstructor
@Slf4j
@Profile("dev")  // 只在开发环境运行
public class DataInitializer {
    
    private final UserRepository userRepository;
    
    @Bean
    public CommandLineRunner initData() {
        return args -> {
            // 清理现有数据
            userRepository.deleteAll();
            
            // 创建测试用户
            User admin = User.builder()
                    .firstName("Admin")
                    .lastName("User")
                    .email("admin@example.com")
                    .age(30)
                    .city("Beijing")
                    .country("China")
                    .status("ACTIVE")
                    .roles(Arrays.asList("ADMIN", "USER"))
                    .registrationDate(LocalDateTime.now())
                    .createdAt(LocalDateTime.now())
                    .updatedAt(LocalDateTime.now())
                    .build();
            
            User testUser = User.builder()
                    .firstName("Test")
                    .lastName("User")
                    .email("test@example.com")
                    .age(25)
                    .city("Shanghai")
                    .country("China")
                    .status("ACTIVE")
                    .roles(Arrays.asList("USER"))
                    .registrationDate(LocalDateTime.now())
                    .createdAt(LocalDateTime.now())
                    .updatedAt(LocalDateTime.now())
                    .build();
            
            userRepository.saveAll(Arrays.asList(admin, testUser));
            
            log.info("Initialized {} users", userRepository.count());
        };
    }
}

十一、Swagger API文档

package com.example.mongodbdemo.config;

import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Contact;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.info.License;
import io.swagger.v3.oas.models.security.SecurityScheme;
import io.swagger.v3.oas.models.servers.Server;
import org.springdoc.core.GroupedOpenApi;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.List;

@Configuration
public class SwaggerConfig {
    
    @Value("${server.servlet.context-path:/api}")
    private String contextPath;
    
    @Bean
    public OpenAPI customOpenAPI() {
        return new OpenAPI()
                .info(new Info()
                        .title("MongoDB Demo API")
                        .description("Spring Boot MongoDB 集成示例")
                        .version("1.0.0")
                        .contact(new Contact()
                                .name("开发团队")
                                .email("dev@example.com"))
                        .license(new License()
                                .name("Apache 2.0")
                                .url("http://springdoc.org")))
                .servers(List.of(
                        new Server().url(contextPath).description("API Server")
                ))
                .components(new io.swagger.v3.oas.models.Components()
                        .addSecuritySchemes("bearer-key",
                                new SecurityScheme()
                                        .type(SecurityScheme.Type.HTTP)
                                        .scheme("bearer")
                                        .bearerFormat("JWT")));
    }
    
    @Bean
    public GroupedOpenApi publicApi() {
        return GroupedOpenApi.builder()
                .group("public")
                .pathsToMatch("/api/**")
                .build();
    }
    
    @Bean
    public GroupedOpenApi adminApi() {
        return GroupedOpenApi.builder()
                .group("admin")
                .pathsToMatch("/api/admin/**")
                .build();
    }
}

十二、部署和配置建议

12.1 生产环境配置

# application-prod.yml
spring:
  data:
    mongodb:
      uri: ${MONGODB_URI:mongodb://username:password@mongodb-host:27017/database?authSource=admin&replicaSet=rs0}
      auto-index-creation: true
      
  # 连接池配置
  mongodb:
    connection-pool:
      max-size: 200
      min-size: 20
      max-wait-time: 300000

# 性能优化
server:
  tomcat:
    max-threads: 200
    min-spare-threads: 20

# 监控
management:
  endpoints:
    web:
      exposure:
        include: health,info,metrics,prometheus
  metrics:
    export:
      prometheus:
        enabled: true

12.2 健康检查端点

package com.example.mongodbdemo.health;

import com.mongodb.client.MongoClient;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.stereotype.Component;

@Component
@RequiredArgsConstructor
public class MongoHealthIndicator implements HealthIndicator {
    
    private final MongoTemplate mongoTemplate;
    private final MongoClient mongoClient;
    
    @Override
    public Health health() {
        try {
            // 执行简单的MongoDB命令检查连接
            mongoTemplate.executeCommand("{ ping: 1 }");
            
            // 获取服务器状态
            var serverStatus = mongoClient.getClusterDescription();
            
            return Health.up()
                    .withDetail("clusterType", serverStatus.getType())
                    .withDetail("servers", serverStatus.getServerDescriptions().size())
                    .build();
        } catch (Exception e) {
            return Health.down()
                    .withDetail("error", e.getMessage())
                    .build();
        }
    }
}

十三、性能优化建议

  1. 索引优化

    • 为常用查询字段创建索引
    • 避免在频繁更新的字段上创建索引
    • 使用复合索引覆盖查询
  2. 查询优化

    • 使用投影只返回需要的字段
    • 避免在应用层进行大量数据处理
    • 使用分页限制返回数据量
  3. 连接池优化

    • 根据并发量调整连接池大小
    • 监控连接使用情况
  4. 监控和日志

    • 启用慢查询日志
    • 监控MongoDB性能指标
    • 使用Spring Boot Actuator
posted @ 2025-12-24 02:04  binlicoder  阅读(0)  评论(0)    收藏  举报