feat: 终端设备乐读派表单接口使用实体与文档更新,移除作品详情接口

Made-with: Cursor
This commit is contained in:
zhonghua 2026-04-16 15:26:29 +08:00
parent 41ecbd216f
commit 889be53346
6 changed files with 366 additions and 62 deletions

View File

@ -12,8 +12,9 @@
1. [发送短信验证码](#1-发送短信验证码) 1. [发送短信验证码](#1-发送短信验证码)
2. [手机验证码登录](#2-手机验证码登录) 2. [手机验证码登录](#2-手机验证码登录)
3. [用户作品列表](#3-用户作品列表) 3. [用户作品列表](#3-用户作品列表)
4. [作品详情](#4-作品详情) 4. [查询乐读派作品表单](#4-查询乐读派作品表单)
5. [通用错误码](#5-通用错误码) 5. [保存乐读派作品表单](#5-保存乐读派作品表单)
6. [通用错误码](#6-通用错误码)
--- ---
@ -194,7 +195,8 @@ GET /api/device/works?page=1&pageSize=10&status=unpublished&keyword=测试
| 字段 | 类型 | 说明 | | 字段 | 类型 | 说明 |
|------|------|------| |------|------|------|
| id | long | 作品ID | | id | long | 作品ID本库主键 |
| remoteWorkId | string | 乐读派作品 ID用于调用下文 `GET/PUT /device/leai-works/{remoteWorkId}/work-form`;可能为 null |
| title | string | 作品标题 | | title | string | 作品标题 |
| coverUrl | string | 封面图URL可为 null | | coverUrl | string | 封面图URL可为 null |
| description | string | 作品描述,可为 null | | description | string | 作品描述,可为 null |
@ -205,6 +207,8 @@ GET /api/device/works?page=1&pageSize=10&status=unpublished&keyword=测试
| createTime | string | 创建时间,格式 `yyyy-MM-ddTHH:mm:ss` | | createTime | string | 创建时间,格式 `yyyy-MM-ddTHH:mm:ss` |
| modifyTime | string | 修改时间,格式 `yyyy-MM-ddTHH:mm:ss` | | modifyTime | string | 修改时间,格式 `yyyy-MM-ddTHH:mm:ss` |
列表项中的 `remoteWorkId` 与乐读派创作链路一致;编目/分页快照读写请使用 [§4 / §5](#4-查询乐读派作品表单)。
**响应示例** **响应示例**
```json ```json
@ -215,6 +219,7 @@ GET /api/device/works?page=1&pageSize=10&status=unpublished&keyword=测试
"list": [ "list": [
{ {
"id": 38, "id": 38,
"remoteWorkId": "2044624699115311104",
"title": "我的绘本", "title": "我的绘本",
"coverUrl": "https://oss.example.com/cover.png", "coverUrl": "https://oss.example.com/cover.png",
"description": "一个有趣的故事", "description": "一个有趣的故事",
@ -227,6 +232,7 @@ GET /api/device/works?page=1&pageSize=10&status=unpublished&keyword=测试
}, },
{ {
"id": 37, "id": 37,
"remoteWorkId": "2044624699115310999",
"title": "春天的故事", "title": "春天的故事",
"coverUrl": "https://oss.example.com/cover2.png", "coverUrl": "https://oss.example.com/cover2.png",
"description": null, "description": null,
@ -247,12 +253,12 @@ GET /api/device/works?page=1&pageSize=10&status=unpublished&keyword=测试
--- ---
### 4. 作品详情 ### 4. 查询乐读派作品表单
获取指定作品的完整信息,包含所有页面内容(图片、文字、音频) 获取本库中该乐读派作品的编目/分页快照(不经乐读派 B2 GET。**必须携带 Token**;仅可操作当前登录用户本人的作品
``` ```
GET /device/works/{id} GET /device/leai-works/{remoteWorkId}/work-form
``` ```
**请求头** **请求头**
@ -265,39 +271,36 @@ GET /device/works/{id}
| 字段 | 类型 | 必填 | 说明 | | 字段 | 类型 | 必填 | 说明 |
|------|------|------|------| |------|------|------|------|
| id | long | 是 | 作品ID | | remoteWorkId | string | 是 | 乐读派作品 ID可能为大整数建议按字符串传递URL 需编码) |
**请求示例** **请求示例**
``` ```
GET /api/device/works/38 GET /api/device/leai-works/2044624699115311104/work-form
``` ```
**响应参数** **成功响应 `data` 字段说明**(与公众端 `GET /public/leai-works/{remoteWorkId}/work-form` 语义一致;设备端以结构化对象返回,字段名与下表一致)
| 字段 | 类型 | 说明 | | 字段 | 类型 | 说明 |
|------|------|------| |------|------|------|
| code | int | 状态码200=成功 | | workId | string | 与 remoteWorkId 一致 |
| data | object | 作品详情 | | status | int | 乐读派创作进度 `leai_status` |
| data.id | long | 作品ID | | title | string | 标题,可为 null |
| data.title | string | 作品标题 | | author | string | 作者署名,可为 null |
| data.authorName | string | 作者名称 | | coverUrl | string | 封面 URL可为 null |
| data.coverUrl | string | 封面图URL | | subtitle | string | 副标题,可为 null |
| data.description | string | 作品描述,可为 null | | intro | string | 简介,可为 null |
| data.status | string | 状态 | | tags | array | 标签列表,元素类型由业务决定,可为 null |
| data.visibility | string | 可见范围:`public`/`designated`/`internal`/`private` | | pageList | array | 分页快照,见下表 |
| data.createTime | string | 创建时间 |
| data.modifyTime | string | 修改时间 |
| data.pages | array | 页面列表,按页码升序排列 |
**pages 数组项字段** **`pageList` 数组项字段**
| 字段 | 类型 | 说明 | | 字段 | 类型 | 说明 |
|------|------|------| |------|------|------|
| pageNo | int | 页码从1开始 | | pageNum | int | 页码 |
| imageUrl | string | 页面图片URL | | imageUrl | string | 页图 URL可为 null |
| text | string | 页面文字内容,可为 null | | text | string | 页文案,可为 null |
| audioUrl | string | 页音频URL可为 null | | audioUrl | string | 页音频 URL可为 null |
**响应示例** **响应示例**
@ -306,36 +309,25 @@ GET /api/device/works/38
"code": 200, "code": 200,
"message": "success", "message": "success",
"data": { "data": {
"id": 38, "workId": "2044624699115311104",
"status": 5,
"title": "我的绘本", "title": "我的绘本",
"authorName": "小明", "author": "小明",
"coverUrl": "https://oss.example.com/cover.png", "coverUrl": "https://oss.example.com/cover.png",
"description": "一个有趣的故事", "subtitle": null,
"status": "unpublished", "intro": "简介内容",
"visibility": "private", "tags": ["童话", "原创"],
"createTime": "2026-04-10T17:07:00", "pageList": [
"modifyTime": "2026-04-10T17:10:53",
"pages": [
{ {
"pageNo": 1, "pageNum": 1,
"imageUrl": "https://oss.example.com/page_0.png", "imageUrl": "https://oss.example.com/p1.png",
"text": "从前有一座山", "text": "从前有一座山",
"audioUrl": "https://oss.example.com/page_0.mp3" "audioUrl": "https://oss.example.com/p1.mp3"
},
{
"pageNo": 2,
"imageUrl": "https://oss.example.com/page_1.png",
"text": "山里有一座庙",
"audioUrl": "https://oss.example.com/page_1.mp3"
},
{
"pageNo": 3,
"imageUrl": "https://oss.example.com/page_2.png",
"text": "庙里有一个老和尚",
"audioUrl": null
} }
] ]
} },
"timestamp": "2026-04-11T12:00:00",
"path": "/api/device/leai-works/2044624699115311104/work-form"
} }
``` ```
@ -343,12 +335,74 @@ GET /api/device/works/38
| code | 说明 | | code | 说明 |
|------|------| |------|------|
| 404 | 作品不存在 | | 401 | 未登录或 Token 无效 |
| 403 | 无权访问该作品(非本人作品 | | 404 | 作品不存在或无权操作(非本人 |
--- ---
## 5. 通用错误码 ### 5. 保存乐读派作品表单
保存编目与分页快照至本库(不经乐读派 PUT。**必须携带 Token**;请求体非空。
```
PUT /device/leai-works/{remoteWorkId}/work-form
```
**请求头**
| 参数 | 值 | 必填 | 说明 |
|------|------|------|------|
| Authorization | Bearer {token} | 是 | 登录接口返回的 Token |
| Content-Type | application/json | 是 | JSON 请求体 |
**路径参数**:同 [§4](#4-查询乐读派作品表单)。
**请求体JSON**:字段均可选,但**至少需一项非空**;服务端将非 null 字段组装为内部 Map 再落库,与 [public-leai-work-form.md](./public-leai-work-form.md) 中 PUT 语义一致。发送 `{}` 或全部为 null 的等价体将返回 **400**`请求体不能为空`)。
| 字段 | 类型 | 说明 |
|------|------|------|
| author | string | 作者署名 |
| title | string | 标题 |
| subtitle | string | 副标题 |
| intro | string | 简介 |
| tags | array | 标签 |
| status | int | 乐读派进度,写入 `leai_status` |
| pageList | array | 分页快照,见下表 |
**`pageList` 数组项字段**
| 字段 | 类型 | 说明 |
|------|------|------|
| pageNum | int | 页码 |
| imageUrl | string | 页图 URL |
| text | string | 页文案 |
| audioUrl | string | 页音频 URL |
**请求示例**
```json
{
"title": "我的绘本",
"author": "小明",
"status": 4,
"pageList": [
{
"pageNum": 1,
"imageUrl": "https://oss.example.com/p1.png",
"text": "从前有一座山",
"audioUrl": "https://oss.example.com/p1.mp3"
}
]
}
```
**成功响应**`code=200``data` 通常为 null。
**错误情况**:同 §4另含参数校验失败或空请求体时 **400**
---
## 6. 通用错误码
所有接口统一响应格式: 所有接口统一响应格式:
@ -391,9 +445,9 @@ GET /api/device/works/38
│ └─ Header: Authorization: Bearer {token} │ │ └─ Header: Authorization: Bearer {token} │
│ └─ 参数: page, pageSize, status(可选), keyword(可选) │ │ └─ 参数: page, pageSize, status(可选), keyword(可选) │
│ │ │ │
│ 4. 作品详情 GET /device/works/{id} │ 4. 乐读派表单 GET/PUT /device/leai-works/{remoteWorkId}/work-form
│ └─ Header: Authorization: Bearer {token} │ └─ 列表项中的 remoteWorkId 作为路径参数
│ └─ 返回: 完整页面数据(图片+文字+音频) │ └─ 用于编目/分页快照同步(与创作壳 work-form 一致)
│ │ │ │
└─────────────────────────────────────────────────────────┘ └─────────────────────────────────────────────────────────┘
``` ```

View File

@ -4,12 +4,15 @@ import com.lesingle.common.annotation.RateLimit;
import com.lesingle.common.result.PageResult; import com.lesingle.common.result.PageResult;
import com.lesingle.common.result.Result; import com.lesingle.common.result.Result;
import com.lesingle.common.util.SecurityUtil; import com.lesingle.common.util.SecurityUtil;
import com.lesingle.common.exception.BusinessException;
import com.lesingle.modules.device.dto.DeviceLeaiWorkFormSaveRequest;
import com.lesingle.modules.device.vo.DeviceLeaiWorkFormVo;
import com.lesingle.modules.device.vo.DeviceLoginVo; import com.lesingle.modules.device.vo.DeviceLoginVo;
import com.lesingle.modules.device.vo.DeviceWorkDetailVo;
import com.lesingle.modules.device.vo.DeviceWorkItemVo; import com.lesingle.modules.device.vo.DeviceWorkItemVo;
import com.lesingle.modules.pub.dto.PublicSmsLoginDto; import com.lesingle.modules.pub.dto.PublicSmsLoginDto;
import com.lesingle.modules.pub.dto.SendSmsCodeDto; import com.lesingle.modules.pub.dto.SendSmsCodeDto;
import com.lesingle.modules.pub.service.PublicAuthService; import com.lesingle.modules.pub.service.PublicAuthService;
import com.lesingle.modules.pub.service.PublicLeaiWorkFormService;
import com.lesingle.modules.pub.service.PublicUserWorkService; import com.lesingle.modules.pub.service.PublicUserWorkService;
import com.lesingle.modules.pub.service.SmsCodeService; import com.lesingle.modules.pub.service.SmsCodeService;
import com.lesingle.modules.ugc.entity.UgcWork; import com.lesingle.modules.ugc.entity.UgcWork;
@ -37,6 +40,7 @@ public class DeviceController {
private final PublicAuthService publicAuthService; private final PublicAuthService publicAuthService;
private final SmsCodeService smsCodeService; private final SmsCodeService smsCodeService;
private final PublicUserWorkService publicUserWorkService; private final PublicUserWorkService publicUserWorkService;
private final PublicLeaiWorkFormService publicLeaiWorkFormService;
@Public @Public
@PostMapping("/auth/sms/send") @PostMapping("/auth/sms/send")
@ -71,11 +75,34 @@ public class DeviceController {
return Result.success(new PageResult<>(voList, workPage.getTotal(), workPage.getPage(), workPage.getPageSize())); return Result.success(new PageResult<>(voList, workPage.getTotal(), workPage.getPage(), workPage.getPageSize()));
} }
@GetMapping("/works/{id}") /**
@Operation(summary = "终端设备-作品详情") * {@code GET /public/leai-works/{remoteWorkId}/work-form} 等价设备端须携带 Token仅可操作本人作品
public Result<DeviceWorkDetailVo> getWorkDetail(@PathVariable Long id) { */
@GetMapping("/leai-works/{remoteWorkId}/work-form")
@Operation(summary = "终端设备-作品详情-查询本库乐读派作品表单(编目/分页快照)")
public Result<DeviceLeaiWorkFormVo> getLeaiWorkForm(@PathVariable String remoteWorkId) {
Long userId = SecurityUtil.getCurrentUserId(); Long userId = SecurityUtil.getCurrentUserId();
Map<String, Object> detailMap = publicUserWorkService.findDetail(id, userId); return Result.success(DeviceLeaiWorkFormVo.fromDetailMap(
return Result.success(DeviceWorkDetailVo.fromDetailMap(detailMap)); publicLeaiWorkFormService.getWorkFormDetail(userId, remoteWorkId)));
}
/**
* {@code PUT /public/leai-works/{remoteWorkId}/work-form} 等价设备端须携带 Token
*/
@PutMapping("/leai-works/{remoteWorkId}/work-form")
@Operation(summary = "终端设备-作品详情编辑-保存本库乐读派作品表单(编目/分页快照)")
public Result<Void> saveLeaiWorkForm(
@PathVariable String remoteWorkId,
@RequestBody DeviceLeaiWorkFormSaveRequest body) {
Long userId = SecurityUtil.getCurrentUserId();
if (body == null) {
throw new BusinessException(400, "请求体不能为空");
}
Map<String, Object> map = body.toBodyMap();
if (map.isEmpty()) {
throw new BusinessException(400, "请求体不能为空");
}
publicLeaiWorkFormService.saveWorkForm(userId, remoteWorkId, map);
return Result.success();
} }
} }

View File

@ -0,0 +1,73 @@
package com.lesingle.modules.device.dto;
import com.lesingle.modules.device.vo.DeviceLeaiWorkFormPageVo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* 终端设备 PUT work-form 请求体字段均可选但至少需一项非空 {@code saveWorkForm} Map 语义一致
*/
@Data
@Schema(description = "终端设备-保存乐读派作品表单")
public class DeviceLeaiWorkFormSaveRequest implements Serializable {
@Schema(description = "作者署名")
private String author;
@Schema(description = "标题")
private String title;
@Schema(description = "副标题")
private String subtitle;
@Schema(description = "简介")
private String intro;
@Schema(description = "标签")
private List<Object> tags;
@Schema(description = "乐读派进度,写入 leai_status")
private Integer status;
@Schema(description = "分页快照")
private List<DeviceLeaiWorkFormPageVo> pageList;
/**
* 转为服务层 {@code Map}仅包含非 null 字段嵌套 pageList 转为 List&lt;Map&gt;
*/
public Map<String, Object> toBodyMap() {
Map<String, Object> body = new LinkedHashMap<>();
if (author != null) {
body.put("author", author);
}
if (title != null) {
body.put("title", title);
}
if (subtitle != null) {
body.put("subtitle", subtitle);
}
if (intro != null) {
body.put("intro", intro);
}
if (tags != null) {
body.put("tags", tags);
}
if (status != null) {
body.put("status", status);
}
if (pageList != null && !pageList.isEmpty()) {
List<Map<String, Object>> rows = new ArrayList<>();
for (DeviceLeaiWorkFormPageVo p : pageList) {
rows.add(p.toRowMap());
}
body.put("pageList", rows);
}
return body;
}
}

View File

@ -0,0 +1,60 @@
package com.lesingle.modules.device.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* 乐读派 work-form 分页项 pageList 中单页结构一致
*/
@Data
@Schema(description = "乐读派作品表单分页项")
public class DeviceLeaiWorkFormPageVo implements Serializable {
@Schema(description = "页码(本库从 1 起)")
private Integer pageNum;
@Schema(description = "插图 URL")
private String imageUrl;
@Schema(description = "本页文案")
private String text;
@Schema(description = "配音 URL")
private String audioUrl;
public static DeviceLeaiWorkFormPageVo fromMap(Map<String, Object> m) {
if (m == null) {
return null;
}
DeviceLeaiWorkFormPageVo vo = new DeviceLeaiWorkFormPageVo();
Object pn = m.get("pageNum");
if (pn instanceof Number) {
vo.setPageNum(((Number) pn).intValue());
}
vo.setImageUrl(m.get("imageUrl") != null ? String.valueOf(m.get("imageUrl")) : null);
vo.setText(m.get("text") != null ? String.valueOf(m.get("text")) : null);
vo.setAudioUrl(m.get("audioUrl") != null ? String.valueOf(m.get("audioUrl")) : null);
return vo;
}
public Map<String, Object> toRowMap() {
Map<String, Object> row = new LinkedHashMap<>();
if (pageNum != null) {
row.put("pageNum", pageNum);
}
if (imageUrl != null) {
row.put("imageUrl", imageUrl);
}
if (text != null) {
row.put("text", text);
}
if (audioUrl != null) {
row.put("audioUrl", audioUrl);
}
return row;
}
}

View File

@ -0,0 +1,86 @@
package com.lesingle.modules.device.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* 终端设备 GET work-form 响应 {@link com.lesingle.modules.pub.service.PublicLeaiWorkFormService#getWorkFormDetail} 返回 Map 对齐
*/
@Data
@Schema(description = "终端设备-乐读派作品表单详情")
public class DeviceLeaiWorkFormVo implements Serializable {
@Schema(description = "乐读派作品 ID")
private String workId;
@Schema(description = "乐读派创作进度 leai_status")
private Integer status;
@Schema(description = "标题")
private String title;
@Schema(description = "作者署名")
private String author;
@Schema(description = "封面 URL")
private String coverUrl;
@Schema(description = "副标题")
private String subtitle;
@Schema(description = "简介")
private String intro;
@Schema(description = "标签")
private List<Object> tags;
@Schema(description = "分页列表")
private List<DeviceLeaiWorkFormPageVo> pageList;
@SuppressWarnings("unchecked")
public static DeviceLeaiWorkFormVo fromDetailMap(Map<String, Object> map) {
if (map == null) {
return null;
}
DeviceLeaiWorkFormVo vo = new DeviceLeaiWorkFormVo();
Object w = map.get("workId");
vo.setWorkId(w != null ? String.valueOf(w) : null);
Object st = map.get("status");
if (st instanceof Number) {
vo.setStatus(((Number) st).intValue());
}
vo.setTitle(str(map.get("title")));
vo.setAuthor(str(map.get("author")));
vo.setCoverUrl(str(map.get("coverUrl")));
vo.setSubtitle(str(map.get("subtitle")));
vo.setIntro(str(map.get("intro")));
Object tagsObj = map.get("tags");
if (tagsObj instanceof List) {
vo.setTags(new ArrayList<>((List<Object>) tagsObj));
}
Object pl = map.get("pageList");
if (pl instanceof List) {
List<DeviceLeaiWorkFormPageVo> pages = new ArrayList<>();
for (Object o : (List<?>) pl) {
if (o instanceof Map) {
pages.add(DeviceLeaiWorkFormPageVo.fromMap((Map<String, Object>) o));
}
}
vo.setPageList(pages);
}
return vo;
}
private static String str(Object o) {
return o == null ? null : String.valueOf(o);
}
}

View File

@ -17,6 +17,9 @@ public class DeviceWorkItemVo implements Serializable {
@Schema(description = "作品ID") @Schema(description = "作品ID")
private Long id; private Long id;
@Schema(description = "乐读派 remoteWorkId与 GET/PUT .../leai-works/{remoteWorkId}/work-form 对应,可能为 null")
private String remoteWorkId;
@Schema(description = "作品标题") @Schema(description = "作品标题")
private String title; private String title;
@ -50,6 +53,7 @@ public class DeviceWorkItemVo implements Serializable {
public static DeviceWorkItemVo fromEntity(UgcWork work) { public static DeviceWorkItemVo fromEntity(UgcWork work) {
DeviceWorkItemVo vo = new DeviceWorkItemVo(); DeviceWorkItemVo vo = new DeviceWorkItemVo();
vo.setId(work.getId()); vo.setId(work.getId());
vo.setRemoteWorkId(work.getRemoteWorkId());
vo.setTitle(work.getTitle()); vo.setTitle(work.getTitle());
vo.setCoverUrl(work.getCoverUrl()); vo.setCoverUrl(work.getCoverUrl());
vo.setDescription(work.getDescription()); vo.setDescription(work.getDescription());