Initial commit

This commit is contained in:
王鹏
2026-04-16 11:25:29 +08:00
commit c4db35b183
39 changed files with 1725 additions and 0 deletions

8
backend/.idea/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml
# Editor-based HTTP Client requests
/httpRequests/

19
backend/.idea/compiler.xml generated Normal file
View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<annotationProcessing>
<profile default="true" name="Default" enabled="true" />
<profile name="Maven default annotation processors profile" enabled="true">
<sourceOutputDir name="target/generated-sources/annotations" />
<sourceTestOutputDir name="target/generated-test-sources/test-annotations" />
<outputRelativeToContentRoot value="true" />
<module name="naming" />
</profile>
</annotationProcessing>
</component>
<component name="JavacSettings">
<option name="ADDITIONAL_OPTIONS_OVERRIDE">
<module name="naming" options="-parameters" />
</option>
</component>
</project>

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

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Encoding">
<file url="file://$PROJECT_DIR$/src/main/java" charset="UTF-8" />
</component>
</project>

20
backend/.idea/jarRepositories.xml generated Normal file
View File

@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RemoteRepositoriesConfiguration">
<remote-repository>
<option name="id" value="central" />
<option name="name" value="Maven Central repository" />
<option name="url" value="https://repo1.maven.org/maven2" />
</remote-repository>
<remote-repository>
<option name="id" value="jboss.community" />
<option name="name" value="JBoss Community repository" />
<option name="url" value="https://repository.jboss.org/nexus/content/repositories/public/" />
</remote-repository>
<remote-repository>
<option name="id" value="central" />
<option name="name" value="Central Repository" />
<option name="url" value="https://maven.aliyun.com/repository/central" />
</remote-repository>
</component>
</project>

12
backend/.idea/misc.xml generated Normal file
View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="MavenProjectsManager">
<option name="originalFiles">
<list>
<option value="$PROJECT_DIR$/pom.xml" />
</list>
</option>
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" default="true" project-jdk-name="1.8" project-jdk-type="JavaSDK" />
</project>

10
backend/.idea/runConfigurations.xml generated Normal file
View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RunConfigurationProducerService">
<option name="ignoredProducers">
<set>
<option value="com.android.tools.idea.compose.preview.runconfiguration.ComposePreviewRunConfigurationProducer" />
</set>
</option>
</component>
</project>

67
backend/pom.xml Normal file
View File

@@ -0,0 +1,67 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.18</version>
<relativePath/>
</parent>
<groupId>com.jiansu</groupId>
<artifactId>naming</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>jiansu-naming</name>
<description>JianSu Naming AI Backend</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.83</version>
</dependency>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>4.12.0</version>
</dependency>
<dependency>
<groupId>com.belerweb</groupId>
<artifactId>pinyin4j</artifactId>
<version>2.5.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,11 @@
package com.jiansu.naming;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class NamingApplication {
public static void main(String[] args) {
SpringApplication.run(NamingApplication.class, args);
}
}

View File

@@ -0,0 +1,22 @@
package com.jiansu.naming.controller;
import com.jiansu.naming.model.NameCard;
import com.jiansu.naming.service.MiniMaxService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/names")
@CrossOrigin(origins = "*")
public class NamingController {
@Autowired
private MiniMaxService miniMaxService;
@GetMapping("/generate")
public List<NameCard> generate(@RequestParam(defaultValue = "清冷") String keyword) {
return miniMaxService.generateNames(keyword);
}
}

View File

@@ -0,0 +1,18 @@
package com.jiansu.naming.model;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class NameCard {
private String name; // 名字
private String origin; // 出处/诗句
private String description; // “通感”叙事描述
private Double score; // 声韵评分(后端计算)
private String tone; // 平仄分析 (如:平仄)
}

View File

@@ -0,0 +1,184 @@
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

@@ -0,0 +1,102 @@
package com.jiansu.naming.service;
import net.sourceforge.pinyin4j.PinyinHelper;
import net.sourceforge.pinyin4j.format.HanyuPinyinOutputFormat;
import net.sourceforge.pinyin4j.format.HanyuPinyinToneType;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
/**
* 见素声韵分析逻辑
* 1. 平仄匹配1,2声为平3,4声为仄
* 2. 优选模式:平仄平、仄平仄、平平仄、仄仄平
* 3. 连读检查:避免连续闭口音
*/
@Service
public class ToneAnalysisService {
private final HanyuPinyinOutputFormat format;
public ToneAnalysisService() {
format = new HanyuPinyinOutputFormat();
format.setToneType(HanyuPinyinToneType.WITH_TONE_NUMBER);
}
public static class ToneResult {
public double score;
public String tonePattern;
public String msg;
}
public ToneResult analyze(String name) {
ToneResult result = new ToneResult();
if (name == null || name.isEmpty()) {
result.score = 0;
return result;
}
List<Integer> tones = new ArrayList<>();
List<String> pinyins = new ArrayList<>();
for (char c : name.toCharArray()) {
try {
String[] pinyinArray = PinyinHelper.toHanyuPinyinStringArray(c, format);
if (pinyinArray != null && pinyinArray.length > 0) {
String p = pinyinArray[0];
pinyins.add(p);
// 取最后一位数字作为声调
int tone = Character.getNumericValue(p.charAt(p.length() - 1));
tones.add(tone);
}
} catch (Exception e) {
// 忽略无法识别的字符
}
}
// 构建平仄模式
StringBuilder pattern = new StringBuilder();
for (int tone : tones) {
if (tone == 1 || tone == 2) pattern.append("");
else if (tone == 3 || tone == 4) pattern.append("");
}
result.tonePattern = pattern.toString();
// 基础分 8.0
double score = 8.0;
// 1. 平仄起伏检查
if (pattern.length() >= 2) {
boolean hasChange = false;
for (int i = 0; i < pattern.length() - 1; i++) {
if (pattern.charAt(i) != pattern.charAt(i + 1)) {
hasChange = true;
break;
}
}
if (hasChange) score += 1.0; // 有起伏感 +1
else score -= 0.5; // 全平或全仄 -0.5
}
// 2. 优选模式加分
String pStr = pattern.toString();
if (pStr.equals("平仄平") || pStr.equals("仄平仄") || pStr.equals("平平仄") || pStr.equals("仄仄平")) {
score += 0.5;
}
// 3. 开口度简单检查 (韵母 a, e, o 结尾通常更响亮)
int openCount = 0;
for (String p : pinyins) {
String yumu = p.substring(0, p.length() - 1);
if (yumu.endsWith("a") || yumu.endsWith("e") || yumu.endsWith("o") || yumu.endsWith("ang") || yumu.endsWith("ong")) {
openCount++;
}
}
if (openCount >= 1) score += 0.3;
// 限制最高分 9.9
result.score = Math.min(score, 9.9);
return result;
}
}

View File

@@ -0,0 +1,11 @@
server:
port: 8080
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

View File

@@ -0,0 +1,11 @@
server:
port: 8080
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

View File

@@ -0,0 +1,5 @@
com\jiansu\naming\model\NameCard$NameCardBuilder.class
com\jiansu\naming\model\NameCard.class
com\jiansu\naming\controller\NamingController.class
com\jiansu\naming\service\MiniMaxService.class
com\jiansu\naming\NamingApplication.class

View File

@@ -0,0 +1,4 @@
C:\Users\<5C><><EFBFBD><EFBFBD>\Desktop\JianSu-Naming\backend\src\main\java\com\jiansu\naming\controller\NamingController.java
C:\Users\<5C><><EFBFBD><EFBFBD>\Desktop\JianSu-Naming\backend\src\main\java\com\jiansu\naming\model\NameCard.java
C:\Users\<5C><><EFBFBD><EFBFBD>\Desktop\JianSu-Naming\backend\src\main\java\com\jiansu\naming\service\GeminiService.java
C:\Users\<5C><><EFBFBD><EFBFBD>\Desktop\JianSu-Naming\backend\src\main\java\com\jiansu\naming\NamingApplication.java