引言:AI生成代码的不稳定性困境

在当今软件开发领域,AI辅助编程已成为提高效率的重要手段。然而,在实际项目中,我们发现AI生成的代码存在明显的不稳定性问题:

  • 领域模型污染:AI常常在领域层代码中混入审计字段(createdAtupdatedAt)、技术ID等基础设施关注点
  • 架构层次混乱:不同层的职责边界模糊,导致代码难以维护
  • 命名不一致:同类组件命名风格各异,增加理解成本
  • 测试覆盖不足:生成的代码往往缺乏必要的单元测试

这些问题的根源在于:AI缺乏项目特定的上下文和约束规则。本文将分享我们如何通过制定严格的project_rules.md规范,结合DDD(领域驱动设计)最佳实践,构建一个稳定、可维护的微服务架构。

一、项目规则:AI协作的基石

1.1 规则文件的核心价值

我们创建了.trae/rules/project_rules.md文件,作为AI生成代码的约束基准:

## 通用约束
1. 严格遵循**领域层→基础设施层→应用层→接口层**依赖方向,禁止反向依赖;
2. 统一包结构、命名规范,代码无冗余、无语法错误,关键逻辑加注释;
3. 强制单元测试,禁止无测试代码上线。

## 领域层(核心)
1. 核心职责:定义业务规则、领域模型,实现纯业务逻辑,**禁止包含数据库ID、审计字段等技术代码**;
2. 模型规范:实体、值对象(VO)、聚合、聚合根(Root)、域服务、仓储接口、领域事件;
3. 类名规范:聚合根`Root`结尾、值对象`VO`结尾、枚举`Enum`结尾;
...

1.2 规则的执行机制

  • 上下文注入:每次与AI交互时,自动加载规则文件作为系统提示
  • 代码审查:通过CI/CD流水线检查代码是否符合规范
  • 单元测试:强制要求测试覆盖率≥90%

二、领域层设计:保持纯粹性

2.1 聚合根的正确姿势

问题场景:AI生成的聚合根常常包含技术字段

// ❌ 错误示例:领域层包含技术字段
public class Sku {
    private Long id;              // 技术ID
    private LocalDateTime createdAt;  // 审计字段
    private LocalDateTime updatedAt;  // 审计字段
    private String skuCode;
    private String name;
    // ...
}

规范方案:领域层只关注业务属性

// ✅ 正确示例:领域层保持纯粹
public class Sku extends BaseAggregateRoot<Sku> {
    private SkuCode skuCode;      // 业务标识
    private String name;
    private SkuStatus status;      // 业务状态
    private String category;
    private String unit;
    private SafetyStockQuantity safetyStock;  // 值对象
    private ABCClassEnum abcClass;            // 枚举
    // ... 纯业务属性,无技术字段
    
    protected Sku() {}  // JPA要求
    
    private Sku(SkuCode skuCode, String name, String category, String unit) {
        this.skuCode = skuCode;
        this.name = name.trim();
        this.status = SkuStatus.DRAFT;
        registerEvent(new SkuCreatedEvent(skuCode.getValue(), name));
    }
}

2.2 领域事件的简化发布

问题场景:手动管理领域事件容易遗漏

// ❌ 错误示例:手动发布事件
public void activate() {
    this.status = WarehouseStatus.ACTIVE;
    eventBus.publish(new WarehouseActivatedEvent(this.warehouseCode));
}

规范方案:继承AbstractAggregateRoot自动管理

// ✅ 正确示例:使用JPA的AbstractAggregateRoot
public class Warehouse extends AbstractAggregateRoot<Warehouse> {
    
    public void activate() {
        if (this.status == WarehouseStatus.ACTIVE) {
            return;  // 幂等性保证
        }
        this.status = WarehouseStatus.ACTIVE;
        registerEvent(new WarehouseActivatedEvent(this.warehouseCode));
    }
}

2.3 值对象的设计陷阱

陷阱1:静态常量初始化循环依赖

// ❌ 错误示例:构造器包含校验逻辑
public class Weight {
    public static final Weight MIN_WEIGHT = new Weight(BigDecimal.ZERO, MeasurementUnit.KG);
    
    private final BigDecimal value;
    private final MeasurementUnit unit;
    
    public Weight(BigDecimal value, MeasurementUnit unit) {
        if (value.compareTo(BigDecimal.ZERO) < 0) {
            throw new IllegalArgumentException("重量不能为负");
        }
        this.value = value;
        this.unit = unit;
    }
}
// 问题:MIN_WEIGHT初始化时需要调用构造器,但构造器依赖MIN_WEIGHT.value
// ✅ 正确示例:构造器不包含复杂校验
public class Weight {
    private final BigDecimal value;
    private final MeasurementUnit unit;
    
    public Weight(BigDecimal value, MeasurementUnit unit) {
        this.value = value;
        this.unit = unit;
    }
    
    public static Weight ofKg(BigDecimal value) {
        if (value.compareTo(BigDecimal.ZERO) < 0) {
            throw new IllegalArgumentException("重量不能为负");
        }
        return new Weight(value, MeasurementUnit.KG);
    }
}

陷阱2:地址值对象的显示问题

// ✅ 正确处理直辖市地址
public class AddressVO {
    public String getDisplayAddress() {
        StringBuilder sb = new StringBuilder();
        sb.append(province);
        if (city != null && !city.equals(province)) {
            sb.append(city);  // 北京市不重复显示"北京市北京市"
        }
        sb.append(district);
        return sb.toString();
    }
}

三、基础设施层:技术实现的边界

3.1 PO类的审计字段自动化

问题场景:每次保存都需要手动设置审计字段

// ❌ 错误示例:手动设置审计字段
public Sku save(Sku sku) {
    SkuPO po = converter.toPO(sku);
    po.setCreatedAt(LocalDateTime.now());  // 容易遗漏
    po.setUpdatedAt(LocalDateTime.now());
    return jpaRepository.save(po);
}

规范方案:使用JPA审计注解

// ✅ 正确示例:PO类使用审计注解
@Entity
@EntityListeners(AuditingEntityListener.class)
public class SkuPO {
    
    @CreatedDate
    @Column(name = "created_at", updatable = false)
    private LocalDateTime createdAt;
    
    @LastModifiedDate
    @Column(name = "updated_at")
    private LocalDateTime updatedAt;
}

3.2 MapStruct转换器的最佳实践

问题场景:DO需要setter方法,破坏封装性

// ❌ 错误示例:暴露所有setter方法
@Data  // Lombok生成所有setter
public class Sku {
    private SkuCode skuCode;
    // ...
}

规范方案:使用全参构造器 + MapStruct配置

// ✅ 正确示例:领域模型使用全参构造器
public class Sku {
    private final SkuCode skuCode;
    private String name;
    
    private Sku(SkuCode skuCode, String name) {
        this.skuCode = skuCode;
        this.name = name;
    }
    
    // 仅提供必要的业务方法
    public void updateName(String name) {
        this.name = name;
    }
}

// MapStruct配置
@Mapper(componentModel = "spring")
public interface SkuConverter {
    
    @Mapping(target = "skuCode", ignore = true)
    @Mapping(target = "status", ignore = true)
    Sku toDO(SkuPO po);
    
    // 使用@ObjectFactory处理复杂转换
    @ObjectFactory
    default Sku createSku(SkuPO po) {
        return Sku.fromPO(
            SkuCode.of(po.getSkuCode()),
            po.getName(),
            po.getStatus() != null ? SkuStatus.fromCode(po.getStatus()) : null
            // ... 其他参数
        );
    }
}

3.3 值对象的扁平化映射

// PO类:扁平化存储值对象
@Entity
public class SkuPO {
    @Column(name = "safety_stock")
    private BigDecimal safetyStock;
    
    @Column(name = "safety_stock_unit")
    private String safetyStockUnit;
    
    @Column(name = "length")
    private BigDecimal length;
    
    @Column(name = "width")
    private BigDecimal width;
}

// DO类:使用值对象封装
public class Sku {
    private SafetyStockQuantity safetyStock;  // 值对象
    private Dimensions dimensions;            // 值对象
}

四、应用层:CQRS读写分离

4.1 为什么需要CQRS?

问题场景:每次查询都加载整个聚合

// ❌ 错误示例:查询加载整个聚合
public Customer getCustomer(Long id) {
    Customer customer = customerRepository.findById(id);
    // 加载了所有关联实体,性能低下
    return customer;
}

规范方案:读写分离架构

// ✅ 命令模型:使用领域模型
@Service
public class CustomerCommandService {
    
    @Transactional
    public Customer createCustomer(CreateCustomerCommand cmd) {
        Customer customer = Customer.create(
            CustomerCode.of(cmd.getCustomerCode()),
            cmd.getName(),
            CustomerType.fromCode(cmd.getType())
        );
        return customerRepository.save(customer);
    }
}

// ✅ 查询模型:使用DTO + 读仓储
@Service
public class CustomerQueryService {
    
    private final CustomerReadRepository readRepository;
    
    @Transactional(readOnly = true)
    public PageResponse<CustomerDTO> query(CustomerQuery query) {
        return readRepository.queryCustomers(
            query.getStatus(),
            query.getType(),
            query.getName(),
            PageRequest.of(query.getPageNum() - 1, query.getPageSize())
        );
    }
}

4.2 读仓储的设计

// 读仓储接口:支持分页、动态查询
public interface CustomerReadRepository {
    Optional<CustomerPO> findById(Long id);
    Optional<CustomerPO> findByCustomerCode(String customerCode);
    Page<CustomerPO> queryCustomers(String status, String type, String name, Pageable pageable);
    List<CustomerPO> findByStatus(String status);
    boolean existsByCustomerCode(String customerCode);
}

五、事件溯源与幂等性保证

5.1 领域事件的幂等设计

// ✅ 业务方法内置幂等性检查
public void activate() {
    if (this.status == WarehouseStatus.ACTIVE) {
        // 幂等:已经是激活状态,不产生新事件
        return;
    }
    
    this.status = WarehouseStatus.ACTIVE;
    registerEvent(new WarehouseActivatedEvent(this.warehouseCode));
}

5.2 Redis分布式幂等检查

@Component
public class RedisIdempotencyChecker {
    
    @Autowired
    private RedisTemplate<String, String> redis;
    
    public boolean isProcessed(String eventId, String consumer) {
        String key = "event:processed:" + eventId + ":" + consumer;
        Boolean success = redis.opsForValue()
            .setIfAbsent(key, "1", Duration.ofDays(7));
        return !Boolean.TRUE.equals(success);
    }
}

5.3 事件消费的标准流程

@Transactional
public void handleWarehouseActivatedEvent(WarehouseActivatedEvent event) {
    // 1. 幂等性检查
    if (idempotencyChecker.isProcessed(event.getEventId(), "inventory-service")) {
        return;
    }
    
    // 2. 版本号验证(防止乱序)
    WarehousePO warehouse = warehouseReadRepository.findByCode(event.getWarehouseCode());
    if (warehouse.getVersion() > event.getVersion()) {
        return;
    }
    
    // 3. 状态机验证(业务幂等)
    if (warehouse.getStatus() == WarehouseStatus.ACTIVE.getCode()) {
        return;
    }
    
    // 4. 业务处理
    inventoryService.initializeWarehouseInventory(event.getWarehouseCode());
    
    // 5. 记录事件处理
    eventStore.save(event);
}

六、单元测试的参数化实践

6.1 使用CSV文件管理测试数据

// ✅ 参数化测试示例
@ParameterizedTest
@CsvFileSource(resources = "/testdata/sku-creation-data.csv", numLinesToSkip = 1)
void testCreateSkuWithVariousInputs(String skuCode, String name, String category, 
                                     String unit, boolean shouldSucceed) {
    if (shouldSucceed) {
        assertDoesNotThrow(() -> Sku.create(
            SkuCode.of(skuCode), name, category, unit
        ));
    } else {
        assertThrows(IllegalArgumentException.class, () -> Sku.create(
            SkuCode.of(skuCode), name, category, unit
        ));
    }
}

6.2 测试数据复用

// ✅ 测试数据工厂
public class SkuTestDataFactory {
    
    public static Sku createDefaultSku() {
        return Sku.create(
            SkuCode.of("SKU001"),
            "测试商品",
            "电子产品",
            "台"
        );
    }
    
    public static Sku createSkuWithCode(String code) {
        Sku sku = createDefaultSku();
        // 使用反射或测试专用方法设置skuCode
        return sku;
    }
}

七、基础设施层的抽象设计

7.1 消息队列接口抽象

// ✅ 抽象MQ接口,支持多种实现
public interface MessageQueuePublisher {
    void publish(String topic, Object message);
    void publish(String topic, String key, Object message);
}

// Kafka实现
@Component
@ConditionalOnProperty(name = "mq.type", havingValue = "kafka")
public class KafkaPublisher implements MessageQueuePublisher {
    @Autowired
    private KafkaTemplate<String, String> kafkaTemplate;
    
    @Override
    public void publish(String topic, Object message) {
        kafkaTemplate.send(topic, toJson(message));
    }
}

// RabbitMQ实现
@Component
@ConditionalOnProperty(name = "mq.type", havingValue = "rabbitmq")
public class RabbitMQPublisher implements MessageQueuePublisher {
    @Autowired
    private RabbitTemplate rabbitTemplate;
    
    @Override
    public void publish(String topic, Object message) {
        rabbitTemplate.convertAndSend(topic, toJson(message));
    }
}

八、总结与最佳实践

8.1 核心原则

层次 核心职责 禁止事项
领域层 纯业务逻辑 技术ID、审计字段、数据库操作
基础设施层 技术实现 业务逻辑、直接暴露领域模型
应用层 用例编排 直接操作PO、跨聚合直接调用
接口层 外部接入 业务逻辑、数据转换逻辑

8.2 AI协作的关键点

  1. 规则先行:在与AI交互前,明确项目规范
  2. 上下文管理:保持规则文件的持续更新
  3. 代码审查:AI生成的代码必须经过人工审查
  4. 测试驱动:先写测试,再让AI生成实现

8.3 常见陷阱清单

  • 领域层包含技术字段(id、createdAt、updatedAt)
  • 值对象嵌套超过3层
  • 聚合根缺少领域事件发布
  • MapStruct转换破坏领域封装
  • 查询操作加载整个聚合
  • 领域事件缺少幂等性保证
  • 单元测试数据硬编码
  • 枚举类缺少displayName

结语

通过建立严格的project_rules.md规范,我们成功解决了AI生成代码的不稳定性问题。这套规范不仅约束了AI的行为,也为团队成员提供了统一的设计准则。

在实际项目中,我们发现:

  • 代码质量提升:遵循规范的代码更易维护
  • 开发效率提高:减少返工和重构
  • 团队协作改善:统一的规范降低沟通成本
  • AI协作顺畅:规则文件让AI理解项目上下文

DDD不是银弹,但在复杂业务场景下,它提供了清晰的架构边界。结合AI辅助编程,我们可以更快地交付高质量的软件系统。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐