# 员工职位类型 (EmployeeType) 重构文档

本文档描述了 `Employee` 模型的职位管理重构，从字符串枚举改为关联外键表 `EmployeeType`。

## 变更概述

### 1. 新增模型：`EmployeeType`

创建了新的 `EmployeeType` 模型来管理员工职位类型，替代原有的硬编码枚举 `EmployeeTypeEnum`。

**模型定义**：

```python
class EmployeeType(ModelBase):
    """员工职位类型"""
    id = models.BigAutoField(primary_key=True)
    merchant = models.ForeignKey('Merchant', on_delete=models.PROTECT, related_name='employee_types', verbose_name='所属商户')
    title = models.CharField(max_length=50, verbose_name='职位名称')
    description = models.TextField(blank=True, null=True, verbose_name='职位描述')
    reverse = models.CharField(max_length=100, blank=True, null=True, verbose_name='预留字段')

    def __str__(self):
        return self.title

    class Meta:
        verbose_name = '员工职位类型'
        verbose_name_plural = '员工职位类型'
        unique_together = ('merchant', 'title')
```

**字段说明**：
- `merchant`: 所属商户（外键）
- `title`: 职位名称
- `description`: 职位描述（可选）
- `reverse`: 预留字段（可选）

**约束**：
- `unique_together`: 同一商户下职位名称唯一

### 2. 修改模型：`Employee`

#### 字段变更

- **删除**: `job_type` 字段（`CharField`）
- **新增**: `position` 字段（`ForeignKey` 指向 `EmployeeType`）

#### 向后兼容

为保持 API 兼容性，`job_type` 现在是一个**只读属性**（`@property`），返回关联的 `EmployeeType.title`：

```python
@property
def job_type(self) -> str:
    """
    返回职位名称（向后兼容）
    
    如果有关联的职位类型，返回其 title，否则返回空字符串
    """
    if self.position:
        return self.position.title
    return ''
```

**模型定义**：

```python
class Employee(ModelBase):
    # ... 其他字段 ...
    position = models.ForeignKey(
        'EmployeeType',
        on_delete=models.PROTECT,
        related_name='employees',
        null=True,
        blank=True,
        verbose_name='职位',
    )
    # ...
```

## 数据库迁移

**迁移文件**: `basic_info/migrations/0011_remove_employee_job_type_employeetype_and_more.py`

**操作步骤**：
1. 创建 `EmployeeType` 模型
2. 删除 `Employee.job_type` 字段
3. 添加 `Employee.position` 字段

**注意**：
- 旧的 `job_type` 数据会在迁移时丢失
- 需要手动创建新的 `EmployeeType` 记录并关联到员工

## API 变更

### 1. 新增接口：`/api/backend/employee-types/`

管理员工职位类型的完整 CRUD 接口。

#### 创建职位类型

**端点**: `POST /api/backend/employee-types/`

**请求体**:
```json
{
  "title": "打纸工",
  "description": "负责打纸工作"
}
```

**响应** (201 Created):
```json
{
  "id": 1,
  "merchant": 1,
  "title": "打纸工",
  "description": "负责打纸工作",
  "reverse": null,
  "created_at": "2025-11-20T10:00:00Z",
  "updated_at": "2025-11-20T10:00:00Z"
}
```

#### 列出职位类型

**端点**: `GET /api/backend/employee-types/`

**响应** (200 OK):
```json
{
  "count": 2,
  "next": null,
  "previous": null,
  "results": [
    {
      "id": 1,
      "merchant": 1,
      "title": "打纸工",
      "description": "负责打纸工作",
      "reverse": null
    },
    {
      "id": 2,
      "merchant": 1,
      "title": "滚筒工",
      "description": "操作滚筒设备",
      "reverse": null
    }
  ]
}
```

#### 更新职位类型

**端点**: `PATCH /api/backend/employee-types/{id}/`

**请求体**:
```json
{
  "description": "更新后的描述"
}
```

#### 删除职位类型

**端点**: `DELETE /api/backend/employee-types/{id}/`

**响应**: 204 No Content

**注意**: 如果有员工关联到该职位类型，删除会失败（`PROTECT` 约束）。

### 2. 修改接口：`/api/backend/employees/`

#### 字段变更

- **新增**: `position` (integer, 外键 ID, 可选)
- **保留**: `job_type` (string, 只读, 向后兼容)

#### 创建员工（带职位）

**端点**: `POST /api/backend/employees/`

**请求体**:
```json
{
  "name": "张三",
  "position": 1,
  "mobile": "13800138000"
}
```

**响应** (201 Created):
```json
{
  "id": 1,
  "merchant": 1,
  "name": "张三",
  "position": 1,
  "job_type": "打纸工",
  "mobile": "13800138000",
  "status": "在职"
}
```

#### 创建员工（不指定职位）

**请求体**:
```json
{
  "name": "李四",
  "mobile": "13900139000"
}
```

**响应**:
```json
{
  "id": 2,
  "merchant": 1,
  "name": "李四",
  "position": null,
  "job_type": "",
  "mobile": "13900139000",
  "status": "在职"
}
```

#### 更新员工职位

**端点**: `PATCH /api/backend/employees/{id}/`

**请求体**:
```json
{
  "position": 2
}
```

**响应** (200 OK):
```json
{
  "id": 1,
  "name": "张三",
  "position": 2,
  "job_type": "滚筒工",
  ...
}
```

#### `job_type` 字段说明

- **只读**: 不能通过 API 直接设置
- **自动计算**: 返回 `position.title` 或空字符串
- **向后兼容**: 保持旧接口的兼容性

## Admin 界面变更

### 1. 新增：`EmployeeTypeAdmin`

管理员工职位类型的 Admin 界面。

**列表显示**: `title`, `description`
**搜索字段**: `title`, `description`

### 2. 修改：`EmployeeAdmin`

#### 列表显示

- **移除**: `job_type` 字段
- **新增**: `position_display` (显示职位名称)

#### 筛选器

- **移除**: `job_type` 筛选器
- **保留**: `status` 筛选器

## 测试

### 模型测试

**文件**: `basic_info/tests.py`

**测试用例**:
- `test_create_employee_type`: 创建职位类型
- `test_employee_type_unique_together`: 唯一性约束
- `test_employee_with_position`: 员工关联职位
- `test_employee_without_position`: 员工无职位
- `test_employee_type_reverse_field`: 预留字段
- `test_multiple_employees_same_position`: 多个员工同一职位

### API 测试

**文件**: `api_man/tests.py`

**职位类型 API 测试**:
- `test_create_employee_type`: 创建职位类型
- `test_list_employee_types`: 列出职位类型
- `test_update_employee_type`: 更新职位类型
- `test_delete_employee_type`: 删除职位类型

**员工 API 测试**:
- `test_create_employee_with_position`: 创建带职位的员工
- `test_create_employee_without_position`: 创建不带职位的员工
- `test_list_employees_includes_job_type`: 列表包含 `job_type`
- `test_update_employee_position`: 更新员工职位
- `test_job_type_is_read_only`: `job_type` 只读验证

**运行测试**:
```bash
# 模型测试
uv run python manage.py test basic_info.tests.EmployeeTypeTestCase

# API 测试
uv run python manage.py test api_man.tests.EmployeeTypeAPITestCase api_man.tests.EmployeeAPITestCase
```

## 迁移指南

### 从旧系统迁移

1. **创建职位类型**

   手动创建所需的 `EmployeeType` 记录：

   ```python
   from basic_info.models import Merchant, EmployeeType
   
   merchant = Merchant.objects.get(id=1)
   
   EmployeeType.objects.create(merchant=merchant, title='打纸')
   EmployeeType.objects.create(merchant=merchant, title='滚筒')
   EmployeeType.objects.create(merchant=merchant, title='仓库')
   EmployeeType.objects.create(merchant=merchant, title='送货')
   ```

2. **更新现有员工**

   将员工关联到对应的职位类型：

   ```python
   from basic_info.models import Employee, EmployeeType
   
   # 为所有员工设置职位
   printer_type = EmployeeType.objects.get(merchant=merchant, title='打纸')
   Employee.objects.filter(merchant=merchant).update(position=printer_type)
   ```

### 前端迁移

#### 读取员工信息

- **兼容**: `job_type` 字段仍然可用（只读）
- **推荐**: 使用 `position` 字段（ID）并根据需要查询 `EmployeeType`

#### 创建/更新员工

- **旧方式** (已废弃): 不能再设置 `job_type`
- **新方式**: 设置 `position` 字段为 `EmployeeType` 的 ID

```javascript
// 旧方式 (不再支持)
// { name: "张三", job_type: "打纸" }

// 新方式
{ name: "张三", position: 1 }  // 1 是 EmployeeType 的 ID
```

## 优势

1. **灵活性**: 职位类型可以动态创建和管理，不需要修改代码
2. **可扩展性**: 每个职位类型可以添加更多属性（如权限、级别等）
3. **多商户支持**: 每个商户可以定义自己的职位类型
4. **描述信息**: 可以为每个职位添加详细描述
5. **向后兼容**: 保留 `job_type` 属性，现有前端代码无需大改

## 注意事项

1. **数据丢失**: 迁移会删除旧的 `job_type` 数据，迁移前请备份
2. **外键约束**: 删除 `EmployeeType` 前需要确保没有员工关联
3. **唯一性**: 同一商户下职位名称必须唯一
4. **只读属性**: `job_type` 现在是只读的，不能通过 API 直接设置
5. **空值处理**: `position` 可以为空，`job_type` 会返回空字符串

## 技术细节

### 序列化器特殊处理

由于 `EmployeeType` 模型有 `unique_together = ('merchant', 'title')` 约束，DRF 会自动添加 `UniqueTogetherValidator`，导致创建时要求提供 `merchant` 字段。

**解决方案**: 在 `EmployeeTypeSerializer.__init__` 中移除验证器对 `merchant` 字段的要求：

```python
def __init__(self, *args, **kwargs):
    super().__init__(*args, **kwargs)
    for validator in self.validators:
        if hasattr(validator, 'fields') and 'merchant' in validator.fields:
            validator.fields = tuple(f for f in validator.fields if f != 'merchant')
```

这样 `merchant` 可以在 `BaseViewSet.perform_create` 中自动设置。

### 属性 vs 字段

`job_type` 使用 `@property` 装饰器实现，这是一个**模型属性**而非**数据库字段**：

- **优点**: 保持接口兼容，自动计算
- **缺点**: 不能用于数据库查询、排序或过滤
- **解决**: 需要过滤时使用 `position__title` 进行关联查询

## 相关文件

- **模型**: `basic_info/models.py`
- **序列化器**: `api_man/serializers.py`
- **视图**: `api_man/views.py`
- **URL**: `api_man/urls.py`
- **Admin**: `basic_info/admin.py`
- **迁移**: `basic_info/migrations/0011_remove_employee_job_type_employeetype_and_more.py`
- **测试**: `basic_info/tests.py`, `api_man/tests.py`

