diff --git a/docs/api/public-leai-work-form.md b/docs/api/public-leai-work-form.md index 310504e..fcd0f25 100644 --- a/docs/api/public-leai-work-form.md +++ b/docs/api/public-leai-work-form.md @@ -6,8 +6,8 @@ |----|------| | Base URL | 部署域名 + **context-path `/api`**(见 `application.yml` `server.servlet.context-path`) | | 完整路径前缀 | `/api/public/leai-works` | -| 认证 | **需要登录**。请求头携带 `Authorization: Bearer `;与 Web 端一致时可加 `X-Tenant-Code`、`X-Tenant-Id`(见前端 `request.ts`) | -| 安全说明 | 该路径**不在** Spring Security 匿名白名单中,未带有效 Token 将返回 **401** | +| 认证 | **可选**。未带 Token 时接口仍可访问(Spring Security 白名单 `/public/leai-works/**`),供外部系统对接;携带 `Authorization: Bearer ` 时,后端会校验作品归属当前用户。Web 端可继续加 `X-Tenant-Code`、`X-Tenant-Id`(见前端 `request.ts`) | +| 安全说明 | 无 Token 时**仅凭路径中的 `remoteWorkId` 定位作品**,请依赖 ID 保密性并在网络层做访问控制;带 Token 时仍按用户维度校验。 | --- @@ -55,10 +55,11 @@ Content-Type: application/json ### 请求体(JSON 对象) - **不能为空**:`{}` 会返回 **400**(`请求体不能为空`)。 -- 服务端从 body 中读取两类数据: - 1. **编目字段**(可选子集):仅处理以下键,其余顶层键若存在会被忽略(除 `pageList` 外): +- 服务端从 body 中读取三类数据: + 1. **编目字段**(可选子集):仅处理以下键,其余顶层键若存在会被忽略(除 `pageList`、`status` 外): `author`、`title`、`subtitle`、`intro`、`tags` - 2. **分页**:`pageList` 为数组时,按乐读派 `pageList` 形态写入本库分页表。 + 2. **进度**:`status`(整数)原样写入本库 `leai_status`,与 GET 返回的 `status` 一致(不做区间收敛)。 + 3. **分页**:`pageList` 为数组时,按乐读派 `pageList` 形态写入本库分页表。 | 字段 | 类型 | 说明 | |------|------|------| @@ -67,6 +68,7 @@ Content-Type: application/json | `subtitle` | string | 副标题 | | `intro` | string | 简介 | | `tags` | string[] | 标签列表 | +| `status` | number | 乐读派创作进度(`leai_status`),见下文「status 取值」 | | `pageList` | object[] | 分页列表;元素字段见下表 | `pageList` 元素: @@ -82,6 +84,7 @@ Content-Type: application/json - 作品必须属于**当前登录用户**且 `remote_work_id` 匹配,否则 **404**(`作品不存在或无权操作`)。 - 若本次请求更新了编目,且作品为草稿、且乐读派进度已 ≥「生成完成」,可能将发布状态从草稿置为未发布(见 `PublicLeaiWorkFormService`)。 +- 若同时携带 `pageList` 且**每一页**均有非空 `audioUrl`,保存分页后会将 `leai_status` 置为 **5(已配音)**,覆盖本次请求中的 `status` 值。 - 若 `pageList` 非空,且**每一页**的 `audioUrl` 均为非空白字符串,则将 `leai_status` 更新为 **已配音(5)**。 ### 成功响应 diff --git a/lesingle-creation-backend/src/main/java/com/lesingle/modules/pub/controller/PublicLeaiWorkController.java b/lesingle-creation-backend/src/main/java/com/lesingle/modules/pub/controller/PublicLeaiWorkController.java index 66d3390..e3b0876 100644 --- a/lesingle-creation-backend/src/main/java/com/lesingle/modules/pub/controller/PublicLeaiWorkController.java +++ b/lesingle-creation-backend/src/main/java/com/lesingle/modules/pub/controller/PublicLeaiWorkController.java @@ -24,7 +24,7 @@ public class PublicLeaiWorkController { @GetMapping("/{remoteWorkId}/work-form") @Operation(summary = "查询本库作品详情(编目/分页快照,不经乐读派 B2 GET)") public Result> getWorkForm(@PathVariable String remoteWorkId) { - Long userId = SecurityUtil.getCurrentUserId(); + Long userId = SecurityUtil.getCurrentUserIdOrNull(); return Result.success(publicLeaiWorkFormService.getWorkFormDetail(userId, remoteWorkId)); } @@ -33,7 +33,7 @@ public class PublicLeaiWorkController { public Result saveWorkForm( @PathVariable String remoteWorkId, @RequestBody Map body) { - Long userId = SecurityUtil.getCurrentUserId(); + Long userId = SecurityUtil.getCurrentUserIdOrNull(); publicLeaiWorkFormService.saveWorkForm(userId, remoteWorkId, body); return Result.success(); } diff --git a/lesingle-creation-backend/src/main/java/com/lesingle/modules/pub/service/PublicLeaiWorkFormService.java b/lesingle-creation-backend/src/main/java/com/lesingle/modules/pub/service/PublicLeaiWorkFormService.java index 7e4756c..0e9b738 100644 --- a/lesingle-creation-backend/src/main/java/com/lesingle/modules/pub/service/PublicLeaiWorkFormService.java +++ b/lesingle-creation-backend/src/main/java/com/lesingle/modules/pub/service/PublicLeaiWorkFormService.java @@ -36,7 +36,7 @@ public class PublicLeaiWorkFormService { private final ILeaiSyncService leaiSyncService; /** - * @param body 可含 author、title、subtitle、intro、tags、pageList(乐读派 pageList 形态) + * @param body 可含 author、title、subtitle、intro、tags、status(乐读派进度,写入 leai_status)、pageList(乐读派 pageList 形态) */ @Transactional(rollbackFor = Exception.class) public void saveWorkForm(Long userId, String remoteWorkId, Map body) { @@ -70,6 +70,16 @@ public class PublicLeaiWorkFormService { } } + // status → leai_status 原样覆盖(不与库内旧值合并、不做区间收敛);先于 pageList;若全页有配音下文仍会置为 DUBBED + if (body.containsKey("status")) { + int st = LeaiUtil.toInt(body.get("status"), 0); + LambdaUpdateWrapper uw = new LambdaUpdateWrapper<>(); + uw.eq(UgcWork::getId, work.getId()) + .set(UgcWork::getLeaiStatus, st) + .set(UgcWork::getModifyTime, LocalDateTime.now()); + ugcWorkMapper.update(null, uw); + } + @SuppressWarnings("unchecked") List> pageList = (List>) body.get("pageList"); if (pageList != null && !pageList.isEmpty()) { @@ -170,11 +180,16 @@ public class PublicLeaiWorkFormService { return m; } + /** + * 定位作品。已登录时校验 {@code remoteWorkId} 属于当前用户;无 Token(外部系统对接)时仅按 {@code remoteWorkId} 查询,请依赖 ID 保密性。 + */ private UgcWork findOwnedWork(Long userId, String remoteWorkId) { LambdaQueryWrapper q = new LambdaQueryWrapper<>(); q.eq(UgcWork::getRemoteWorkId, remoteWorkId) - .eq(UgcWork::getUserId, userId) .eq(UgcWork::getIsDeleted, 0); + if (userId != null) { + q.eq(UgcWork::getUserId, userId); + } UgcWork work = ugcWorkMapper.selectOne(q); if (work == null) { throw new BusinessException(404, "作品不存在或无权操作"); diff --git a/lesingle-creation-backend/src/main/java/com/lesingle/security/config/SecurityConfig.java b/lesingle-creation-backend/src/main/java/com/lesingle/security/config/SecurityConfig.java index 7743fd7..89a7841 100644 --- a/lesingle-creation-backend/src/main/java/com/lesingle/security/config/SecurityConfig.java +++ b/lesingle-creation-backend/src/main/java/com/lesingle/security/config/SecurityConfig.java @@ -58,6 +58,8 @@ public class SecurityConfig { .requestMatchers(HttpMethod.GET, "/public/gallery", "/public/gallery/**").permitAll() .requestMatchers(HttpMethod.GET, "/public/tags", "/public/tags/**").permitAll() .requestMatchers(HttpMethod.GET, "/public/users/*/works").permitAll() + // 乐读派作品表单(编目/分页快照):支持外部系统无 Token 调用;有 Token 时仍校验作品归属 + .requestMatchers("/public/leai-works/**").permitAll() // 乐读派 Webhook 回调(无用户上下文,由乐读派服务端调用) .requestMatchers("/webhook/leai").permitAll() // Knife4j 文档 diff --git a/lesingle-creation-frontend/src/views/public/create/views/DubbingView.vue b/lesingle-creation-frontend/src/views/public/create/views/DubbingView.vue index 6d0b99a..5857d86 100644 --- a/lesingle-creation-frontend/src/views/public/create/views/DubbingView.vue +++ b/lesingle-creation-frontend/src/views/public/create/views/DubbingView.vue @@ -456,6 +456,10 @@ function buildWorkFormPayload() { text: p.text, audioUrl: p.audioUrl || undefined, })) + + if (body.status < 4) { + body.status = 4 + } return body } diff --git a/lesingle-creation-frontend/src/views/public/create/views/EditInfoView.vue b/lesingle-creation-frontend/src/views/public/create/views/EditInfoView.vue index eff272f..7b8278a 100644 --- a/lesingle-creation-frontend/src/views/public/create/views/EditInfoView.vue +++ b/lesingle-creation-frontend/src/views/public/create/views/EditInfoView.vue @@ -269,7 +269,9 @@ async function saveFormToServer() { payload.subtitle = form.value.subtitle.trim() payload.intro = form.value.intro.trim() payload.tags = [...selectedTags.value] - + if (payload.status < 4) { + payload.status = 4 + } await saveLeaiWorkForm(id, payload) workFormSnapshot.value = cloneWorkFormSnapshot(payload)