feat: 完善人生轨迹详情页功能及微信小程序登录优化
This commit is contained in:
@@ -59,6 +59,7 @@ public class WebMvcConfig implements WebMvcConfigurer {
|
||||
"/swagger-resources/**", // Swagger resources sub-paths
|
||||
"/actuator/**", // Actuator endpoints
|
||||
"/admin/**", // 排除管理员路径,由管理员拦截器处理
|
||||
"/auth/wechat/login", // WeChat mini program login endpoint
|
||||
"/error" // Spring Boot error page
|
||||
)
|
||||
.order(2); // 优先级2
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.emotion.config;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@Data
|
||||
@Configuration
|
||||
@ConfigurationProperties(prefix = "emotion.wechat.mini-program")
|
||||
public class WechatMiniProgramProperties {
|
||||
|
||||
private String appId;
|
||||
|
||||
private String appSecret;
|
||||
|
||||
private String baseUrl = "https://api.weixin.qq.com";
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import com.emotion.dto.request.LoginRequest;
|
||||
import com.emotion.dto.request.RegisterRequest;
|
||||
import com.emotion.dto.request.RefreshTokenRequest;
|
||||
import com.emotion.dto.request.ResetPasswordRequest;
|
||||
import com.emotion.dto.request.WechatLoginRequest;
|
||||
import com.emotion.dto.response.ResetPasswordResponse;
|
||||
|
||||
import com.emotion.dto.response.AuthResponse;
|
||||
@@ -53,6 +54,13 @@ public class AuthController {
|
||||
/**
|
||||
* 用户注册(简化版:仅需手机号、密码和短信验证码)
|
||||
*/
|
||||
@PostMapping("/wechat/login")
|
||||
@Operation(summary = "微信小程序登录", description = "使用微信小程序登录 code 换取 openid 并登录")
|
||||
public Result<AuthResponse> wechatLogin(@Valid @RequestBody WechatLoginRequest request) {
|
||||
AuthResponse response = authService.wechatLogin(request);
|
||||
return Result.success("登录成功", response);
|
||||
}
|
||||
|
||||
@PostMapping(value = "/register")
|
||||
@Operation(summary = "用户注册", description = "使用手机号、密码和短信验证码进行注册")
|
||||
public Result<AuthResponse> register(@Valid @RequestBody RegisterRequest request) {
|
||||
@@ -177,4 +185,4 @@ public class AuthController {
|
||||
boolean exists = authService.existsByPhone(phone);
|
||||
return Result.success(exists);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.emotion.dto.request;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import javax.validation.constraints.NotBlank;
|
||||
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class WechatLoginRequest extends BaseRequest {
|
||||
|
||||
@NotBlank(message = "Wechat login code is required")
|
||||
private String code;
|
||||
|
||||
private String nickname;
|
||||
|
||||
private String avatar;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.emotion.dto.wechat;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.Data;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
@Data
|
||||
public class WechatCodeSessionResponse {
|
||||
|
||||
private String openid;
|
||||
|
||||
@JsonProperty("session_key")
|
||||
private String sessionKey;
|
||||
|
||||
private String unionid;
|
||||
|
||||
private Integer errcode;
|
||||
|
||||
private String errmsg;
|
||||
|
||||
public boolean isSuccess() {
|
||||
return (errcode == null || errcode == 0) && StringUtils.hasText(openid);
|
||||
}
|
||||
}
|
||||
@@ -98,6 +98,7 @@ public class JwtAuthInterceptor implements HandlerInterceptor {
|
||||
// 公开接口列表
|
||||
String[] publicEndpoints = {
|
||||
"/api/auth/login",
|
||||
"/api/auth/wechat/login",
|
||||
"/api/auth/register",
|
||||
"/api/auth/captcha",
|
||||
"/api/auth/refresh-token",
|
||||
|
||||
+13
-2
@@ -7,6 +7,7 @@ import org.springframework.http.client.ClientHttpRequestInterceptor;
|
||||
import org.springframework.http.client.ClientHttpResponse;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
@@ -34,13 +35,14 @@ public class WechatApiLoggingInterceptor implements ClientHttpRequestInterceptor
|
||||
ClientHttpRequestExecution execution) throws IOException {
|
||||
|
||||
long start = System.currentTimeMillis();
|
||||
log.info("[WeChatAPI] ---> {} {}", request.getMethod(), request.getURI());
|
||||
String safeUri = maskSensitiveQuery(request.getURI());
|
||||
log.info("[WeChatAPI] ---> {} {}", request.getMethod(), safeUri);
|
||||
|
||||
ClientHttpResponse response = execution.execute(request, body);
|
||||
long duration = System.currentTimeMillis() - start;
|
||||
|
||||
log.info("[WeChatAPI] <--- {} {} ({}ms) content-type={}",
|
||||
response.getStatusCode(), request.getURI(), duration,
|
||||
response.getStatusCode(), safeUri, duration,
|
||||
response.getHeaders().getContentType());
|
||||
|
||||
// body 读取(已由 BufferingClientHttpRequestFactory 缓存,可重复读)
|
||||
@@ -66,4 +68,13 @@ public class WechatApiLoggingInterceptor implements ClientHttpRequestInterceptor
|
||||
}
|
||||
return SENSITIVE_FIELDS.matcher(body).replaceAll("\"$1\":\"***\"");
|
||||
}
|
||||
|
||||
private String maskSensitiveQuery(URI uri) {
|
||||
if (uri == null) {
|
||||
return "";
|
||||
}
|
||||
String value = uri.toString();
|
||||
return value
|
||||
.replaceAll("(?i)([?&](?:secret|appsecret|js_code|access_token)=)[^&]*", "$1***");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.emotion.service;
|
||||
import com.emotion.dto.request.LoginRequest;
|
||||
import com.emotion.dto.request.RegisterRequest;
|
||||
import com.emotion.dto.request.ResetPasswordRequest;
|
||||
import com.emotion.dto.request.WechatLoginRequest;
|
||||
import com.emotion.dto.response.ResetPasswordResponse;
|
||||
|
||||
import com.emotion.dto.response.AuthResponse;
|
||||
@@ -28,6 +29,8 @@ public interface AuthService {
|
||||
*/
|
||||
AuthResponse login(LoginRequest request);
|
||||
|
||||
AuthResponse wechatLogin(WechatLoginRequest request);
|
||||
|
||||
/**
|
||||
* 用户注册
|
||||
*
|
||||
@@ -165,4 +168,4 @@ public interface AuthService {
|
||||
* @return 是否验证成功
|
||||
*/
|
||||
boolean validateSmsCode(String phone, String code);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,8 @@ public interface TtsTaskService extends IService<TtsTask> {
|
||||
|
||||
TtsTaskResponse createOrReuse(TtsTaskCreateRequest request);
|
||||
|
||||
void prewarmEpicScript(String userId, String sourceId);
|
||||
|
||||
TtsTaskResponse getTask(String id);
|
||||
|
||||
TtsTaskResponse getBySource(String sourceType, String sourceId, String voice,
|
||||
|
||||
@@ -69,6 +69,8 @@ public interface UserService extends IService<User> {
|
||||
*/
|
||||
User getByPhone(String phone);
|
||||
|
||||
User getByThirdParty(String thirdPartyType, String thirdPartyId);
|
||||
|
||||
/**
|
||||
* 创建用户
|
||||
*
|
||||
@@ -88,4 +90,4 @@ public interface UserService extends IService<User> {
|
||||
* @param lastActiveTime 最后活跃时间
|
||||
*/
|
||||
void updateLastActiveTime(String userId, LocalDateTime lastActiveTime);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.emotion.service;
|
||||
|
||||
import com.emotion.dto.wechat.WechatCodeSessionResponse;
|
||||
|
||||
public interface WechatMiniProgramService {
|
||||
|
||||
WechatCodeSessionResponse code2Session(String code);
|
||||
}
|
||||
@@ -3,11 +3,13 @@ package com.emotion.service.impl;
|
||||
import com.emotion.dto.request.LoginRequest;
|
||||
import com.emotion.dto.request.RegisterRequest;
|
||||
import com.emotion.dto.request.ResetPasswordRequest;
|
||||
import com.emotion.dto.request.WechatLoginRequest;
|
||||
import com.emotion.dto.response.AuthResponse;
|
||||
import com.emotion.dto.response.CaptchaResponse;
|
||||
import com.emotion.dto.response.ResetPasswordResponse;
|
||||
import com.emotion.dto.response.SmsCodeResponse;
|
||||
import com.emotion.dto.response.UserInfoResponse;
|
||||
import com.emotion.dto.wechat.WechatCodeSessionResponse;
|
||||
import com.emotion.entity.User;
|
||||
import com.emotion.exception.AuthException;
|
||||
import com.emotion.exception.BusinessException;
|
||||
@@ -15,6 +17,7 @@ import com.emotion.exception.CaptchaException;
|
||||
import com.emotion.exception.TokenException;
|
||||
import com.emotion.service.AuthService;
|
||||
import com.emotion.service.UserService;
|
||||
import com.emotion.service.WechatMiniProgramService;
|
||||
import com.emotion.util.JwtUtil;
|
||||
import com.emotion.util.TokenUtil;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@@ -51,6 +54,9 @@ public class AuthServiceImpl implements AuthService {
|
||||
@Autowired
|
||||
private UserService userService;
|
||||
|
||||
@Autowired
|
||||
private WechatMiniProgramService wechatMiniProgramService;
|
||||
|
||||
@Autowired
|
||||
private RedisTemplate<String, Object> redisTemplate;
|
||||
|
||||
@@ -72,6 +78,7 @@ public class AuthServiceImpl implements AuthService {
|
||||
private static final int TOKEN_EXPIRE_HOURS = 24;
|
||||
private static final int REFRESH_TOKEN_EXPIRE_DAYS = 7;
|
||||
private static final String DEFAULT_SMS_CODE = "123456";
|
||||
private static final String WECHAT_MP_TYPE = "wechat-mp";
|
||||
private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||
|
||||
@Override
|
||||
@@ -114,6 +121,32 @@ public class AuthServiceImpl implements AuthService {
|
||||
return response;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AuthResponse wechatLogin(WechatLoginRequest request) {
|
||||
WechatCodeSessionResponse session = wechatMiniProgramService.code2Session(request.getCode());
|
||||
String openid = session.getOpenid();
|
||||
|
||||
User user = userService.getByThirdParty(WECHAT_MP_TYPE, openid);
|
||||
if (user == null) {
|
||||
user = createWechatUser(openid, request);
|
||||
log.info("Wechat mini program user created: userId={}", user.getId());
|
||||
} else if (user.getStatus() != 1) {
|
||||
throw new AuthException("账号已被禁用");
|
||||
}
|
||||
|
||||
String accessToken = generateAccessToken(user);
|
||||
String refreshToken = generateRefreshToken(user);
|
||||
userService.updateLastActiveTime(user.getId(), LocalDateTime.now());
|
||||
|
||||
AuthResponse response = new AuthResponse();
|
||||
response.setAccessToken(accessToken);
|
||||
response.setRefreshToken(refreshToken);
|
||||
response.setExpiresIn((long) TOKEN_EXPIRE_HOURS * 3600);
|
||||
response.setUserInfo(convertToUserInfoResponse(user));
|
||||
response.setLoginTime(LocalDateTime.now().format(DATE_TIME_FORMATTER));
|
||||
return response;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AuthResponse register(RegisterRequest request) {
|
||||
// 验证短信验证码
|
||||
@@ -626,6 +659,24 @@ public class AuthServiceImpl implements AuthService {
|
||||
*
|
||||
* @return 随机密码
|
||||
*/
|
||||
private User createWechatUser(String openid, WechatLoginRequest request) {
|
||||
String username = generateRandomUsername();
|
||||
User user = new User();
|
||||
user.setAccount("wx_" + openid);
|
||||
user.setUsername(username);
|
||||
user.setNickname(StringUtils.hasText(request.getNickname()) ? request.getNickname() : username);
|
||||
user.setAvatar(StringUtils.hasText(request.getAvatar()) ? request.getAvatar() : null);
|
||||
user.setPassword(passwordEncoder.encode(UUID.randomUUID().toString()));
|
||||
user.setMemberLevel("free");
|
||||
user.setStatus(1);
|
||||
user.setIsVerified(1);
|
||||
user.setLastActiveTime(LocalDateTime.now());
|
||||
user.setThirdPartyType(WECHAT_MP_TYPE);
|
||||
user.setThirdPartyId(openid);
|
||||
userService.save(user);
|
||||
return user;
|
||||
}
|
||||
|
||||
private String generateRandomPassword() {
|
||||
String chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||
Random random = new Random();
|
||||
@@ -638,4 +689,4 @@ public class AuthServiceImpl implements AuthService {
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import com.emotion.service.AiRuntimeService;
|
||||
import com.emotion.service.EpicScriptService;
|
||||
import com.emotion.service.LifePathService;
|
||||
import com.emotion.service.ScriptContextService;
|
||||
import com.emotion.service.TtsTaskService;
|
||||
import com.emotion.util.UserContextHolder;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
@@ -75,6 +76,9 @@ public class EpicScriptServiceImpl extends ServiceImpl<EpicScriptMapper, EpicScr
|
||||
@Autowired
|
||||
private ScriptContextService scriptContextService;
|
||||
|
||||
@Autowired
|
||||
private TtsTaskService ttsTaskService;
|
||||
|
||||
@Override
|
||||
public PageResult<EpicScriptResponse> getPageByCurrentUser(EpicScriptPageRequest request) {
|
||||
String currentUserId = UserContextHolder.getCurrentUserId();
|
||||
@@ -195,6 +199,7 @@ public class EpicScriptServiceImpl extends ServiceImpl<EpicScriptMapper, EpicScr
|
||||
}
|
||||
|
||||
this.save(script);
|
||||
prewarmScriptTts(script);
|
||||
return convertToResponse(script);
|
||||
}
|
||||
|
||||
@@ -513,9 +518,22 @@ public class EpicScriptServiceImpl extends ServiceImpl<EpicScriptMapper, EpicScr
|
||||
}
|
||||
|
||||
this.updateById(script);
|
||||
prewarmScriptTts(script);
|
||||
return convertToResponse(script);
|
||||
}
|
||||
|
||||
private void prewarmScriptTts(EpicScript script) {
|
||||
if (script == null || !StringUtils.hasText(script.getId()) || !StringUtils.hasText(script.getUserId())) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
ttsTaskService.prewarmEpicScript(script.getUserId(), script.getId());
|
||||
} catch (Exception e) {
|
||||
log.warn("Epic script TTS prewarm failed, scriptId={}, userId={}, error={}",
|
||||
script.getId(), script.getUserId(), e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 调用Coze AI重新生成爽文剧本内容
|
||||
*
|
||||
|
||||
@@ -2,11 +2,14 @@ package com.emotion.service.impl;
|
||||
|
||||
import com.emotion.service.TtsEngineClient;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.web.client.RestTemplateBuilder;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
import java.io.File;
|
||||
import java.time.Duration;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@@ -14,12 +17,19 @@ import java.util.Map;
|
||||
public class HttpTtsEngineClient implements TtsEngineClient {
|
||||
|
||||
private final RestTemplate restTemplate;
|
||||
private final String engineUrl;
|
||||
|
||||
@Value("${emotion.tts.engine-url:http://127.0.0.1:19110}")
|
||||
private String engineUrl;
|
||||
|
||||
public HttpTtsEngineClient(RestTemplate restTemplate) {
|
||||
this.restTemplate = restTemplate;
|
||||
public HttpTtsEngineClient(RestTemplateBuilder restTemplateBuilder,
|
||||
@Value("${emotion.tts.engine-url:http://127.0.0.1:19110}") String engineUrl,
|
||||
@Value("${emotion.tts.engine-connect-timeout-ms:5000}") long connectTimeoutMs,
|
||||
@Value("${emotion.tts.engine-read-timeout-ms:240000}") long readTimeoutMs) {
|
||||
this.engineUrl = normalizeEngineUrl(engineUrl);
|
||||
this.restTemplate = restTemplateBuilder
|
||||
.setConnectTimeout(Duration.ofMillis(connectTimeoutMs))
|
||||
.setReadTimeout(Duration.ofMillis(readTimeoutMs))
|
||||
.defaultHeader("User-Agent", "EmotionMuseum-TTS/1.0")
|
||||
.defaultHeader("Accept", "application/json, text/plain, */*")
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -45,6 +55,9 @@ public class HttpTtsEngineClient implements TtsEngineClient {
|
||||
boolean success = data != null && Boolean.TRUE.equals(data.get("success"));
|
||||
if (!success) {
|
||||
String message = data == null ? "empty response" : String.valueOf(data.get("errorMessage"));
|
||||
if (hasGeneratedAudio(outputPath)) {
|
||||
return new TtsEngineResult(true, outputPath, null, null);
|
||||
}
|
||||
return new TtsEngineResult(false, null, null, message);
|
||||
}
|
||||
Long durationMs = data.get("durationMs") instanceof Number
|
||||
@@ -52,7 +65,26 @@ public class HttpTtsEngineClient implements TtsEngineClient {
|
||||
: null;
|
||||
return new TtsEngineResult(true, String.valueOf(data.get("audioPath")), durationMs, null);
|
||||
} catch (Exception e) {
|
||||
if (hasGeneratedAudio(outputPath)) {
|
||||
return new TtsEngineResult(true, outputPath, null, null);
|
||||
}
|
||||
return new TtsEngineResult(false, null, null, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private static String normalizeEngineUrl(String value) {
|
||||
if (!StringUtils.hasText(value)) {
|
||||
return "http://127.0.0.1:19110";
|
||||
}
|
||||
String trimmed = value.trim();
|
||||
return trimmed.endsWith("/") ? trimmed.substring(0, trimmed.length() - 1) : trimmed;
|
||||
}
|
||||
|
||||
private static boolean hasGeneratedAudio(String outputPath) {
|
||||
if (!StringUtils.hasText(outputPath)) {
|
||||
return false;
|
||||
}
|
||||
File file = new File(outputPath);
|
||||
return file.isFile() && file.length() > 0L;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.DigestUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
@@ -83,6 +84,25 @@ public class TtsTaskServiceImpl extends ServiceImpl<TtsTaskMapper, TtsTask> impl
|
||||
}
|
||||
|
||||
String userId = currentUserId();
|
||||
return createOrReuseForUser(userId, request);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void prewarmEpicScript(String userId, String sourceId) {
|
||||
if (!enabled || !StringUtils.hasText(userId) || !StringUtils.hasText(sourceId)) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
TtsTaskCreateRequest request = new TtsTaskCreateRequest();
|
||||
request.setSourceType(SOURCE_TYPE_EPIC_SCRIPT);
|
||||
request.setSourceId(sourceId);
|
||||
createOrReuseForUser(userId, request);
|
||||
} catch (Exception e) {
|
||||
log.warn("TTS prewarm skipped, userId={}, sourceId={}, error={}", userId, sourceId, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private TtsTaskResponse createOrReuseForUser(String userId, TtsTaskCreateRequest request) {
|
||||
String sourceType = normalizeSourceType(request.getSourceType());
|
||||
String sourceId = request.getSourceId().trim();
|
||||
String voice = resolveVoice(request.getVoice());
|
||||
@@ -98,8 +118,13 @@ public class TtsTaskServiceImpl extends ServiceImpl<TtsTaskMapper, TtsTask> impl
|
||||
String hash = DigestUtils.md5DigestAsHex((voice + "\n" + options.cacheKey() + "\n" + cleaned).getBytes(StandardCharsets.UTF_8));
|
||||
TtsTask owned = findOwnedTask(userId, sourceType, sourceId, voice, hash);
|
||||
if (owned != null) {
|
||||
incrementRequestCount(owned);
|
||||
return toResponse(owned);
|
||||
if (STATUS_FAILED.equals(owned.getStatus()) && !recoverGeneratedAudio(owned)) {
|
||||
log.info("Retrying failed TTS task, oldTaskId={}, sourceType={}, sourceId={}",
|
||||
owned.getId(), sourceType, sourceId);
|
||||
} else {
|
||||
incrementRequestCount(owned);
|
||||
return toResponse(owned);
|
||||
}
|
||||
}
|
||||
|
||||
TtsTask cachedSuccess = findSuccessfulCache(voice, hash);
|
||||
@@ -128,6 +153,9 @@ public class TtsTaskServiceImpl extends ServiceImpl<TtsTaskMapper, TtsTask> impl
|
||||
if (task == null || !userId.equals(task.getUserId())) {
|
||||
return null;
|
||||
}
|
||||
if (STATUS_FAILED.equals(task.getStatus())) {
|
||||
recoverGeneratedAudio(task);
|
||||
}
|
||||
return toResponse(task);
|
||||
}
|
||||
|
||||
@@ -144,9 +172,27 @@ public class TtsTaskServiceImpl extends ServiceImpl<TtsTaskMapper, TtsTask> impl
|
||||
}
|
||||
String hash = DigestUtils.md5DigestAsHex((normalizedVoice + "\n" + options.cacheKey() + "\n" + cleaned).getBytes(StandardCharsets.UTF_8));
|
||||
TtsTask task = findOwnedTask(userId, normalizedSourceType, sourceId, normalizedVoice, hash);
|
||||
if (task != null && STATUS_FAILED.equals(task.getStatus())) {
|
||||
recoverGeneratedAudio(task);
|
||||
}
|
||||
return task == null ? null : toResponse(task);
|
||||
}
|
||||
|
||||
private boolean recoverGeneratedAudio(TtsTask task) {
|
||||
if (task == null || !StringUtils.hasText(task.getAudioPath())) {
|
||||
return false;
|
||||
}
|
||||
File file = new File(task.getAudioPath());
|
||||
if (!file.isFile() || file.length() <= 0L) {
|
||||
return false;
|
||||
}
|
||||
task.setStatus(STATUS_SUCCESS);
|
||||
task.setErrorMessage(null);
|
||||
updateById(task);
|
||||
log.info("Recovered generated TTS audio, taskId={}, audioPath={}", task.getId(), task.getAudioPath());
|
||||
return true;
|
||||
}
|
||||
|
||||
private void process(String taskId, String text, String voice, String outputPath, TtsEngineClient.SynthesisOptions options) {
|
||||
try {
|
||||
TtsTask task = getById(taskId);
|
||||
@@ -178,6 +224,9 @@ public class TtsTaskServiceImpl extends ServiceImpl<TtsTaskMapper, TtsTask> impl
|
||||
log.warn("TTS task processing failed, taskId={}", taskId, e);
|
||||
TtsTask task = getById(taskId);
|
||||
if (task != null) {
|
||||
if (recoverGeneratedAudio(task)) {
|
||||
return;
|
||||
}
|
||||
task.setStatus(STATUS_FAILED);
|
||||
task.setErrorMessage(limitError(e.getMessage()));
|
||||
updateById(task);
|
||||
|
||||
@@ -278,6 +278,18 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements Us
|
||||
return this.getOne(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public User getByThirdParty(String thirdPartyType, String thirdPartyId) {
|
||||
if (!StringUtils.hasText(thirdPartyType) || !StringUtils.hasText(thirdPartyId)) {
|
||||
return null;
|
||||
}
|
||||
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(User::getThirdPartyType, thirdPartyType)
|
||||
.eq(User::getThirdPartyId, thirdPartyId)
|
||||
.eq(User::getIsDeleted, 0);
|
||||
return this.getOne(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public User createUser(String account, String username, String password, String email, String phone) {
|
||||
User user = new User();
|
||||
@@ -326,4 +338,4 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements Us
|
||||
}
|
||||
return response;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+36
-36
@@ -17,9 +17,6 @@ import org.springframework.web.client.RestClientException;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@@ -43,56 +40,37 @@ public class WechatMiniProgramServiceImpl implements WechatMiniProgramService {
|
||||
try {
|
||||
responseEntity = restTemplate.getForEntity(url, String.class);
|
||||
} catch (ResourceAccessException e) {
|
||||
// 网络层异常(连接超时、连接拒绝、DNS 失败)
|
||||
log.error("[WeChatAPI] 网络异常,jsCode={}", code, e);
|
||||
throw new WechatApiException(502, "微信API网络异常: " + e.getMessage(), null);
|
||||
log.error("[WeChatAPI] network error, jsCode={}", code, e);
|
||||
throw new WechatApiException(502, "Wechat API network error: " + e.getMessage(), null);
|
||||
} catch (RestClientException e) {
|
||||
// 其他 RestTemplate 异常(如 SSL、URI 解析)
|
||||
log.error("[WeChatAPI] 客户端异常,jsCode={}", code, e);
|
||||
throw new WechatApiException(502, "微信API客户端异常: " + e.getMessage(), null);
|
||||
log.error("[WeChatAPI] client error, jsCode={}", code, e);
|
||||
throw new WechatApiException(502, "Wechat API client error: " + e.getMessage(), null);
|
||||
}
|
||||
|
||||
// Step 2: HTTP 状态码检查(4xx/5xx 不会抛异常,因为 WechatResponseErrorHandler 处理了)
|
||||
String responseBody = responseEntity.getBody();
|
||||
if (!responseEntity.getStatusCode().is2xxSuccessful()) {
|
||||
log.error("[WeChatAPI] HTTP 非 2xx 响应,status={}", responseEntity.getStatusCode());
|
||||
log.error("[WeChatAPI] non-2xx response, status={}, body={}",
|
||||
responseEntity.getStatusCode(), responseBody);
|
||||
throw new WechatApiException(502,
|
||||
"微信API返回非2xx状态: " + responseEntity.getStatusCode(), responseEntity.getBody());
|
||||
"Wechat API returned non-2xx status: " + responseEntity.getStatusCode(), responseBody);
|
||||
}
|
||||
|
||||
// Step 3: Content-Type 检查(防御 WAF 返回 text/plain 拦截页)
|
||||
MediaType contentType = responseEntity.getHeaders().getContentType();
|
||||
if (contentType == null || !MediaType.APPLICATION_JSON.isCompatibleWith(contentType)) {
|
||||
log.error("[WeChatAPI] Content-Type 不是 JSON,contentType={}, body={}",
|
||||
contentType, responseEntity.getBody());
|
||||
throw new WechatApiException(502,
|
||||
"微信API返回非JSON Content-Type: " + contentType, responseEntity.getBody());
|
||||
}
|
||||
JSONObject result = parseWechatJsonBody(responseEntity);
|
||||
|
||||
// Step 4: JSON 解析
|
||||
JSONObject result;
|
||||
try {
|
||||
result = JSONObject.parseObject(responseEntity.getBody());
|
||||
} catch (Exception e) {
|
||||
log.error("[WeChatAPI] JSON 解析失败,body={}", responseEntity.getBody(), e);
|
||||
throw new WechatApiException(500, "微信API响应JSON解析失败: " + e.getMessage(), responseEntity.getBody());
|
||||
}
|
||||
|
||||
// Step 5: 业务错误码检查
|
||||
int errcode = result.getIntValue("errcode");
|
||||
if (errcode != 0) {
|
||||
String errmsg = result.getString("errmsg");
|
||||
log.error("[WeChatAPI] 业务错误,errcode={}, errmsg={}", errcode, errmsg);
|
||||
log.error("[WeChatAPI] business error, errcode={}, errmsg={}", errcode, errmsg);
|
||||
throw new WechatApiException(400,
|
||||
"微信API业务错误,errcode=" + errcode + ", errmsg=" + errmsg, responseEntity.getBody());
|
||||
"Wechat API business error: errcode=" + errcode + ", errmsg=" + errmsg, responseBody);
|
||||
}
|
||||
|
||||
// 成功:提取 openid 和 session_key
|
||||
String openid = result.getString("openid");
|
||||
String sessionKey = result.getString("session_key");
|
||||
if (openid == null || sessionKey == null) {
|
||||
log.error("[WeChatAPI] 响应缺少必要字段 (openid 或 session_key)");
|
||||
if (!StringUtils.hasText(openid) || !StringUtils.hasText(sessionKey)) {
|
||||
log.error("[WeChatAPI] response missing openid or session_key, body={}", responseBody);
|
||||
throw new WechatApiException(502,
|
||||
"微信API响应缺少 openid 或 session_key", responseEntity.getBody());
|
||||
"Wechat API response missing openid or session_key", responseBody);
|
||||
}
|
||||
|
||||
WechatCodeSessionResponse response = new WechatCodeSessionResponse();
|
||||
@@ -102,6 +80,28 @@ public class WechatMiniProgramServiceImpl implements WechatMiniProgramService {
|
||||
return response;
|
||||
}
|
||||
|
||||
private JSONObject parseWechatJsonBody(ResponseEntity<String> responseEntity) {
|
||||
String responseBody = responseEntity.getBody();
|
||||
MediaType contentType = responseEntity.getHeaders().getContentType();
|
||||
boolean jsonContentType = contentType != null && MediaType.APPLICATION_JSON.isCompatibleWith(contentType);
|
||||
boolean jsonLikeBody = StringUtils.hasText(responseBody)
|
||||
&& (responseBody.trim().startsWith("{") || responseBody.trim().startsWith("["));
|
||||
|
||||
if (!jsonContentType && !jsonLikeBody) {
|
||||
log.error("[WeChatAPI] non-JSON response, contentType={}, body={}", contentType, responseBody);
|
||||
throw new WechatApiException(502,
|
||||
"Wechat API returned a non-JSON response: " + contentType, responseBody);
|
||||
}
|
||||
|
||||
try {
|
||||
return JSONObject.parseObject(responseBody);
|
||||
} catch (Exception e) {
|
||||
log.error("[WeChatAPI] JSON parse failed, contentType={}, body={}", contentType, responseBody, e);
|
||||
throw new WechatApiException(jsonContentType ? 500 : 502,
|
||||
"Wechat API response JSON parse failed: " + e.getMessage(), responseBody);
|
||||
}
|
||||
}
|
||||
|
||||
private void validateConfigured() {
|
||||
if (!StringUtils.hasText(properties.getAppId()) || !StringUtils.hasText(properties.getAppSecret())) {
|
||||
throw new BusinessException("微信小程序登录未配置");
|
||||
|
||||
@@ -65,6 +65,8 @@ emotion:
|
||||
tts:
|
||||
enabled: true
|
||||
engine-url: http://127.0.0.1:19110
|
||||
engine-connect-timeout-ms: 5000
|
||||
engine-read-timeout-ms: 240000
|
||||
output-dir: /data/uploads/emotion-museum/tts
|
||||
public-url-prefix: /tts/audio
|
||||
max-text-length: 5000
|
||||
|
||||
@@ -73,6 +73,12 @@ emotion:
|
||||
header: Authorization
|
||||
prefix: "Bearer "
|
||||
|
||||
wechat:
|
||||
mini-program:
|
||||
app-id: ${WECHAT_MINI_PROGRAM_APP_ID:wxaf2eaba72d28f0e4}
|
||||
app-secret: ${WECHAT_MINI_PROGRAM_APP_SECRET:4f087bd363a4147ef543d634f9f6950d}
|
||||
base-url: https://api.weixin.qq.com
|
||||
|
||||
# Coze API配置 - 所有环境统一
|
||||
coze:
|
||||
api:
|
||||
@@ -102,6 +108,8 @@ emotion:
|
||||
tts:
|
||||
enabled: true
|
||||
engine-url: http://127.0.0.1:19110
|
||||
engine-connect-timeout-ms: 5000
|
||||
engine-read-timeout-ms: 240000
|
||||
output-dir: /data/uploads/emotion-museum/tts
|
||||
public-url-prefix: /tts/audio
|
||||
max-text-length: 5000
|
||||
|
||||
Reference in New Issue
Block a user