# 广场作品TaskId查询功能说明 ## 📋 功能概述 新增了通过任务编号(taskId/taskNo)查询作品详情的功能,具有以下特点: - ✅ 使用taskId查询时**不会增加浏览数**,避免对作品热度数据产生影响 - ✅ **支持查询未发布的任务**,即使任务还没发布到广场也能查看详情 - ✅ 自动大小写转换,兼容大小写输入 --- ## 🎯 业务需求 在某些场景下(如用户从自己的任务列表查看作品),需要通过任务编号直接查看详情,包括: 1. **已发布作品**:查看广场作品的完整信息(包含浏览数、点赞数等社交数据) 2. **未发布任务**:查看任务的基本信息(不包含社交数据) 此时不应计入公开浏览数,以保证统计数据的准确性。 --- ## 🔧 实现方案 ### 1. 接口修改 **接口路径**: `GET /user/plaza/works/{identifier}` **新增查询参数**: `queryType` | 参数名 | 类型 | 必填 | 默认值 | 说明 | |-------|------|------|--------|------| | `queryType` | String | 否 | `workNo` | 查询类型:
- `workNo`: 通过作品编号查询(记录浏览)
- `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 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 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 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 findWorkDetailByTaskNo(@Param("taskNo") String taskNo); ``` #### AiTaskMapper(查询AI任务) **文件**: `AiTaskMapper.java` 和 `AiTaskMapper.xml` **新增方法**: ```java /** * 通过任务编号查询任务详情(包含作者信息) */ java.util.Map findTaskDetailByTaskNo(@Param("taskNo") String taskNo); ``` **XML实现**: ```xml ``` **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查询作品详情` - `该任务对应的作品不存在`