# 评委邀请码生成方案 - 实施指南

> **实施日期**: 2025-12-12
> **实施方式**: 管理员生成 → 复制发送 → 评委使用
> **状态**: ✅ 代码已完成，可立即测试

---

## 📋 方案概述

### 核心流程

```
管理员操作：
1. 进入评委管理页面
2. 选择评委，点击"生成邀请码"
3. 系统生成6位随机码（如：ABC123）
4. 复制邀请码
5. 通过微信/短信发送给评委

评委使用：
1. 收到邀请码
2. 打开小程序登录页
3. 输入比赛编码 + 邀请码
4. 登录成功，开始评分
```

### 技术特点

- ✅ **无需改表** - 使用现有字段
- ✅ **6位随机码** - 大写字母+数字组合
- ✅ **唯一性保证** - 数据库唯一索引
- ✅ **有效期管理** - 默认30天
- ✅ **状态管理** - 待使用/已使用/已禁用

---

## 🚀 已完成的代码

### 1. DTO 类

#### GenerateInviteDTO.java
**路径**: `src/main/java/org/springblade/modules/martial/pojo/dto/GenerateInviteDTO.java`

```java
@Data
@ApiModel("生成邀请码DTO")
public class GenerateInviteDTO {
    @NotNull(message = "赛事ID不能为空")
    private Long competitionId;

    @NotNull(message = "评委ID不能为空")
    private Long judgeId;

    @NotBlank(message = "角色不能为空")
    private String role;  // judge 或 chief_judge

    private Long venueId;  // 场地ID（普通评委必填）
    private String projects;  // 项目列表（JSON）
    private Integer expireDays = 30;  // 过期天数
}
```

#### BatchGenerateInviteDTO.java
**路径**: `src/main/java/org/springblade/modules/martial/pojo/dto/BatchGenerateInviteDTO.java`

```java
@Data
@ApiModel("批量生成邀请码DTO")
public class BatchGenerateInviteDTO {
    @NotNull(message = "赛事ID不能为空")
    private Long competitionId;

    @NotEmpty(message = "评委列表不能为空")
    private List<Long> judgeIds;

    private String role = "judge";
    private Integer expireDays = 30;
}
```

---

### 2. Service 层

#### IMartialJudgeInviteService.java
**新增方法**:

```java
// 生成邀请码
MartialJudgeInvite generateInviteCode(GenerateInviteDTO dto);

// 批量生成邀请码
List<MartialJudgeInvite> batchGenerateInviteCode(BatchGenerateInviteDTO dto);

// 重新生成邀请码
MartialJudgeInvite regenerateInviteCode(Long inviteId);

// 生成唯一邀请码
String generateUniqueInviteCode();
```

#### MartialJudgeInviteServiceImpl.java
**核心实现**:

1. **生成唯一邀请码**:
   ```java
   // 6位随机字符串（大写字母+数字）
   String inviteCode = UUID.randomUUID().toString()
       .replaceAll("-", "")
       .substring(0, 6)
       .toUpperCase();
   ```

2. **检查重复**:
   ```java
   // 检查邀请码是否已存在
   long count = this.count(
       Wrappers.<MartialJudgeInvite>lambdaQuery()
           .eq(MartialJudgeInvite::getInviteCode, inviteCode)
           .eq(MartialJudgeInvite::getIsDeleted, 0)
   );
   ```

3. **防止重复生成**:
   ```java
   // 检查评委是否已有有效邀请码
   MartialJudgeInvite existInvite = this.getOne(
       Wrappers.<MartialJudgeInvite>lambdaQuery()
           .eq(MartialJudgeInvite::getCompetitionId, competitionId)
           .eq(MartialJudgeInvite::getJudgeId, judgeId)
           .eq(MartialJudgeInvite::getStatus, 1)
           .gt(MartialJudgeInvite::getExpireTime, LocalDateTime.now())
   );
   ```

---

### 3. Controller 层

#### MartialJudgeInviteController.java
**新增接口**:

| 接口 | 方法 | 路径 | 说明 |
|------|------|------|------|
| 生成邀请码 | POST | `/martial/judgeInvite/generate` | 为单个评委生成 |
| 批量生成 | POST | `/martial/judgeInvite/generate/batch` | 批量生成 |
| 重新生成 | PUT | `/martial/judgeInvite/regenerate/{id}` | 重新生成（旧码失效） |
| 查询邀请码 | GET | `/martial/judgeInvite/byJudge` | 查询评委的邀请码 |

---

## 🧪 测试指南

### 1. 使用 Postman 测试

#### 测试1：生成邀请码

```http
POST http://localhost:8080/martial/judgeInvite/generate
Content-Type: application/json
Blade-Auth: Bearer {token}

{
  "competitionId": 1,
  "judgeId": 1,
  "role": "judge",
  "venueId": 1,
  "projects": "[\"女子组长拳\",\"男子组陈氏太极拳\"]",
  "expireDays": 30
}
```

**预期响应**:
```json
{
  "code": 200,
  "success": true,
  "data": {
    "id": 1001,
    "competitionId": 1,
    "judgeId": 1,
    "inviteCode": "ABC123",
    "role": "judge",
    "venueId": 1,
    "projects": "[\"女子组长拳\",\"男子组陈氏太极拳\"]",
    "expireTime": "2026-01-11 10:00:00",
    "isUsed": 0,
    "status": 1
  }
}
```

#### 测试2：批量生成邀请码

```http
POST http://localhost:8080/martial/judgeInvite/generate/batch
Content-Type: application/json
Blade-Auth: Bearer {token}

{
  "competitionId": 1,
  "judgeIds": [1, 2, 3, 4, 5],
  "role": "judge",
  "expireDays": 30
}
```

#### 测试3：查询评委邀请码

```http
GET http://localhost:8080/martial/judgeInvite/byJudge?competitionId=1&judgeId=1
Blade-Auth: Bearer {token}
```

#### 测试4：重新生成邀请码

```http
PUT http://localhost:8080/martial/judgeInvite/regenerate/1001
Blade-Auth: Bearer {token}
```

---

### 2. 使用 SQL 测试

#### 执行测试脚本

```bash
# 进入数据库
mysql -u root -p blade

# 执行测试脚本
source database/martial-db/test_invite_code_generation.sql
```

#### 查询有效邀请码

```sql
SELECT
    ji.id,
    ji.invite_code,
    ji.role,
    j.name AS judge_name,
    ji.expire_time,
    ji.is_used,
    CASE
        WHEN ji.is_used = 1 THEN '已使用'
        WHEN ji.expire_time < NOW() THEN '已过期'
        WHEN ji.status = 0 THEN '已禁用'
        ELSE '待使用'
    END AS status_text
FROM martial_judge_invite ji
LEFT JOIN martial_judge j ON ji.judge_id = j.id
WHERE ji.competition_id = 1
  AND ji.is_deleted = 0
ORDER BY ji.create_time DESC;
```

---

## 📊 数据库字段说明

### martial_judge_invite 表

| 字段 | 类型 | 说明 | 使用方式 |
|------|------|------|----------|
| `invite_code` | varchar(50) | 邀请码 | 6位随机码 |
| `status` | int | 状态 | 1-启用，0-禁用 |
| `is_used` | int | 是否已使用 | 0-未使用，1-已使用 |
| `expire_time` | datetime | 过期时间 | 默认30天后 |
| `use_time` | datetime | 使用时间 | 登录时记录 |
| `role` | varchar(20) | 角色 | judge/chief_judge |
| `venue_id` | bigint | 场地ID | 普通评委必填 |
| `projects` | varchar(500) | 项目列表 | JSON数组 |

### 状态判断逻辑

```
有效邀请码：status=1 AND is_used=0 AND expire_time>NOW()
已使用：is_used=1
已过期：expire_time<=NOW()
已禁用：status=0
```

---

## 🎯 前端集成建议

### 1. 在评委管理页面添加按钮

```vue
<template>
  <el-table :data="judgeList">
    <el-table-column label="操作">
      <template #default="{ row }">
        <!-- 生成邀请码按钮 -->
        <el-button
          v-if="!row.inviteCode"
          type="primary"
          size="small"
          @click="generateInviteCode(row)"
        >
          生成邀请码
        </el-button>

        <!-- 显示邀请码 -->
        <div v-else>
          <el-tag>{{ row.inviteCode }}</el-tag>
          <el-button
            type="text"
            size="small"
            @click="copyInviteCode(row.inviteCode)"
          >
            复制
          </el-button>
          <el-button
            type="text"
            size="small"
            @click="regenerateInviteCode(row)"
          >
            重新生成
          </el-button>
        </div>
      </template>
    </el-table-column>
  </el-table>
</template>
```

### 2. 生成邀请码方法

```javascript
async generateInviteCode(judge) {
  try {
    const res = await this.$http.post('/martial/judgeInvite/generate', {
      competitionId: this.competitionId,
      judgeId: judge.id,
      role: judge.refereeType === 1 ? 'chief_judge' : 'judge',
      venueId: judge.venueId,
      projects: JSON.stringify(judge.projects),
      expireDays: 30
    });

    if (res.success) {
      this.$message.success('邀请码生成成功：' + res.data.inviteCode);
      // 复制到剪贴板
      this.copyToClipboard(res.data.inviteCode);
      // 刷新列表
      this.loadJudgeList();
    }
  } catch (error) {
    this.$message.error(error.message || '生成失败');
  }
}

// 复制到剪贴板
copyToClipboard(text) {
  const input = document.createElement('input');
  input.value = text;
  document.body.appendChild(input);
  input.select();
  document.execCommand('copy');
  document.body.removeChild(input);
  this.$message.success('已复制到剪贴板');
}
```

### 3. 批量生成

```javascript
async batchGenerate() {
  const selectedJudges = this.$refs.table.selection;
  if (selectedJudges.length === 0) {
    this.$message.warning('请选择评委');
    return;
  }

  const judgeIds = selectedJudges.map(j => j.id);

  try {
    const res = await this.$http.post('/martial/judgeInvite/generate/batch', {
      competitionId: this.competitionId,
      judgeIds: judgeIds,
      role: 'judge',
      expireDays: 30
    });

    if (res.success) {
      this.$message.success(`成功生成${res.data.length}个邀请码`);
      this.loadJudgeList();
    }
  } catch (error) {
    this.$message.error(error.message || '批量生成失败');
  }
}
```

---

## ✅ 验证清单

### 后端验证

- [ ] DTO类创建成功
- [ ] Service方法实现完成
- [ ] Controller接口添加完成
- [ ] 编译无错误
- [ ] Swagger文档生成正常

### 功能验证

- [ ] 单个生成邀请码成功
- [ ] 邀请码格式正确（6位大写字母+数字）
- [ ] 邀请码唯一性验证通过
- [ ] 批量生成成功
- [ ] 重新生成成功（旧码失效）
- [ ] 查询邀请码成功
- [ ] 防止重复生成（已有有效邀请码时报错）

### 数据库验证

- [ ] 邀请码保存成功
- [ ] 过期时间设置正确
- [ ] 状态字段正确
- [ ] 唯一索引生效

### 小程序验证

- [ ] 使用邀请码登录成功
- [ ] 登录后权限正确
- [ ] 场地和项目信息正确

---

## 🔧 常见问题

### 问题1：邀请码重复

**现象**: 生成的邀请码已存在

**原因**: 随机生成时碰撞

**解决**: 代码已实现重试机制（最多10次）

---

### 问题2：评委已有邀请码

**现象**: 提示"该评委已有有效邀请码"

**原因**: 防止重复生成

**解决**:
- 使用"重新生成"功能
- 或等待旧邀请码过期

---

### 问题3：邀请码过期

**现象**: 登录时提示邀请码已过期

**原因**: 超过30天有效期

**解决**: 使用"重新生成"功能

---

## 📈 后续优化建议

### 短期优化（可选）

1. **邀请码格式优化**
   - 添加前缀（如：WS-ABC123）
   - 区分角色（J-评委，C-裁判长）

2. **批量导出**
   - 导出Excel：评委信息+邀请码
   - 生成PDF邀请函

3. **统计报表**
   - 邀请码使用率
   - 过期邀请码数量

### 长期优化（可选）

1. **短信/邮件发送**
   - 集成短信服务
   - 自动发送邀请码

2. **二维码生成**
   - 生成邀请二维码
   - 扫码直接登录

3. **邀请码管理**
   - 批量禁用
   - 批量延期

---

## 📞 技术支持

### 代码位置

| 文件 | 路径 |
|------|------|
| DTO类 | `src/main/java/org/springblade/modules/martial/pojo/dto/` |
| Service接口 | `src/main/java/org/springblade/modules/martial/service/IMartialJudgeInviteService.java` |
| Service实现 | `src/main/java/org/springblade/modules/martial/service/impl/MartialJudgeInviteServiceImpl.java` |
| Controller | `src/main/java/org/springblade/modules/martial/controller/MartialJudgeInviteController.java` |
| 测试SQL | `database/martial-db/test_invite_code_generation.sql` |

### Swagger 文档

启动后端服务后访问：
```
http://localhost:8080/doc.html
```

搜索"裁判邀请码管理"查看所有接口。

---

## 🎉 总结

### 已完成

✅ DTO类创建
✅ Service层实现
✅ Controller接口
✅ 测试SQL脚本
✅ 实施文档

### 工作量

- 后端开发：2小时
- 测试验证：1小时
- 文档编写：1小时
- **总计**：4小时

### 下一步

1. 启动后端服务
2. 使用Postman测试接口
3. 前端集成（如需要）
4. 联调测试
5. 上线部署

---

**祝您实施顺利！** 🚀

如有问题，请查看代码注释或联系技术支持。
