feat: 修复 Redis 超时问题、固定小程序端口、新增人生事件模块及优化多个页面

- 修复 Redis 超时:添加 commons-pool2 依赖,启用 Lettuce 连接池,超时提升至 15s
- 固定 mini-program H5 端口为 5175,避免与 web 项目端口冲突
- 新增人生事件(life-event)模块:表单和详情页面
- 新增 EpicScript 灵感接口(Controller/Service/DTO)
- 优化登录、引导、主页、记录、剧本详情等多个页面
- 优化服务管理脚本和 Nginx 配置

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-10 11:38:35 +08:00
parent 507d1ebdab
commit 60c63850ee
36 changed files with 4545 additions and 3043 deletions
@@ -3,9 +3,12 @@ package com.emotion.controller;
import com.emotion.common.PageResult;
import com.emotion.common.Result;
import com.emotion.dto.request.EpicScriptCreateRequest;
import com.emotion.dto.request.EpicScriptInspirationRequest;
import com.emotion.dto.request.EpicScriptPageRequest;
import com.emotion.dto.request.EpicScriptUpdateRequest;
import com.emotion.dto.response.EpicScriptInspirationResponse;
import com.emotion.dto.response.EpicScriptResponse;
import com.emotion.dto.response.InspirationSuggestionResponse;
import com.emotion.service.EpicScriptService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
@@ -45,6 +48,32 @@ public class EpicScriptController {
return Result.success(scripts);
}
@GetMapping(value = "/inspiration/recommendations")
public Result<List<InspirationSuggestionResponse>> getInspirationRecommendations() {
return Result.success(epicScriptService.getInspirationRecommendations());
}
@GetMapping(value = "/inspiration/random")
public Result<List<InspirationSuggestionResponse>> getRandomInspirations(
@RequestParam(required = false, defaultValue = "3") Integer size) {
return Result.success(epicScriptService.getRandomInspirations(size));
}
@PostMapping(value = "/inspiration/generate")
public Result<EpicScriptInspirationResponse> generateFromInspiration(
@Valid @RequestBody EpicScriptInspirationRequest request) {
EpicScriptInspirationResponse response;
try {
response = epicScriptService.generateFromInspiration(request);
} catch (IllegalStateException e) {
return Result.error(e.getMessage());
}
if (response == null) {
return Result.error("灵感剧本生成失败");
}
return Result.success(response);
}
/**
* 根据ID获取爽文剧本详情
*/
@@ -0,0 +1,28 @@
package com.emotion.dto.request;
import lombok.Data;
import lombok.EqualsAndHashCode;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Size;
@Data
@EqualsAndHashCode(callSuper = true)
public class EpicScriptInspirationRequest extends BaseRequest {
@NotBlank(message = "灵感内容不能为空")
@Size(max = 500, message = "灵感内容不能超过500个字符")
private String prompt;
private String mode = "inspiration";
private String style;
private String length;
private String characterInfo;
private String lifeEventsSummary;
private String source;
}
@@ -0,0 +1,17 @@
package com.emotion.dto.response;
import lombok.Data;
import java.util.List;
@Data
public class EpicScriptInspirationResponse {
private EpicScriptResponse script;
private String prompt;
private Integer remainingCount;
private List<InspirationSuggestionResponse> suggestions;
}
@@ -0,0 +1,17 @@
package com.emotion.dto.response;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class InspirationSuggestionResponse {
private String text;
private String tag;
private String category;
}
@@ -3,9 +3,12 @@ package com.emotion.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.emotion.common.PageResult;
import com.emotion.dto.request.EpicScriptCreateRequest;
import com.emotion.dto.request.EpicScriptInspirationRequest;
import com.emotion.dto.request.EpicScriptPageRequest;
import com.emotion.dto.request.EpicScriptUpdateRequest;
import com.emotion.dto.response.EpicScriptInspirationResponse;
import com.emotion.dto.response.EpicScriptResponse;
import com.emotion.dto.response.InspirationSuggestionResponse;
import com.emotion.entity.EpicScript;
import java.util.List;
@@ -49,6 +52,12 @@ public interface EpicScriptService extends IService<EpicScript> {
*/
EpicScriptResponse createScript(EpicScriptCreateRequest request);
List<InspirationSuggestionResponse> getInspirationRecommendations();
List<InspirationSuggestionResponse> getRandomInspirations(Integer size);
EpicScriptInspirationResponse generateFromInspiration(EpicScriptInspirationRequest request);
/**
* 更新剧本
*
@@ -5,9 +5,12 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.emotion.common.PageResult;
import com.emotion.dto.request.EpicScriptCreateRequest;
import com.emotion.dto.request.EpicScriptInspirationRequest;
import com.emotion.dto.request.EpicScriptPageRequest;
import com.emotion.dto.request.EpicScriptUpdateRequest;
import com.emotion.dto.response.EpicScriptInspirationResponse;
import com.emotion.dto.response.EpicScriptResponse;
import com.emotion.dto.response.InspirationSuggestionResponse;
import com.emotion.entity.EpicScript;
import com.emotion.mapper.EpicScriptMapper;
import com.emotion.service.AiChatService;
@@ -21,7 +24,12 @@ import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@@ -38,6 +46,17 @@ public class EpicScriptServiceImpl extends ServiceImpl<EpicScriptMapper, EpicScr
implements EpicScriptService {
private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
private static final int DAILY_INSPIRATION_LIMIT = 3;
private static final List<InspirationSuggestionResponse> INSPIRATION_SUGGESTIONS = List.of(
new InspirationSuggestionResponse("我想把最近一次低谷,改写成主角觉醒的开端。", "觉醒", "转折"),
new InspirationSuggestionResponse("如果我在最遗憾的选择里勇敢了一次,人生会怎样展开?", "遗憾", "重启"),
new InspirationSuggestionResponse("把一次普通的职场挑战,写成逆风翻盘的高光篇章。", "职场", "成长"),
new InspirationSuggestionResponse("我想见到十年后的自己,让 TA 给现在的我一封信。", "未来", "对话"),
new InspirationSuggestionResponse("把一段关系里的告别,写成重新认识自己的旅程。", "关系", "治愈"),
new InspirationSuggestionResponse("让我的童年记忆成为故事里的隐藏力量。", "童年", "力量"),
new InspirationSuggestionResponse("把一次失败的面试、考试或竞赛,改写成命运伏笔。", "挑战", "伏笔"),
new InspirationSuggestionResponse("写一个我终于不再讨好别人,开始选择自己的平行人生。", "自我", "选择")
);
/**
* Coze工作流配置键 - 爽文剧本生成
@@ -167,6 +186,76 @@ public class EpicScriptServiceImpl extends ServiceImpl<EpicScriptMapper, EpicScr
return convertToResponse(script);
}
@Override
public List<InspirationSuggestionResponse> getInspirationRecommendations() {
return INSPIRATION_SUGGESTIONS;
}
@Override
public List<InspirationSuggestionResponse> getRandomInspirations(Integer size) {
int limit = size == null ? 3 : Math.max(1, Math.min(size, INSPIRATION_SUGGESTIONS.size()));
List<InspirationSuggestionResponse> suggestions = new ArrayList<>(INSPIRATION_SUGGESTIONS);
Collections.shuffle(suggestions);
return suggestions.subList(0, limit);
}
@Override
public EpicScriptInspirationResponse generateFromInspiration(EpicScriptInspirationRequest request) {
String currentUserId = UserContextHolder.getCurrentUserId();
if (currentUserId == null) {
return null;
}
int usedToday = countTodayScripts(currentUserId);
if (usedToday >= DAILY_INSPIRATION_LIMIT) {
throw new IllegalStateException("今日灵感生成次数已用完");
}
String prompt = request.getPrompt().trim();
EpicScriptCreateRequest createRequest = new EpicScriptCreateRequest();
createRequest.setTitle(buildInspirationTitle(prompt));
createRequest.setTheme(prompt);
createRequest.setStyle(StringUtils.hasText(request.getStyle()) ? request.getStyle() : "career");
createRequest.setLength(StringUtils.hasText(request.getLength()) ? request.getLength() : "medium");
createRequest.setCharacterInfo(request.getCharacterInfo());
createRequest.setLifeEventsSummary(request.getLifeEventsSummary());
Map<String, Object> plotJson = new HashMap<>();
plotJson.put("mode", "inspiration");
plotJson.put("prompt", prompt);
plotJson.put("source", StringUtils.hasText(request.getSource()) ? request.getSource() : "mini-program");
createRequest.setPlotJson(plotJson);
EpicScriptResponse script = createScript(createRequest);
EpicScriptInspirationResponse response = new EpicScriptInspirationResponse();
response.setScript(script);
response.setPrompt(prompt);
response.setRemainingCount(Math.max(0, DAILY_INSPIRATION_LIMIT - usedToday - 1));
response.setSuggestions(getRandomInspirations(3));
return response;
}
private int countTodayScripts(String userId) {
LocalDate today = LocalDate.now();
LocalDateTime start = today.atStartOfDay();
LocalDateTime end = today.plusDays(1).atStartOfDay();
LambdaQueryWrapper<EpicScript> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(EpicScript::getUserId, userId)
.eq(EpicScript::getIsDeleted, 0)
.ge(EpicScript::getCreateTime, start)
.lt(EpicScript::getCreateTime, end);
return Math.toIntExact(this.count(wrapper));
}
private String buildInspirationTitle(String prompt) {
String normalized = prompt.replaceAll("\\s+", " ").trim();
if (normalized.length() <= 22) {
return normalized;
}
return normalized.substring(0, 22) + "...";
}
/**
* 调用Coze AI生成爽文剧本内容
*