# 广场作品TaskId查询功能说明

## 📋 功能概述

新增了通过任务编号（taskId/taskNo）查询作品详情的功能，具有以下特点：
- ✅ 使用taskId查询时**不会增加浏览数**，避免对作品热度数据产生影响
- ✅ **支持查询未发布的任务**，即使任务还没发布到广场也能查看详情
- ✅ 自动大小写转换，兼容大小写输入

---

## 🎯 业务需求

在某些场景下（如用户从自己的任务列表查看作品），需要通过任务编号直接查看详情，包括：
1. **已发布作品**：查看广场作品的完整信息（包含浏览数、点赞数等社交数据）
2. **未发布任务**：查看任务的基本信息（不包含社交数据）

此时不应计入公开浏览数，以保证统计数据的准确性。

---

## 🔧 实现方案

### 1. 接口修改

**接口路径**: `GET /user/plaza/works/{identifier}`

**新增查询参数**: `queryType`

| 参数名 | 类型 | 必填 | 默认值 | 说明 |
|-------|------|------|--------|------|
| `queryType` | String | 否 | `workNo` | 查询类型：<br>- `workNo`: 通过作品编号查询（记录浏览）<br>- `taskId`: 通过任务编号查询（不记录浏览） |

---

## 📡 接口使用示例

### 示例1：通过作品编号查询（默认方式，记录浏览）

```bash
curl -X 'GET' \
  'http://116.62.4.26:8081/user/plaza/works/WORK-1762440663366-3093' \
  -H 'accept: */*'
```

或明确指定：

```bash
curl -X 'GET' \
  'http://116.62.4.26:8081/user/plaza/works/WORK-1762440663366-3093?queryType=workNo' \
  -H 'accept: */*'
```

**行为**: 
- ✅ 返回作品详情
- ✅ 增加浏览数 +1

---

### 示例2：通过任务编号查询（不记录浏览）

```bash
curl -X 'GET' \
  'http://116.62.4.26:8081/user/plaza/works/TASK-20251026183750127-8554?queryType=taskId' \
  -H 'accept: */*'
```

**行为**: 
- ✅ 返回作品详情（已发布）或任务详情（未发布）
- ❌ **不增加浏览数**（保持原有数据）

**返回数据区别**:
- 如果已发布到广场：返回完整作品信息，包含标题、描述、标签、浏览数、点赞数等
- 如果未发布：返回任务基本信息，社交数据（浏览数、点赞数等）为0，标题/描述/标签为空

---

## 🔄 业务流程对比

### 通过workNo查询（原有逻辑）

```
用户请求 GET /plaza/works/{workNo}
    ↓
提取当前用户ID
    ↓
【记录浏览】增加view_count +1
    ↓
查询作品详情（联表查询）
    ↓
检查点赞状态
    ↓
返回作品详情
```

### 通过taskId查询（新增逻辑）

```
用户请求 GET /plaza/works/{taskNo}?queryType=taskId
    ↓
提取当前用户ID
    ↓
【跳过浏览记录】不增加view_count
    ↓
【步骤1】查询广场作品表（plaza_work）
    ├── 找到 → 返回完整作品详情（含社交数据）
    └── 未找到 → 继续下一步
    ↓
【步骤2】查询AI任务表（ai_task）
    ├── 找到 → 返回任务基本信息（社交数据为0）
    └── 未找到 → 抛出"任务不存在"异常
    ↓
返回详情
```

---

## 💻 代码实现

### 1. 控制器层修改

**文件**: `PlazaController.java`

**修改位置**: `getWorkDetail` 方法

```java
@GetMapping("/works/{workNo}")
public Result<WorkDetailResponse> getWorkDetail(
        @PathVariable String workNo,
        @RequestParam(defaultValue = "workNo") String queryType) {
    
    try {
        Long userId = SecurityUtil.getCurrentUserIdOrNull();
        WorkDetailResponse response;
        
        // 根据查询类型处理
        if ("taskId".equalsIgnoreCase(queryType)) {
            // 通过taskId查询，不记录浏览
            log.info("通过taskId查询作品详情，taskNo: {}", workNo);
            response = plazaService.getWorkDetailByTaskNo(userId, workNo);
        } else {
            // 通过workNo查询，记录浏览
            log.info("通过workNo查询作品详情，workNo: {}", workNo);
            plazaService.recordView(workNo, userId);
            response = plazaService.getWorkDetail(userId, workNo);
        }
        
        return Result.success(response);
    } catch (Exception e) {
        log.error("查询作品详情失败，identifier: {}, queryType: {}", workNo, queryType, e);
        return Result.error(e.getMessage());
    }
}
```

**关键逻辑**:
- 通过 `queryType` 参数判断查询类型
- `taskId` 模式：直接调用 `getWorkDetailByTaskNo`，跳过 `recordView`
- `workNo` 模式：保持原有逻辑，先记录浏览再查询

---

### 2. 服务接口层

**文件**: `PlazaService.java`

**新增方法**:

```java
/**
 * 通过任务编号查询作品详情（不记录浏览）
 * 
 * @param userId 当前用户ID（可为null）
 * @param taskNo 任务编号
 * @return 作品详情
 */
WorkDetailResponse getWorkDetailByTaskNo(Long userId, String taskNo);
```

---

### 3. 服务实现层

**文件**: `PlazaServiceImpl.java`

**新增实现**:

```java
@Override
public WorkDetailResponse getWorkDetailByTaskNo(Long userId, String taskNo) {
    log.info("通过任务编号查询作品详情（不记录浏览），taskNo: {}", taskNo);
    
    // 1. 统一转换为大写（兼容大小写输入）
    String normalizedTaskNo = taskNo.toUpperCase();
    
    // 2. 先查询广场作品（已发布）
    Map<String, Object> workMap = plazaWorkMapper.findWorkDetailByTaskNo(normalizedTaskNo);
    
    if (workMap != null) {
        // 2.1 找到广场作品，检查是否点赞
        boolean isLiked = false;
        if (userId != null) {
            Long workId = (Long) workMap.get("id");
            isLiked = plazaWorkLikeMapper.existsByWorkIdAndUserId(workId, userId) > 0;
        }
        return convertToDetailResponse(workMap, isLiked);
    }
    
    // 3. 广场中没找到，查询AI任务表（未发布的任务）
    log.info("广场中未找到作品，尝试从任务表查询 - taskNo: {}", normalizedTaskNo);
    Map<String, Object> taskMap = aiTaskMapper.findTaskDetailByTaskNo(normalizedTaskNo);
    
    if (taskMap == null) {
        throw new IllegalArgumentException("该任务不存在: " + taskNo);
    }
    
    // 4. 从任务数据构建作品详情响应（未发布状态）
    return convertTaskToWorkDetail(taskMap);
}
```

**特点**:
- ✅ **两级查询**：优先查询广场作品，未找到则查询任务表
- ✅ **大小写兼容**：自动转换为大写，兼容小写输入
- ✅ **完整回退**：未发布的任务也能正常返回
- ✅ **不记录浏览**：保证浏览数不变

---

### 4. 数据访问层

#### PlazaWorkMapper（查询广场作品）

**文件**: `PlazaWorkMapper.java`

**新增SQL查询**:

```java
/**
 * 通过任务编号查询作品详情（包含作者信息）
 */
@Select("SELECT pw.id, pw.work_no, pw.user_id, pw.task_no, pw.task_type, pw.model_name, " +
        "       pw.prompt, pw.result_url, pw.image_url, pw.aspect_ratio, pw.title, " +
        "       pw.description, pw.tags, pw.view_count, pw.like_count, pw.share_count, " +
        "       pw.comment_count, pw.is_public, pw.status, pw.create_time, pw.update_time, " +
        "       u.username as nickname, u.avatar_url " +
        "FROM plaza_work pw " +
        "LEFT JOIN user u ON pw.user_id = u.id " +
        "WHERE pw.task_no = #{taskNo} AND pw.is_deleted = 0")
Map<String, Object> findWorkDetailByTaskNo(@Param("taskNo") String taskNo);
```

#### AiTaskMapper（查询AI任务）

**文件**: `AiTaskMapper.java` 和 `AiTaskMapper.xml`

**新增方法**:

```java
/**
 * 通过任务编号查询任务详情（包含作者信息）
 */
java.util.Map<String, Object> findTaskDetailByTaskNo(@Param("taskNo") String taskNo);
```

**XML实现**:

```xml
<!-- 通过任务编号查询任务详情（包含作者信息） -->
<select id="findTaskDetailByTaskNo" resultType="java.util.HashMap">
    SELECT 
        at.id,
        at.task_no,
        at.user_id,
        at.model_name,
        at.task_type,
        at.prompt,
        at.result_url,
        at.image_url,
        at.aspect_ratio,
        at.status,
        at.progress,
        at.error_message,
        at.create_time,
        at.update_time,
        u.username as nickname,
        u.avatar_url
    FROM ai_task at
    LEFT JOIN user u ON at.user_id = u.id
    WHERE at.task_no = #{taskNo} AND at.is_deleted = 0
</select>
```

**SQL特点**:
- 查询AI任务表的基本字段
- 通过LEFT JOIN关联用户信息
- 包含任务状态、进度、错误信息等

---

## 📊 响应数据结构

### 情况1：查询已发布到广场的作品

```json
{
  "code": 200,
  "message": "success",
  "data": {
    "workNo": "WORK-1762440663366-3093",
    "taskNo": "TASK-20251026183750127-8554",
    "taskType": "text_to_video",
    "modelName": "minimax-video-01",
    "prompt": "战争场景，恢弘气势...",
    "resultUrl": "https://oss.example.com/videos/xxx.mp4",
    "imageUrl": "https://oss.example.com/images/ref.jpg",
    "aspectRatio": "16:9",
    "title": "战场气氛短视频",
    "description": "根据参考图生成的恢弘战场场景",
    "tags": ["视频", "战争", "特效"],
    "viewCount": 125,
    "likeCount": 38,
    "shareCount": 5,
    "isLiked": true,
    "author": {
      "userId": 17563793187762127,
      "nickname": "用户123456",
      "avatarUrl": "https://img.remit.ee/api/file/xxx.png"
    },
    "createTime": "2024-11-06T10:30:00",
    "updateTime": "2024-11-07T15:45:00"
  }
}
```

### 情况2：查询未发布的任务

#### 2.1 任务已完成

```json
{
  "code": 200,
  "message": "success",
  "data": {
    "workNo": null,  // 未发布，无作品编号
    "taskNo": "TASK-20251106195754218-9940",
    "taskType": "text_to_video",
    "modelName": "minimax-video-01",
    "prompt": "战争场景，恢弘气势...",
    "resultUrl": "https://oss.example.com/videos/xxx.mp4",  // ✅ 已完成，有结果
    "imageUrl": null,  // text_to_video 任务通常无参考图
    "aspectRatio": "16:9",
    "title": null,  // 未发布，无标题
    "description": null,  // 未发布，无描述
    "tags": [],  // 未发布，无标签
    "viewCount": 0,  // 未发布，浏览数为0
    "likeCount": 0,  // 未发布，点赞数为0
    "shareCount": 0,  // 未发布，分享数为0
    "isLiked": false,  // 未发布，不可点赞
    "author": {
      "userId": 17563793187762127,
      "nickname": "用户123456",
      "avatarUrl": "https://img.remit.ee/api/file/xxx.png"
    },
    "createTime": "2024-11-06T19:57:54",
    "updateTime": "2024-11-06T20:05:30",
    "taskStatus": "completed",  // ✅ 任务状态：已完成
    "taskProgress": 100,  // ✅ 任务进度：100%
    "errorMessage": null  // ✅ 无错误
  }
}
```

#### 2.2 任务处理中

```json
{
  "code": 200,
  "message": "success",
  "data": {
    "workNo": null,
    "taskNo": "TASK-20251106195754218-9940",
    "taskType": "text_to_video",
    "modelName": "minimax-video-01",
    "prompt": "战争场景，恢弘气势...",
    "resultUrl": null,  // ⏳ 处理中，暂无结果
    "imageUrl": null,
    "aspectRatio": "16:9",
    "title": null,
    "description": null,
    "tags": [],
    "viewCount": 0,
    "likeCount": 0,
    "shareCount": 0,
    "isLiked": false,
    "author": {
      "userId": 17563793187762127,
      "nickname": "用户123456",
      "avatarUrl": "https://img.remit.ee/api/file/xxx.png"
    },
    "createTime": "2024-11-06T19:57:54",
    "updateTime": "2024-11-06T19:58:55",
    "taskStatus": "processing",  // ⏳ 任务状态：处理中
    "taskProgress": 45,  // ⏳ 任务进度：45%
    "errorMessage": null
  }
}
```

#### 2.3 任务失败

```json
{
  "code": 200,
  "message": "success",
  "data": {
    "workNo": null,
    "taskNo": "TASK-20251106195754218-9940",
    "taskType": "text_to_video",
    "modelName": "minimax-video-01",
    "prompt": "战争场景，恢弘气势...",
    "resultUrl": null,  // ❌ 失败，无结果
    "imageUrl": null,
    "aspectRatio": "16:9",
    "title": null,
    "description": null,
    "tags": [],
    "viewCount": 0,
    "likeCount": 0,
    "shareCount": 0,
    "isLiked": false,
    "author": {
      "userId": 17563793187762127,
      "nickname": "用户123456",
      "avatarUrl": "https://img.remit.ee/api/file/xxx.png"
    },
    "createTime": "2024-11-06T19:57:54",
    "updateTime": "2024-11-06T20:10:30",
    "taskStatus": "failed",  // ❌ 任务状态：失败
    "taskProgress": 0,  // ❌ 任务进度：0%
    "errorMessage": "生成超时或服务异常"  // ❌ 错误信息
  }
}
```

**关键区别**:

| 字段 | 已发布作品 | 未发布任务 |
|-----|-----------|-----------|
| `workNo` | 有值 | `null` |
| `title` | 用户设置的标题 | `null` |
| `description` | 用户设置的描述 | `null` |
| `tags` | 用户设置的标签数组 | `[]` 空数组 |
| `viewCount` | 真实浏览数 | `0` |
| `likeCount` | 真实点赞数 | `0` |
| `shareCount` | 真实分享数 | `0` |
| `isLiked` | 真实点赞状态 | `false` |
| `taskStatus` | `null` | 任务状态（queued/processing/completed/failed） |
| `taskProgress` | `null` | 任务进度（0-100） |
| `errorMessage` | `null` | 错误信息（失败时才有） |
| `resultUrl` | 总是有值 | 仅completed时有值 |
| `imageUrl` | 可能有值 | text_to_video通常为null |

---

## 🔍 任务状态说明

### 任务状态（taskStatus）

| 状态 | 说明 | resultUrl | 前端建议 |
|-----|------|-----------|---------|
| `queued` | 排队中 | `null` | 显示"排队中..." |
| `processing` | 处理中 | `null` | 显示进度条（使用taskProgress） |
| `completed` | 已完成 | ✅ 有值 | 显示视频播放器 |
| `failed` | 失败 | `null` | 显示错误提示（使用errorMessage） |

### 为什么resultUrl为null？

1. **任务未完成**：`taskStatus` 为 `queued` 或 `processing`
   - 解决方案：轮询查询，等待任务完成

2. **任务失败**：`taskStatus` 为 `failed`
   - 解决方案：显示 `errorMessage` 给用户

3. **任务刚完成**：数据库更新延迟
   - 解决方案：刷新重试

### 为什么imageUrl为null？

1. **text_to_video任务**：文本生成视频，不需要参考图 ✅ 正常
2. **image_to_video任务**：图生视频，应该有参考图URL

---

## 🎯 使用场景

### 场景1：公开广场浏览（使用workNo）

**场景描述**: 用户在广场浏览其他人的作品

**调用方式**: `GET /plaza/works/{workNo}` 或 `?queryType=workNo`

**预期行为**: 
- ✅ 记录浏览数
- ✅ 用于热度排序
- ✅ 统计作品受欢迎程度

---

### 场景2：用户查看自己的作品（使用taskId）

**场景描述**: 用户从"我的任务"列表点击查看已发布的作品

**调用方式**: `GET /plaza/works/{taskNo}?queryType=taskId`

**预期行为**: 
- ❌ 不记录浏览数（避免刷数据）
- ✅ 查看真实的浏览统计
- ✅ 预览作品效果

---

### 场景3：外部分享链接（使用workNo）

**场景描述**: 通过分享链接访问作品

**调用方式**: `GET /plaza/works/{workNo}`

**预期行为**: 
- ✅ 记录浏览数
- ✅ 统计分享传播效果

---

## 🔒 安全性考虑

### 1. 权限控制

- ✅ 两种查询方式都支持未登录用户访问
- ✅ 已登录用户可查看个性化点赞状态
- ✅ 查询条件都包含 `is_deleted = 0`，过滤已删除作品

### 2. 数据一致性

- ✅ `task_no` 与 `work_no` 在数据库中都有索引（建议）
- ✅ 一个任务最多对应一个作品（发布时已验证）
- ✅ 查询结果经过软删除过滤

### 3. 异常处理

```java
// taskId不存在时的错误提示
throw new IllegalArgumentException("该任务对应的作品不存在: " + taskNo);

// workNo不存在时的错误提示
throw new IllegalArgumentException("作品不存在: " + workNo);
```

---

## 📈 数据库优化建议

### 推荐索引

```sql
-- 任务编号索引（如果尚未创建）
CREATE INDEX idx_task_no ON plaza_work(task_no);

-- 作品编号索引（应该已存在）
CREATE UNIQUE INDEX uk_work_no ON plaza_work(work_no);

-- 复合索引（优化查询性能）
CREATE INDEX idx_task_no_deleted ON plaza_work(task_no, is_deleted);
```

---

## 🧪 测试建议

### 测试用例1: 通过workNo查询（记录浏览）

```bash
# 第一次查询
curl 'http://116.62.4.26:8081/user/plaza/works/WORK-xxx'
# 预期: viewCount = N

# 第二次查询
curl 'http://116.62.4.26:8081/user/plaza/works/WORK-xxx'
# 预期: viewCount = N + 1
```

---

### 测试用例2: 通过taskId查询（不记录浏览）

```bash
# 查询前记录当前浏览数
curl 'http://116.62.4.26:8081/user/plaza/works/WORK-xxx'
# 假设 viewCount = 100

# 通过taskId查询多次
curl 'http://116.62.4.26:8081/user/plaza/works/TASK-xxx?queryType=taskId'
curl 'http://116.62.4.26:8081/user/plaza/works/TASK-xxx?queryType=taskId'
curl 'http://116.62.4.26:8081/user/plaza/works/TASK-xxx?queryType=taskId'

# 再次通过workNo查询确认
curl 'http://116.62.4.26:8081/user/plaza/works/WORK-xxx'
# 预期: viewCount 仍然为 100（未增加）
```

---

### 测试用例3: 错误的taskId

```bash
curl 'http://116.62.4.26:8081/user/plaza/works/TASK-invalid?queryType=taskId'

# 预期响应:
{
  "code": 400,
  "message": "该任务对应的作品不存在: TASK-invalid",
  "data": null
}
```

---

### 测试用例4: 未发布作品的taskId

```bash
# 对于未发布到广场的任务
curl 'http://116.62.4.26:8081/user/plaza/works/TASK-未发布?queryType=taskId'

# 预期响应:
{
  "code": 400,
  "message": "该任务对应的作品不存在: TASK-未发布",
  "data": null
}
```

---

## 📝 日志记录

### 通过workNo查询的日志

```log
INFO - 通过workNo查询作品详情，workNo: WORK-1762440663366-3093
DEBUG - 记录作品浏览，workNo: WORK-1762440663366-3093, userId: 17563793187762127
INFO - 查询作品详情，workNo: WORK-1762440663366-3093
```

### 通过taskId查询的日志

```log
INFO - 通过taskId查询作品详情，taskNo: TASK-20251026183750127-8554
INFO - 通过任务编号查询作品详情（不记录浏览），taskNo: TASK-20251026183750127-8554
```

---

## ✅ 修改文件清单

| 文件路径 | 修改内容 |
|---------|---------|
| `PlazaController.java` | 新增 `queryType` 参数，根据类型分发请求 |
| `PlazaService.java` | 新增 `getWorkDetailByTaskNo()` 接口方法 |
| `PlazaServiceImpl.java` | 实现两级查询逻辑、大小写转换、`convertTaskToWorkDetail()` 转换方法 |
| `PlazaWorkMapper.java` | 新增 `findWorkDetailByTaskNo()` SQL查询 |
| `AiTaskMapper.java` | 新增 `findTaskDetailByTaskNo()` 接口方法 |
| `AiTaskMapper.xml` | 新增 `findTaskDetailByTaskNo` SQL实现（关联user表） |

---

## 🎬 总结

✅ **向后兼容** - 原有接口调用方式不受影响  
✅ **灵活查询** - 支持通过作品编号或任务编号查询  
✅ **完整回退** - 未发布任务也能正常查询  
✅ **大小写兼容** - 自动转换大小写，提升用户体验  
✅ **数据准确** - taskId查询不影响浏览统计  
✅ **完善日志** - 详细记录查询类型和来源  
✅ **异常处理** - 明确的错误提示信息  

该功能完美满足了在不同场景下查询作品详情的需求，包括已发布作品和未发布任务，同时保证了统计数据的准确性！

---

## 🔍 Bug修复记录

### Bug 1: 大小写敏感问题

**现象**: 传入小写的taskId（如 `task-xxx`）查询失败

**原因**: 数据库中存储的是大写格式（如 `TASK-xxx`）

**解决**: 在所有查询方法中添加 `.toUpperCase()` 转换

**影响范围**: `getWorkDetail`, `getWorkDetailByTaskNo`, `likeWork`, `unlikeWork`, `deleteWork`, `recordView`

### Bug 2: 未发布任务无法查询

**现象**: 传入未发布任务的taskId返回"作品不存在"

**原因**: 只查询了plaza_work表，未发布的任务不在该表中

**解决**: 实现两级查询，先查plaza_work，未找到再查ai_task表

**新增方法**: `AiTaskMapper.findTaskDetailByTaskNo()`, `PlazaServiceImpl.convertTaskToWorkDetail()`

---

## 📞 技术支持

如有问题，请查看日志文件或联系技术团队。

**日志关键字**:
- `通过taskId查询作品详情`
- `通过workNo查询作品详情`
- `该任务对应的作品不存在`

