cursor开发-电商平台开发供应商管理模块实战详解

🎯 深度剖析:智能CRUD生成器的实战详解

📋 场景设定

我们接到一个真实需求:为一个电商平台开发供应商管理模块

业务需求

  • 供应商基本信息管理
  • 资质文件上传管理
  • 合作状态跟踪
  • 绩效评估记录

🔍 第一步:原始需求转化

传统开发方式

  1. 分析需求 → 2. 设计数据库 → 3. 写Model → 4. 写Service → 5. 写Controller → 6. 写前端 → 7. 写测试 → 8. 部署...

预计时间: 2-3天

使用Cursor智能CRUD模板

预计时间: 30分钟


🚀 第二步:模板实战演示

模板原始内容

基于以下实体生成完整的CRUD系统:
实体:[实体名称]
属性:
- id: number (主键)
- name: string (必填)
- [其他属性]

需要生成:
1. 数据库迁移脚本
2. 实体类/模型
3. Service层(业务逻辑)
4. Controller层(API接口)
5. DTO/验证器
6. 前端组件(列表/表单/详情)
7. 对应的API测试
技术栈:[后端框架] + [前端框架]

🎯 第三步:具体化模板参数

修改后的精准Prompt

基于以下实体生成完整的CRUD系统:

实体:Supplier(供应商)
属性:
- id: string (UUID主键)
- name: string (供应商名称,必填,唯一)
- code: string (供应商编码,唯一)
- type: enum('manufacturer', 'distributor', 'wholesaler')
- contactPerson: string (联系人)
- phone: string (联系电话,验证格式)
- email: string (邮箱,验证格式)
- address: object (地址对象,包含省市区详情)
- status: enum('active', 'inactive', 'pending', 'blacklisted')
- creditRating: number (信用评级 1-5)
- registeredAt: Date (注册时间)
- updatedAt: Date (更新时间)
- documents: array (资质文件列表,支持多文件)

关联实体:
- 供应商资质文件 (SupplierDocument)
- 供应商联系人 (SupplierContact)
- 采购订单 (PurchaseOrder)

需要生成:
1. 数据库迁移脚本 (MySQL 8.0)
2. Sequelize模型 (Node.js + TypeScript)
3. Service层 (包含业务逻辑和验证)
4. Controller层 (RESTful API,包含分页、过滤、排序)
5. DTO/验证器 (使用class-validator)
6. 前端组件 (React + TypeScript + Ant Design)
   - 供应商列表页 (带搜索、筛选、批量操作)
   - 供应商详情页
   - 供应商表单 (创建/编辑)
   - 资质文件上传组件
7. 对应的API测试 (Jest + Supertest)
8. Swagger文档注解

技术栈:
- 后端:Node.js + Express + Sequelize + TypeScript
- 前端:React 18 + TypeScript + Ant Design 5
- 数据库:MySQL 8.0

要求:
- 遵循RESTful最佳实践
- 完整的错误处理
- 输入验证和清理
- 日志记录
- 分页性能优化
- 文件上传到AWS S3
- 操作审计日志

🧠 第四步:Cursor的处理过程解析

阶段1:数据库设计(2分钟)

Cursor会生成:

1.1 迁移脚本

-- suppliers表
CREATE TABLE suppliers (
  id VARCHAR(36) PRIMARY KEY DEFAULT (UUID()),
  name VARCHAR(255) NOT NULL UNIQUE,
  code VARCHAR(50) NOT NULL UNIQUE,
  type ENUM('manufacturer', 'distributor', 'wholesaler') NOT NULL,
  contact_person VARCHAR(100),
  phone VARCHAR(20),
  email VARCHAR(255),
  address JSON,
  status ENUM('active', 'inactive', 'pending', 'blacklisted') DEFAULT 'pending',
  credit_rating TINYINT CHECK (credit_rating BETWEEN 1 AND 5),
  registered_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  deleted_at TIMESTAMP NULL,
  INDEX idx_status (status),
  INDEX idx_type (type),
  INDEX idx_credit_rating (credit_rating)
);

-- supplier_documents表(关联)
CREATE TABLE supplier_documents (
  id VARCHAR(36) PRIMARY KEY DEFAULT (UUID()),
  supplier_id VARCHAR(36) NOT NULL,
  document_type ENUM('business_license', 'tax_certificate', 'quality_certificate') NOT NULL,
  file_url VARCHAR(500) NOT NULL,
  verified BOOLEAN DEFAULT FALSE,
  verified_at TIMESTAMP NULL,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  FOREIGN KEY (supplier_id) REFERENCES suppliers(id) ON DELETE CASCADE,
  INDEX idx_supplier_id (supplier_id),
  INDEX idx_document_type (document_type)
);

1.2 Sequelize模型

// src/models/Supplier.ts
import { Model, DataTypes, Optional } from 'sequelize';
import sequelize from '../config/database';

interface SupplierAttributes {
  id: string;
  name: string;
  code: string;
  type: 'manufacturer' | 'distributor' | 'wholesaler';
  contactPerson: string;
  phone: string;
  email: string;
  address: {
    province: string;
    city: string;
    district: string;
    detail: string;
    postalCode?: string;
  };
  status: 'active' | 'inactive' | 'pending' | 'blacklisted';
  creditRating: number;
  registeredAt: Date;
  updatedAt: Date;
}

interface SupplierCreationAttributes 
  extends Optional<SupplierAttributes, 'id' | 'status' | 'creditRating' | 'registeredAt'> {}

class Supplier extends Model<SupplierAttributes, SupplierCreationAttributes> 
  implements SupplierAttributes {
  
  public id!: string;
  public name!: string;
  // ... 所有属性定义
  
  // 关联定义
  public readonly documents?: SupplierDocument[];
  
  static associate(models: any) {
    Supplier.hasMany(models.SupplierDocument, {
      foreignKey: 'supplierId',
      as: 'documents'
    });
  }
}

Supplier.init({
  // 详细的字段定义
}, {
  sequelize,
  modelName: 'Supplier',
  tableName: 'suppliers',
  timestamps: true,
  paranoid: true, // 软删除
});

阶段2:业务逻辑层(3分钟)

2.1 DTO和验证器

// src/dto/create-supplier.dto.ts
import { 
  IsString, IsEmail, IsPhoneNumber, IsEnum, 
  IsNumber, Min, Max, IsObject, ValidateNested,
  IsOptional, IsArray, Length 
} from 'class-validator';
import { Type } from 'class-transformer';

class AddressDto {
  @IsString()
  province: string;
  
  @IsString()
  city: string;
  
  @IsString()
  district: string;
  
  @IsString()
  detail: string;
  
  @IsOptional()
  @IsString()
  @Length(6, 6)
  postalCode?: string;
}

export class CreateSupplierDto {
  @IsString()
  @Length(2, 255)
  name: string;
  
  @IsString()
  @Length(3, 50)
  code: string;
  
  @IsEnum(['manufacturer', 'distributor', 'wholesaler'])
  type: string;
  
  @IsOptional()
  @IsString()
  @Length(2, 100)
  contactPerson?: string;
  
  @IsOptional()
  @IsPhoneNumber('CN')
  phone?: string;
  
  @IsOptional()
  @IsEmail()
  email?: string;
  
  @IsOptional()
  @IsObject()
  @ValidateNested()
  @Type(() => AddressDto)
  address?: AddressDto;
  
  @IsOptional()
  @IsNumber()
  @Min(1)
  @Max(5)
  creditRating?: number;
  
  @IsOptional()
  @IsArray()
  documentIds?: string[];
}

2.2 Service层核心逻辑

// src/services/supplier.service.ts
import { Injectable, NotFoundException, ConflictException } from '@nestjs/common';
import { InjectModel } from '@nestjs/sequelize';
import { Supplier } from '../models/Supplier';
import { CreateSupplierDto, UpdateSupplierDto } from '../dto';
import { Op } from 'sequelize';
import { DocumentService } from './document.service';

@Injectable()
export class SupplierService {
  constructor(
    @InjectModel(Supplier)
    private supplierModel: typeof Supplier,
    private documentService: DocumentService
  ) {}

  async create(createDto: CreateSupplierDto, userId: string): Promise<Supplier> {
    // 检查唯一性
    const existing = await this.supplierModel.findOne({
      where: {
        [Op.or]: [
          { name: createDto.name },
          { code: createDto.code }
        ]
      }
    });
    
    if (existing) {
      throw new ConflictException(
        existing.name === createDto.name 
          ? '供应商名称已存在' 
          : '供应商编码已存在'
      );
    }
    
    // 创建供应商
    const supplier = await this.supplierModel.create({
      ...createDto,
      createdBy: userId,
      status: 'pending'
    });
    
    // 关联文档
    if (createDto.documentIds?.length) {
      await this.documentService.linkToSupplier(
        createDto.documentIds, 
        supplier.id
      );
    }
    
    return this.findById(supplier.id);
  }

  async findAll(params: {
    page?: number;
    limit?: number;
    search?: string;
    status?: string;
    type?: string;
    minRating?: number;
    sortBy?: string;
    sortOrder?: 'ASC' | 'DESC';
  }): Promise<{ data: Supplier[]; total: number; page: number }> {
    const {
      page = 1,
      limit = 20,
      search,
      status,
      type,
      minRating,
      sortBy = 'createdAt',
      sortOrder = 'DESC'
    } = params;
    
    const offset = (page - 1) * limit;
    const where: any = {};
    
    // 构建查询条件
    if (search) {
      where[Op.or] = [
        { name: { [Op.like]: `%${search}%` } },
        { code: { [Op.like]: `%${search}%` } },
        { contactPerson: { [Op.like]: `%${search}%` } }
      ];
    }
    
    if (status) where.status = status;
    if (type) where.type = type;
    if (minRating) where.creditRating = { [Op.gte]: minRating };
    
    // 性能优化:分开查询计数和数据
    const { count, rows } = await this.supplierModel.findAndCountAll({
      where,
      limit,
      offset,
      order: [[sortBy, sortOrder]],
      include: [
        {
          association: 'documents',
          required: false,
          where: { verified: true },
          limit: 3 // 只加载前3个已验证文档
        }
      ],
      distinct: true // 正确计数
    });
    
    return {
      data: rows,
      total: count,
      page: Math.ceil(count / limit)
    };
  }
  
  // ... 其他方法:findById, update, delete, changeStatus, etc.
}

阶段3:API层(2分钟)

3.1 RESTful Controller

// src/controllers/supplier.controller.ts
import {
  Controller,
  Get,
  Post,
  Put,
  Delete,
  Body,
  Param,
  Query,
  UseGuards,
  UseInterceptors,
  UploadedFiles,
  ParseIntPipe,
  DefaultValuePipe
} from '@nestjs/common';
import { FileFieldsInterceptor } from '@nestjs/platform-express';
import { ApiTags, ApiOperation, ApiResponse, ApiConsumes } from '@nestjs/swagger';
import { SupplierService } from '../services/supplier.service';
import { CreateSupplierDto, UpdateSupplierDto } from '../dto';
import { JwtAuthGuard } from '../guards/jwt-auth.guard';
import { RolesGuard } from '../guards/roles.guard';
import { Roles } from '../decorators/roles.decorator';
import { SupplierResponseDto } from '../dto/supplier-response.dto';

@ApiTags('suppliers')
@Controller('api/v1/suppliers')
@UseGuards(JwtAuthGuard, RolesGuard)
export class SupplierController {
  constructor(private readonly supplierService: SupplierService) {}

  @Post()
  @Roles('admin', 'purchaser')
  @ApiOperation({ summary: '创建新供应商' })
  @ApiResponse({ status: 201, type: SupplierResponseDto })
  @ApiConsumes('multipart/form-data')
  @UseInterceptors(FileFieldsInterceptor([
    { name: 'businessLicense', maxCount: 1 },
    { name: 'otherDocuments', maxCount: 10 }
  ]))
  async create(
    @Body() createDto: CreateSupplierDto,
    @UploadedFiles() files: any,
    @Request() req
  ) {
    // 处理文件上传
    const documentIds = await this.uploadDocuments(files);
    createDto.documentIds = documentIds;
    
    return this.supplierService.create(createDto, req.user.id);
  }

  @Get()
  @ApiOperation({ summary: '获取供应商列表(分页)' })
  @ApiResponse({ status: 200, type: [SupplierResponseDto] })
  async findAll(
    @Query('page', new DefaultValuePipe(1), ParseIntPipe) page: number,
    @Query('limit', new DefaultValuePipe(20), ParseIntPipe) limit: number,
    @Query('search') search?: string,
    @Query('status') status?: string,
    @Query('type') type?: string,
    @Query('minRating', new DefaultValuePipe(1), ParseIntPipe) minRating?: number,
    @Query('sortBy') sortBy?: string,
    @Query('sortOrder') sortOrder?: 'ASC' | 'DESC'
  ) {
    return this.supplierService.findAll({
      page,
      limit,
      search,
      status,
      type,
      minRating,
      sortBy,
      sortOrder
    });
  }

  @Get(':id')
  @ApiOperation({ summary: '获取供应商详情' })
  @ApiResponse({ status: 200, type: SupplierResponseDto })
  async findOne(@Param('id') id: string) {
    const supplier = await this.supplierService.findById(id);
    if (!supplier) {
      throw new NotFoundException(`供应商 ${id} 不存在`);
    }
    return supplier;
  }

  // ... PUT, DELETE, PATCH endpoints
}

阶段4:前端组件(5分钟)

4.1 React供应商列表页

// src/pages/supplier/SupplierList.tsx
import React, { useState, useEffect } from 'react';
import {
  Table,
  Button,
  Space,
  Input,
  Select,
  Card,
  Tag,
  Modal,
  Form,
  message,
  Row,
  Col,
  Statistic
} from 'antd';
import {
  SearchOutlined,
  PlusOutlined,
  EditOutlined,
  EyeOutlined,
  DeleteOutlined,
  FilterOutlined
} from '@ant-design/icons';
import { useRequest, useToggle } from 'ahooks';
import SupplierForm from './components/SupplierForm';
import SupplierService from '@/services/supplierService';
import type { Supplier, SupplierListParams } from '@/types/supplier';

const { Option } = Select;

const SupplierList: React.FC = () => {
  const [form] = Form.useForm();
  const [selectedRows, setSelectedRows] = useState<Supplier[]>([]);
  const [params, setParams] = useState<SupplierListParams>({
    page: 1,
    limit: 20,
    sortBy: 'createdAt',
    sortOrder: 'DESC'
  });
  
  const [formVisible, { toggle: toggleForm }] = useToggle(false);
  const [detailVisible, { toggle: toggleDetail }] = useToggle(false);
  const [selectedSupplier, setSelectedSupplier] = useState<Supplier | null>(null);

  // 使用ahooks的useRequest进行数据请求
  const { data, loading, run: fetchSuppliers } = useRequest(
    () => SupplierService.getList(params),
    {
      refreshDeps: [params],
      onError: (error) => {
        message.error(`加载失败: ${error.message}`);
      }
    }
  );

  // 列定义
  const columns = [
    {
      title: '供应商编码',
      dataIndex: 'code',
      key: 'code',
      sorter: true,
      width: 120
    },
    {
      title: '供应商名称',
      dataIndex: 'name',
      key: 'name',
      render: (text: string, record: Supplier) => (
        <a onClick={() => showDetail(record)}>{text}</a>
      )
    },
    {
      title: '类型',
      dataIndex: 'type',
      key: 'type',
      render: (type: string) => {
        const typeMap: Record<string, { text: string; color: string }> = {
          manufacturer: { text: '生产商', color: 'blue' },
          distributor: { text: '经销商', color: 'green' },
          wholesaler: { text: '批发商', color: 'orange' }
        };
        const info = typeMap[type] || { text: type, color: 'default' };
        return <Tag color={info.color}>{info.text}</Tag>;
      }
    },
    {
      title: '状态',
      dataIndex: 'status',
      key: 'status',
      render: (status: string) => {
        const statusMap: Record<string, { text: string; color: string }> = {
          active: { text: '活跃', color: 'success' },
          inactive: { text: '停用', color: 'default' },
          pending: { text: '待审核', color: 'warning' },
          blacklisted: { text: '黑名单', color: 'error' }
        };
        const info = statusMap[status] || { text: status, color: 'default' };
        return <Tag color={info.color}>{info.text}</Tag>;
      }
    },
    {
      title: '信用评级',
      dataIndex: 'creditRating',
      key: 'creditRating',
      render: (rating: number) => (
        <span style={{ color: rating >= 4 ? '#52c41a' : rating >= 3 ? '#faad14' : '#f5222d' }}>
          {'★'.repeat(rating)}{'☆'.repeat(5 - rating)}
        </span>
      )
    },
    {
      title: '操作',
      key: 'action',
      width: 180,
      render: (_: any, record: Supplier) => (
        <Space size="small">
          <Button
            type="text"
            icon={<EyeOutlined />}
            onClick={() => showDetail(record)}
          />
          <Button
            type="text"
            icon={<EditOutlined />}
            onClick={() => handleEdit(record)}
          />
          <Button
            type="text"
            danger
            icon={<DeleteOutlined />}
            onClick={() => handleDelete(record.id)}
            disabled={record.status === 'active'}
          />
        </Space>
      )
    }
  ];

  // 处理表格变化
  const handleTableChange = (pagination: any, filters: any, sorter: any) => {
    setParams(prev => ({
      ...prev,
      page: pagination.current,
      sortBy: sorter.field || 'createdAt',
      sortOrder: sorter.order === 'ascend' ? 'ASC' : 'DESC'
    }));
  };

  // 搜索处理
  const handleSearch = (values: any) => {
    setParams(prev => ({
      ...prev,
      page: 1,
      ...values
    }));
  };

  // 显示详情
  const showDetail = (supplier: Supplier) => {
    setSelectedSupplier(supplier);
    toggleDetail();
  };

  // 批量操作
  const handleBatchActivate = async () => {
    if (selectedRows.length === 0) return;
    
    Modal.confirm({
      title: '确认激活选中的供应商?',
      content: `将激活 ${selectedRows.length} 个供应商`,
      onOk: async () => {
        try {
          await SupplierService.batchUpdateStatus(
            selectedRows.map(r => r.id),
            'active'
          );
          message.success('激活成功');
          fetchSuppliers();
          setSelectedRows([]);
        } catch (error) {
          message.error('激活失败');
        }
      }
    });
  };

  return (
    <div className="supplier-list-page">
      <Card 
        title={
          <Space>
            <span>供应商管理</span>
            <Tag color="blue">总数: {data?.total || 0}</Tag>
          </Space>
        }
        extra={
          <Space>
            <Button
              type="primary"
              icon={<PlusOutlined />}
              onClick={toggleForm}
            >
              新建供应商
            </Button>
          </Space>
        }
      >
        {/* 搜索和筛选区域 */}
        <Card size="small" style={{ marginBottom: 16 }}>
          <Form
            form={form}
            layout="inline"
            onFinish={handleSearch}
          >
            <Row gutter={[16, 16]} style={{ width: '100%' }}>
              <Col xs={24} sm={12} md={8} lg={6}>
                <Form.Item name="search">
                  <Input
                    placeholder="搜索名称/编码/联系人"
                    prefix={<SearchOutlined />}
                    allowClear
                  />
                </Form.Item>
              </Col>
              <Col xs={24} sm={12} md={8} lg={6}>
                <Form.Item name="status">
                  <Select placeholder="状态筛选" allowClear>
                    <Option value="active">活跃</Option>
                    <Option value="inactive">停用</Option>
                    <Option value="pending">待审核</Option>
                    <Option value="blacklisted">黑名单</Option>
                  </Select>
                </Form.Item>
              </Col>
              <Col xs={24} sm={12} md={8} lg={6}>
                <Form.Item name="type">
                  <Select placeholder="类型筛选" allowClear>
                    <Option value="manufacturer">生产商</Option>
                    <Option value="distributor">经销商</Option>
                    <Option value="wholesaler">批发商</Option>
                  </Select>
                </Form.Item>
              </Col>
              <Col xs={24} sm={12} md={8} lg={6}>
                <Space>
                  <Button
                    type="primary"
                    htmlType="submit"
                    icon={<FilterOutlined />}
                  >
                    筛选
                  </Button>
                  <Button
                    onClick={() => {
                      form.resetFields();
                      setParams({
                        page: 1,
                        limit: 20,
                        sortBy: 'createdAt',
                        sortOrder: 'DESC'
                      });
                    }}
                  >
                    重置
                  </Button>
                </Space>
              </Col>
            </Row>
          </Form>
        </Card>

        {/* 批量操作栏 */}
        {selectedRows.length > 0 && (
          <Card size="small" style={{ marginBottom: 16, background: '#f6ffed' }}>
            <Space>
              <span>已选择 {selectedRows.length} 项</span>
              <Button onClick={() => setSelectedRows([])}>取消选择</Button>
              <Button onClick={handleBatchActivate}>批量激活</Button>
              <Button danger>批量停用</Button>
            </Space>
          </Card>
        )}

        {/* 统计卡片 */}
        <Row gutter={[16, 16]} style={{ marginBottom: 16 }}>
          <Col xs={24} sm={12} md={6}>
            <Card size="small">
              <Statistic
                title="活跃供应商"
                value={data?.stats?.active || 0}
                valueStyle={{ color: '#52c41a' }}
              />
            </Card>
          </Col>
          <Col xs={24} sm={12} md={6}>
            <Card size="small">
              <Statistic
                title="待审核"
                value={data?.stats?.pending || 0}
                valueStyle={{ color: '#fa8c16' }}
              />
            </Card>
          </Col>
          <Col xs={24} sm={12} md={6}>
            <Card size="small">
              <Statistic
                title="平均信用评级"
                value={data?.stats?.avgRating || 0}
                precision={1}
              />
            </Card>
          </Col>
        </Row>

        {/* 数据表格 */}
        <Table
          rowKey="id"
          columns={columns}
          dataSource={data?.data || []}
          loading={loading}
          pagination={{
            current: params.page,
            pageSize: params.limit,
            total: data?.total || 0,
            showSizeChanger: true,
            showQuickJumper: true,
            showTotal: (total) => `共 ${total} 条记录`
          }}
          rowSelection={{
            selectedRowKeys: selectedRows.map(r => r.id),
            onChange: (_, rows) => setSelectedRows(rows)
          }}
          onChange={handleTableChange}
          scroll={{ x: 1200 }}
        />
      </Card>

      {/* 表单模态框 */}
      <Modal
        title={selectedSupplier ? '编辑供应商' : '新建供应商'}
        open={formVisible}
        onCancel={toggleForm}
        width={800}
        footer={null}
        destroyOnClose
      >
        <SupplierForm
          initialValues={selectedSupplier}
          onSuccess={() => {
            toggleForm();
            fetchSuppliers();
            setSelectedSupplier(null);
          }}
        />
      </Modal>

      {/* 详情模态框 */}
      <Modal
        title="供应商详情"
        open={detailVisible}
        onCancel={toggleDetail}
        width={700}
        footer={null}
      >
        {/* 详情组件 */}
      </Modal>
    </div>
  );
};

export default SupplierList;

阶段5:测试用例(3分钟)

// tests/supplier.service.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { getModelToken } from '@nestjs/sequelize';
import { SupplierService } from '../src/services/supplier.service';
import { Supplier } from '../src/models/Supplier';
import { ConflictException, NotFoundException } from '@nestjs/common';

describe('SupplierService', () => {
  let service: SupplierService;
  let mockSupplierModel: any;

  beforeEach(async () => {
    mockSupplierModel = {
      findOne: jest.fn(),
      create: jest.fn(),
      findAndCountAll: jest.fn(),
      findByPk: jest.fn(),
      update: jest.fn(),
      destroy: jest.fn()
    };

    const module: TestingModule = await Test.createTestingModule({
      providers: [
        SupplierService,
        {
          provide: getModelToken(Supplier),
          useValue: mockSupplierModel
        }
      ]
    }).compile();

    service = module.get<SupplierService>(SupplierService);
  });

  describe('create', () => {
    it('应该成功创建供应商', async () => {
      const createDto = {
        name: '测试供应商',
        code: 'TEST001',
        type: 'manufacturer',
        contactPerson: '张三',
        phone: '13800138000',
        email: 'test@example.com'
      };

      mockSupplierModel.findOne.mockResolvedValue(null);
      mockSupplierModel.create.mockResolvedValue({
        id: 'uuid-123',
        ...createDto,
        status: 'pending'
      });

      const result = await service.create(createDto, 'user-123');
      
      expect(result).toHaveProperty('id', 'uuid-123');
      expect(result.name).toBe('测试供应商');
      expect(mockSupplierModel.create).toHaveBeenCalledWith(
        expect.objectContaining({
          name: '测试供应商',
          createdBy: 'user-123'
        })
      );
    });

    it('名称重复时应该抛出冲突异常', async () => {
      const createDto = {
        name: '重复名称',
        code: 'TEST001',
        type: 'manufacturer'
      };

      mockSupplierModel.findOne.mockResolvedValue({
        name: '重复名称',
        code: 'OTHER001'
      });

      await expect(service.create(createDto, 'user-123'))
        .rejects
        .toThrow(ConflictException);
    });
  });

  describe('findAll', () => {
    it('应该返回分页数据', async () => {
      const mockData = [
        { id: '1', name: '供应商1', status: 'active' },
        { id: '2', name: '供应商2', status: 'active' }
      ];

      mockSupplierModel.findAndCountAll.mockResolvedValue({
        count: 2,
        rows: mockData
      });

      const result = await service.findAll({
        page: 1,
        limit: 20
      });

      expect(result.data).toHaveLength(2);
      expect(result.total).toBe(2);
      expect(result.page).toBe(1);
    });

    it('应该支持搜索功能', async () => {
      mockSupplierModel.findAndCountAll.mockResolvedValue({
        count: 1,
        rows: [{ id: '1', name: '匹配的供应商' }]
      });

      await service.findAll({
        search: '匹配'
      });

      expect(mockSupplierModel.findAndCountAll).toHaveBeenCalledWith(
        expect.objectContaining({
          where: expect.objectContaining({
            [Symbol.for('or')]: expect.arrayContaining([
              expect.objectContaining({
                name: expect.objectContaining({
                  [Symbol.for('like')]: '%匹配%'
                })
              })
            ])
          })
        })
      );
    });
  });
});

📊 第六步:生成结果总结

生成文件清单

📁 generated/
├── database/
│   ├── migrations/
│   │   ├── 20240101_create_suppliers_table.sql
│   │   └── 20240101_create_supplier_documents_table.sql
│   └── seeders/
│       └── 20240101_seed_suppliers.sql
├── src/
│   ├── models/
│   │   ├── Supplier.ts
│   │   ├── SupplierDocument.ts
│   │   └── index.ts
│   ├── dto/
│   │   ├── create-supplier.dto.ts
│   │   ├── update-supplier.dto.ts
│   │   └── supplier-response.dto.ts
│   ├── services/
│   │   ├── supplier.service.ts
│   │   └── document.service.ts
│   ├── controllers/
│   │   └── supplier.controller.ts
│   ├── validators/
│   │   └── supplier.validator.ts
│   └── types/
│       └── supplier.ts
├── frontend/
│   ├── pages/
│   │   └── supplier/
│   │       ├── SupplierList.tsx
│   │       ├── SupplierDetail.tsx
│   │       └── components/
│   │           ├── SupplierForm.tsx
│   │           ├── DocumentUpload.tsx
│   │           └── StatusTag.tsx
│   └── services/
│       └── supplierService.ts
├── tests/
│   ├── supplier.service.spec.ts
│   ├── supplier.controller.spec.ts
│   └── e2e/
│       └── suppliers.e2e-spec.ts
├── docs/
│   └── api/
│       └── suppliers.swagger.yaml
└── docker/
    ├── Dockerfile
    └── docker-compose.yml

🎯 第七步:模板使用技巧深度分析

技巧1:分层递进描述

# 第一层:核心需求
实体定义 → 属性列表 → 关联关系

# 第二层:技术栈指定
框架版本 → 数据库类型 → 前端库

# 第三层:质量要求
错误处理 → 性能优化 → 安全考虑

# 第四层:额外功能
文件上传 → 审计日志 → 监控指标

技巧2:约束条件明确化

# 不好的描述
"做一个供应商管理系统"

# 好的描述
"创建供应商CRUD,要求:
1. 支持MySQL软删除
2. 文件上传到S3
3. 操作记录审计日志
4. 接口限流100次/分钟
5. 前端使用Ant Design表格"

技巧3:利用现有代码上下文

# 先引用现有项目结构
@package.json  # 了解技术栈
@src/models/User.ts  # 参考现有模型风格
@README.md  # 了解项目规范

# 再生成新代码
基于现有项目规范,创建Supplier实体...

📈 效率对比分析

任务 手动开发 Cursor生成 效率提升
数据库设计 1小时 2分钟 30倍
模型定义 45分钟 1分钟 45倍
Service层 2小时 3分钟 40倍
Controller层 1.5小时 2分钟 45倍
前端组件 4小时 5分钟 48倍
测试用例 2小时 3分钟 40倍
总计 11.25小时 16分钟 42倍

⚠️ 注意事项

需要人工检查的要点

  1. 业务逻辑正确性:生成的业务规则是否符合实际需求
  2. 安全漏洞:检查SQL注入、XSS等安全问题
  3. 性能优化:生成的查询是否有效率问题
  4. 架构一致性:是否符合项目整体架构
  5. 边界条件:异常处理是否完整

建议工作流

使用Cursor生成 → 代码审查 → 单元测试 → 
集成测试 → 性能测试 → 部署上线

🚀 进阶用法:模板定制化

创建自己的模板变体

# 我的快速CRUD模板(简版)
生成[实体名]的CRUD:
- 5个核心API
- 基础验证
- 分页查询
- 使用现有项目的代码风格
- 不包含前端界面

# 我的企业级CRUD模板(完整版)
生成[实体名]的企业级CRUD:
- 完整的四层架构
- 审计日志
- 数据权限控制
- 导入导出功能
- 前端管理界面
- 性能监控
- 自动化测试

💡 实战建议

  1. 从简单开始:先用简单模板,逐步增加复杂度
  2. 建立模板库:分类保存常用模板
  3. 持续优化:根据生成结果反馈调整模板
  4. 团队共享:建立团队模板规范
  5. 结合文档:为生成的代码添加详细注释

🎁 模板优化挑战

尝试改进这个模板:

当前模板有什么可以优化的地方?
如何让它更适合大型企业项目?
需要添加哪些安全考虑?

你的思考:

  1. 模板哪些地方可以更详细?
  2. 哪些地方可以更灵活?
  3. 如何支持更多技术栈选择?

posted @ 2025-12-18 14:52  XiaoZhengTou  阅读(2)  评论(0)    收藏  举报