fix(二次编目): 修复DUBBED状态下编辑绘本信息保存后仍显示旧数据的问题

根因:用户在DUBBED(5)终态下二次编辑绘本信息时,LeAI C1接口不持久化元数据更新,
但前端loadWork()始终从LeAI拉取数据,忽略了本地DB中已保存的新数据。

修复内容:
- 后端proxyGetWork新增mergeLocalMetadata,将本地DB元数据合并到LeAI响应中
- applyLocalMetadataFromProxyPut增加tags/subtitle/intro写入ai_meta(JSON)
- 前端EditInfoView和独立客户端EditInfo移除catch块中status>=4/5的错误掩盖逻辑

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
En 2026-04-15 15:14:38 +08:00
parent 61415eebe6
commit 12dc429d73
5 changed files with 152 additions and 15 deletions

View File

@ -195,15 +195,6 @@ async function handleSave() {
store.workDetail = null //
router.push(`/dubbing/${workId.value}`)
} catch (e) {
// CAS
try {
const check = await getWorkDetail(workId.value)
if (check?.data?.status >= 4) {
store.workDetail = null
router.push(`/dubbing/${workId.value}`)
return
}
} catch { /* ignore */ }
alert(e.message || '保存失败,请重试')
} finally {
saving.value = false

View File

@ -122,6 +122,7 @@ public class LeaiProxyController {
/**
* 查询作品详情
* GET /leai-proxy/work/{id} 乐读派 /api/v1/query/work/{id}
* 合并本地 DB 元数据确保二次编目后的修改不丢失
*/
@GetMapping("/work/{id}")
@Operation(summary = "查询作品详情代理")
@ -130,7 +131,10 @@ public class LeaiProxyController {
params.put("orgId", leaiConfig.getOrgId());
params.put("phone", getPhoneOrThrow());
log.info("[乐读派代理] 查询作品详情, workId={}", id);
return jsonOk(leaiApiClient.proxyGet("/query/work/" + id, params));
String responseBody = leaiApiClient.proxyGet("/query/work/" + id, params);
// 合并本地元数据二次编目后 LeAI 可能返回旧数据用本地 DB 覆盖
responseBody = leaiSyncService.mergeLocalMetadata(id, responseBody);
return jsonOk(responseBody);
}
/**
@ -160,6 +164,7 @@ public class LeaiProxyController {
body.putAll(requestBody);
log.info("[乐读派代理] 编辑作品, workId={}", id);
String responseBody = leaiApiClient.proxyPut("/update/work/" + id, body);
log.debug("[乐读派代理] 编辑作品响应, workId={}, response={}", id, responseBody);
syncLocalAfterLeaiUpdate(id, responseBody, requestBody);
return jsonOk(responseBody);
}

View File

@ -31,4 +31,14 @@ public interface ILeaiSyncService {
* @param proxyRequestBody 前端传入的 JSON通常含 authorsubtitleintrotags 不含 orgId/phone
*/
void applyLocalMetadataFromProxyPut(String remoteWorkId, Map<String, Object> proxyRequestBody);
/**
* 将本地 DB 中已保存的元数据authordescription合并到 LeAI B2 响应 JSON
* 避免二次编目后 LeAI 返回旧数据覆盖用户的本地编辑
*
* @param remoteWorkId 乐读派作品 ID
* @param leaiResponse LeAI B2 接口返回的原始 JSON 字符串
* @return 合并本地元数据后的 JSON 字符串
*/
String mergeLocalMetadata(String remoteWorkId, String leaiResponse);
}

View File

@ -2,6 +2,8 @@ package com.lesingle.modules.leai.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.lesingle.common.enums.Visibility;
import com.lesingle.modules.leai.enums.LeaiCreationStatus;
import com.lesingle.modules.leai.util.LeaiUtil;
@ -35,6 +37,7 @@ public class LeaiSyncService implements ILeaiSyncService {
private final UgcWorkPageMapper ugcWorkPageMapper;
private final LeaiApiClient leaiApiClient;
private final SysUserMapper sysUserMapper;
private final ObjectMapper objectMapper;
/**
* V4.0 核心同步逻辑
@ -424,6 +427,7 @@ public class LeaiSyncService implements ILeaiSyncService {
}
@Override
@SuppressWarnings("unchecked")
public void applyLocalMetadataFromProxyPut(String remoteWorkId, Map<String, Object> proxyRequestBody) {
if (remoteWorkId == null || remoteWorkId.isEmpty() || proxyRequestBody == null || proxyRequestBody.isEmpty()) {
return;
@ -435,6 +439,13 @@ public class LeaiSyncService implements ILeaiSyncService {
LambdaUpdateWrapper<UgcWork> wrapper = new LambdaUpdateWrapper<>();
wrapper.eq(UgcWork::getId, work.getId());
boolean any = false;
// 用于存储 subtitle/intro/tags ai_meta mergeLocalMetadata 精确读取
Map<String, Object> aiMetaMap = new HashMap<>();
if (work.getAiMeta() instanceof Map) {
aiMetaMap.putAll((Map<String, Object>) work.getAiMeta());
}
if (proxyRequestBody.containsKey("author")) {
wrapper.set(UgcWork::getAuthorName, LeaiUtil.toString(proxyRequestBody.get("author"), null));
any = true;
@ -460,11 +471,28 @@ public class LeaiSyncService implements ILeaiSyncService {
}
desc = desc.trim();
wrapper.set(UgcWork::getDescription, desc.isEmpty() ? null : desc);
// 精确存储 subtitle/intro ai_meta避免 description 反拆分丢失信息
aiMetaMap.put("_subtitle", sub);
aiMetaMap.put("_intro", intro);
any = true;
}
if (proxyRequestBody.containsKey("tags")) {
Object tags = proxyRequestBody.get("tags");
if (tags instanceof List) {
aiMetaMap.put("_tags", tags);
any = true;
}
}
if (!any) {
return;
}
// LambdaUpdateWrapper.set() 不会自动应用 JacksonTypeHandler
// 需要手动序列化为 JSON 字符串否则 Map 会以 Java toString 格式存入 DB
try {
wrapper.set(UgcWork::getAiMeta, objectMapper.writeValueAsString(aiMetaMap));
} catch (Exception e) {
log.warn("[ProxyPut] ai_meta 序列化失败,跳过 ai_meta 写入: remoteWorkId={}", remoteWorkId, e);
}
wrapper.set(UgcWork::getModifyTime, LocalDateTime.now());
ugcWorkMapper.update(null, wrapper);
log.info("[ProxyPut] 本地元数据已覆盖 remoteWorkId={}", remoteWorkId);
@ -490,4 +518,112 @@ public class LeaiSyncService implements ILeaiSyncService {
SysUser user = sysUserMapper.selectOne(wrapper);
return user != null ? user.getId() : null;
}
/**
* 将本地 DB 中已保存的元数据authorsubtitle/introtitletags合并到 LeAI B2 响应 JSON
* 避免二次编目后 LeAI 返回旧数据覆盖用户的本地编辑
* <p>
* 场景用户在 DUBBED(5) 状态下再次编辑绘本信息 applyLocalMetadataFromProxyPut 已将新数据
* 写入本地 DB 但前端 loadWork() 通过 getWorkDetail LeAI 拉取数据时拿到旧值
* 此方法在代理层将本地 DB 的元数据覆盖回 LeAI 响应确保前端看到最新编辑
*/
@Override
@SuppressWarnings("unchecked")
public String mergeLocalMetadata(String remoteWorkId, String leaiResponse) {
if (remoteWorkId == null || remoteWorkId.isEmpty() || leaiResponse == null || leaiResponse.isEmpty()) {
return leaiResponse;
}
UgcWork localWork = findByRemoteWorkId(remoteWorkId);
if (localWork == null) {
return leaiResponse;
}
try {
Map<String, Object> root = objectMapper.readValue(leaiResponse, new TypeReference<Map<String, Object>>() {});
// 定位实际的作品数据对象可能在 root.data 也可能直接就是 root
Map<String, Object> data = (Map<String, Object>) root.get("data");
if (data == null) {
// 没有 data 包装层直接用 root说明已经是作品对象
data = root;
}
boolean localUpdated = false;
// 1. 覆盖 author
if (localWork.getAuthorName() != null && !localWork.getAuthorName().isEmpty()) {
String remoteAuthor = LeaiUtil.toString(data.get("author"), null);
if (!localWork.getAuthorName().equals(remoteAuthor)) {
data.put("author", localWork.getAuthorName());
localUpdated = true;
}
}
// 2. 覆盖 title
if (localWork.getTitle() != null && !localWork.getTitle().isEmpty()) {
String remoteTitle = LeaiUtil.toString(data.get("title"), null);
if (!localWork.getTitle().equals(remoteTitle)) {
data.put("title", localWork.getTitle());
localUpdated = true;
}
}
// 3. 覆盖 subtitle/intro优先从 ai_meta 精确读取兜底用 description 反拆分
String localSubtitle = null;
String localIntro = null;
if (localWork.getAiMeta() instanceof Map) {
Map<String, Object> metaMap = (Map<String, Object>) localWork.getAiMeta();
if (metaMap.containsKey("_subtitle")) {
localSubtitle = LeaiUtil.toString(metaMap.get("_subtitle"), "");
}
if (metaMap.containsKey("_intro")) {
localIntro = LeaiUtil.toString(metaMap.get("_intro"), "");
}
}
// 兜底ai_meta 中没有精确值时 description 反拆分
if (localSubtitle == null && localIntro == null
&& localWork.getDescription() != null && !localWork.getDescription().isEmpty()) {
String desc = localWork.getDescription();
int newlineIdx = desc.indexOf('\n');
if (newlineIdx >= 0) {
localSubtitle = desc.substring(0, newlineIdx);
localIntro = desc.substring(newlineIdx + 1);
} else {
localIntro = desc;
}
}
if (localSubtitle != null) {
String remoteSubtitle = LeaiUtil.toString(data.get("subtitle"), "");
if (!localSubtitle.equals(remoteSubtitle)) {
data.put("subtitle", localSubtitle);
localUpdated = true;
}
}
if (localIntro != null) {
String remoteIntro = LeaiUtil.toString(data.get("intro"), "");
if (!localIntro.equals(remoteIntro)) {
data.put("intro", localIntro);
localUpdated = true;
}
}
// 4. 覆盖 tags ai_meta 读取
if (localWork.getAiMeta() instanceof Map) {
Map<String, Object> metaMap = (Map<String, Object>) localWork.getAiMeta();
if (metaMap.containsKey("_tags") && metaMap.get("_tags") instanceof List) {
List<Object> localTags = (List<Object>) metaMap.get("_tags");
data.put("tags", localTags);
localUpdated = true;
}
}
if (localUpdated) {
log.info("[合并元数据] 本地元数据已覆盖LeAI响应 remoteWorkId={}", remoteWorkId);
return objectMapper.writeValueAsString(root);
}
return leaiResponse;
} catch (Exception e) {
log.warn("[合并元数据] 解析LeAI响应失败返回原始数据: remoteWorkId={}", remoteWorkId, e);
return leaiResponse;
}
}
}

View File

@ -257,11 +257,6 @@ async function saveFormToServer() {
}
return true
} catch (e) {
// CAS
try {
const check = await getWorkDetail(resolvedWorkIdStr())
if (check?.status >= 4) return true
} catch { /* ignore */ }
message.error(e.message || '保存失败,请重试')
return false
}