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:
parent
61415eebe6
commit
12dc429d73
@ -195,15 +195,6 @@ async function handleSave() {
|
|||||||
store.workDetail = null // 清除缓存
|
store.workDetail = null // 清除缓存
|
||||||
router.push(`/dubbing/${workId.value}`)
|
router.push(`/dubbing/${workId.value}`)
|
||||||
} catch (e) {
|
} 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 || '保存失败,请重试')
|
alert(e.message || '保存失败,请重试')
|
||||||
} finally {
|
} finally {
|
||||||
saving.value = false
|
saving.value = false
|
||||||
|
|||||||
@ -122,6 +122,7 @@ public class LeaiProxyController {
|
|||||||
/**
|
/**
|
||||||
* 查询作品详情
|
* 查询作品详情
|
||||||
* GET /leai-proxy/work/{id} → 乐读派 /api/v1/query/work/{id}
|
* GET /leai-proxy/work/{id} → 乐读派 /api/v1/query/work/{id}
|
||||||
|
* 合并本地 DB 元数据,确保二次编目后的修改不丢失
|
||||||
*/
|
*/
|
||||||
@GetMapping("/work/{id}")
|
@GetMapping("/work/{id}")
|
||||||
@Operation(summary = "查询作品详情代理")
|
@Operation(summary = "查询作品详情代理")
|
||||||
@ -130,7 +131,10 @@ public class LeaiProxyController {
|
|||||||
params.put("orgId", leaiConfig.getOrgId());
|
params.put("orgId", leaiConfig.getOrgId());
|
||||||
params.put("phone", getPhoneOrThrow());
|
params.put("phone", getPhoneOrThrow());
|
||||||
log.info("[乐读派代理] 查询作品详情, workId={}", id);
|
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);
|
body.putAll(requestBody);
|
||||||
log.info("[乐读派代理] 编辑作品, workId={}", id);
|
log.info("[乐读派代理] 编辑作品, workId={}", id);
|
||||||
String responseBody = leaiApiClient.proxyPut("/update/work/" + id, body);
|
String responseBody = leaiApiClient.proxyPut("/update/work/" + id, body);
|
||||||
|
log.debug("[乐读派代理] 编辑作品响应, workId={}, response={}", id, responseBody);
|
||||||
syncLocalAfterLeaiUpdate(id, responseBody, requestBody);
|
syncLocalAfterLeaiUpdate(id, responseBody, requestBody);
|
||||||
return jsonOk(responseBody);
|
return jsonOk(responseBody);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -31,4 +31,14 @@ public interface ILeaiSyncService {
|
|||||||
* @param proxyRequestBody 前端传入的 JSON(通常含 author、subtitle、intro、tags 等,不含 orgId/phone)
|
* @param proxyRequestBody 前端传入的 JSON(通常含 author、subtitle、intro、tags 等,不含 orgId/phone)
|
||||||
*/
|
*/
|
||||||
void applyLocalMetadataFromProxyPut(String remoteWorkId, Map<String, Object> proxyRequestBody);
|
void applyLocalMetadataFromProxyPut(String remoteWorkId, Map<String, Object> proxyRequestBody);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将本地 DB 中已保存的元数据(author、description)合并到 LeAI B2 响应 JSON 中,
|
||||||
|
* 避免二次编目后 LeAI 返回旧数据覆盖用户的本地编辑。
|
||||||
|
*
|
||||||
|
* @param remoteWorkId 乐读派作品 ID
|
||||||
|
* @param leaiResponse LeAI B2 接口返回的原始 JSON 字符串
|
||||||
|
* @return 合并本地元数据后的 JSON 字符串
|
||||||
|
*/
|
||||||
|
String mergeLocalMetadata(String remoteWorkId, String leaiResponse);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,6 +2,8 @@ package com.lesingle.modules.leai.service;
|
|||||||
|
|
||||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
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.common.enums.Visibility;
|
||||||
import com.lesingle.modules.leai.enums.LeaiCreationStatus;
|
import com.lesingle.modules.leai.enums.LeaiCreationStatus;
|
||||||
import com.lesingle.modules.leai.util.LeaiUtil;
|
import com.lesingle.modules.leai.util.LeaiUtil;
|
||||||
@ -35,6 +37,7 @@ public class LeaiSyncService implements ILeaiSyncService {
|
|||||||
private final UgcWorkPageMapper ugcWorkPageMapper;
|
private final UgcWorkPageMapper ugcWorkPageMapper;
|
||||||
private final LeaiApiClient leaiApiClient;
|
private final LeaiApiClient leaiApiClient;
|
||||||
private final SysUserMapper sysUserMapper;
|
private final SysUserMapper sysUserMapper;
|
||||||
|
private final ObjectMapper objectMapper;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* V4.0 核心同步逻辑
|
* V4.0 核心同步逻辑
|
||||||
@ -424,6 +427,7 @@ public class LeaiSyncService implements ILeaiSyncService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
public void applyLocalMetadataFromProxyPut(String remoteWorkId, Map<String, Object> proxyRequestBody) {
|
public void applyLocalMetadataFromProxyPut(String remoteWorkId, Map<String, Object> proxyRequestBody) {
|
||||||
if (remoteWorkId == null || remoteWorkId.isEmpty() || proxyRequestBody == null || proxyRequestBody.isEmpty()) {
|
if (remoteWorkId == null || remoteWorkId.isEmpty() || proxyRequestBody == null || proxyRequestBody.isEmpty()) {
|
||||||
return;
|
return;
|
||||||
@ -435,6 +439,13 @@ public class LeaiSyncService implements ILeaiSyncService {
|
|||||||
LambdaUpdateWrapper<UgcWork> wrapper = new LambdaUpdateWrapper<>();
|
LambdaUpdateWrapper<UgcWork> wrapper = new LambdaUpdateWrapper<>();
|
||||||
wrapper.eq(UgcWork::getId, work.getId());
|
wrapper.eq(UgcWork::getId, work.getId());
|
||||||
boolean any = false;
|
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")) {
|
if (proxyRequestBody.containsKey("author")) {
|
||||||
wrapper.set(UgcWork::getAuthorName, LeaiUtil.toString(proxyRequestBody.get("author"), null));
|
wrapper.set(UgcWork::getAuthorName, LeaiUtil.toString(proxyRequestBody.get("author"), null));
|
||||||
any = true;
|
any = true;
|
||||||
@ -460,11 +471,28 @@ public class LeaiSyncService implements ILeaiSyncService {
|
|||||||
}
|
}
|
||||||
desc = desc.trim();
|
desc = desc.trim();
|
||||||
wrapper.set(UgcWork::getDescription, desc.isEmpty() ? null : desc);
|
wrapper.set(UgcWork::getDescription, desc.isEmpty() ? null : desc);
|
||||||
|
// 精确存储 subtitle/intro 到 ai_meta,避免 description 反拆分丢失信息
|
||||||
|
aiMetaMap.put("_subtitle", sub);
|
||||||
|
aiMetaMap.put("_intro", intro);
|
||||||
any = true;
|
any = true;
|
||||||
}
|
}
|
||||||
|
if (proxyRequestBody.containsKey("tags")) {
|
||||||
|
Object tags = proxyRequestBody.get("tags");
|
||||||
|
if (tags instanceof List) {
|
||||||
|
aiMetaMap.put("_tags", tags);
|
||||||
|
any = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
if (!any) {
|
if (!any) {
|
||||||
return;
|
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());
|
wrapper.set(UgcWork::getModifyTime, LocalDateTime.now());
|
||||||
ugcWorkMapper.update(null, wrapper);
|
ugcWorkMapper.update(null, wrapper);
|
||||||
log.info("[ProxyPut] 本地元数据已覆盖 remoteWorkId={}", remoteWorkId);
|
log.info("[ProxyPut] 本地元数据已覆盖 remoteWorkId={}", remoteWorkId);
|
||||||
@ -490,4 +518,112 @@ public class LeaiSyncService implements ILeaiSyncService {
|
|||||||
SysUser user = sysUserMapper.selectOne(wrapper);
|
SysUser user = sysUserMapper.selectOne(wrapper);
|
||||||
return user != null ? user.getId() : null;
|
return user != null ? user.getId() : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将本地 DB 中已保存的元数据(author、subtitle/intro、title、tags)合并到 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -257,11 +257,6 @@ async function saveFormToServer() {
|
|||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// 容错:保存报错时检查实际状态,可能已经成功但重试导致 CAS 失败
|
|
||||||
try {
|
|
||||||
const check = await getWorkDetail(resolvedWorkIdStr())
|
|
||||||
if (check?.status >= 4) return true
|
|
||||||
} catch { /* ignore */ }
|
|
||||||
message.error(e.message || '保存失败,请重试')
|
message.error(e.message || '保存失败,请重试')
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user