From 889be533469ae881c874902a46c419df477def9d Mon Sep 17 00:00:00 2001 From: zhonghua Date: Thu, 16 Apr 2026 15:26:29 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E7=BB=88=E7=AB=AF=E8=AE=BE=E5=A4=87?= =?UTF-8?q?=E4=B9=90=E8=AF=BB=E6=B4=BE=E8=A1=A8=E5=8D=95=E6=8E=A5=E5=8F=A3?= =?UTF-8?q?=E4=BD=BF=E7=94=A8=E5=AE=9E=E4=BD=93=E4=B8=8E=E6=96=87=E6=A1=A3?= =?UTF-8?q?=E6=9B=B4=E6=96=B0=EF=BC=8C=E7=A7=BB=E9=99=A4=E4=BD=9C=E5=93=81?= =?UTF-8?q?=E8=AF=A6=E6=83=85=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Made-with: Cursor --- docs/api/device-api.md | 166 ++++++++++++------ .../device/controller/DeviceController.java | 39 +++- .../dto/DeviceLeaiWorkFormSaveRequest.java | 73 ++++++++ .../device/vo/DeviceLeaiWorkFormPageVo.java | 60 +++++++ .../device/vo/DeviceLeaiWorkFormVo.java | 86 +++++++++ .../modules/device/vo/DeviceWorkItemVo.java | 4 + 6 files changed, 366 insertions(+), 62 deletions(-) create mode 100644 lesingle-creation-backend/src/main/java/com/lesingle/modules/device/dto/DeviceLeaiWorkFormSaveRequest.java create mode 100644 lesingle-creation-backend/src/main/java/com/lesingle/modules/device/vo/DeviceLeaiWorkFormPageVo.java create mode 100644 lesingle-creation-backend/src/main/java/com/lesingle/modules/device/vo/DeviceLeaiWorkFormVo.java diff --git a/docs/api/device-api.md b/docs/api/device-api.md index 37f3211..96304ce 100644 --- a/docs/api/device-api.md +++ b/docs/api/device-api.md @@ -12,8 +12,9 @@ 1. [发送短信验证码](#1-发送短信验证码) 2. [手机验证码登录](#2-手机验证码登录) 3. [用户作品列表](#3-用户作品列表) -4. [作品详情](#4-作品详情) -5. [通用错误码](#5-通用错误码) +4. [查询乐读派作品表单](#4-查询乐读派作品表单) +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 | 作品标题 | | coverUrl | string | 封面图URL,可为 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` | | modifyTime | string | 修改时间,格式 `yyyy-MM-ddTHH:mm:ss` | +列表项中的 `remoteWorkId` 与乐读派创作链路一致;编目/分页快照读写请使用 [§4 / §5](#4-查询乐读派作品表单)。 + **响应示例** ```json @@ -215,6 +219,7 @@ GET /api/device/works?page=1&pageSize=10&status=unpublished&keyword=测试 "list": [ { "id": 38, + "remoteWorkId": "2044624699115311104", "title": "我的绘本", "coverUrl": "https://oss.example.com/cover.png", "description": "一个有趣的故事", @@ -227,6 +232,7 @@ GET /api/device/works?page=1&pageSize=10&status=unpublished&keyword=测试 }, { "id": 37, + "remoteWorkId": "2044624699115310999", "title": "春天的故事", "coverUrl": "https://oss.example.com/cover2.png", "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=成功 | -| data | object | 作品详情 | -| data.id | long | 作品ID | -| data.title | string | 作品标题 | -| data.authorName | string | 作者名称 | -| data.coverUrl | string | 封面图URL | -| data.description | string | 作品描述,可为 null | -| data.status | string | 状态 | -| data.visibility | string | 可见范围:`public`/`designated`/`internal`/`private` | -| data.createTime | string | 创建时间 | -| data.modifyTime | string | 修改时间 | -| data.pages | array | 页面列表,按页码升序排列 | +| workId | string | 与 remoteWorkId 一致 | +| status | int | 乐读派创作进度 `leai_status` | +| title | string | 标题,可为 null | +| author | string | 作者署名,可为 null | +| coverUrl | string | 封面 URL,可为 null | +| subtitle | string | 副标题,可为 null | +| intro | string | 简介,可为 null | +| tags | array | 标签列表,元素类型由业务决定,可为 null | +| pageList | array | 分页快照,见下表 | -**pages 数组项字段** +**`pageList` 数组项字段** | 字段 | 类型 | 说明 | |------|------|------| -| pageNo | int | 页码,从1开始 | -| imageUrl | string | 页面图片URL | -| text | string | 页面文字内容,可为 null | -| audioUrl | string | 页面音频URL,可为 null | +| pageNum | int | 页码 | +| imageUrl | string | 页图 URL,可为 null | +| text | string | 页文案,可为 null | +| audioUrl | string | 页音频 URL,可为 null | **响应示例** @@ -306,36 +309,25 @@ GET /api/device/works/38 "code": 200, "message": "success", "data": { - "id": 38, + "workId": "2044624699115311104", + "status": 5, "title": "我的绘本", - "authorName": "小明", + "author": "小明", "coverUrl": "https://oss.example.com/cover.png", - "description": "一个有趣的故事", - "status": "unpublished", - "visibility": "private", - "createTime": "2026-04-10T17:07:00", - "modifyTime": "2026-04-10T17:10:53", - "pages": [ + "subtitle": null, + "intro": "简介内容", + "tags": ["童话", "原创"], + "pageList": [ { - "pageNo": 1, - "imageUrl": "https://oss.example.com/page_0.png", + "pageNum": 1, + "imageUrl": "https://oss.example.com/p1.png", "text": "从前有一座山", - "audioUrl": "https://oss.example.com/page_0.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 + "audioUrl": "https://oss.example.com/p1.mp3" } ] - } + }, + "timestamp": "2026-04-11T12:00:00", + "path": "/api/device/leai-works/2044624699115311104/work-form" } ``` @@ -343,12 +335,74 @@ GET /api/device/works/38 | code | 说明 | |------|------| -| 404 | 作品不存在 | -| 403 | 无权访问该作品(非本人作品) | +| 401 | 未登录或 Token 无效 | +| 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} │ │ └─ 参数: page, pageSize, status(可选), keyword(可选) │ │ │ -│ 4. 作品详情 GET /device/works/{id} │ -│ └─ Header: Authorization: Bearer {token} │ -│ └─ 返回: 完整页面数据(图片+文字+音频) │ +│ 4. 乐读派表单 GET/PUT /device/leai-works/{remoteWorkId}/work-form │ +│ └─ 列表项中的 remoteWorkId 作为路径参数 │ +│ └─ 用于编目/分页快照同步(与创作壳 work-form 一致) │ │ │ └─────────────────────────────────────────────────────────┘ ``` diff --git a/lesingle-creation-backend/src/main/java/com/lesingle/modules/device/controller/DeviceController.java b/lesingle-creation-backend/src/main/java/com/lesingle/modules/device/controller/DeviceController.java index 3036808..8210a97 100644 --- a/lesingle-creation-backend/src/main/java/com/lesingle/modules/device/controller/DeviceController.java +++ b/lesingle-creation-backend/src/main/java/com/lesingle/modules/device/controller/DeviceController.java @@ -4,12 +4,15 @@ import com.lesingle.common.annotation.RateLimit; import com.lesingle.common.result.PageResult; import com.lesingle.common.result.Result; 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.DeviceWorkDetailVo; import com.lesingle.modules.device.vo.DeviceWorkItemVo; import com.lesingle.modules.pub.dto.PublicSmsLoginDto; import com.lesingle.modules.pub.dto.SendSmsCodeDto; 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.SmsCodeService; import com.lesingle.modules.ugc.entity.UgcWork; @@ -37,6 +40,7 @@ public class DeviceController { private final PublicAuthService publicAuthService; private final SmsCodeService smsCodeService; private final PublicUserWorkService publicUserWorkService; + private final PublicLeaiWorkFormService publicLeaiWorkFormService; @Public @PostMapping("/auth/sms/send") @@ -71,11 +75,34 @@ public class DeviceController { return Result.success(new PageResult<>(voList, workPage.getTotal(), workPage.getPage(), workPage.getPageSize())); } - @GetMapping("/works/{id}") - @Operation(summary = "终端设备-作品详情") - public Result getWorkDetail(@PathVariable Long id) { + /** + * 与 {@code GET /public/leai-works/{remoteWorkId}/work-form} 等价,设备端须携带 Token,仅可操作本人作品。 + */ + @GetMapping("/leai-works/{remoteWorkId}/work-form") + @Operation(summary = "终端设备-作品详情-查询本库乐读派作品表单(编目/分页快照)") + public Result getLeaiWorkForm(@PathVariable String remoteWorkId) { Long userId = SecurityUtil.getCurrentUserId(); - Map detailMap = publicUserWorkService.findDetail(id, userId); - return Result.success(DeviceWorkDetailVo.fromDetailMap(detailMap)); + return Result.success(DeviceLeaiWorkFormVo.fromDetailMap( + publicLeaiWorkFormService.getWorkFormDetail(userId, remoteWorkId))); + } + + /** + * 与 {@code PUT /public/leai-works/{remoteWorkId}/work-form} 等价,设备端须携带 Token。 + */ + @PutMapping("/leai-works/{remoteWorkId}/work-form") + @Operation(summary = "终端设备-作品详情编辑-保存本库乐读派作品表单(编目/分页快照)") + public Result saveLeaiWorkForm( + @PathVariable String remoteWorkId, + @RequestBody DeviceLeaiWorkFormSaveRequest body) { + Long userId = SecurityUtil.getCurrentUserId(); + if (body == null) { + throw new BusinessException(400, "请求体不能为空"); + } + Map map = body.toBodyMap(); + if (map.isEmpty()) { + throw new BusinessException(400, "请求体不能为空"); + } + publicLeaiWorkFormService.saveWorkForm(userId, remoteWorkId, map); + return Result.success(); } } diff --git a/lesingle-creation-backend/src/main/java/com/lesingle/modules/device/dto/DeviceLeaiWorkFormSaveRequest.java b/lesingle-creation-backend/src/main/java/com/lesingle/modules/device/dto/DeviceLeaiWorkFormSaveRequest.java new file mode 100644 index 0000000..601cab2 --- /dev/null +++ b/lesingle-creation-backend/src/main/java/com/lesingle/modules/device/dto/DeviceLeaiWorkFormSaveRequest.java @@ -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 tags; + + @Schema(description = "乐读派进度,写入 leai_status") + private Integer status; + + @Schema(description = "分页快照") + private List pageList; + + /** + * 转为服务层 {@code Map},仅包含非 null 字段(嵌套 pageList 转为 List<Map>) + */ + public Map toBodyMap() { + Map 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> rows = new ArrayList<>(); + for (DeviceLeaiWorkFormPageVo p : pageList) { + rows.add(p.toRowMap()); + } + body.put("pageList", rows); + } + return body; + } +} diff --git a/lesingle-creation-backend/src/main/java/com/lesingle/modules/device/vo/DeviceLeaiWorkFormPageVo.java b/lesingle-creation-backend/src/main/java/com/lesingle/modules/device/vo/DeviceLeaiWorkFormPageVo.java new file mode 100644 index 0000000..51669df --- /dev/null +++ b/lesingle-creation-backend/src/main/java/com/lesingle/modules/device/vo/DeviceLeaiWorkFormPageVo.java @@ -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 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 toRowMap() { + Map 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; + } +} diff --git a/lesingle-creation-backend/src/main/java/com/lesingle/modules/device/vo/DeviceLeaiWorkFormVo.java b/lesingle-creation-backend/src/main/java/com/lesingle/modules/device/vo/DeviceLeaiWorkFormVo.java new file mode 100644 index 0000000..d18427c --- /dev/null +++ b/lesingle-creation-backend/src/main/java/com/lesingle/modules/device/vo/DeviceLeaiWorkFormVo.java @@ -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 tags; + + @Schema(description = "分页列表") + private List pageList; + + @SuppressWarnings("unchecked") + public static DeviceLeaiWorkFormVo fromDetailMap(Map 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) tagsObj)); + } + + Object pl = map.get("pageList"); + if (pl instanceof List) { + List pages = new ArrayList<>(); + for (Object o : (List) pl) { + if (o instanceof Map) { + pages.add(DeviceLeaiWorkFormPageVo.fromMap((Map) o)); + } + } + vo.setPageList(pages); + } + return vo; + } + + private static String str(Object o) { + return o == null ? null : String.valueOf(o); + } +} diff --git a/lesingle-creation-backend/src/main/java/com/lesingle/modules/device/vo/DeviceWorkItemVo.java b/lesingle-creation-backend/src/main/java/com/lesingle/modules/device/vo/DeviceWorkItemVo.java index e958d88..5478ff3 100644 --- a/lesingle-creation-backend/src/main/java/com/lesingle/modules/device/vo/DeviceWorkItemVo.java +++ b/lesingle-creation-backend/src/main/java/com/lesingle/modules/device/vo/DeviceWorkItemVo.java @@ -17,6 +17,9 @@ public class DeviceWorkItemVo implements Serializable { @Schema(description = "作品ID") private Long id; + @Schema(description = "乐读派 remoteWorkId,与 GET/PUT .../leai-works/{remoteWorkId}/work-form 对应,可能为 null") + private String remoteWorkId; + @Schema(description = "作品标题") private String title; @@ -50,6 +53,7 @@ public class DeviceWorkItemVo implements Serializable { public static DeviceWorkItemVo fromEntity(UgcWork work) { DeviceWorkItemVo vo = new DeviceWorkItemVo(); vo.setId(work.getId()); + vo.setRemoteWorkId(work.getRemoteWorkId()); vo.setTitle(work.getTitle()); vo.setCoverUrl(work.getCoverUrl()); vo.setDescription(work.getDescription());