feat: work-form 无 Token 白名单与 leai_status 原样持久化
- Security: /public/leai-works/** 放行;Controller 使用 getCurrentUserIdOrNull;匿名仅按 remoteWorkId 定位作品 - saveWorkForm: 请求体 status 写入 leai_status(原样覆盖,不做区间收敛) - 文档与编目/配音页相关调整 Made-with: Cursor
This commit is contained in:
parent
b2ae6653d5
commit
5ae9233afc
@ -6,8 +6,8 @@
|
||||
|----|------|
|
||||
| Base URL | 部署域名 + **context-path `/api`**(见 `application.yml` `server.servlet.context-path`) |
|
||||
| 完整路径前缀 | `/api/public/leai-works` |
|
||||
| 认证 | **需要登录**。请求头携带 `Authorization: Bearer <JWT>`;与 Web 端一致时可加 `X-Tenant-Code`、`X-Tenant-Id`(见前端 `request.ts`) |
|
||||
| 安全说明 | 该路径**不在** Spring Security 匿名白名单中,未带有效 Token 将返回 **401** |
|
||||
| 认证 | **可选**。未带 Token 时接口仍可访问(Spring Security 白名单 `/public/leai-works/**`),供外部系统对接;携带 `Authorization: Bearer <JWT>` 时,后端会校验作品归属当前用户。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)**。
|
||||
|
||||
### 成功响应
|
||||
|
||||
@ -24,7 +24,7 @@ public class PublicLeaiWorkController {
|
||||
@GetMapping("/{remoteWorkId}/work-form")
|
||||
@Operation(summary = "查询本库作品详情(编目/分页快照,不经乐读派 B2 GET)")
|
||||
public Result<Map<String, Object>> 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<Void> saveWorkForm(
|
||||
@PathVariable String remoteWorkId,
|
||||
@RequestBody Map<String, Object> body) {
|
||||
Long userId = SecurityUtil.getCurrentUserId();
|
||||
Long userId = SecurityUtil.getCurrentUserIdOrNull();
|
||||
publicLeaiWorkFormService.saveWorkForm(userId, remoteWorkId, body);
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
@ -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<String, Object> 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<UgcWork> 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<Map<String, Object>> pageList = (List<Map<String, Object>>) 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<UgcWork> 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, "作品不存在或无权操作");
|
||||
|
||||
@ -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 文档
|
||||
|
||||
@ -456,6 +456,10 @@ function buildWorkFormPayload() {
|
||||
text: p.text,
|
||||
audioUrl: p.audioUrl || undefined,
|
||||
}))
|
||||
|
||||
if (body.status < 4) {
|
||||
body.status = 4
|
||||
}
|
||||
return body
|
||||
}
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user