Compare commits

..

2 Commits

Author SHA1 Message Date
王鹏
2c47fb8f65 feat: 完善见素起名小程序功能
- 添加收藏锦囊功能,支持查看和删除收藏
- 实现积分系统,每日赠送5次灵感次数
- 添加静心阅读功能,阅读15秒可获得额外次数
- 实现灵感广场,展示用户分享的名字
- 添加字源溯源组件,长按汉字查看详情
- 优化空状态和结语卡片样式统一
- 添加音频控制(静音/风铃/雨落/古琴/白噪音/森林/溪流)
- 优化名字生成逻辑,确保每次返回5个不重复名字
- 修复卡片翻转样式问题
- 移除首页动态提醒气泡
2026-04-18 16:56:31 +08:00
王鹏
be1f5722ab feat: 完成见素起名小程序核心功能
- 实现 AI 起名功能(Kimi API 接入)
- 添加用户收藏功能(MySQL 数据库)
- 实现海报生成与分享
- 添加音效和触觉反馈
- 配置生产环境部署(WAR 包 + Nginx)
- 支持多种起名模式(经典、诗词、自然、现代)
- 实现分批加载优化体验
2026-04-17 15:34:51 +08:00
171 changed files with 7852 additions and 452 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,74 @@
package com.jiansu.naming.controller;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.client.RestTemplate;
import java.util.HashMap;
import java.util.Map;
@RestController
@RequestMapping("/api/auth")
@CrossOrigin(origins = "*")
@Slf4j
public class AuthController {
@Value("${wechat.miniapp.app-id}")
private String appId;
@Value("${wechat.miniapp.app-secret}")
private String appSecret;
private final RestTemplate restTemplate = new RestTemplate();
private final ObjectMapper objectMapper = new ObjectMapper();
/**
* 微信登录 - 用 code 换取 openid
*/
@PostMapping("/login")
public ResponseEntity<?> wxLogin(@RequestBody Map<String, String> request) {
String code = request.get("code");
if (code == null || code.isEmpty()) {
Map<String, Object> response = new HashMap<>();
response.put("success", false);
response.put("message", "code 不能为空");
return ResponseEntity.badRequest().body(response);
}
try {
// 调用微信接口换取 openid
String url = String.format(
"https://api.weixin.qq.com/sns/jscode2session?appid=%s&secret=%s&js_code=%s&grant_type=authorization_code",
appId, appSecret, code
);
String result = restTemplate.getForObject(url, String.class);
JsonNode jsonNode = objectMapper.readTree(result);
Map<String, Object> response = new HashMap<>();
if (jsonNode.has("openid")) {
String openid = jsonNode.get("openid").asText();
response.put("success", true);
response.put("openid", openid);
log.info("微信登录成功openid: {}", openid);
} else {
response.put("success", false);
response.put("message", jsonNode.has("errmsg") ? jsonNode.get("errmsg").asText() : "登录失败");
log.error("微信登录失败: {}", result);
}
return ResponseEntity.ok(response);
} catch (Exception e) {
log.error("微信登录异常", e);
Map<String, Object> response = new HashMap<>();
response.put("success", false);
response.put("message", "登录异常: " + e.getMessage());
return ResponseEntity.ok(response);
}
}
}

View File

@@ -0,0 +1,401 @@
package com.jiansu.naming.controller;
import lombok.extern.slf4j.Slf4j;
import net.sourceforge.pinyin4j.PinyinHelper;
import net.sourceforge.pinyin4j.format.HanyuPinyinOutputFormat;
import net.sourceforge.pinyin4j.format.HanyuPinyinToneType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.Map;
@RestController
@RequestMapping("/api/char")
@CrossOrigin(origins = "*")
@Slf4j
public class CharController {
/**
* 获取汉字详情
*/
@GetMapping("/detail")
public ResponseEntity<?> getCharDetail(@RequestParam String character) {
Map<String, Object> response = new HashMap<>();
if (character == null || character.length() != 1) {
response.put("success", false);
response.put("message", "请输入单个汉字");
return ResponseEntity.badRequest().body(response);
}
try {
Map<String, Object> data = new HashMap<>();
data.put("char", character);
data.put("pinyin", getPinyin(character));
data.put("radical", getRadical(character));
data.put("strokes", getStrokes(character));
data.put("wuxing", getWuxing(character));
data.put("meaning", getMeaning(character));
data.put("imagery", getImagery(character));
data.put("poetry", getPoetry(character));
response.put("success", true);
response.put("data", data);
log.info("获取汉字详情: {}", character);
} catch (Exception e) {
log.error("获取汉字详情失败", e);
response.put("success", false);
response.put("message", "获取失败: " + e.getMessage());
}
return ResponseEntity.ok(response);
}
/**
* 获取拼音
*/
private String getPinyin(String chinese) {
try {
HanyuPinyinOutputFormat format = new HanyuPinyinOutputFormat();
format.setToneType(HanyuPinyinToneType.WITH_TONE_MARK);
String[] pinyinArray = PinyinHelper.toHanyuPinyinStringArray(chinese.charAt(0));
if (pinyinArray != null && pinyinArray.length > 0) {
return pinyinArray[0];
}
} catch (Exception e) {
log.error("拼音转换失败", e);
}
return "";
}
/**
* 获取部首(简化版,实际应该查字典)
*/
private String getRadical(String chinese) {
// 这里简化处理,实际应该查询汉字字典数据库
Map<String, String> radicalMap = new HashMap<>();
radicalMap.put("", "");
radicalMap.put("", "");
radicalMap.put("", "");
radicalMap.put("", "");
radicalMap.put("", "");
radicalMap.put("", "");
radicalMap.put("", "");
radicalMap.put("", "");
radicalMap.put("", "");
radicalMap.put("", "");
radicalMap.put("", "");
radicalMap.put("", "");
radicalMap.put("", "");
radicalMap.put("", "");
radicalMap.put("", "");
radicalMap.put("", "");
radicalMap.put("", "");
radicalMap.put("", "");
radicalMap.put("", "");
radicalMap.put("", "");
radicalMap.put("", "");
radicalMap.put("", "");
radicalMap.put("", "");
radicalMap.put("", "");
radicalMap.put("", "");
radicalMap.put("", "");
radicalMap.put("", "");
radicalMap.put("", "");
radicalMap.put("", "");
radicalMap.put("", "");
radicalMap.put("", "");
radicalMap.put("", "");
radicalMap.put("", "");
radicalMap.put("", "");
radicalMap.put("", "");
radicalMap.put("", "");
radicalMap.put("", "");
radicalMap.put("", "");
radicalMap.put("", "");
radicalMap.put("", "");
radicalMap.put("", "");
radicalMap.put("", "");
radicalMap.put("", "");
radicalMap.put("", "");
radicalMap.put("", "");
radicalMap.put("", "");
radicalMap.put("", "");
radicalMap.put("", "");
radicalMap.put("", "");
return radicalMap.getOrDefault(chinese, "");
}
/**
* 获取笔画数(简化版)
*/
private int getStrokes(String chinese) {
// 简化处理,实际应该查询汉字字典
Map<String, Integer> strokesMap = new HashMap<>();
strokesMap.put("", 5);
strokesMap.put("", 8);
strokesMap.put("", 15);
strokesMap.put("", 11);
strokesMap.put("", 4);
strokesMap.put("", 4);
strokesMap.put("", 3);
strokesMap.put("", 4);
strokesMap.put("", 12);
strokesMap.put("", 8);
strokesMap.put("", 13);
strokesMap.put("", 4);
strokesMap.put("", 8);
strokesMap.put("", 9);
strokesMap.put("", 11);
strokesMap.put("", 11);
strokesMap.put("", 8);
strokesMap.put("", 4);
strokesMap.put("", 8);
strokesMap.put("", 12);
strokesMap.put("", 9);
strokesMap.put("", 6);
strokesMap.put("", 8);
strokesMap.put("", 11);
strokesMap.put("", 11);
strokesMap.put("", 10);
strokesMap.put("", 10);
strokesMap.put("", 10);
strokesMap.put("", 7);
strokesMap.put("", 7);
strokesMap.put("", 11);
strokesMap.put("", 12);
strokesMap.put("", 11);
strokesMap.put("", 10);
strokesMap.put("", 8);
strokesMap.put("", 9);
strokesMap.put("", 7);
strokesMap.put("", 9);
strokesMap.put("", 13);
strokesMap.put("", 11);
strokesMap.put("", 13);
strokesMap.put("", 10);
strokesMap.put("", 15);
strokesMap.put("", 9);
strokesMap.put("", 16);
strokesMap.put("", 9);
return strokesMap.getOrDefault(chinese, 0);
}
/**
* 获取五行属性(简化版)
*/
private String getWuxing(String chinese) {
Map<String, String> wuxingMap = new HashMap<>();
wuxingMap.put("", "");
wuxingMap.put("", "");
wuxingMap.put("", "");
wuxingMap.put("", "");
wuxingMap.put("", "");
wuxingMap.put("", "");
wuxingMap.put("", "");
wuxingMap.put("", "");
wuxingMap.put("", "");
wuxingMap.put("", "");
wuxingMap.put("", "");
wuxingMap.put("", "");
wuxingMap.put("", "");
wuxingMap.put("", "");
wuxingMap.put("", "");
wuxingMap.put("", "");
wuxingMap.put("", "");
wuxingMap.put("", "");
wuxingMap.put("", "");
wuxingMap.put("", "");
wuxingMap.put("", "");
wuxingMap.put("", "");
wuxingMap.put("", "");
wuxingMap.put("", "");
wuxingMap.put("", "");
wuxingMap.put("", "");
wuxingMap.put("", "");
wuxingMap.put("", "");
wuxingMap.put("", "");
wuxingMap.put("", "");
wuxingMap.put("", "");
wuxingMap.put("", "");
wuxingMap.put("", "");
wuxingMap.put("", "");
wuxingMap.put("", "");
wuxingMap.put("", "");
wuxingMap.put("", "");
wuxingMap.put("", "");
wuxingMap.put("", "");
wuxingMap.put("", "");
wuxingMap.put("", "");
wuxingMap.put("", "");
wuxingMap.put("", "");
wuxingMap.put("", "");
wuxingMap.put("", "");
wuxingMap.put("", "");
return wuxingMap.getOrDefault(chinese, "未知");
}
/**
* 获取字义(简化版)
*/
private String getMeaning(String chinese) {
Map<String, String> meaningMap = new HashMap<>();
meaningMap.put("", "本义指兰花,象征高洁、典雅、坚贞不渝的品格。");
meaningMap.put("", "本义指水汇聚的地方,引申为恩泽、润泽、仁慈。");
meaningMap.put("", "本义指书写用的黑色颜料,象征文雅、深沉、有学问。");
meaningMap.put("", "本义指水清澈透明,引申为纯洁、清明、高洁。");
meaningMap.put("", "本义指空气流动,引申为风度、风采、气节。");
meaningMap.put("", "本义指天空中的云彩,象征自由、飘逸、高远。");
meaningMap.put("", "本义指地面高耸的部分,象征稳重、坚毅、可靠。");
meaningMap.put("", "本义指无色无味的液体,象征智慧、柔韧、包容。");
meaningMap.put("", "本义指规范、标准,引申为高雅、文雅、美好。");
meaningMap.put("", "本义指顺从、如同,引申为好像、选择,寓意温婉。");
meaningMap.put("", "本义指山间小河,引申为清澈、灵动、源远流长。");
meaningMap.put("", "本义指月亮,象征光明、圆满、清雅高洁。");
meaningMap.put("", "本义指光明、明亮,引申为聪慧、明理、通达。");
meaningMap.put("", "本义指思考、思念,引申为智慧、深情、细腻。");
meaningMap.put("", "本义指睡眠中的幻象,引申为理想、憧憬、浪漫。");
meaningMap.put("", "本义指天空降落的白色晶体,象征纯洁、高洁、清雅。");
meaningMap.put("", "本义指诗歌、文学,象征文采、才情、雅致。");
meaningMap.put("", "本义指书写、典籍,象征学识、修养、文雅。");
meaningMap.put("", "本义指绘画、描绘,象征艺术、美感、才情。");
meaningMap.put("", "本义指古琴,象征高雅、艺术、知音。");
meaningMap.put("", "本义指茶树、茶叶,象征清雅、淡泊、禅意。");
meaningMap.put("", "本义指竹子,象征气节、坚韧、虚心。");
meaningMap.put("", "本义指松树,象征坚贞、长寿、高洁。");
meaningMap.put("", "本义指梅花,象征傲骨、坚韧、清雅。");
meaningMap.put("", "本义指菊花,象征隐逸、高洁、长寿。");
meaningMap.put("", "本义指荷花,象征纯洁、高雅、出淤泥而不染。");
meaningMap.put("", "本义指莲花,象征纯洁、美好、佛教圣花。");
meaningMap.put("", "本义指桃树、桃花,象征美好、爱情、长寿。");
meaningMap.put("", "本义指李树,象征果实、成就、春天。");
meaningMap.put("", "本义指杏树,象征春天、美好、医学。");
meaningMap.put("", "本义指梨树,象征纯洁、离别、清雅。");
meaningMap.put("", "本义指棠梨树,象征美丽、温和、春天。");
meaningMap.put("", "本义指梧桐树,象征高洁、忠贞、吉祥。");
meaningMap.put("", "本义指桐树,象征高洁、坚贞、美好。");
meaningMap.put("", "本义指枫树,象征热情、浪漫、秋天。");
meaningMap.put("", "本义指柳树,象征柔美、离别、春天。");
meaningMap.put("", "本义指杨树,象征挺拔、向上、正直。");
meaningMap.put("", "本义指柏树,象征坚贞、长寿、高洁。");
meaningMap.put("", "本义指楠木,象征珍贵、高贵、坚韧。");
meaningMap.put("", "本义指梓树,象征故乡、生机、成长。");
meaningMap.put("", "本义指榆树,象征坚韧、朴实、春天。");
meaningMap.put("", "本义指白桦树,象征纯洁、高洁、挺拔。");
meaningMap.put("", "本义指樱花树,象征美丽、浪漫、短暂。");
meaningMap.put("", "本义指柠檬,象征清新、活力、阳光。");
meaningMap.put("", "本义指橙子,象征丰收、喜悦、温暖。");
meaningMap.put("", "本义指柚子,象征团圆、保佑、清新。");
return meaningMap.getOrDefault(chinese, "暂无解析");
}
/**
* 获取起名意象
*/
private String getImagery(String chinese) {
Map<String, String> imageryMap = new HashMap<>();
imageryMap.put("", "兰花幽香,寓意品格高洁、气质优雅,适合期望孩子有高尚情操的家庭。");
imageryMap.put("", "泽被万物,寓意恩泽深厚、仁慈宽厚,适合期望孩子有博爱之心的家庭。");
imageryMap.put("", "墨香书卷,寓意文采斐然、学识渊博,适合期望孩子有文学才华的家庭。");
imageryMap.put("", "清风明月,寓意心地纯净、志向高远,适合期望孩子有清正品格的家庭。");
imageryMap.put("", "风度翩翩,寓意气宇轩昂、潇洒不凡,适合期望孩子有出众气质的家庭。");
imageryMap.put("", "云游四方,寓意胸怀广阔、志向高远,适合期望孩子有远大理想的家庭。");
imageryMap.put("", "高山仰止,寓意稳重可靠、坚毅不拔,适合期望孩子有坚强品格的家庭。");
imageryMap.put("", "上善若水,寓意智慧灵动、柔韧包容,适合期望孩子有聪慧性情的家庭。");
imageryMap.put("", "温文尔雅,寓意举止得体、品味高雅,如芝兰玉树,生于庭阶。");
imageryMap.put("", "虚怀若谷,寓意谦逊包容、从容自在,如兰若生春阳。");
imageryMap.put("", "溪水潺潺,寓意灵动清澈、生生不息,如溪水绕石,柔中带刚。");
imageryMap.put("", "明月清风,寓意光明磊落、清雅高洁,如月出东山,照彻千里。");
imageryMap.put("", "明德惟馨,寓意聪慧明理、通达事理,如日月之光,普照万物。");
imageryMap.put("", "深思熟虑,寓意智慧深邃、情感细腻,如思接千载,视通万里。");
imageryMap.put("", "梦寐以求,寓意理想远大、憧憬美好,如梦笔生花,才华横溢。");
imageryMap.put("", "雪中送炭,寓意纯洁无瑕、高洁清雅,如冰雪聪明,晶莹剔透。");
imageryMap.put("", "诗情画意,寓意文采斐然、才情横溢,如诗中有画,画中有诗。");
imageryMap.put("", "书香门第,寓意学识渊博、修养深厚,如博览群书,学富五车。");
imageryMap.put("", "画龙点睛,寓意艺术天赋、审美高雅,如画中有诗,意境深远。");
imageryMap.put("", "琴瑟和鸣,寓意高雅艺术、知音难觅,如琴心剑胆,刚柔并济。");
imageryMap.put("", "茶余饭后,寓意清雅淡泊、禅意悠然,如品茶悟道,静心养性。");
imageryMap.put("", "竹报平安,寓意气节高尚、虚心向上,如竹影清风,高洁自持。");
imageryMap.put("", "松柏之志,寓意坚贞不屈、长寿安康,如松鹤延年,傲雪凌霜。");
imageryMap.put("", "梅香自苦寒,寓意傲骨铮铮、坚韧不拔,如梅花三弄,清雅高洁。");
imageryMap.put("", "菊残犹有傲霜枝,寓意隐逸高洁、淡泊明志,如采菊东篱,悠然自得。");
imageryMap.put("", "出淤泥而不染,寓意纯洁高雅、不染尘俗,如荷风送香,竹露滴清。");
imageryMap.put("", "莲开并蒂,寓意纯洁美好、百年好合,如步步生莲,清雅脱俗。");
imageryMap.put("", "桃李满天下,寓意美好爱情、长寿安康,如人面桃花,相映成趣。");
imageryMap.put("", "李下不正冠,寓意果实累累、成就斐然,如投桃报李,知恩图报。");
imageryMap.put("", "杏林春暖,寓意春天美好、医术高明,如红杏出墙,生机勃勃。");
imageryMap.put("", "梨花带雨,寓意纯洁清雅、楚楚动人,如梨园弟子,才艺双全。");
imageryMap.put("", "棠棣之花,寓意兄弟和睦、家庭美满,如海棠春睡,娇艳欲滴。");
imageryMap.put("", "梧桐更兼细雨,寓意高洁忠贞、吉祥美好,如凤栖梧桐,人才荟萃。");
imageryMap.put("", "桐花万里丹山路,寓意高洁坚贞、前程似锦,如桐叶封弟,仁义道德。");
imageryMap.put("", "枫叶荻花秋瑟瑟,寓意热情浪漫、成熟稳重,如丹枫迎秋,层林尽染。");
imageryMap.put("", "柳暗花明又一村,寓意柔美婉约、生机勃勃,如柳岸花明,春意盎然。");
imageryMap.put("", "杨花落尽子规啼,寓意挺拔向上、正直不阿,如百步穿杨,技艺精湛。");
imageryMap.put("", "柏舟之誓,寓意坚贞不渝、长寿安康,如松柏后凋,岁寒知友。");
imageryMap.put("", "楠木参天,寓意珍贵高贵、坚韧不拔,如楠梓之材,栋梁之器。");
imageryMap.put("", "梓里桑梓,寓意故乡情深、生机勃勃,如敬恭桑梓,不忘根本。");
imageryMap.put("", "榆柳荫后檐,寓意坚韧朴实、春天希望,如榆火相传,文明延续。");
imageryMap.put("", "白桦亭亭,寓意纯洁高洁、挺拔向上,如桦皮做纸,文化传承。");
imageryMap.put("", "樱花烂漫,寓意美丽浪漫、珍惜当下,如樱唇一点,娇艳动人。");
imageryMap.put("", "柠檬清香,寓意清新活力、阳光开朗,如柠月清风,沁人心脾。");
imageryMap.put("", "橙黄橘绿,寓意丰收喜悦、温暖阳光,如橙香四溢,甜蜜幸福。");
imageryMap.put("", "柚香满园,寓意团圆美满、保佑平安,如柚叶青青,清新怡人。");
return imageryMap.getOrDefault(chinese, "");
}
/**
* 获取诗词典故
*/
private String getPoetry(String chinese) {
Map<String, String> poetryMap = new HashMap<>();
poetryMap.put("", "「兰之猗猗,扬扬其香。」——《诗经》");
poetryMap.put("", "「芳与泽其杂糅兮,唯昭质其犹未亏。」——《离骚》");
poetryMap.put("", "「墨池飞出北溟鱼,笔锋杀尽中山兔。」——李白");
poetryMap.put("", "「清风徐来,水波不兴。」——苏轼《赤壁赋》");
poetryMap.put("", "「随风潜入夜,润物细无声。」——杜甫");
poetryMap.put("", "「行到水穷处,坐看云起时。」——王维");
poetryMap.put("", "「采菊东篱下,悠然见南山。」——陶渊明");
poetryMap.put("", "「问君能有几多愁,恰似一江春水向东流。」——李煜");
poetryMap.put("", "「雅步擢纤腰,巧笑发皓齿。」——曹植");
poetryMap.put("", "「若有人兮山之阿,被薜荔兮带女萝。」——屈原");
poetryMap.put("", "「旧时茅店社林边,路转溪桥忽见。」——辛弃疾");
poetryMap.put("", "「举杯邀明月,对影成三人。」——李白");
poetryMap.put("", "「明月几时有,把酒问青天。」——苏轼");
poetryMap.put("", "「举头望明月,低头思故乡。」——李白");
poetryMap.put("", "「庄生晓梦迷蝴蝶,望帝春心托杜鹃。」——李商隐");
poetryMap.put("", "「忽如一夜春风来,千树万树梨花开。」——岑参");
poetryMap.put("", "「诗中有画,画中有诗。」——苏轼评王维");
poetryMap.put("", "「书中自有黄金屋,书中自有颜如玉。」——赵恒");
poetryMap.put("", "「远看山有色,近听水无声。」——王维");
poetryMap.put("", "「欲将心事付瑶琴,知音少,弦断有谁听。」——岳飞");
poetryMap.put("", "「茶烟轻扬落花风,竹影半墙如画。」——苏轼");
poetryMap.put("", "「宁可食无肉,不可居无竹。」——苏轼");
poetryMap.put("", "「明月松间照,清泉石上流。」——王维");
poetryMap.put("", "「墙角数枝梅,凌寒独自开。」——王安石");
poetryMap.put("", "「采菊东篱下,悠然见南山。」——陶渊明");
poetryMap.put("", "「接天莲叶无穷碧,映日荷花别样红。」——杨万里");
poetryMap.put("", "「出淤泥而不染,濯清涟而不妖。」——周敦颐");
poetryMap.put("", "「人面不知何处去,桃花依旧笑春风。」——崔护");
poetryMap.put("", "「桃李不言,下自成蹊。」——《史记》");
poetryMap.put("", "「春色满园关不住,一枝红杏出墙来。」——叶绍翁");
poetryMap.put("", "「梨花院落溶溶月,柳絮池塘淡淡风。」——晏殊");
poetryMap.put("", "「棠棣之华,鄂不韡韡。」——《诗经》");
poetryMap.put("", "「梧桐更兼细雨,到黄昏、点点滴滴。」——李清照");
poetryMap.put("", "「桐花万里丹山路,雏凤清于老凤声。」——李商隐");
poetryMap.put("", "「停车坐爱枫林晚,霜叶红于二月花。」——杜牧");
poetryMap.put("", "「碧玉妆成一树高,万条垂下绿丝绦。」——贺知章");
poetryMap.put("", "「杨花落尽子规啼,闻道龙标过五溪。」——李白");
poetryMap.put("", "「丞相祠堂何处寻,锦官城外柏森森。」——杜甫");
poetryMap.put("", "「楠树色冥冥,江边一盖青。」——杜甫");
poetryMap.put("", "「梓泽丘墟,姑苏台榭。」——王勃");
poetryMap.put("", "「榆柳荫后檐,桃李罗堂前。」——陶渊明");
poetryMap.put("", "「白桦亭亭立,清风徐自来。」——佚名");
poetryMap.put("", "「樱花红陌上,柳叶绿池边。」——周恩来");
poetryMap.put("", "「柠檬香气远,清风送爽来。」——佚名");
poetryMap.put("", "「橙黄橘绿时,正是好风景。」——苏轼");
poetryMap.put("", "「柚叶青青,秋实离离。」——佚名");
return poetryMap.getOrDefault(chinese, "");
}
}

View File

@@ -0,0 +1,124 @@
package com.jiansu.naming.controller;
import com.jiansu.naming.entity.UserCredits;
import com.jiansu.naming.service.CreditsService;
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.Map;
@RestController
@RequestMapping("/api/credits")
@CrossOrigin(origins = "*")
@Slf4j
public class CreditsController {
@Autowired
private CreditsService creditsService;
/**
* 获取用户积分信息
*/
@GetMapping("/info")
public ResponseEntity<?> getCreditsInfo(@RequestParam String openid) {
Map<String, Object> response = new HashMap<>();
if (openid == null || openid.isEmpty()) {
response.put("success", false);
response.put("message", "openid 不能为空");
return ResponseEntity.badRequest().body(response);
}
try {
UserCredits credits = creditsService.getUserCredits(openid);
Map<String, Object> data = new HashMap<>();
data.put("dailyCredits", credits.getDailyCredits());
data.put("totalCredits", credits.getTotalCredits());
data.put("watchedAdCount", credits.getWatchedAdCount());
data.put("maxAdPerDay", 5);
data.put("adReward", 3);
response.put("success", true);
response.put("data", data);
} catch (Exception e) {
log.error("获取积分信息失败", e);
response.put("success", false);
response.put("message", "获取失败: " + e.getMessage());
}
return ResponseEntity.ok(response);
}
/**
* 观看广告奖励
*/
@PostMapping("/reward")
public ResponseEntity<?> rewardForAd(@RequestParam String openid) {
Map<String, Object> response = new HashMap<>();
if (openid == null || openid.isEmpty()) {
response.put("success", false);
response.put("message", "openid 不能为空");
return ResponseEntity.badRequest().body(response);
}
try {
boolean success = creditsService.rewardForWatchingAd(openid);
if (success) {
UserCredits credits = creditsService.getUserCredits(openid);
response.put("success", true);
response.put("message", "观看成功,获得 3 次生成机会");
response.put("dailyCredits", credits.getDailyCredits());
response.put("watchedAdCount", credits.getWatchedAdCount());
} else {
response.put("success", false);
response.put("message", "今日观看次数已达上限");
}
} catch (Exception e) {
log.error("奖励失败", e);
response.put("success", false);
response.put("message", "奖励失败: " + e.getMessage());
}
return ResponseEntity.ok(response);
}
/**
* 检查是否可生成
*/
@GetMapping("/check")
public ResponseEntity<?> checkCanGenerate(@RequestParam String openid) {
Map<String, Object> response = new HashMap<>();
if (openid == null || openid.isEmpty()) {
response.put("success", false);
response.put("message", "openid 不能为空");
return ResponseEntity.badRequest().body(response);
}
try {
boolean canGenerate = creditsService.hasEnoughCredits(openid);
int remaining = creditsService.getRemainingCredits(openid);
response.put("success", true);
response.put("canGenerate", canGenerate);
response.put("remainingCredits", remaining);
} catch (Exception e) {
log.error("检查失败", e);
response.put("success", false);
response.put("message", "检查失败: " + e.getMessage());
}
return ResponseEntity.ok(response);
}
}

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,94 @@
package com.jiansu.naming.controller;
import com.jiansu.naming.model.NameCard;
import com.jiansu.naming.service.MiniMaxService;
import com.jiansu.naming.service.CreditsService;
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.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
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;
@Autowired
private CreditsService creditsService;
@GetMapping("/generate")
public List<NameCard> generate(@RequestParam(defaultValue = "清冷") String keyword) {
return miniMaxService.generateNames(keyword);
public ResponseEntity<?> generate(
@RequestParam(defaultValue = "清冷") String keyword,
@RequestParam(required = false) String mode,
@RequestParam(required = false) String surname,
@RequestParam(required = false, defaultValue = "5") Integer count,
@RequestParam(required = false) String batch,
@RequestParam(required = false) String openid,
@RequestParam(required = false) String excludeNames) {
log.info("生成名字请求 - 关键词: {}, 模式: {}, 批次: {}, 数量: {}, openid: {}", keyword, mode, batch, count, openid);
// 检查并消耗积分(原子操作)
if (openid != null && !openid.isEmpty()) {
boolean consumed = creditsService.consumeCredit(openid);
if (!consumed) {
Map<String, Object> response = new HashMap<>();
response.put("success", false);
response.put("message", "今日灵感次数已用完,请前往个人中心静心阅读获取更多次数");
return ResponseEntity.ok(response);
}
}
// 解析需要排除的名字列表
List<String> excludeList = new ArrayList<String>();
if (excludeNames != null && !excludeNames.isEmpty()) {
excludeList = java.util.Arrays.asList(excludeNames.split(","));
}
// 使用异步方法获取结果确保5个不重复的名字
CompletableFuture<List<NameCard>> future = kimiService.generateUniqueNamesAsync(keyword, mode, surname, 5, excludeList);
List<NameCard> result = future.join();
Map<String, Object> response = new HashMap<>();
response.put("success", true);
response.put("data", result);
return ResponseEntity.ok(response);
}
/**
* AI 深度解析名字
*/
@PostMapping("/explain")
public ResponseEntity<?> explainName(@RequestBody Map<String, String> request) {
String name = request.get("name");
String context = request.get("context"); // 如:从事艺术行业
if (name == null || name.isEmpty()) {
Map<String, Object> response = new HashMap<>();
response.put("success", false);
response.put("message", "名字不能为空");
return ResponseEntity.badRequest().body(response);
}
log.info("AI 解析名字: {}, 语境: {}", name, context);
String explanation = kimiService.explainName(name, context);
Map<String, Object> response = new HashMap<>();
response.put("success", true);
response.put("data", explanation);
return ResponseEntity.ok(response);
}
}

View File

@@ -0,0 +1,149 @@
package com.jiansu.naming.controller;
import com.jiansu.naming.entity.SquarePost;
import com.jiansu.naming.service.SquareService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.Map;
@RestController
@RequestMapping("/api/square")
@CrossOrigin(origins = "*")
@Slf4j
public class SquareController {
@Autowired
private SquareService squareService;
/**
* 获取广场帖子列表
*/
@GetMapping("/posts")
public ResponseEntity<?> getPosts(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size,
@RequestParam(required = false) String tag,
@RequestParam(required = false) String keyword,
@RequestParam(defaultValue = "latest") String sort) {
Page<SquarePost> posts;
if (tag != null && !tag.isEmpty()) {
posts = squareService.getPostsByTag(tag, page, size);
} else if (keyword != null && !keyword.isEmpty()) {
posts = squareService.getPostsByKeyword(keyword, page, size);
} else if ("hot".equals(sort)) {
posts = squareService.getHotPosts(page, size);
} else {
posts = squareService.getPosts(page, size);
}
Map<String, Object> response = new HashMap<>();
response.put("success", true);
response.put("data", posts.getContent());
response.put("total", posts.getTotalElements());
response.put("hasMore", posts.hasNext());
return ResponseEntity.ok(response);
}
/**
* 发布帖子
*/
@PostMapping("/posts")
public ResponseEntity<?> createPost(
@RequestParam String name,
@RequestParam String origin,
@RequestParam String description,
@RequestParam String keyword,
@RequestParam String mode,
@RequestParam String openid,
@RequestParam(required = false) String imageUrl) {
log.info("发布帖子: {}", name);
SquarePost post = new SquarePost();
post.setName(name);
post.setOrigin(origin);
post.setDescription(description);
post.setKeyword(keyword);
post.setMode(mode);
post.setOpenid(openid);
post.setImageUrl(imageUrl);
post.setIsPublic(true);
post.setLikeCount(0);
SquarePost saved = squareService.createPost(post);
Map<String, Object> response = new HashMap<>();
response.put("success", true);
response.put("data", saved);
response.put("message", "发布成功");
return ResponseEntity.ok(response);
}
/**
* 点赞
*/
@PostMapping("/posts/{id}/like")
public ResponseEntity<?> likePost(@PathVariable Long id) {
squareService.likePost(id);
Map<String, Object> response = new HashMap<>();
response.put("success", true);
response.put("message", "点赞成功");
return ResponseEntity.ok(response);
}
/**
* 获取帖子详情
*/
@GetMapping("/posts/{id}")
public ResponseEntity<?> getPostDetail(@PathVariable Long id) {
SquarePost post = squareService.getPostById(id).orElse(null);
Map<String, Object> response = new HashMap<>();
if (post != null) {
response.put("success", true);
response.put("data", post);
} else {
response.put("success", false);
response.put("message", "帖子不存在");
}
return ResponseEntity.ok(response);
}
/**
* 删除帖子
*/
@DeleteMapping("/posts/{id}")
public ResponseEntity<?> deletePost(@PathVariable Long id, @RequestParam String openid) {
squareService.deletePost(id, openid);
Map<String, Object> response = new HashMap<>();
response.put("success", true);
response.put("message", "删除成功");
return ResponseEntity.ok(response);
}
/**
* 获取用户的帖子
*/
@GetMapping("/user/posts")
public ResponseEntity<?> getUserPosts(@RequestParam String openid) {
Map<String, Object> response = new HashMap<>();
response.put("success", true);
response.put("data", squareService.getUserPosts(openid));
return ResponseEntity.ok(response);
}
}

View File

@@ -0,0 +1,54 @@
package com.jiansu.naming.entity;
import lombok.Data;
import org.hibernate.annotations.CreationTimestamp;
import javax.persistence.*;
import java.time.LocalDateTime;
@Entity
@Table(name = "square_posts")
@Data
public class SquarePost {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "name_card_id")
private Long nameCardId;
@Column(name = "name", length = 50)
private String name;
@Column(name = "origin", length = 500)
private String origin;
@Column(name = "description", length = 1000)
private String description;
@Column(name = "image_url", length = 500)
private String imageUrl;
@Column(name = "tags", length = 200)
private String tags;
@Column(name = "keyword", length = 50)
private String keyword;
@Column(name = "mode", length = 20)
private String mode;
@Column(name = "like_count")
private Integer likeCount = 0;
@Column(name = "is_public")
private Boolean isPublic = true;
@Column(name = "openid", length = 100)
private String openid;
@CreationTimestamp
@Column(name = "created_at")
private LocalDateTime createdAt;
}

View File

@@ -0,0 +1,42 @@
package com.jiansu.naming.entity;
import lombok.Data;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;
import javax.persistence.*;
import java.time.LocalDate;
import java.time.LocalDateTime;
@Entity
@Table(name = "user_credits")
@Data
public class UserCredits {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "openid", length = 100, unique = true, nullable = false)
private String openid;
@Column(name = "total_credits")
private Integer totalCredits = 0;
@Column(name = "daily_credits")
private Integer dailyCredits = 5; // 每日赠送次数
@Column(name = "last_refresh_date")
private LocalDate lastRefreshDate;
@Column(name = "watched_ad_count")
private Integer watchedAdCount = 0; // 今日观看广告次数
@CreationTimestamp
@Column(name = "created_at")
private LocalDateTime createdAt;
@UpdateTimestamp
@Column(name = "updated_at")
private LocalDateTime updatedAt;
}

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

@@ -12,7 +12,9 @@ import lombok.NoArgsConstructor;
public class NameCard {
private String name; // 名字
private String origin; // 出处/诗句
private String description; // 通感叙事描述
private String description; // "通感"叙事描述
private Double score; // 声韵评分(后端计算)
private String tone; // 平仄分析 (如:平仄)
private Boolean publicShared; // 是否已分享到广场
private Long squarePostId; // 广场帖子ID如果已分享
}

View File

@@ -0,0 +1,51 @@
package com.jiansu.naming.repository;
import com.jiansu.naming.entity.SquarePost;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface SquarePostRepository extends JpaRepository<SquarePost, Long> {
/**
* 查询公开的帖子
*/
Page<SquarePost> findByIsPublicTrueOrderByCreatedAtDesc(Pageable pageable);
/**
* 根据标签查询
*/
@Query("SELECT s FROM SquarePost s WHERE s.isPublic = true AND s.tags LIKE %:tag% ORDER BY s.createdAt DESC")
Page<SquarePost> findByTag(@Param("tag") String tag, Pageable pageable);
/**
* 根据关键词查询
*/
@Query("SELECT s FROM SquarePost s WHERE s.isPublic = true AND s.keyword = :keyword ORDER BY s.createdAt DESC")
Page<SquarePost> findByKeyword(@Param("keyword") String keyword, Pageable pageable);
/**
* 增加点赞数
*/
@Modifying
@Query("UPDATE SquarePost s SET s.likeCount = s.likeCount + 1 WHERE s.id = :id")
void incrementLikeCount(@Param("id") Long id);
/**
* 查询用户的帖子
*/
List<SquarePost> findByOpenidOrderByCreatedAtDesc(String openid);
/**
* 查询热门帖子
*/
@Query("SELECT s FROM SquarePost s WHERE s.isPublic = true ORDER BY s.likeCount DESC, s.createdAt DESC")
Page<SquarePost> findHotPosts(Pageable pageable);
}

View File

@@ -0,0 +1,15 @@
package com.jiansu.naming.repository;
import com.jiansu.naming.entity.UserCredits;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.Optional;
@Repository
public interface UserCreditsRepository extends JpaRepository<UserCredits, Long> {
Optional<UserCredits> findByOpenid(String openid);
boolean existsByOpenid(String openid);
}

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,124 @@
package com.jiansu.naming.service;
import com.jiansu.naming.entity.UserCredits;
import com.jiansu.naming.repository.UserCreditsRepository;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDate;
import java.util.Optional;
@Service
@Slf4j
public class CreditsService {
private static final int DAILY_FREE_CREDITS = 5; // 每日免费次数
private static final int AD_REWARD_CREDITS = 3; // 观看广告奖励次数
private static final int MAX_AD_PER_DAY = 5; // 每日最多观看广告次数
@Autowired
private UserCreditsRepository userCreditsRepository;
/**
* 获取或创建用户积分记录
*/
@Transactional
public UserCredits getOrCreateUserCredits(String openid) {
Optional<UserCredits> optional = userCreditsRepository.findByOpenid(openid);
if (optional.isPresent()) {
UserCredits credits = optional.get();
// 检查是否需要刷新每日次数
if (credits.getLastRefreshDate() == null ||
!credits.getLastRefreshDate().equals(LocalDate.now())) {
credits.setDailyCredits(DAILY_FREE_CREDITS);
credits.setWatchedAdCount(0);
credits.setLastRefreshDate(LocalDate.now());
userCreditsRepository.save(credits);
log.info("刷新用户每日次数: {}", openid);
}
return credits;
} else {
// 创建新记录
UserCredits credits = new UserCredits();
credits.setOpenid(openid);
credits.setTotalCredits(0);
credits.setDailyCredits(DAILY_FREE_CREDITS);
credits.setLastRefreshDate(LocalDate.now());
credits.setWatchedAdCount(0);
userCreditsRepository.save(credits);
log.info("创建用户积分记录: {}", openid);
return credits;
}
}
/**
* 检查是否有足够的次数
*/
public boolean hasEnoughCredits(String openid) {
UserCredits credits = getOrCreateUserCredits(openid);
int remaining = credits.getDailyCredits();
log.info("检查用户 {} 的剩余次数: {}", openid, remaining);
return remaining > 0;
}
/**
* 消耗一次生成次数(包含检查和消耗,原子操作)
*/
@Transactional
public boolean consumeCredit(String openid) {
UserCredits credits = getOrCreateUserCredits(openid);
int remaining = credits.getDailyCredits();
log.info("用户 {} 尝试消耗次数,当前剩余: {}", openid, remaining);
if (remaining > 0) {
credits.setDailyCredits(remaining - 1);
credits.setTotalCredits(credits.getTotalCredits() + 1);
userCreditsRepository.save(credits);
log.info("用户 {} 成功消耗一次生成次数,剩余: {}", openid, credits.getDailyCredits());
return true;
}
log.warn("用户 {} 次数不足,无法消耗", openid);
return false;
}
/**
* 观看广告奖励次数
*/
@Transactional
public boolean rewardForWatchingAd(String openid) {
UserCredits credits = getOrCreateUserCredits(openid);
if (credits.getWatchedAdCount() >= MAX_AD_PER_DAY) {
log.info("用户 {} 今日广告观看次数已达上限", openid);
return false;
}
credits.setDailyCredits(credits.getDailyCredits() + AD_REWARD_CREDITS);
credits.setWatchedAdCount(credits.getWatchedAdCount() + 1);
userCreditsRepository.save(credits);
log.info("用户 {} 观看广告获得 {} 次机会,今日已观看: {}",
openid, AD_REWARD_CREDITS, credits.getWatchedAdCount());
return true;
}
/**
* 获取用户积分信息
*/
public UserCredits getUserCredits(String openid) {
return getOrCreateUserCredits(openid);
}
/**
* 获取剩余次数
*/
public int getRemainingCredits(String openid) {
UserCredits credits = getOrCreateUserCredits(openid);
return credits.getDailyCredits();
}
}

View File

@@ -0,0 +1,78 @@
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 org.springframework.transaction.annotation.Transactional;
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());
}
/**
* 取消收藏
*/
@Transactional
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,550 @@
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 static final String MODEL_STANDARD = "moonshot-v1-8k";
private static final String MODEL_PREMIUM = "moonshot-v1-32k";
private static final double PREMIUM_THRESHOLD = 9.5; // 高分名字使用高级模型
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) {
return generateUniqueNamesAsync(keyword, mode, surname, count, new ArrayList<>());
}
/**
* 异步生成指定数量的不重复名字
* @param keyword 关键词
* @param mode 模式
* @param surname 姓氏
* @param targetCount 目标数量默认5个
* @param excludeNames 需要排除的名字列表
* @return CompletableFuture<List<NameCard>>
*/
public CompletableFuture<List<NameCard>> generateUniqueNamesAsync(String keyword, String mode, String surname, int targetCount, List<String> excludeNames) {
long startTime = System.currentTimeMillis();
log.info("开始异步生成 {} 个不重复名字,模式: {},排除: {}", targetCount, mode, excludeNames);
// 后门:当关键词为 "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;
// 使用 Set 存储已生成的名字,确保唯一性
java.util.Set<String> generatedNames = new java.util.HashSet<>(excludeNames);
java.util.List<NameCard> results = new ArrayList<>();
// 最多尝试 3 轮,每轮生成 targetCount 个
int maxRounds = 3;
int round = 0;
while (results.size() < targetCount && round < maxRounds) {
round++;
int needCount = targetCount - results.size();
log.info("第 {} 轮生成,需要 {} 个名字", round, needCount);
// 创建本轮并行任务
List<CompletableFuture<NameCard>> futures = new ArrayList<>();
for (int i = 0; i < needCount; i++) {
final int index = i + (round - 1) * targetCount;
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);
}
// 等待本轮任务完成
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
// 收集本轮结果,去重
for (CompletableFuture<NameCard> future : futures) {
NameCard card = future.join();
if (card != null && card.getName() != null) {
String nameKey = card.getName().trim();
// 检查是否与已生成的重复
if (!generatedNames.contains(nameKey)) {
generatedNames.add(nameKey);
results.add(card);
log.debug("添加名字: {}", nameKey);
// 如果已经收集够,提前退出
if (results.size() >= targetCount) {
break;
}
} else {
log.debug("跳过重复名字: {}", nameKey);
}
}
}
}
long duration = System.currentTimeMillis() - startTime;
log.info("异步生成完成,成功 {} 个不重复名字,耗时 {}ms共进行 {} 轮", results.size(), duration, round);
return CompletableFuture.completedFuture(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 : "")
+ " 请确保创意独特,避免与之前生成的名字重复。";
// 模型选择策略前2个使用标准模型第3个及以后使用高级模型
String model = seed < 2 ? MODEL_STANDARD : MODEL_PREMIUM;
log.info("使用模型 {} 生成第 {} 个名字", model, seed + 1);
// 构建 Kimi API 请求体 (OpenAI 兼容格式)
JSONObject jsonBody = new JSONObject();
jsonBody.put("model", model);
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;
}
/**
* AI 深度解析名字
*/
public String explainName(String name, String context) {
String prompt = buildExplainPrompt(name, context);
Map<String, Object> jsonBody = new HashMap<>();
jsonBody.put("model", "moonshot-v1-8k");
jsonBody.put("temperature", 0.7);
List<Map<String, String>> messages = new ArrayList<>();
Map<String, String> systemMsg = new HashMap<>();
systemMsg.put("role", "system");
systemMsg.put("content", "你是一位精通中国传统文化、姓名学、诗词典故的起名专家。请用优美、富有诗意的语言解析名字。");
messages.add(systemMsg);
Map<String, String> userMsg = new HashMap<>();
userMsg.put("role", "user");
userMsg.put("content", prompt);
messages.add(userMsg);
jsonBody.put("messages", messages);
RequestBody body = RequestBody.create(
JSON.toJSONString(jsonBody),
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();
return parseExplanation(responseBody);
} else {
String errorMsg = response.body() != null ? response.body().string() : response.message();
log.error("Kimi API Error: {} - {}", response.code(), errorMsg);
return "抱歉AI 解析暂时不可用,请稍后重试。";
}
} catch (IOException e) {
log.error("API Error: ", e);
return "网络请求失败,请检查网络连接。";
}
}
/**
* 构建解析 Prompt
*/
private String buildExplainPrompt(String name, String context) {
StringBuilder prompt = new StringBuilder();
prompt.append("请深度解析名字\"").append(name).append("\"");
if (context != null && !context.isEmpty()) {
prompt.append(",针对\"").append(context).append("\"这个背景");
}
prompt.append("。请从以下几个方面分析:\n\n" +
"1. 字面意境:每个字的含义和组合后的整体意象\n" +
"2. 诗词出处:是否有相关的诗词典故\n" +
"3. 性格暗示:这个名字给人的感觉和性格特质\n" +
"4. 适用场景:适合什么样的人或场合\n\n" +
"请用优美、富有诗意的语言回答控制在300字以内。"
);
return prompt.toString();
}
/**
* 解析 AI 回复
*/
private String parseExplanation(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);
return "解析失败,请稍后重试。";
}
JSONArray choices = root.getJSONArray("choices");
if (choices == null || choices.isEmpty()) {
return "暂无解析内容。";
}
JSONObject firstChoice = choices.getJSONObject(0);
JSONObject message = firstChoice.getJSONObject("message");
return message.getString("content");
} catch (Exception e) {
log.error("解析 AI 回复失败", e);
return "解析失败,请稍后重试。";
}
}
// ==================== 模型策略方法 ====================
/**
* 根据分数选择模型
* 高分名字使用高级模型以获得更好的质量
*/
private String selectModelByScore(double score) {
return score >= PREMIUM_THRESHOLD ? MODEL_PREMIUM : MODEL_STANDARD;
}
/**
* 根据请求类型选择模型
*/
private String selectModelByRequestType(String requestType) {
switch (requestType) {
case "deep_explain": // 深度解析
case "premium": // 高级生成
return MODEL_PREMIUM;
case "standard":
default:
return MODEL_STANDARD;
}
}
/**
* 使用高级模型生成名字(用于高质量需求)
*/
public NameCard generatePremiumName(String keyword, String mode, String surname) {
log.info("使用高级模型生成名字 - 关键词: {}, 模式: {}", keyword, mode);
String promptTemplate = PROMPT_TEMPLATES.getOrDefault(mode, PROMPT_TEMPLATES.get("classic"));
String prompt = promptTemplate
.replace("{keyword}", keyword != null ? keyword : "")
.replace("{surname}", surname != null ? surname : "");
Map<String, Object> jsonBody = new HashMap<>();
jsonBody.put("model", MODEL_PREMIUM);
jsonBody.put("temperature", 0.7);
List<Map<String, String>> messages = new ArrayList<>();
Map<String, String> systemMsg = new HashMap<>();
systemMsg.put("role", "system");
systemMsg.put("content", "你是「见素」的首席起名大师,精通中国传统文化、诗词典故、音韵学。请创作极具意境和美感的名字。");
messages.add(systemMsg);
Map<String, String> userMsg = new HashMap<>();
userMsg.put("role", "user");
userMsg.put("content", prompt);
messages.add(userMsg);
jsonBody.put("messages", messages);
RequestBody body = RequestBody.create(
JSON.toJSONString(jsonBody),
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();
NameCard card = parseSingleNameCard(responseBody);
// 高级模型生成的名字给予更高评分
card.setScore(Math.min(10.0, card.getScore() + 0.3));
return card;
} else {
String errorMsg = response.body() != null ? response.body().string() : response.message();
log.error("Kimi Premium API Error: {} - {}", response.code(), errorMsg);
throw new RuntimeException("高级模型调用失败");
}
} catch (IOException e) {
log.error("Premium API Error: ", e);
throw new RuntimeException("网络请求失败");
}
}
}

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

@@ -0,0 +1,98 @@
package com.jiansu.naming.service;
import com.jiansu.naming.entity.SquarePost;
import com.jiansu.naming.repository.SquarePostRepository;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.Optional;
@Service
@Slf4j
public class SquareService {
@Autowired
private SquarePostRepository squarePostRepository;
/**
* 发布帖子
*/
@Transactional
public SquarePost createPost(SquarePost post) {
log.info("发布广场帖子: {}", post.getName());
return squarePostRepository.save(post);
}
/**
* 获取帖子列表(分页)
*/
public Page<SquarePost> getPosts(int page, int size) {
Pageable pageable = PageRequest.of(page, size);
return squarePostRepository.findByIsPublicTrueOrderByCreatedAtDesc(pageable);
}
/**
* 获取热门帖子
*/
public Page<SquarePost> getHotPosts(int page, int size) {
Pageable pageable = PageRequest.of(page, size);
return squarePostRepository.findHotPosts(pageable);
}
/**
* 根据标签查询
*/
public Page<SquarePost> getPostsByTag(String tag, int page, int size) {
Pageable pageable = PageRequest.of(page, size);
return squarePostRepository.findByTag(tag, pageable);
}
/**
* 根据关键词查询
*/
public Page<SquarePost> getPostsByKeyword(String keyword, int page, int size) {
Pageable pageable = PageRequest.of(page, size);
return squarePostRepository.findByKeyword(keyword, pageable);
}
/**
* 点赞
*/
@Transactional
public void likePost(Long postId) {
log.info("帖子点赞: {}", postId);
squarePostRepository.incrementLikeCount(postId);
}
/**
* 获取帖子详情
*/
public Optional<SquarePost> getPostById(Long id) {
return squarePostRepository.findById(id);
}
/**
* 删除帖子
*/
@Transactional
public void deletePost(Long id, String openid) {
Optional<SquarePost> post = squarePostRepository.findById(id);
if (post.isPresent() && post.get().getOpenid().equals(openid)) {
squarePostRepository.deleteById(id);
log.info("删除帖子: {}", id);
}
}
/**
* 获取用户的帖子
*/
public List<SquarePost> getUserPosts(String openid) {
return squarePostRepository.findByOpenidOrderByCreatedAtDesc(openid);
}
}

View File

@@ -1,11 +1,51 @@
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
# 微信小程序配置
wechat:
miniapp:
app-id: wx4793dc8fe6f34d2d
app-secret: d6514ef9115f2fa5224da98800a7bd8d
# 日志配置
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,51 @@
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
# 微信小程序配置
wechat:
miniapp:
app-id: wx4793dc8fe6f34d2d
app-secret: d6514ef9115f2fa5224da98800a7bd8d
# 日志配置
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