feat: 完成见素起名小程序核心功能

- 实现 AI 起名功能(Kimi API 接入)
- 添加用户收藏功能(MySQL 数据库)
- 实现海报生成与分享
- 添加音效和触觉反馈
- 配置生产环境部署(WAR 包 + Nginx)
- 支持多种起名模式(经典、诗词、自然、现代)
- 实现分批加载优化体验
This commit is contained in:
王鹏
2026-04-17 15:34:51 +08:00
parent 1a749cdf71
commit be1f5722ab
136 changed files with 3322 additions and 420 deletions

View File

@@ -1,6 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="FrameworkDetectionExcludesConfiguration">
<file type="web" url="file://$PROJECT_DIR$" />
</component>
<component name="MavenProjectsManager">
<option name="originalFiles">
<list>

6
backend/.idea/vcs.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$/.." vcs="Git" />
</component>
</project>

View File

@@ -11,6 +11,7 @@
<groupId>com.jiansu</groupId>
<artifactId>naming</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>war</packaging>
<name>jiansu-naming</name>
<description>JianSu Naming AI Backend</description>
<properties>
@@ -21,6 +22,12 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 打包为 WARTomcat 由外部容器提供 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
@@ -46,6 +53,24 @@
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- JPA 支持 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- H2 内存数据库(开发测试用) -->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<!-- MySQL 5.7 驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.49</version>
<scope>runtime</scope>
</dependency>
</dependencies>
<build>

View File

@@ -2,10 +2,18 @@ package com.jiansu.naming;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;
@SpringBootApplication
public class NamingApplication {
public class NamingApplication extends SpringBootServletInitializer {
public static void main(String[] args) {
SpringApplication.run(NamingApplication.class, args);
}
@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
return application.sources(NamingApplication.class);
}
}

View File

@@ -0,0 +1,35 @@
package com.jiansu.naming.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;
@Configuration
@EnableAsync
public class AsyncConfig {
@Bean("jianSuAiExecutor")
public Executor jianSuAiExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
// 核心线程数
executor.setCorePoolSize(10);
// 最大线程数
executor.setMaxPoolSize(20);
// 队列容量
executor.setQueueCapacity(50);
// 线程名前缀
executor.setThreadNamePrefix("jiansu-ai-");
// 拒绝策略:由调用线程处理
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
// 等待所有任务完成后再关闭
executor.setWaitForTasksToCompleteOnShutdown(true);
// 等待时间
executor.setAwaitTerminationSeconds(60);
executor.initialize();
return executor;
}
}

View File

@@ -0,0 +1,90 @@
package com.jiansu.naming.controller;
import com.jiansu.naming.entity.UserFavorite;
import com.jiansu.naming.model.NameCard;
import com.jiansu.naming.service.FavoriteService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@RestController
@RequestMapping("/api/favorites")
@CrossOrigin(origins = "*")
@Slf4j
public class FavoriteController {
@Autowired
private FavoriteService favoriteService;
/**
* 添加收藏
*/
@PostMapping("/add")
public ResponseEntity<?> addFavorite(
@RequestParam String openid,
@RequestBody NameCard nameCard,
@RequestParam(required = false) String mode,
@RequestParam(required = false) String keyword) {
UserFavorite favorite = favoriteService.addFavorite(openid, nameCard, mode, keyword);
Map<String, Object> response = new HashMap<>();
if (favorite != null) {
response.put("success", true);
response.put("message", "收藏成功");
response.put("data", favorite);
} else {
response.put("success", false);
response.put("message", "已收藏过该名字");
}
return ResponseEntity.ok(response);
}
/**
* 获取用户收藏列表
*/
@GetMapping("/list")
public ResponseEntity<?> getFavorites(@RequestParam String openid) {
List<NameCard> favorites = favoriteService.getUserFavorites(openid);
Map<String, Object> response = new HashMap<>();
response.put("success", true);
response.put("data", favorites);
return ResponseEntity.ok(response);
}
/**
* 取消收藏
*/
@PostMapping("/remove")
public ResponseEntity<?> removeFavorite(
@RequestParam String openid,
@RequestParam String name) {
favoriteService.removeFavorite(openid, name);
Map<String, Object> response = new HashMap<>();
response.put("success", true);
response.put("message", "取消收藏成功");
return ResponseEntity.ok(response);
}
/**
* 检查是否已收藏
*/
@GetMapping("/check")
public ResponseEntity<?> checkFavorite(
@RequestParam String openid,
@RequestParam String name) {
boolean isFavorited = favoriteService.isFavorited(openid, name);
Map<String, Object> response = new HashMap<>();
response.put("success", true);
response.put("isFavorited", isFavorited);
return ResponseEntity.ok(response);
}
}

View File

@@ -1,22 +1,36 @@
package com.jiansu.naming.controller;
import com.jiansu.naming.model.NameCard;
import com.jiansu.naming.service.MiniMaxService;
import com.jiansu.naming.service.KimiService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.concurrent.CompletableFuture;
@RestController
@RequestMapping("/api/names")
@RequestMapping(value = "/api/names", produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
@CrossOrigin(origins = "*")
@Slf4j
public class NamingController {
@Autowired
private MiniMaxService miniMaxService;
private KimiService kimiService;
@GetMapping("/generate")
public List<NameCard> generate(@RequestParam(defaultValue = "清冷") String keyword) {
return miniMaxService.generateNames(keyword);
public List<NameCard> generate(
@RequestParam(defaultValue = "清冷") String keyword,
@RequestParam(required = false) String mode,
@RequestParam(required = false) String surname,
@RequestParam(required = false, defaultValue = "3") Integer count,
@RequestParam(required = false) String batch) {
log.info("生成名字请求 - 关键词: {}, 模式: {}, 批次: {}, 数量: {}", keyword, mode, batch, count);
// 使用异步方法获取结果
CompletableFuture<List<NameCard>> future = kimiService.generateNamesAsync(keyword, mode, surname, count);
return future.join();
}
}

View File

@@ -0,0 +1,54 @@
package com.jiansu.naming.entity;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.persistence.*;
import java.time.LocalDateTime;
@Entity
@Table(name = "user_favorites")
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class UserFavorite {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "openid", nullable = false, length = 100)
private String openid;
@Column(name = "name", nullable = false, length = 50)
private String name;
@Column(name = "origin", length = 500)
private String origin;
@Column(name = "description", length = 1000)
private String description;
@Column(name = "tone", length = 20)
private String tone;
@Column(name = "score")
private Double score;
@Column(name = "mode", length = 20)
private String mode;
@Column(name = "keyword", length = 100)
private String keyword;
@Column(name = "create_time")
private LocalDateTime createTime;
@PrePersist
protected void onCreate() {
createTime = LocalDateTime.now();
}
}

View File

@@ -0,0 +1,32 @@
package com.jiansu.naming.repository;
import com.jiansu.naming.entity.UserFavorite;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Optional;
@Repository
public interface UserFavoriteRepository extends JpaRepository<UserFavorite, Long> {
/**
* 根据 openid 查询用户的所有收藏
*/
List<UserFavorite> findByOpenidOrderByCreateTimeDesc(String openid);
/**
* 根据 openid 和名字查询收藏
*/
Optional<UserFavorite> findByOpenidAndName(String openid, String name);
/**
* 检查用户是否已收藏某个名字
*/
boolean existsByOpenidAndName(String openid, String name);
/**
* 删除用户的某个收藏
*/
void deleteByOpenidAndName(String openid, String name);
}

View File

@@ -0,0 +1,76 @@
package com.jiansu.naming.service;
import com.jiansu.naming.entity.UserFavorite;
import com.jiansu.naming.model.NameCard;
import com.jiansu.naming.repository.UserFavoriteRepository;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.stream.Collectors;
@Slf4j
@Service
public class FavoriteService {
@Autowired
private UserFavoriteRepository userFavoriteRepository;
/**
* 添加收藏
*/
public UserFavorite addFavorite(String openid, NameCard nameCard, String mode, String keyword) {
// 检查是否已收藏
if (userFavoriteRepository.existsByOpenidAndName(openid, nameCard.getName())) {
log.info("用户 {} 已收藏名字 {}", openid, nameCard.getName());
return null;
}
UserFavorite favorite = UserFavorite.builder()
.openid(openid)
.name(nameCard.getName())
.origin(nameCard.getOrigin())
.description(nameCard.getDescription())
.tone(nameCard.getTone())
.score(nameCard.getScore())
.mode(mode)
.keyword(keyword)
.build();
UserFavorite saved = userFavoriteRepository.save(favorite);
log.info("用户 {} 收藏名字 {} 成功", openid, nameCard.getName());
return saved;
}
/**
* 获取用户的所有收藏
*/
public List<NameCard> getUserFavorites(String openid) {
List<UserFavorite> favorites = userFavoriteRepository.findByOpenidOrderByCreateTimeDesc(openid);
return favorites.stream()
.map(fav -> NameCard.builder()
.name(fav.getName())
.origin(fav.getOrigin())
.description(fav.getDescription())
.tone(fav.getTone())
.score(fav.getScore())
.build())
.collect(Collectors.toList());
}
/**
* 取消收藏
*/
public void removeFavorite(String openid, String name) {
userFavoriteRepository.deleteByOpenidAndName(openid, name);
log.info("用户 {} 取消收藏名字 {}", openid, name);
}
/**
* 检查是否已收藏
*/
public boolean isFavorited(String openid, String name) {
return userFavoriteRepository.existsByOpenidAndName(openid, name);
}
}

View File

@@ -0,0 +1,314 @@
package com.jiansu.naming.service;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.jiansu.naming.model.NameCard;
import lombok.extern.slf4j.Slf4j;
import okhttp3.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import java.io.IOException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
@Slf4j
@Service
public class KimiService {
@Value("${kimi.api-key}")
private String apiKey;
@Value("${kimi.api-url}")
private String apiUrl;
private final ToneAnalysisService toneAnalysisService;
@Autowired
@Qualifier("jianSuAiExecutor")
private Executor jianSuAiExecutor;
public KimiService(ToneAnalysisService toneAnalysisService) {
this.toneAnalysisService = toneAnalysisService;
}
private final OkHttpClient client = createTrustAllOkHttpClient();
/**
* 创建信任所有证书的 OkHttpClient仅用于开发环境解决 SSL 证书问题)
*/
private static OkHttpClient createTrustAllOkHttpClient() {
try {
// 创建信任所有证书的信任管理器
TrustManager[] trustAllCerts = new TrustManager[]{
new X509TrustManager() {
@Override
public void checkClientTrusted(X509Certificate[] chain, String authType) {}
@Override
public void checkServerTrusted(X509Certificate[] chain, String authType) {}
@Override
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[]{};
}
}
};
// 安装信任管理器
SSLContext sslContext = SSLContext.getInstance("SSL");
sslContext.init(null, trustAllCerts, new java.security.SecureRandom());
return new OkHttpClient.Builder()
.sslSocketFactory(sslContext.getSocketFactory(), (X509TrustManager) trustAllCerts[0])
.hostnameVerifier((hostname, session) -> true)
.connectTimeout(60, TimeUnit.SECONDS)
.writeTimeout(60, TimeUnit.SECONDS)
.readTimeout(120, TimeUnit.SECONDS)
.build();
} catch (Exception e) {
throw new RuntimeException("创建 OkHttpClient 失败", e);
}
}
// 多模式 Prompt 模板
private static final Map<String, String> PROMPT_TEMPLATES = new HashMap<>();
static {
// 宝宝起名模式 - 单次生成1个名字
PROMPT_TEMPLATES.put("baby",
"你是「见素」的起名大师。请为姓氏为{surname}的宝宝起名,结合关键词'{keyword}'的期待从《诗经》、《楚辞》或宋词中提取意象生成1个极具美感的名字。" +
"名字必须配有一段 50 字以内的'通感'描述,包含气味、光线或声音的描写,拒绝说教。" +
"请直接以 JSON 对象格式返回JSON 对象包含三个字段name (完整姓名), origin (诗词出处), description (通感描述)。不要包含任何 Markdown 格式。");
// 人设起名模式 - 单次生成1个名字
PROMPT_TEMPLATES.put("persona",
"你是「见素」的人设命名师。请为想要塑造'{keyword}'人设的用户起名从《诗经》、《楚辞》或宋词中提取意象生成1个极具美感的名字。" +
"名字必须配有一段 50 字以内的'通感'描述,包含气味、光线或声音的描写,拒绝说教。" +
"请直接以 JSON 对象格式返回JSON 对象包含三个字段name (名字), origin (诗词出处), description (通感描述)。不要包含任何 Markdown 格式。");
// 拾遗模式(经典模式)- 单次生成1个名字
PROMPT_TEMPLATES.put("classic",
"你是「见素」的灵魂导师。当用户输入期待或关键词'{keyword}'时请从《诗经》、《楚辞》或宋词中提取意象生成1个极具美感的名字。" +
"名字必须配有一段 50 字以内的'通感'描述,包含气味、光线或声音的描写,拒绝说教。" +
"请直接以 JSON 对象格式返回JSON 对象包含三个字段name (名字), origin (诗词出处), description (通感描述)。不要包含任何 Markdown 格式。");
}
// 原方法保留,默认使用 classic 模式
public List<NameCard> generateNames(String keyword) {
return generateNames(keyword, "classic", null);
}
// 原同步方法保留
public List<NameCard> generateNames(String keyword, String mode, String surname) {
return generateNamesAsync(keyword, mode, surname, 3).join();
}
/**
* 异步并行生成名字
* @param keyword 关键词
* @param mode 模式
* @param surname 姓氏
* @param count 生成数量
* @return CompletableFuture<List<NameCard>>
*/
public CompletableFuture<List<NameCard>> generateNamesAsync(String keyword, String mode, String surname, int count) {
long startTime = System.currentTimeMillis();
log.info("开始异步生成 {} 个名字,模式: {}", count, mode);
// 后门:当关键词为 "test" 时,返回固定的测试数据
if ("test".equalsIgnoreCase(keyword)) {
log.info("Keyword is 'test', returning hardcoded test data.");
return CompletableFuture.completedFuture(createTestData());
}
// 默认使用 classic 模式
final String finalMode = (mode == null || mode.isEmpty()) ? "classic" : mode;
// 创建 count 个并行任务
List<CompletableFuture<NameCard>> futures = new ArrayList<>();
for (int i = 0; i < count; i++) {
final int index = i;
CompletableFuture<NameCard> future = CompletableFuture.supplyAsync(() -> {
try {
return callAiToGenerateSingleName(keyword, finalMode, surname, index);
} catch (Exception e) {
log.error("第 {} 个名字生成失败: {}", index, e.getMessage());
return null;
}
}, jianSuAiExecutor).exceptionally(ex -> {
log.error("第 {} 个任务异常: {}", index, ex.getMessage());
return null;
});
futures.add(future);
}
// 等待所有任务完成并合并结果
return CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]))
.thenApply(v -> {
List<NameCard> results = futures.stream()
.map(CompletableFuture::join)
.filter(card -> card != null)
.collect(Collectors.toList());
long duration = System.currentTimeMillis() - startTime;
log.info("异步生成完成,成功 {} 个,耗时 {}ms", results.size(), duration);
return results;
});
}
/**
* 调用 Kimi AI 生成单个名字
*/
private NameCard callAiToGenerateSingleName(String keyword, String mode, String surname, int seed) {
String promptTemplate = PROMPT_TEMPLATES.getOrDefault(mode, PROMPT_TEMPLATES.get("classic"));
// 动态构建 Prompt加入 seed 使每次请求略有不同
String systemPrompt = promptTemplate
.replace("{keyword}", keyword != null ? keyword : "")
.replace("{surname}", surname != null ? surname : "")
+ " 请确保创意独特,避免与之前生成的名字重复。";
// 构建 Kimi API 请求体 (OpenAI 兼容格式)
JSONObject jsonBody = new JSONObject();
jsonBody.put("model", "moonshot-v1-8k");
jsonBody.put("temperature", 0.7);
// 使用随机种子确保每次生成结果不同
jsonBody.put("seed", (int) (Math.random() * 1000000) + seed * 1000);
JSONArray messages = new JSONArray();
JSONObject systemMsg = new JSONObject();
systemMsg.put("role", "system");
systemMsg.put("content", systemPrompt);
messages.add(systemMsg);
JSONObject userMsg = new JSONObject();
userMsg.put("role", "user");
userMsg.put("content", "用户期待/关键词是:" + keyword + ",请生成第" + (seed + 1) + "个名字。");
messages.add(userMsg);
jsonBody.put("messages", messages);
RequestBody body = RequestBody.create(
jsonBody.toJSONString(),
MediaType.parse("application/json; charset=utf-8")
);
Request request = new Request.Builder()
.url(apiUrl)
.post(body)
.addHeader("Authorization", "Bearer " + apiKey)
.addHeader("Content-Type", "application/json")
.build();
try (Response response = client.newCall(request).execute()) {
if (response.isSuccessful() && response.body() != null) {
String responseBody = response.body().string();
log.debug("Kimi raw response: {}", responseBody);
return parseSingleNameCard(responseBody);
} else {
String errorMsg = response.body() != null ? response.body().string() : response.message();
log.error("Kimi API Error: {} - {}", response.code(), errorMsg);
throw new RuntimeException("Kimi API 调用失败: " + errorMsg);
}
} catch (IOException e) {
log.error("API Error: ", e);
throw new RuntimeException("网络请求失败");
}
}
/**
* 解析单个名字卡片 (Kimi 格式)
*/
private NameCard parseSingleNameCard(String responseBody) {
try {
JSONObject root = JSON.parseObject(responseBody);
// 检查错误
JSONObject error = root.getJSONObject("error");
if (error != null) {
String errorMsg = error.getString("message");
log.error("Kimi API error: {}", errorMsg);
throw new RuntimeException("Kimi API 错误: " + errorMsg);
}
JSONArray choices = root.getJSONArray("choices");
if (choices == null || choices.isEmpty()) {
throw new RuntimeException("Kimi 返回内容为空");
}
JSONObject firstChoice = choices.getJSONObject(0);
JSONObject message = firstChoice.getJSONObject("message");
String aiText = message.getString("content");
if (aiText == null || aiText.isEmpty()) {
throw new RuntimeException("Kimi 返回内容为空");
}
// 清理可能存在的 Markdown 代码块标记
aiText = aiText.replaceAll("```json", "").replaceAll("```", "").trim();
JSONObject jsonObject = JSON.parseObject(aiText);
String name = jsonObject.getString("name");
// 进行声韵分析
ToneAnalysisService.ToneResult tr = toneAnalysisService.analyze(name);
return NameCard.builder()
.name(name)
.origin(jsonObject.getString("origin"))
.description(jsonObject.getString("description"))
.score(tr.score)
.tone(tr.tonePattern)
.build();
} catch (RuntimeException e) {
throw e;
} catch (Exception e) {
log.error("Parse Error: ", e);
throw new RuntimeException("数据解析失败");
}
}
private List<NameCard> createTestData() {
List<NameCard> testData = new ArrayList<>();
testData.add(NameCard.builder()
.name("月白")
.origin("《月出》:月出皎兮,佼人僚兮。")
.description("像月光洒在雪地,清冷又干净,带着一丝不易察觉的、风吹过竹林的微响。")
.score(9.8)
.tone("仄仄")
.build());
testData.add(NameCard.builder()
.name("南絮")
.origin("晏几道《御街行》:街南绿树春饶絮。")
.description("春天街巷里柔软的柳絮,随风飘浮,带着南方阳光的暖意和青草的气息。")
.score(9.5)
.tone("平仄")
.build());
testData.add(NameCard.builder()
.name("疏影")
.origin("林逋《山园小梅》:疏影横斜水清浅,暗香浮动月黄昏。")
.description("黄昏月下,梅枝稀疏的影子落在清浅的水面,空气中浮动着若有似无的冷香。")
.score(9.7)
.tone("平仄")
.build());
return testData;
}
}

View File

@@ -1,184 +0,0 @@
package com.jiansu.naming.service;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.jiansu.naming.model.NameCard;
import lombok.extern.slf4j.Slf4j;
import okhttp3.*;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
@Slf4j
@Service
public class MiniMaxService {
@Value("${minimax.api-key}")
private String apiKey;
@Value("${minimax.api-url}")
private String apiUrl;
private final ToneAnalysisService toneAnalysisService;
public MiniMaxService(ToneAnalysisService toneAnalysisService) {
this.toneAnalysisService = toneAnalysisService;
}
private final OkHttpClient client = new OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
.readTimeout(60, TimeUnit.SECONDS)
.build();
private static final String SYSTEM_PROMPT =
"你是「见素」的灵魂导师。当用户输入期待或关键词时请从《诗经》、《楚辞》或宋词中提取意象生成3个极具美感的名字。" +
"每个名字必须配有一段 50 字以内的'通感'描述,包含气味、光线或声音的描写,拒绝说教。" +
"请直接以 JSON 数组格式返回JSON 对象包含三个字段name (名字), origin (诗词出处), description (通感描述)。不要包含任何 Markdown 格式。";
public List<NameCard> generateNames(String keyword) {
// 后门:当关键词为 "test" 时,返回固定的测试数据
if ("test".equalsIgnoreCase(keyword)) {
log.info("Keyword is 'test', returning hardcoded test data.");
return createTestData();
}
// 构建 MiniMax Token Plan (OpenAI 兼容) 请求体
JSONObject jsonBody = new JSONObject();
jsonBody.put("model", "MiniMax-M2.7");
JSONArray messages = new JSONArray();
JSONObject systemMsg = new JSONObject();
systemMsg.put("role", "system");
systemMsg.put("content", SYSTEM_PROMPT);
messages.add(systemMsg);
JSONObject userMsg = new JSONObject();
userMsg.put("role", "user");
userMsg.put("content", "用户期待/关键词是:" + keyword);
messages.add(userMsg);
jsonBody.put("messages", messages);
RequestBody body = RequestBody.create(
jsonBody.toJSONString(),
MediaType.parse("application/json; charset=utf-8")
);
Request request = new Request.Builder()
.url(apiUrl)
.post(body)
.addHeader("Authorization", "Bearer " + apiKey)
.addHeader("Content-Type", "application/json")
.build();
try (Response response = client.newCall(request).execute()) {
if (response.isSuccessful() && response.body() != null) {
String responseBody = response.body().string();
log.info("MiniMax raw response: {}", responseBody);
return parseMiniMaxResponse(responseBody);
} else {
String errorMsg = response.body() != null ? response.body().string() : response.message();
log.error("MiniMax API Error: {} - {}", response.code(), errorMsg);
throw new RuntimeException("MiniMax API 调用失败: " + errorMsg);
}
} catch (IOException e) {
log.error("API Error: ", e);
throw new RuntimeException("网络请求失败");
}
}
private List<NameCard> parseMiniMaxResponse(String responseBody) {
List<NameCard> nameCards = new ArrayList<>();
try {
JSONObject root = JSON.parseObject(responseBody);
// 检查 API 错误
JSONObject baseResp = root.getJSONObject("base_resp");
if (baseResp != null) {
int statusCode = baseResp.getIntValue("status_code");
if (statusCode != 0) {
String statusMsg = baseResp.getString("status_msg");
log.error("MiniMax API error: {} - {}", statusCode, statusMsg);
throw new RuntimeException("MiniMax API 错误: " + statusMsg);
}
}
JSONArray choices = root.getJSONArray("choices");
if (choices == null || choices.isEmpty()) {
throw new RuntimeException("MiniMax 返回内容为空");
}
JSONObject firstChoice = choices.getJSONObject(0);
JSONObject message = firstChoice.getJSONObject("message");
String aiText = message.getString("content");
if (aiText == null || aiText.isEmpty()) {
throw new RuntimeException("MiniMax 返回内容为空");
}
// 清理可能存在的 Markdown 代码块标记
aiText = aiText.replaceAll("```json", "").replaceAll("```", "").trim();
JSONArray jsonArray = JSON.parseArray(aiText);
for (int i = 0; i < jsonArray.size(); i++) {
JSONObject obj = jsonArray.getJSONObject(i);
String name = obj.getString("name");
// 进行声韵分析
ToneAnalysisService.ToneResult tr = toneAnalysisService.analyze(name);
NameCard card = NameCard.builder()
.name(name)
.origin(obj.getString("origin"))
.description(obj.getString("description"))
.score(tr.score)
.tone(tr.tonePattern)
.build();
nameCards.add(card);
}
} catch (RuntimeException e) {
throw e;
} catch (Exception e) {
log.error("Parse Error: ", e);
throw new RuntimeException("数据解析失败");
}
return nameCards;
}
private List<NameCard> createTestData() {
List<NameCard> testData = new ArrayList<>();
testData.add(NameCard.builder()
.name("月白")
.origin("《月出》:月出皎兮,佼人僚兮。")
.description("像月光洒在雪地,清冷又干净,带着一丝不易察觉的、风吹过竹林的微响。")
.score(9.8)
.tone("仄仄")
.build());
testData.add(NameCard.builder()
.name("南絮")
.origin("晏几道《御街行》:街南绿树春饶絮。")
.description("春天街巷里柔软的柳絮,随风飘浮,带着南方阳光的暖意和青草的气息。")
.score(9.5)
.tone("平仄")
.build());
testData.add(NameCard.builder()
.name("疏影")
.origin("林逋《山园小梅》:疏影横斜水清浅,暗香浮动月黄昏。")
.description("黄昏月下,梅枝稀疏的影子落在清浅的水面,空气中浮动着若有似无的冷香。")
.score(9.7)
.tone("平仄")
.build());
return testData;
}
}

View File

@@ -1,11 +1,45 @@
server:
port: 8080
servlet:
encoding:
charset: UTF-8
enabled: true
force: true
spring:
application:
name: jiansu-naming
# MiniMax API 配置
minimax:
api-key: sk-cp-n0eCZgH5s-NpduAVPo8rpWM9eUBsMOBnIroISIaH6y8eFIpT0VSrCMttzE4bVDbQ-loiMR1b8ZpIsgotQ_yqQRk8_fcUxKHsbhtLfN70oCVaV6-94ZC9Wjk
api-url: https://api.minimax.chat/v1/text/chatcompletion_v2
# 数据库配置 - MySQL 5.7
datasource:
url: jdbc:mysql://47.115.201.202:3306/jiansu_db?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=UTF-8&useUnicode=true
driver-class-name: com.mysql.jdbc.Driver
username: root
password: Feastcoding@2023
# 连接池配置
hikari:
minimum-idle: 5
maximum-pool-size: 20
idle-timeout: 300000
max-lifetime: 1200000
connection-timeout: 20000
jpa:
hibernate:
ddl-auto: update
show-sql: false
properties:
hibernate:
dialect: org.hibernate.dialect.MySQL57Dialect
format_sql: true
# Kimi (Moonshot AI) API 配置
kimi:
api-key: ${KIMI_API_KEY:sk-EORjVwYTlXMTIFmelkt6ebWlOOLk9qCkm2PR0tvKXdkAnSdd}
api-url: https://api.moonshot.cn/v1/chat/completions
# 日志配置
logging:
level:
com.jiansu.naming: INFO
org.hibernate.SQL: WARN

View File

@@ -0,0 +1,28 @@
-- 见素起名 - MySQL 5.7 数据库建表语句
-- 创建数据库
CREATE DATABASE IF NOT EXISTS jiansu_db
CHARACTER SET utf8mb4
COLLATE utf8mb4_unicode_ci;
USE jiansu_db;
-- 用户收藏表
CREATE TABLE IF NOT EXISTS user_favorites (
id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID',
openid VARCHAR(100) NOT NULL COMMENT '用户微信openid',
name VARCHAR(50) NOT NULL COMMENT '名字',
origin VARCHAR(500) COMMENT '出处/诗词',
description VARCHAR(1000) COMMENT '解析文案',
tone VARCHAR(20) COMMENT '声韵',
score DOUBLE COMMENT '见素评分',
mode VARCHAR(20) COMMENT '生成模式',
keyword VARCHAR(100) COMMENT '关键词',
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '收藏时间',
INDEX idx_openid (openid),
INDEX idx_openid_name (openid, name),
INDEX idx_create_time (create_time)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户收藏表';
-- 可选:创建唯一索引防止重复收藏
-- ALTER TABLE user_favorites ADD UNIQUE INDEX uk_openid_name (openid, name);

View File

@@ -1,11 +1,45 @@
server:
port: 8080
servlet:
encoding:
charset: UTF-8
enabled: true
force: true
spring:
application:
name: jiansu-naming
# MiniMax API 配置
minimax:
api-key: sk-cp-n0eCZgH5s-NpduAVPo8rpWM9eUBsMOBnIroISIaH6y8eFIpT0VSrCMttzE4bVDbQ-loiMR1b8ZpIsgotQ_yqQRk8_fcUxKHsbhtLfN70oCVaV6-94ZC9Wjk
api-url: https://api.minimax.chat/v1/text/chatcompletion_v2
# 数据库配置 - MySQL 5.7
datasource:
url: jdbc:mysql://47.115.201.202:3306/jiansu_db?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=UTF-8&useUnicode=true
driver-class-name: com.mysql.jdbc.Driver
username: root
password: Feastcoding@2023
# 连接池配置
hikari:
minimum-idle: 5
maximum-pool-size: 20
idle-timeout: 300000
max-lifetime: 1200000
connection-timeout: 20000
jpa:
hibernate:
ddl-auto: update
show-sql: false
properties:
hibernate:
dialect: org.hibernate.dialect.MySQL57Dialect
format_sql: true
# Kimi (Moonshot AI) API 配置
kimi:
api-key: ${KIMI_API_KEY:sk-EORjVwYTlXMTIFmelkt6ebWlOOLk9qCkm2PR0tvKXdkAnSdd}
api-url: https://api.moonshot.cn/v1/chat/completions
# 日志配置
logging:
level:
com.jiansu.naming: INFO
org.hibernate.SQL: WARN

View File

@@ -0,0 +1,28 @@
-- 见素起名 - MySQL 5.7 数据库建表语句
-- 创建数据库
CREATE DATABASE IF NOT EXISTS jiansu_db
CHARACTER SET utf8mb4
COLLATE utf8mb4_unicode_ci;
USE jiansu_db;
-- 用户收藏表
CREATE TABLE IF NOT EXISTS user_favorites (
id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID',
openid VARCHAR(100) NOT NULL COMMENT '用户微信openid',
name VARCHAR(50) NOT NULL COMMENT '名字',
origin VARCHAR(500) COMMENT '出处/诗词',
description VARCHAR(1000) COMMENT '解析文案',
tone VARCHAR(20) COMMENT '声韵',
score DOUBLE COMMENT '见素评分',
mode VARCHAR(20) COMMENT '生成模式',
keyword VARCHAR(100) COMMENT '关键词',
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '收藏时间',
INDEX idx_openid (openid),
INDEX idx_openid_name (openid, name),
INDEX idx_create_time (create_time)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户收藏表';
-- 可选:创建唯一索引防止重复收藏
-- ALTER TABLE user_favorites ADD UNIQUE INDEX uk_openid_name (openid, name);

View File

@@ -0,0 +1,3 @@
artifactId=naming
groupId=com.jiansu
version=0.0.1-SNAPSHOT

View File

@@ -1,5 +1,14 @@
com\jiansu\naming\model\NameCard$NameCardBuilder.class
com\jiansu\naming\model\NameCard.class
com\jiansu\naming\repository\UserFavoriteRepository.class
com\jiansu\naming\service\KimiService.class
com\jiansu\naming\config\AsyncConfig.class
com\jiansu\naming\controller\NamingController.class
com\jiansu\naming\service\MiniMaxService.class
com\jiansu\naming\entity\UserFavorite.class
com\jiansu\naming\model\NameCard$NameCardBuilder.class
com\jiansu\naming\service\ToneAnalysisService.class
com\jiansu\naming\service\KimiService$1.class
com\jiansu\naming\model\NameCard.class
com\jiansu\naming\service\ToneAnalysisService$ToneResult.class
com\jiansu\naming\controller\FavoriteController.class
com\jiansu\naming\NamingApplication.class
com\jiansu\naming\entity\UserFavorite$UserFavoriteBuilder.class
com\jiansu\naming\service\FavoriteService.class

View File

@@ -1,4 +1,10 @@
C:\Users\<EFBFBD><EFBFBD><EFBFBD><EFBFBD>\Desktop\JianSu-Naming\backend\src\main\java\com\jiansu\naming\controller\NamingController.java
C:\Users\<EFBFBD><EFBFBD><EFBFBD><EFBFBD>\Desktop\JianSu-Naming\backend\src\main\java\com\jiansu\naming\model\NameCard.java
C:\Users\<EFBFBD><EFBFBD><EFBFBD><EFBFBD>\Desktop\JianSu-Naming\backend\src\main\java\com\jiansu\naming\service\GeminiService.java
C:\Users\<EFBFBD><EFBFBD><EFBFBD><EFBFBD>\Desktop\JianSu-Naming\backend\src\main\java\com\jiansu\naming\NamingApplication.java
C:\Users\南音\Desktop\JianSu-Naming\backend\src\main\java\com\jiansu\naming\entity\UserFavorite.java
C:\Users\南音\Desktop\JianSu-Naming\backend\src\main\java\com\jiansu\naming\config\AsyncConfig.java
C:\Users\南音\Desktop\JianSu-Naming\backend\src\main\java\com\jiansu\naming\controller\FavoriteController.java
C:\Users\南音\Desktop\JianSu-Naming\backend\src\main\java\com\jiansu\naming\controller\NamingController.java
C:\Users\南音\Desktop\JianSu-Naming\backend\src\main\java\com\jiansu\naming\model\NameCard.java
C:\Users\南音\Desktop\JianSu-Naming\backend\src\main\java\com\jiansu\naming\repository\UserFavoriteRepository.java
C:\Users\南音\Desktop\JianSu-Naming\backend\src\main\java\com\jiansu\naming\service\ToneAnalysisService.java
C:\Users\南音\Desktop\JianSu-Naming\backend\src\main\java\com\jiansu\naming\NamingApplication.java
C:\Users\南音\Desktop\JianSu-Naming\backend\src\main\java\com\jiansu\naming\service\KimiService.java
C:\Users\南音\Desktop\JianSu-Naming\backend\src\main\java\com\jiansu\naming\service\FavoriteService.java

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,45 @@
server:
port: 8080
servlet:
encoding:
charset: UTF-8
enabled: true
force: true
spring:
application:
name: jiansu-naming
# 数据库配置 - MySQL 5.7
datasource:
url: jdbc:mysql://47.115.201.202:3306/jiansu_db?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=UTF-8&useUnicode=true
driver-class-name: com.mysql.jdbc.Driver
username: root
password: Feastcoding@2023
# 连接池配置
hikari:
minimum-idle: 5
maximum-pool-size: 20
idle-timeout: 300000
max-lifetime: 1200000
connection-timeout: 20000
jpa:
hibernate:
ddl-auto: update
show-sql: false
properties:
hibernate:
dialect: org.hibernate.dialect.MySQL57Dialect
format_sql: true
# Kimi (Moonshot AI) API 配置
kimi:
api-key: ${KIMI_API_KEY:sk-EORjVwYTlXMTIFmelkt6ebWlOOLk9qCkm2PR0tvKXdkAnSdd}
api-url: https://api.moonshot.cn/v1/chat/completions
# 日志配置
logging:
level:
com.jiansu.naming: INFO
org.hibernate.SQL: WARN

View File

@@ -0,0 +1,28 @@
-- 见素起名 - MySQL 5.7 数据库建表语句
-- 创建数据库
CREATE DATABASE IF NOT EXISTS jiansu_db
CHARACTER SET utf8mb4
COLLATE utf8mb4_unicode_ci;
USE jiansu_db;
-- 用户收藏表
CREATE TABLE IF NOT EXISTS user_favorites (
id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID',
openid VARCHAR(100) NOT NULL COMMENT '用户微信openid',
name VARCHAR(50) NOT NULL COMMENT '名字',
origin VARCHAR(500) COMMENT '出处/诗词',
description VARCHAR(1000) COMMENT '解析文案',
tone VARCHAR(20) COMMENT '声韵',
score DOUBLE COMMENT '见素评分',
mode VARCHAR(20) COMMENT '生成模式',
keyword VARCHAR(100) COMMENT '关键词',
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '收藏时间',
INDEX idx_openid (openid),
INDEX idx_openid_name (openid, name),
INDEX idx_create_time (create_time)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户收藏表';
-- 可选:创建唯一索引防止重复收藏
-- ALTER TABLE user_favorites ADD UNIQUE INDEX uk_openid_name (openid, name);

Some files were not shown because too many files have changed in this diff Show More