refactor: 乐读派(leai)模块规范化改造
按照项目 Java 后端规范对 leai 模块进行全面重构: - 新增 ILeaiWebhookEventService/ILeaiSyncService 接口,遵循 IService 模式 - Controller 层通过 Service 接口调用,不再直接注入 Mapper - 新增 LeaiTokenVO/LeaiAuthRedirectDTO,替代 Map<String,String> 入参出参 - RuntimeException 替换为 BusinessException - 添加 @Tag/@Operation Swagger 注解 - 提取共享工具类 LeaiUtil,消除 4 处重复的 toInt/toString 方法 - LeaiWebhookEvent 实体添加 @Schema 注解 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
9b5c24c49c
commit
bc7c17b281
@ -1,85 +1,89 @@
|
|||||||
package com.competition.modules.leai.controller;
|
package com.competition.modules.leai.controller;
|
||||||
|
|
||||||
|
import com.competition.common.exception.BusinessException;
|
||||||
import com.competition.common.result.Result;
|
import com.competition.common.result.Result;
|
||||||
import com.competition.common.util.SecurityUtil;
|
import com.competition.common.util.SecurityUtil;
|
||||||
import com.competition.modules.leai.config.LeaiConfig;
|
import com.competition.modules.leai.config.LeaiConfig;
|
||||||
|
import com.competition.modules.leai.dto.LeaiAuthRedirectDTO;
|
||||||
import com.competition.modules.leai.service.LeaiApiClient;
|
import com.competition.modules.leai.service.LeaiApiClient;
|
||||||
|
import com.competition.modules.leai.vo.LeaiTokenVO;
|
||||||
import com.competition.modules.sys.entity.SysUser;
|
import com.competition.modules.sys.entity.SysUser;
|
||||||
import com.competition.modules.sys.mapper.SysUserMapper;
|
import com.competition.modules.sys.service.ISysUserService;
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.*;
|
||||||
import org.springframework.web.bind.annotation.RequestParam;
|
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.URLEncoder;
|
import java.net.URLEncoder;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.util.LinkedHashMap;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 乐读派认证入口控制器
|
* 乐读派认证入口控制器
|
||||||
* 前端 iframe 模式的主入口
|
* 前端 iframe 模式的主入口
|
||||||
*/
|
*/
|
||||||
@Slf4j
|
@Slf4j
|
||||||
|
@Tag(name = "乐读派认证")
|
||||||
@RestController
|
@RestController
|
||||||
|
@RequestMapping("/leai-auth")
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class LeaiAuthController {
|
public class LeaiAuthController {
|
||||||
|
|
||||||
private final LeaiApiClient leaiApiClient;
|
private final LeaiApiClient leaiApiClient;
|
||||||
private final LeaiConfig leaiConfig;
|
private final LeaiConfig leaiConfig;
|
||||||
private final SysUserMapper sysUserMapper;
|
private final ISysUserService sysUserService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 前端 iframe 主入口:返回 token 信息 JSON
|
* 前端 iframe 主入口:返回 token 信息 JSON
|
||||||
* GET /leai-auth/token
|
|
||||||
* 需要登录认证
|
* 需要登录认证
|
||||||
*/
|
*/
|
||||||
@GetMapping("/leai-auth/token")
|
@GetMapping("/token")
|
||||||
public Result<Map<String, String>> getToken() {
|
@Operation(summary = "获取乐读派创作 Token")
|
||||||
|
public Result<LeaiTokenVO> getToken() {
|
||||||
Long userId = SecurityUtil.getCurrentUserId();
|
Long userId = SecurityUtil.getCurrentUserId();
|
||||||
SysUser user = sysUserMapper.selectById(userId);
|
SysUser user = sysUserService.getById(userId);
|
||||||
if (user == null) {
|
if (user == null) {
|
||||||
return Result.error(404, "用户不存在");
|
throw new BusinessException(404, "用户不存在");
|
||||||
}
|
}
|
||||||
|
|
||||||
String phone = user.getPhone();
|
String phone = user.getPhone();
|
||||||
if (phone == null || phone.isEmpty()) {
|
if (phone == null || phone.isEmpty()) {
|
||||||
return Result.error(400, "用户未绑定手机号,无法使用创作功能");
|
throw new BusinessException(400, "用户未绑定手机号,无法使用创作功能");
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
String token = leaiApiClient.exchangeToken(phone);
|
String token = leaiApiClient.exchangeToken(phone);
|
||||||
|
|
||||||
Map<String, String> data = new LinkedHashMap<>();
|
// Entity → VO 转换(Controller 层负责)
|
||||||
data.put("token", token);
|
// 注意: orgId 对应本项目的租户 code(tenant_code)
|
||||||
data.put("orgId", leaiConfig.getOrgId());
|
LeaiTokenVO vo = new LeaiTokenVO();
|
||||||
data.put("h5Url", leaiConfig.getH5Url());
|
vo.setToken(token);
|
||||||
data.put("phone", phone);
|
vo.setOrgId(leaiConfig.getOrgId()); // 即租户 tenant_code
|
||||||
|
vo.setH5Url(leaiConfig.getH5Url());
|
||||||
|
vo.setPhone(phone);
|
||||||
|
|
||||||
log.info("[乐读派] 获取创作Token成功, userId={}, phone={}", userId, phone);
|
log.info("[乐读派] 获取创作Token成功, userId={}, phone={}", userId, phone);
|
||||||
return Result.success(data);
|
return Result.success(vo);
|
||||||
|
|
||||||
|
} catch (BusinessException e) {
|
||||||
|
throw e;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("[乐读派] 获取创作Token失败, userId={}", userId, e);
|
log.error("[乐读派] 获取创作Token失败, userId={}", userId, e);
|
||||||
return Result.error(500, "获取创作Token失败: " + e.getMessage());
|
throw new BusinessException(500, "获取创作Token失败: " + e.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 跳转模式备选:换 token + 302 重定向到 H5
|
* 跳转模式备选:换 token + 302 重定向到 H5
|
||||||
* GET /leai-auth
|
|
||||||
* 需要登录认证
|
* 需要登录认证
|
||||||
*/
|
*/
|
||||||
@GetMapping("/leai-auth")
|
@GetMapping
|
||||||
public void authRedirect(
|
@Operation(summary = "重定向到乐读派 H5 创作页")
|
||||||
@RequestParam(required = false) String returnPath,
|
public void authRedirect(LeaiAuthRedirectDTO dto, HttpServletResponse response) throws IOException {
|
||||||
HttpServletResponse response) throws IOException {
|
|
||||||
|
|
||||||
Long userId = SecurityUtil.getCurrentUserId();
|
Long userId = SecurityUtil.getCurrentUserId();
|
||||||
SysUser user = sysUserMapper.selectById(userId);
|
SysUser user = sysUserService.getById(userId);
|
||||||
if (user == null || user.getPhone() == null || user.getPhone().isEmpty()) {
|
if (user == null || user.getPhone() == null || user.getPhone().isEmpty()) {
|
||||||
response.sendError(401, "请先登录并绑定手机号");
|
response.sendError(401, "请先登录并绑定手机号");
|
||||||
return;
|
return;
|
||||||
@ -94,8 +98,8 @@ public class LeaiAuthController {
|
|||||||
.append("&orgId=").append(URLEncoder.encode(leaiConfig.getOrgId(), StandardCharsets.UTF_8))
|
.append("&orgId=").append(URLEncoder.encode(leaiConfig.getOrgId(), StandardCharsets.UTF_8))
|
||||||
.append("&phone=").append(URLEncoder.encode(phone, StandardCharsets.UTF_8));
|
.append("&phone=").append(URLEncoder.encode(phone, StandardCharsets.UTF_8));
|
||||||
|
|
||||||
if (returnPath != null && !returnPath.isEmpty()) {
|
if (dto.getReturnPath() != null && !dto.getReturnPath().isEmpty()) {
|
||||||
url.append("&returnPath=").append(URLEncoder.encode(returnPath, StandardCharsets.UTF_8));
|
url.append("&returnPath=").append(URLEncoder.encode(dto.getReturnPath(), StandardCharsets.UTF_8));
|
||||||
}
|
}
|
||||||
|
|
||||||
log.info("[乐读派] 重定向到H5, userId={}, phone={}", userId, phone);
|
log.info("[乐读派] 重定向到H5, userId={}, phone={}", userId, phone);
|
||||||
@ -109,33 +113,35 @@ public class LeaiAuthController {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* iframe 内 Token 刷新接口
|
* iframe 内 Token 刷新接口
|
||||||
* GET /leai-auth/refresh-token
|
|
||||||
* 需要登录认证
|
|
||||||
* 前端 JS 在收到 TOKEN_EXPIRED postMessage 时调用此接口
|
* 前端 JS 在收到 TOKEN_EXPIRED postMessage 时调用此接口
|
||||||
*/
|
*/
|
||||||
@GetMapping("/leai-auth/refresh-token")
|
@GetMapping("/refresh-token")
|
||||||
public Result<Map<String, String>> refreshToken() {
|
@Operation(summary = "刷新乐读派 Token")
|
||||||
|
public Result<LeaiTokenVO> refreshToken() {
|
||||||
Long userId = SecurityUtil.getCurrentUserId();
|
Long userId = SecurityUtil.getCurrentUserId();
|
||||||
SysUser user = sysUserMapper.selectById(userId);
|
SysUser user = sysUserService.getById(userId);
|
||||||
if (user == null || user.getPhone() == null || user.getPhone().isEmpty()) {
|
if (user == null || user.getPhone() == null || user.getPhone().isEmpty()) {
|
||||||
return Result.error(401, "请先登录并绑定手机号");
|
throw new BusinessException(401, "请先登录并绑定手机号");
|
||||||
}
|
}
|
||||||
|
|
||||||
String phone = user.getPhone();
|
String phone = user.getPhone();
|
||||||
try {
|
try {
|
||||||
String token = leaiApiClient.exchangeToken(phone);
|
String token = leaiApiClient.exchangeToken(phone);
|
||||||
|
|
||||||
Map<String, String> data = new LinkedHashMap<>();
|
// Entity → VO 转换(Controller 层负责)
|
||||||
data.put("token", token);
|
LeaiTokenVO vo = new LeaiTokenVO();
|
||||||
data.put("orgId", leaiConfig.getOrgId());
|
vo.setToken(token);
|
||||||
data.put("phone", phone);
|
vo.setOrgId(leaiConfig.getOrgId());
|
||||||
|
vo.setPhone(phone);
|
||||||
|
|
||||||
log.info("[乐读派] Token刷新成功, userId={}", userId);
|
log.info("[乐读派] Token刷新成功, userId={}", userId);
|
||||||
return Result.success(data);
|
return Result.success(vo);
|
||||||
|
|
||||||
|
} catch (BusinessException e) {
|
||||||
|
throw e;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("[乐读派] Token刷新失败, userId={}", userId, e);
|
log.error("[乐读派] Token刷新失败, userId={}", userId, e);
|
||||||
return Result.error(500, "Token刷新失败: " + e.getMessage());
|
throw new BusinessException(500, "Token刷新失败: " + e.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,17 +1,18 @@
|
|||||||
package com.competition.modules.leai.controller;
|
package com.competition.modules.leai.controller;
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
import com.competition.common.exception.BusinessException;
|
||||||
import com.competition.modules.leai.entity.LeaiWebhookEvent;
|
import com.competition.modules.leai.service.ILeaiSyncService;
|
||||||
import com.competition.modules.leai.mapper.LeaiWebhookEventMapper;
|
import com.competition.modules.leai.service.ILeaiWebhookEventService;
|
||||||
import com.competition.modules.leai.service.LeaiApiClient;
|
import com.competition.modules.leai.service.LeaiApiClient;
|
||||||
import com.competition.modules.leai.service.LeaiSyncService;
|
import com.competition.modules.leai.util.LeaiUtil;
|
||||||
import com.fasterxml.jackson.core.type.TypeReference;
|
import com.fasterxml.jackson.core.type.TypeReference;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -19,19 +20,20 @@ import java.util.*;
|
|||||||
* 无需认证(由乐读派服务端调用,通过 HMAC 签名验证)
|
* 无需认证(由乐读派服务端调用,通过 HMAC 签名验证)
|
||||||
*/
|
*/
|
||||||
@Slf4j
|
@Slf4j
|
||||||
|
@Tag(name = "乐读派 Webhook")
|
||||||
@RestController
|
@RestController
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class LeaiWebhookController {
|
public class LeaiWebhookController {
|
||||||
|
|
||||||
private final LeaiApiClient leaiApiClient;
|
private final LeaiApiClient leaiApiClient;
|
||||||
private final LeaiSyncService leaiSyncService;
|
private final ILeaiSyncService leaiSyncService;
|
||||||
private final LeaiWebhookEventMapper webhookEventMapper;
|
private final ILeaiWebhookEventService webhookEventService;
|
||||||
private final ObjectMapper objectMapper;
|
private final ObjectMapper objectMapper;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 接收乐读派 Webhook 回调
|
* 接收乐读派 Webhook 回调
|
||||||
* POST /webhook/leai
|
* POST /webhook/leai
|
||||||
*
|
* <p>
|
||||||
* Header:
|
* Header:
|
||||||
* X-Webhook-Id: 事件唯一 ID
|
* X-Webhook-Id: 事件唯一 ID
|
||||||
* X-Webhook-Event: 事件类型 (work.status_changed / work.progress)
|
* X-Webhook-Event: 事件类型 (work.status_changed / work.progress)
|
||||||
@ -39,6 +41,7 @@ public class LeaiWebhookController {
|
|||||||
* X-Webhook-Signature: HMAC-SHA256={hex}
|
* X-Webhook-Signature: HMAC-SHA256={hex}
|
||||||
*/
|
*/
|
||||||
@PostMapping("/webhook/leai")
|
@PostMapping("/webhook/leai")
|
||||||
|
@Operation(summary = "接收乐读派 Webhook 回调")
|
||||||
public Map<String, String> webhook(
|
public Map<String, String> webhook(
|
||||||
@RequestBody String rawBody,
|
@RequestBody String rawBody,
|
||||||
@RequestHeader("X-Webhook-Id") String webhookId,
|
@RequestHeader("X-Webhook-Id") String webhookId,
|
||||||
@ -54,17 +57,15 @@ public class LeaiWebhookController {
|
|||||||
ts = Long.parseLong(timestamp);
|
ts = Long.parseLong(timestamp);
|
||||||
} catch (NumberFormatException e) {
|
} catch (NumberFormatException e) {
|
||||||
log.warn("[Webhook] 时间戳格式错误: {}", timestamp);
|
log.warn("[Webhook] 时间戳格式错误: {}", timestamp);
|
||||||
throw new RuntimeException("时间戳格式错误");
|
throw new BusinessException(400, "时间戳格式错误");
|
||||||
}
|
}
|
||||||
if (Math.abs(System.currentTimeMillis() - ts) > 300_000) {
|
if (Math.abs(System.currentTimeMillis() - ts) > 300_000) {
|
||||||
log.warn("[Webhook] 时间戳已过期: {}", timestamp);
|
log.warn("[Webhook] 时间戳已过期: {}", timestamp);
|
||||||
throw new RuntimeException("时间戳已过期");
|
throw new BusinessException(400, "时间戳已过期");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 幂等去重
|
// 2. 幂等去重
|
||||||
LambdaQueryWrapper<LeaiWebhookEvent> dupCheck = new LambdaQueryWrapper<>();
|
if (webhookEventService.existsByEventId(webhookId)) {
|
||||||
dupCheck.eq(LeaiWebhookEvent::getEventId, webhookId);
|
|
||||||
if (webhookEventMapper.selectCount(dupCheck) > 0) {
|
|
||||||
log.info("[Webhook] 重复事件,跳过: {}", webhookId);
|
log.info("[Webhook] 重复事件,跳过: {}", webhookId);
|
||||||
return Collections.singletonMap("status", "duplicate");
|
return Collections.singletonMap("status", "duplicate");
|
||||||
}
|
}
|
||||||
@ -72,7 +73,7 @@ public class LeaiWebhookController {
|
|||||||
// 3. 验证 HMAC-SHA256 签名
|
// 3. 验证 HMAC-SHA256 签名
|
||||||
if (!leaiApiClient.verifyWebhookSignature(webhookId, timestamp, rawBody, signature)) {
|
if (!leaiApiClient.verifyWebhookSignature(webhookId, timestamp, rawBody, signature)) {
|
||||||
log.warn("[Webhook] 签名验证失败! webhookId={}", webhookId);
|
log.warn("[Webhook] 签名验证失败! webhookId={}", webhookId);
|
||||||
throw new RuntimeException("签名验证失败");
|
throw new BusinessException(401, "签名验证失败");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. 解析事件 payload
|
// 4. 解析事件 payload
|
||||||
@ -81,17 +82,17 @@ public class LeaiWebhookController {
|
|||||||
payload = objectMapper.readValue(rawBody, new TypeReference<Map<String, Object>>() {});
|
payload = objectMapper.readValue(rawBody, new TypeReference<Map<String, Object>>() {});
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("[Webhook] payload 解析失败", e);
|
log.error("[Webhook] payload 解析失败", e);
|
||||||
throw new RuntimeException("payload 解析失败");
|
throw new BusinessException(400, "payload 解析失败");
|
||||||
}
|
}
|
||||||
|
|
||||||
String event = toString(payload.get("event"), "");
|
String event = LeaiUtil.toString(payload.get("event"), "");
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
Map<String, Object> data = (Map<String, Object>) payload.get("data");
|
Map<String, Object> data = (Map<String, Object>) payload.get("data");
|
||||||
if (data == null) {
|
if (data == null) {
|
||||||
data = new HashMap<>();
|
data = new HashMap<>();
|
||||||
}
|
}
|
||||||
|
|
||||||
String remoteWorkId = toString(data.get("work_id"), null);
|
String remoteWorkId = LeaiUtil.toString(data.get("work_id"), null);
|
||||||
|
|
||||||
// 5. 按 V4.0 同步规则处理
|
// 5. 按 V4.0 同步规则处理
|
||||||
if (remoteWorkId != null && !remoteWorkId.isEmpty()) {
|
if (remoteWorkId != null && !remoteWorkId.isEmpty()) {
|
||||||
@ -103,19 +104,8 @@ public class LeaiWebhookController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 6. 记录事件(幂等去重)
|
// 6. 记录事件(幂等去重)
|
||||||
LeaiWebhookEvent webhookEventEntity = new LeaiWebhookEvent();
|
webhookEventService.saveEvent(webhookId, webhookEvent, remoteWorkId, payload);
|
||||||
webhookEventEntity.setEventId(webhookId);
|
|
||||||
webhookEventEntity.setEventType(webhookEvent);
|
|
||||||
webhookEventEntity.setRemoteWorkId(remoteWorkId);
|
|
||||||
webhookEventEntity.setPayload(payload);
|
|
||||||
webhookEventEntity.setProcessed(1);
|
|
||||||
webhookEventEntity.setCreateTime(LocalDateTime.now());
|
|
||||||
webhookEventMapper.insert(webhookEventEntity);
|
|
||||||
|
|
||||||
return Collections.singletonMap("status", "ok");
|
return Collections.singletonMap("status", "ok");
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String toString(Object obj, String defaultVal) {
|
|
||||||
return obj != null ? obj.toString() : defaultVal;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,15 @@
|
|||||||
|
package com.competition.modules.leai.dto;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 乐读派重定向请求 DTO
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Schema(description = "乐读派重定向请求")
|
||||||
|
public class LeaiAuthRedirectDTO {
|
||||||
|
|
||||||
|
@Schema(description = "重定向后的路径")
|
||||||
|
private String returnPath;
|
||||||
|
}
|
||||||
@ -1,45 +1,51 @@
|
|||||||
package com.competition.modules.leai.entity;
|
package com.competition.modules.leai.entity;
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.annotation.IdType;
|
|
||||||
import com.baomidou.mybatisplus.annotation.TableField;
|
import com.baomidou.mybatisplus.annotation.TableField;
|
||||||
import com.baomidou.mybatisplus.annotation.TableId;
|
|
||||||
import com.baomidou.mybatisplus.annotation.TableName;
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
|
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
import java.io.Serializable;
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 乐读派 Webhook 事件去重表
|
* 乐读派 Webhook 事件去重表
|
||||||
|
* <p>
|
||||||
|
* 注意:此表为追加-only日志表,不需要 BaseEntity 的审计字段(updateBy、modifyTime、deleted 等),
|
||||||
|
* 因此独立定义 id 和 createTime,不继承 BaseEntity。
|
||||||
*/
|
*/
|
||||||
@Data
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = false)
|
||||||
@TableName(value = "t_leai_webhook_event", autoResultMap = true)
|
@TableName(value = "t_leai_webhook_event", autoResultMap = true)
|
||||||
public class LeaiWebhookEvent implements Serializable {
|
@Schema(description = "乐读派 Webhook 事件")
|
||||||
|
public class LeaiWebhookEvent {
|
||||||
|
|
||||||
@TableId(type = IdType.AUTO)
|
@Schema(description = "主键ID")
|
||||||
|
@TableField("id")
|
||||||
private Long id;
|
private Long id;
|
||||||
|
|
||||||
/** 事件唯一ID (X-Webhook-Id) */
|
@Schema(description = "事件唯一ID(X-Webhook-Id)")
|
||||||
@TableField("event_id")
|
@TableField("event_id")
|
||||||
private String eventId;
|
private String eventId;
|
||||||
|
|
||||||
/** 事件类型 */
|
@Schema(description = "事件类型")
|
||||||
@TableField("event_type")
|
@TableField("event_type")
|
||||||
private String eventType;
|
private String eventType;
|
||||||
|
|
||||||
/** 乐读派作品ID */
|
@Schema(description = "乐读派作品ID")
|
||||||
@TableField("remote_work_id")
|
@TableField("remote_work_id")
|
||||||
private String remoteWorkId;
|
private String remoteWorkId;
|
||||||
|
|
||||||
/** 事件原始载荷 */
|
@Schema(description = "事件原始载荷")
|
||||||
@TableField(value = "payload", typeHandler = JacksonTypeHandler.class)
|
@TableField(value = "payload", typeHandler = JacksonTypeHandler.class)
|
||||||
private Object payload;
|
private Object payload;
|
||||||
|
|
||||||
/** 是否已处理 */
|
@Schema(description = "是否已处理:0-未处理,1-已处理")
|
||||||
|
@TableField("processed")
|
||||||
private Integer processed;
|
private Integer processed;
|
||||||
|
|
||||||
/** 创建时间 */
|
@Schema(description = "创建时间")
|
||||||
@TableField("create_time")
|
@TableField("create_time")
|
||||||
private LocalDateTime createTime;
|
private LocalDateTime createTime;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,26 @@
|
|||||||
|
package com.competition.modules.leai.service;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 乐读派作品同步 Service 接口
|
||||||
|
* <p>
|
||||||
|
* Webhook 回调和 B3 对账共用此服务
|
||||||
|
*/
|
||||||
|
public interface ILeaiSyncService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 同步乐读派作品到本地
|
||||||
|
* <p>
|
||||||
|
* 同步规则:
|
||||||
|
* - remoteStatus == -1 → 强制更新(FAILED)
|
||||||
|
* - remoteStatus == 2 → 强制更新(PROCESSING 进度变化)
|
||||||
|
* - remoteStatus > localStatus → 全量更新(状态前进)
|
||||||
|
* - else → 忽略
|
||||||
|
*
|
||||||
|
* @param remoteWorkId 乐读派作品ID
|
||||||
|
* @param remoteData 远程作品数据(来自 Webhook payload 或 B2/B3 查询结果)
|
||||||
|
* @param source 来源标识(用于日志,如 "Webhook[work.status_changed]")
|
||||||
|
*/
|
||||||
|
void syncWork(String remoteWorkId, Map<String, Object> remoteData, String source);
|
||||||
|
}
|
||||||
@ -0,0 +1,28 @@
|
|||||||
|
package com.competition.modules.leai.service;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.extension.service.IService;
|
||||||
|
import com.competition.modules.leai.entity.LeaiWebhookEvent;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 乐读派 Webhook 事件 Service 接口
|
||||||
|
*/
|
||||||
|
public interface ILeaiWebhookEventService extends IService<LeaiWebhookEvent> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查事件是否已存在(幂等去重)
|
||||||
|
*
|
||||||
|
* @param eventId 事件唯一ID
|
||||||
|
* @return true-已存在(重复事件)
|
||||||
|
*/
|
||||||
|
boolean existsByEventId(String eventId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存 Webhook 事件记录
|
||||||
|
*
|
||||||
|
* @param eventId 事件唯一ID
|
||||||
|
* @param eventType 事件类型
|
||||||
|
* @param remoteWorkId 乐读派作品ID
|
||||||
|
* @param payload 事件载荷
|
||||||
|
*/
|
||||||
|
void saveEvent(String eventId, String eventType, String remoteWorkId, Object payload);
|
||||||
|
}
|
||||||
@ -1,6 +1,8 @@
|
|||||||
package com.competition.modules.leai.service;
|
package com.competition.modules.leai.service;
|
||||||
|
|
||||||
|
import com.competition.common.exception.BusinessException;
|
||||||
import com.competition.modules.leai.config.LeaiConfig;
|
import com.competition.modules.leai.config.LeaiConfig;
|
||||||
|
import com.competition.modules.leai.util.LeaiUtil;
|
||||||
import com.fasterxml.jackson.core.type.TypeReference;
|
import com.fasterxml.jackson.core.type.TypeReference;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
@ -64,30 +66,30 @@ public class LeaiApiClient {
|
|||||||
Map<String, Object> result = objectMapper.readValue(response.getBody(),
|
Map<String, Object> result = objectMapper.readValue(response.getBody(),
|
||||||
new TypeReference<Map<String, Object>>() {});
|
new TypeReference<Map<String, Object>>() {});
|
||||||
|
|
||||||
int code = toInt(result.get("code"), 0);
|
int code = LeaiUtil.toInt(result.get("code"), 0);
|
||||||
if (code != 200) {
|
if (code != 200) {
|
||||||
throw new RuntimeException("令牌交换失败: code=" + code
|
throw new BusinessException(502, "令牌交换失败: code=" + code
|
||||||
+ ", msg=" + toString(result.get("msg"), "unknown"));
|
+ ", msg=" + LeaiUtil.toString(result.get("msg"), "unknown"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
Map<String, Object> data = (Map<String, Object>) result.get("data");
|
Map<String, Object> data = (Map<String, Object>) result.get("data");
|
||||||
if (data == null) {
|
if (data == null) {
|
||||||
throw new RuntimeException("令牌交换失败: data 为 null");
|
throw new BusinessException(502, "令牌交换失败: data 为 null");
|
||||||
}
|
}
|
||||||
|
|
||||||
String sessionToken = toString(data.get("sessionToken"), null);
|
String sessionToken = LeaiUtil.toString(data.get("sessionToken"), null);
|
||||||
if (sessionToken == null || sessionToken.isEmpty()) {
|
if (sessionToken == null || sessionToken.isEmpty()) {
|
||||||
throw new RuntimeException("令牌交换失败: sessionToken 为空");
|
throw new BusinessException(502, "令牌交换失败: sessionToken 为空");
|
||||||
}
|
}
|
||||||
|
|
||||||
log.info("[乐读派] 令牌交换成功, phone={}, expiresIn={}s", phone, data.get("expiresIn"));
|
log.info("[乐读派] 令牌交换成功, phone={}, expiresIn={}s", phone, data.get("expiresIn"));
|
||||||
return sessionToken;
|
return sessionToken;
|
||||||
|
|
||||||
} catch (RuntimeException e) {
|
} catch (BusinessException e) {
|
||||||
throw e;
|
throw e;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
throw new RuntimeException("令牌交换请求失败: " + e.getMessage(), e);
|
throw new BusinessException(502, "令牌交换请求失败: " + e.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -116,7 +118,7 @@ public class LeaiApiClient {
|
|||||||
Map<String, Object> result = objectMapper.readValue(response.getBody(),
|
Map<String, Object> result = objectMapper.readValue(response.getBody(),
|
||||||
new TypeReference<Map<String, Object>>() {});
|
new TypeReference<Map<String, Object>>() {});
|
||||||
|
|
||||||
int code = toInt(result.get("code"), 0);
|
int code = LeaiUtil.toInt(result.get("code"), 0);
|
||||||
if (code != 200) {
|
if (code != 200) {
|
||||||
log.warn("[乐读派] B2查询失败: workId={}, code={}", workId, code);
|
log.warn("[乐读派] B2查询失败: workId={}, code={}", workId, code);
|
||||||
return null;
|
return null;
|
||||||
@ -166,7 +168,7 @@ public class LeaiApiClient {
|
|||||||
Map<String, Object> result = objectMapper.readValue(response.getBody(),
|
Map<String, Object> result = objectMapper.readValue(response.getBody(),
|
||||||
new TypeReference<Map<String, Object>>() {});
|
new TypeReference<Map<String, Object>>() {});
|
||||||
|
|
||||||
int code = toInt(result.get("code"), 0);
|
int code = LeaiUtil.toInt(result.get("code"), 0);
|
||||||
if (code != 200) {
|
if (code != 200) {
|
||||||
log.warn("[乐读派] B3查询失败: code={}, msg={}", code, result.get("msg"));
|
log.warn("[乐读派] B3查询失败: code={}, msg={}", code, result.get("msg"));
|
||||||
return Collections.emptyList();
|
return Collections.emptyList();
|
||||||
@ -245,7 +247,7 @@ public class LeaiApiClient {
|
|||||||
}
|
}
|
||||||
return sb.toString();
|
return sb.toString();
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
throw new RuntimeException("HMAC-SHA256 签名失败", e);
|
throw new BusinessException(500, "HMAC-SHA256 签名失败");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -256,14 +258,4 @@ public class LeaiApiClient {
|
|||||||
return ZonedDateTime.ofInstant(Instant.now().minusSeconds(7200), ZoneOffset.UTC)
|
return ZonedDateTime.ofInstant(Instant.now().minusSeconds(7200), ZoneOffset.UTC)
|
||||||
.format(DateTimeFormatter.ISO_INSTANT);
|
.format(DateTimeFormatter.ISO_INSTANT);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int toInt(Object obj, int defaultVal) {
|
|
||||||
if (obj == null) return defaultVal;
|
|
||||||
if (obj instanceof Number) return ((Number) obj).intValue();
|
|
||||||
try { return Integer.parseInt(obj.toString()); } catch (Exception e) { return defaultVal; }
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String toString(Object obj, String defaultVal) {
|
|
||||||
return obj != null ? obj.toString() : defaultVal;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,7 +2,7 @@ package com.competition.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.competition.modules.leai.config.LeaiConfig;
|
import com.competition.modules.leai.util.LeaiUtil;
|
||||||
import com.competition.modules.ugc.entity.UgcWork;
|
import com.competition.modules.ugc.entity.UgcWork;
|
||||||
import com.competition.modules.ugc.entity.UgcWorkPage;
|
import com.competition.modules.ugc.entity.UgcWorkPage;
|
||||||
import com.competition.modules.ugc.mapper.UgcWorkMapper;
|
import com.competition.modules.ugc.mapper.UgcWorkMapper;
|
||||||
@ -22,12 +22,11 @@ import java.util.*;
|
|||||||
@Slf4j
|
@Slf4j
|
||||||
@Service
|
@Service
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class LeaiSyncService {
|
public class LeaiSyncService implements ILeaiSyncService {
|
||||||
|
|
||||||
private final UgcWorkMapper ugcWorkMapper;
|
private final UgcWorkMapper ugcWorkMapper;
|
||||||
private final UgcWorkPageMapper ugcWorkPageMapper;
|
private final UgcWorkPageMapper ugcWorkPageMapper;
|
||||||
private final LeaiApiClient leaiApiClient;
|
private final LeaiApiClient leaiApiClient;
|
||||||
private final LeaiConfig leaiConfig;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* V4.0 核心同步逻辑
|
* V4.0 核心同步逻辑
|
||||||
@ -42,10 +41,11 @@ public class LeaiSyncService {
|
|||||||
* @param remoteData 远程作品数据(来自 Webhook payload 或 B2/B3 查询结果)
|
* @param remoteData 远程作品数据(来自 Webhook payload 或 B2/B3 查询结果)
|
||||||
* @param source 来源标识(用于日志,如 "Webhook[work.status_changed]")
|
* @param source 来源标识(用于日志,如 "Webhook[work.status_changed]")
|
||||||
*/
|
*/
|
||||||
|
@Override
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
public void syncWork(String remoteWorkId, Map<String, Object> remoteData, String source) {
|
public void syncWork(String remoteWorkId, Map<String, Object> remoteData, String source) {
|
||||||
int remoteStatus = toInt(remoteData.get("status"), 0);
|
int remoteStatus = LeaiUtil.toInt(remoteData.get("status"), 0);
|
||||||
String phone = toString(remoteData.get("phone"), null);
|
String phone = LeaiUtil.toString(remoteData.get("phone"), null);
|
||||||
|
|
||||||
// 查找本地作品(通过 remoteWorkId)
|
// 查找本地作品(通过 remoteWorkId)
|
||||||
UgcWork localWork = findByRemoteWorkId(remoteWorkId);
|
UgcWork localWork = findByRemoteWorkId(remoteWorkId);
|
||||||
@ -94,8 +94,8 @@ public class LeaiSyncService {
|
|||||||
private void insertNewWork(String remoteWorkId, Map<String, Object> remoteData, String phone) {
|
private void insertNewWork(String remoteWorkId, Map<String, Object> remoteData, String phone) {
|
||||||
UgcWork work = new UgcWork();
|
UgcWork work = new UgcWork();
|
||||||
work.setRemoteWorkId(remoteWorkId);
|
work.setRemoteWorkId(remoteWorkId);
|
||||||
work.setTitle(toString(remoteData.get("title"), "未命名作品"));
|
work.setTitle(LeaiUtil.toString(remoteData.get("title"), "未命名作品"));
|
||||||
work.setStatus(toInt(remoteData.get("status"), LeaiApiClient.STATUS_PENDING));
|
work.setStatus(LeaiUtil.toInt(remoteData.get("status"), LeaiApiClient.STATUS_PENDING));
|
||||||
work.setVisibility("private");
|
work.setVisibility("private");
|
||||||
work.setIsDeleted(0);
|
work.setIsDeleted(0);
|
||||||
work.setIsRecommended(false);
|
work.setIsRecommended(false);
|
||||||
@ -104,11 +104,11 @@ public class LeaiSyncService {
|
|||||||
work.setFavoriteCount(0);
|
work.setFavoriteCount(0);
|
||||||
work.setCommentCount(0);
|
work.setCommentCount(0);
|
||||||
work.setShareCount(0);
|
work.setShareCount(0);
|
||||||
work.setProgress(toInt(remoteData.get("progress"), 0));
|
work.setProgress(LeaiUtil.toInt(remoteData.get("progress"), 0));
|
||||||
work.setProgressMessage(toString(remoteData.get("progressMessage"), null));
|
work.setProgressMessage(LeaiUtil.toString(remoteData.get("progressMessage"), null));
|
||||||
work.setStyle(toString(remoteData.get("style"), null));
|
work.setStyle(LeaiUtil.toString(remoteData.get("style"), null));
|
||||||
work.setAuthorName(toString(remoteData.get("author"), null));
|
work.setAuthorName(LeaiUtil.toString(remoteData.get("author"), null));
|
||||||
work.setFailReason(toString(remoteData.get("failReason"), null));
|
work.setFailReason(LeaiUtil.toString(remoteData.get("failReason"), null));
|
||||||
work.setCreateTime(LocalDateTime.now());
|
work.setCreateTime(LocalDateTime.now());
|
||||||
work.setModifyTime(LocalDateTime.now());
|
work.setModifyTime(LocalDateTime.now());
|
||||||
|
|
||||||
@ -145,7 +145,7 @@ public class LeaiSyncService {
|
|||||||
LambdaUpdateWrapper<UgcWork> wrapper = new LambdaUpdateWrapper<>();
|
LambdaUpdateWrapper<UgcWork> wrapper = new LambdaUpdateWrapper<>();
|
||||||
wrapper.eq(UgcWork::getId, work.getId())
|
wrapper.eq(UgcWork::getId, work.getId())
|
||||||
.set(UgcWork::getStatus, LeaiApiClient.STATUS_FAILED)
|
.set(UgcWork::getStatus, LeaiApiClient.STATUS_FAILED)
|
||||||
.set(UgcWork::getFailReason, toString(remoteData.get("failReason"), "未知错误"))
|
.set(UgcWork::getFailReason, LeaiUtil.toString(remoteData.get("failReason"), "未知错误"))
|
||||||
.set(UgcWork::getModifyTime, LocalDateTime.now());
|
.set(UgcWork::getModifyTime, LocalDateTime.now());
|
||||||
ugcWorkMapper.update(null, wrapper);
|
ugcWorkMapper.update(null, wrapper);
|
||||||
}
|
}
|
||||||
@ -159,10 +159,10 @@ public class LeaiSyncService {
|
|||||||
.set(UgcWork::getStatus, LeaiApiClient.STATUS_PROCESSING);
|
.set(UgcWork::getStatus, LeaiApiClient.STATUS_PROCESSING);
|
||||||
|
|
||||||
if (remoteData.containsKey("progress")) {
|
if (remoteData.containsKey("progress")) {
|
||||||
wrapper.set(UgcWork::getProgress, toInt(remoteData.get("progress"), 0));
|
wrapper.set(UgcWork::getProgress, LeaiUtil.toInt(remoteData.get("progress"), 0));
|
||||||
}
|
}
|
||||||
if (remoteData.containsKey("progressMessage")) {
|
if (remoteData.containsKey("progressMessage")) {
|
||||||
wrapper.set(UgcWork::getProgressMessage, toString(remoteData.get("progressMessage"), null));
|
wrapper.set(UgcWork::getProgressMessage, LeaiUtil.toString(remoteData.get("progressMessage"), null));
|
||||||
}
|
}
|
||||||
wrapper.set(UgcWork::getModifyTime, LocalDateTime.now());
|
wrapper.set(UgcWork::getModifyTime, LocalDateTime.now());
|
||||||
|
|
||||||
@ -181,22 +181,22 @@ public class LeaiSyncService {
|
|||||||
|
|
||||||
// 更新可变字段
|
// 更新可变字段
|
||||||
if (remoteData.containsKey("title")) {
|
if (remoteData.containsKey("title")) {
|
||||||
wrapper.set(UgcWork::getTitle, toString(remoteData.get("title"), null));
|
wrapper.set(UgcWork::getTitle, LeaiUtil.toString(remoteData.get("title"), null));
|
||||||
}
|
}
|
||||||
if (remoteData.containsKey("progress")) {
|
if (remoteData.containsKey("progress")) {
|
||||||
wrapper.set(UgcWork::getProgress, toInt(remoteData.get("progress"), 0));
|
wrapper.set(UgcWork::getProgress, LeaiUtil.toInt(remoteData.get("progress"), 0));
|
||||||
}
|
}
|
||||||
if (remoteData.containsKey("progressMessage")) {
|
if (remoteData.containsKey("progressMessage")) {
|
||||||
wrapper.set(UgcWork::getProgressMessage, toString(remoteData.get("progressMessage"), null));
|
wrapper.set(UgcWork::getProgressMessage, LeaiUtil.toString(remoteData.get("progressMessage"), null));
|
||||||
}
|
}
|
||||||
if (remoteData.containsKey("style")) {
|
if (remoteData.containsKey("style")) {
|
||||||
wrapper.set(UgcWork::getStyle, toString(remoteData.get("style"), null));
|
wrapper.set(UgcWork::getStyle, LeaiUtil.toString(remoteData.get("style"), null));
|
||||||
}
|
}
|
||||||
if (remoteData.containsKey("author")) {
|
if (remoteData.containsKey("author")) {
|
||||||
wrapper.set(UgcWork::getAuthorName, toString(remoteData.get("author"), null));
|
wrapper.set(UgcWork::getAuthorName, LeaiUtil.toString(remoteData.get("author"), null));
|
||||||
}
|
}
|
||||||
if (remoteData.containsKey("failReason")) {
|
if (remoteData.containsKey("failReason")) {
|
||||||
wrapper.set(UgcWork::getFailReason, toString(remoteData.get("failReason"), null));
|
wrapper.set(UgcWork::getFailReason, LeaiUtil.toString(remoteData.get("failReason"), null));
|
||||||
}
|
}
|
||||||
Object coverUrl = remoteData.get("coverUrl");
|
Object coverUrl = remoteData.get("coverUrl");
|
||||||
if (coverUrl == null) coverUrl = remoteData.get("cover_url");
|
if (coverUrl == null) coverUrl = remoteData.get("cover_url");
|
||||||
@ -247,9 +247,9 @@ public class LeaiSyncService {
|
|||||||
UgcWorkPage page = new UgcWorkPage();
|
UgcWorkPage page = new UgcWorkPage();
|
||||||
page.setWorkId(workId);
|
page.setWorkId(workId);
|
||||||
page.setPageNo(i + 1);
|
page.setPageNo(i + 1);
|
||||||
page.setImageUrl(toString(pageData.get("imageUrl"), null));
|
page.setImageUrl(LeaiUtil.toString(pageData.get("imageUrl"), null));
|
||||||
page.setText(toString(pageData.get("text"), null));
|
page.setText(LeaiUtil.toString(pageData.get("text"), null));
|
||||||
page.setAudioUrl(toString(pageData.get("audioUrl"), null));
|
page.setAudioUrl(LeaiUtil.toString(pageData.get("audioUrl"), null));
|
||||||
ugcWorkPageMapper.insert(page);
|
ugcWorkPageMapper.insert(page);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -275,14 +275,4 @@ public class LeaiSyncService {
|
|||||||
// 当前简化处理:直接通过手机号查用户
|
// 当前简化处理:直接通过手机号查用户
|
||||||
return null; // 暂时不自动关联用户,后续通过 phone + orgId 查询
|
return null; // 暂时不自动关联用户,后续通过 phone + orgId 查询
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int toInt(Object obj, int defaultVal) {
|
|
||||||
if (obj == null) return defaultVal;
|
|
||||||
if (obj instanceof Number) return ((Number) obj).intValue();
|
|
||||||
try { return Integer.parseInt(obj.toString()); } catch (Exception e) { return defaultVal; }
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String toString(Object obj, String defaultVal) {
|
|
||||||
return obj != null ? obj.toString() : defaultVal;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,41 @@
|
|||||||
|
package com.competition.modules.leai.service.impl;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
|
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||||
|
import com.competition.modules.leai.entity.LeaiWebhookEvent;
|
||||||
|
import com.competition.modules.leai.mapper.LeaiWebhookEventMapper;
|
||||||
|
import com.competition.modules.leai.service.ILeaiWebhookEventService;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 乐读派 Webhook 事件 Service 实现
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class LeaiWebhookEventServiceImpl extends ServiceImpl<LeaiWebhookEventMapper, LeaiWebhookEvent>
|
||||||
|
implements ILeaiWebhookEventService {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean existsByEventId(String eventId) {
|
||||||
|
LambdaQueryWrapper<LeaiWebhookEvent> wrapper = new LambdaQueryWrapper<>();
|
||||||
|
wrapper.eq(LeaiWebhookEvent::getEventId, eventId);
|
||||||
|
return count(wrapper) > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void saveEvent(String eventId, String eventType, String remoteWorkId, Object payload) {
|
||||||
|
LeaiWebhookEvent event = new LeaiWebhookEvent();
|
||||||
|
event.setEventId(eventId);
|
||||||
|
event.setEventType(eventType);
|
||||||
|
event.setRemoteWorkId(remoteWorkId);
|
||||||
|
event.setPayload(payload);
|
||||||
|
event.setProcessed(1);
|
||||||
|
event.setCreateTime(LocalDateTime.now());
|
||||||
|
save(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,7 +1,8 @@
|
|||||||
package com.competition.modules.leai.task;
|
package com.competition.modules.leai.task;
|
||||||
|
|
||||||
|
import com.competition.modules.leai.service.ILeaiSyncService;
|
||||||
import com.competition.modules.leai.service.LeaiApiClient;
|
import com.competition.modules.leai.service.LeaiApiClient;
|
||||||
import com.competition.modules.leai.service.LeaiSyncService;
|
import com.competition.modules.leai.util.LeaiUtil;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.scheduling.annotation.Scheduled;
|
import org.springframework.scheduling.annotation.Scheduled;
|
||||||
@ -13,7 +14,7 @@ import java.util.Map;
|
|||||||
/**
|
/**
|
||||||
* B3 定时对账任务
|
* B3 定时对账任务
|
||||||
* 每 30 分钟调用 B3 接口对账,补偿 Webhook 遗漏
|
* 每 30 分钟调用 B3 接口对账,补偿 Webhook 遗漏
|
||||||
*
|
* <p>
|
||||||
* 查询范围:最近 2 小时内更新的作品(覆盖 2 个对账周期,确保不遗漏边界数据)
|
* 查询范围:最近 2 小时内更新的作品(覆盖 2 个对账周期,确保不遗漏边界数据)
|
||||||
*/
|
*/
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@ -22,7 +23,7 @@ import java.util.Map;
|
|||||||
public class LeaiReconcileTask {
|
public class LeaiReconcileTask {
|
||||||
|
|
||||||
private final LeaiApiClient leaiApiClient;
|
private final LeaiApiClient leaiApiClient;
|
||||||
private final LeaiSyncService leaiSyncService;
|
private final ILeaiSyncService leaiSyncService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 每30分钟执行一次,初始延迟60秒
|
* 每30分钟执行一次,初始延迟60秒
|
||||||
@ -42,10 +43,10 @@ public class LeaiReconcileTask {
|
|||||||
|
|
||||||
int synced = 0;
|
int synced = 0;
|
||||||
for (Map<String, Object> work : works) {
|
for (Map<String, Object> work : works) {
|
||||||
String workId = toString(work.get("workId"), null);
|
String workId = LeaiUtil.toString(work.get("workId"), null);
|
||||||
if (workId == null) continue;
|
if (workId == null) {
|
||||||
|
continue;
|
||||||
int remoteStatus = toInt(work.get("status"), 0);
|
}
|
||||||
|
|
||||||
// 尝试调 B2 获取完整数据
|
// 尝试调 B2 获取完整数据
|
||||||
Map<String, Object> fullData = leaiApiClient.fetchWorkDetail(workId);
|
Map<String, Object> fullData = leaiApiClient.fetchWorkDetail(workId);
|
||||||
@ -73,14 +74,4 @@ public class LeaiReconcileTask {
|
|||||||
log.error("[B3对账] 执行异常", e);
|
log.error("[B3对账] 执行异常", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int toInt(Object obj, int defaultVal) {
|
|
||||||
if (obj == null) return defaultVal;
|
|
||||||
if (obj instanceof Number) return ((Number) obj).intValue();
|
|
||||||
try { return Integer.parseInt(obj.toString()); } catch (Exception e) { return defaultVal; }
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String toString(Object obj, String defaultVal) {
|
|
||||||
return obj != null ? obj.toString() : defaultVal;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,44 @@
|
|||||||
|
package com.competition.modules.leai.util;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 乐读派模块共享工具类
|
||||||
|
* 提取多处重复使用的类型转换方法
|
||||||
|
*/
|
||||||
|
public final class LeaiUtil {
|
||||||
|
|
||||||
|
private LeaiUtil() {
|
||||||
|
// 工具类禁止实例化
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 安全转换为 int
|
||||||
|
*
|
||||||
|
* @param obj 待转换对象
|
||||||
|
* @param defaultVal 默认值
|
||||||
|
* @return 转换结果
|
||||||
|
*/
|
||||||
|
public static int toInt(Object obj, int defaultVal) {
|
||||||
|
if (obj == null) {
|
||||||
|
return defaultVal;
|
||||||
|
}
|
||||||
|
if (obj instanceof Number) {
|
||||||
|
return ((Number) obj).intValue();
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return Integer.parseInt(obj.toString());
|
||||||
|
} catch (Exception e) {
|
||||||
|
return defaultVal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 安全转换为 String
|
||||||
|
*
|
||||||
|
* @param obj 待转换对象
|
||||||
|
* @param defaultVal 默认值
|
||||||
|
* @return 转换结果
|
||||||
|
*/
|
||||||
|
public static String toString(Object obj, String defaultVal) {
|
||||||
|
return obj != null ? obj.toString() : defaultVal;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,24 @@
|
|||||||
|
package com.competition.modules.leai.vo;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 乐读派 Token 响应 VO
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Schema(description = "乐读派 Token 信息")
|
||||||
|
public class LeaiTokenVO {
|
||||||
|
|
||||||
|
@Schema(description = "Session Token")
|
||||||
|
private String token;
|
||||||
|
|
||||||
|
@Schema(description = "机构ID(对应本项目的租户 code,即 tenant_code)")
|
||||||
|
private String orgId;
|
||||||
|
|
||||||
|
@Schema(description = "H5 前端地址")
|
||||||
|
private String h5Url;
|
||||||
|
|
||||||
|
@Schema(description = "用户手机号")
|
||||||
|
private String phone;
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user