refactor: 重命名 backend-single 目录为 server

This commit is contained in:
2026-06-27 17:23:53 +08:00
parent 8daf51301a
commit 0d77d76858
464 changed files with 0 additions and 0 deletions
@@ -0,0 +1,440 @@
package com.emotion.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.emotion.common.PageResult;
import com.emotion.dto.request.achievement.AchievementCreateRequest;
import com.emotion.dto.request.achievement.AchievementPageRequest;
import com.emotion.dto.request.achievement.AchievementUpdateRequest;
import com.emotion.dto.response.achievement.AchievementResponse;
import com.emotion.entity.Achievement;
import com.emotion.mapper.AchievementMapper;
import com.emotion.service.AchievementService;
import com.emotion.util.SnowflakeIdGenerator;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.stream.Collectors;
/**
* 成就服务实现类
*
* @author huazhongmin
* @date 2025-09-08
*/
@Service
public class AchievementServiceImpl extends ServiceImpl<AchievementMapper, Achievement> implements AchievementService {
@Autowired
private SnowflakeIdGenerator snowflakeIdGenerator;
private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
@Override
public IPage<Achievement> getPage(AchievementPageRequest request) {
Page<Achievement> page = new Page<>(request.getCurrent(), request.getSize());
LambdaQueryWrapper<Achievement> wrapper = new LambdaQueryWrapper<>();
// 关键词搜索
if (StringUtils.hasText(request.getKeyword())) {
wrapper.and(w -> w.like(Achievement::getTitle, request.getKeyword())
.or().like(Achievement::getDescription, request.getKeyword()));
}
// 分类筛选
if (StringUtils.hasText(request.getCategory())) {
wrapper.eq(Achievement::getCategory, request.getCategory());
}
// 稀有度筛选
if (StringUtils.hasText(request.getRarity())) {
wrapper.eq(Achievement::getRarity, request.getRarity());
}
// 解锁状态筛选
if (request.getUnlocked() != null) {
if (request.getUnlocked()) {
wrapper.isNotNull(Achievement::getUnlockedTime);
} else {
wrapper.isNull(Achievement::getUnlockedTime);
}
}
// 隐藏状态筛选
if (request.getHidden() != null) {
wrapper.eq(Achievement::getIsHidden, request.getHidden() ? 1 : 0);
}
wrapper.eq(Achievement::getIsDeleted, 0);
// 排序
if (StringUtils.hasText(request.getOrderBy())) {
if ("asc".equalsIgnoreCase(request.getOrderDirection())) {
wrapper.orderByAsc(Achievement::getCreateTime);
} else {
wrapper.orderByDesc(Achievement::getCreateTime);
}
} else {
wrapper.orderByDesc(Achievement::getCreateTime);
}
return this.page(page, wrapper);
}
@Override
public List<Achievement> getByCategory(String category) {
LambdaQueryWrapper<Achievement> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(Achievement::getCategory, category)
.eq(Achievement::getIsDeleted, 0)
.orderByDesc(Achievement::getCreateTime);
return this.list(wrapper);
}
@Override
public List<Achievement> getByRarity(String rarity) {
LambdaQueryWrapper<Achievement> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(Achievement::getRarity, rarity)
.eq(Achievement::getIsDeleted, 0)
.orderByDesc(Achievement::getCreateTime);
return this.list(wrapper);
}
@Override
public List<Achievement> getByConditionType(String conditionType) {
LambdaQueryWrapper<Achievement> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(Achievement::getConditionType, conditionType)
.eq(Achievement::getIsDeleted, 0)
.orderByDesc(Achievement::getCreateTime);
return this.list(wrapper);
}
@Override
public List<Achievement> getUnlockedAchievements() {
LambdaQueryWrapper<Achievement> wrapper = new LambdaQueryWrapper<>();
wrapper.isNotNull(Achievement::getUnlockedTime)
.eq(Achievement::getIsDeleted, 0)
.orderByDesc(Achievement::getUnlockedTime);
return this.list(wrapper);
}
@Override
public List<Achievement> getLockedAchievements() {
LambdaQueryWrapper<Achievement> wrapper = new LambdaQueryWrapper<>();
wrapper.isNull(Achievement::getUnlockedTime)
.eq(Achievement::getIsDeleted, 0)
.orderByDesc(Achievement::getCreateTime);
return this.list(wrapper);
}
@Override
public List<Achievement> getHiddenAchievements() {
LambdaQueryWrapper<Achievement> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(Achievement::getIsHidden, 1)
.eq(Achievement::getIsDeleted, 0)
.orderByDesc(Achievement::getCreateTime);
return this.list(wrapper);
}
@Override
public List<Achievement> getVisibleAchievements() {
LambdaQueryWrapper<Achievement> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(Achievement::getIsHidden, 0)
.eq(Achievement::getIsDeleted, 0)
.orderByDesc(Achievement::getCreateTime);
return this.list(wrapper);
}
@Override
public List<Achievement> getByProgressRange(Double minProgress, Double maxProgress) {
LambdaQueryWrapper<Achievement> wrapper = new LambdaQueryWrapper<>();
wrapper.between(Achievement::getProgress, minProgress, maxProgress)
.eq(Achievement::getIsDeleted, 0)
.orderByDesc(Achievement::getProgress);
return this.list(wrapper);
}
@Override
public List<Achievement> getByUnlockTimeRange(LocalDateTime startTime, LocalDateTime endTime) {
LambdaQueryWrapper<Achievement> wrapper = new LambdaQueryWrapper<>();
wrapper.between(Achievement::getUnlockedTime, startTime, endTime)
.eq(Achievement::getIsDeleted, 0)
.orderByDesc(Achievement::getUnlockedTime);
return this.list(wrapper);
}
@Override
public Long countUnlockedAchievements() {
LambdaQueryWrapper<Achievement> wrapper = new LambdaQueryWrapper<>();
wrapper.isNotNull(Achievement::getUnlockedTime)
.eq(Achievement::getIsDeleted, 0);
return this.count(wrapper);
}
@Override
public Long countLockedAchievements() {
LambdaQueryWrapper<Achievement> wrapper = new LambdaQueryWrapper<>();
wrapper.isNull(Achievement::getUnlockedTime)
.eq(Achievement::getIsDeleted, 0);
return this.count(wrapper);
}
@Override
public Long countByCategory(String category) {
LambdaQueryWrapper<Achievement> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(Achievement::getCategory, category)
.eq(Achievement::getIsDeleted, 0);
return this.count(wrapper);
}
@Override
public Long countByRarity(String rarity) {
LambdaQueryWrapper<Achievement> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(Achievement::getRarity, rarity)
.eq(Achievement::getIsDeleted, 0);
return this.count(wrapper);
}
@Override
public Double getAvgProgress() {
List<Achievement> achievements = this.list(new LambdaQueryWrapper<Achievement>()
.eq(Achievement::getIsDeleted, 0)
.isNotNull(Achievement::getProgress));
return achievements.stream()
.mapToDouble(a -> a.getProgress() != null ? a.getProgress().doubleValue() : 0.0)
.average()
.orElse(0.0);
}
@Override
public Double getAvgProgressByCategory(String category) {
List<Achievement> achievements = this.list(new LambdaQueryWrapper<Achievement>()
.eq(Achievement::getCategory, category)
.eq(Achievement::getIsDeleted, 0)
.isNotNull(Achievement::getProgress));
return achievements.stream()
.mapToDouble(a -> a.getProgress() != null ? a.getProgress().doubleValue() : 0.0)
.average()
.orElse(0.0);
}
@Override
public List<Achievement> getRecentlyUnlocked(Integer limit) {
LambdaQueryWrapper<Achievement> wrapper = new LambdaQueryWrapper<>();
wrapper.isNotNull(Achievement::getUnlockedTime)
.eq(Achievement::getIsDeleted, 0)
.orderByDesc(Achievement::getUnlockedTime)
.last("LIMIT " + limit);
return this.list(wrapper);
}
@Override
public List<Achievement> getNearCompletion() {
LambdaQueryWrapper<Achievement> wrapper = new LambdaQueryWrapper<>();
wrapper.gt(Achievement::getProgress, 80)
.isNull(Achievement::getUnlockedTime)
.eq(Achievement::getIsDeleted, 0)
.orderByDesc(Achievement::getProgress);
return this.list(wrapper);
}
@Override
public List<Achievement> getRareAchievements() {
LambdaQueryWrapper<Achievement> wrapper = new LambdaQueryWrapper<>();
wrapper.in(Achievement::getRarity, "legendary", "epic")
.eq(Achievement::getIsDeleted, 0)
.orderByDesc(Achievement::getCreateTime);
return this.list(wrapper);
}
@Override
public boolean unlockAchievement(String id, LocalDateTime unlockedTime) {
LambdaUpdateWrapper<Achievement> wrapper = new LambdaUpdateWrapper<>();
wrapper.eq(Achievement::getId, id)
.set(Achievement::getUnlockedTime, unlockedTime)
.set(Achievement::getProgress, 100)
.set(Achievement::getUpdateTime, LocalDateTime.now());
return this.update(wrapper);
}
@Override
public boolean updateProgress(String id, Double progress) {
LambdaUpdateWrapper<Achievement> wrapper = new LambdaUpdateWrapper<>();
wrapper.eq(Achievement::getId, id)
.set(Achievement::getProgress, progress)
.set(Achievement::getUpdateTime, LocalDateTime.now());
return this.update(wrapper);
}
@Override
public boolean updateHiddenStatus(String id, Integer isHidden) {
LambdaUpdateWrapper<Achievement> wrapper = new LambdaUpdateWrapper<>();
wrapper.eq(Achievement::getId, id)
.set(Achievement::getIsHidden, isHidden)
.set(Achievement::getUpdateTime, LocalDateTime.now());
return this.update(wrapper);
}
@Override
public List<Achievement> getRecommendedAchievements(String category, String rarity, Integer limit) {
LambdaQueryWrapper<Achievement> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(Achievement::getCategory, category)
.eq(Achievement::getRarity, rarity)
.isNull(Achievement::getUnlockedTime)
.eq(Achievement::getIsHidden, 0)
.eq(Achievement::getIsDeleted, 0)
.orderByDesc(Achievement::getCreateTime)
.last("LIMIT " + limit);
return this.list(wrapper);
}
// 新增的Response相关方法实现
@Override
public PageResult<AchievementResponse> getPageWithResponse(AchievementPageRequest request) {
IPage<Achievement> page = getPage(request);
List<AchievementResponse> responses = page.getRecords().stream()
.map(this::convertToResponse)
.collect(Collectors.toList());
PageResult<AchievementResponse> pageResult = new PageResult<>();
pageResult.setCurrent(page.getCurrent());
pageResult.setSize(page.getSize());
pageResult.setTotal(page.getTotal());
pageResult.setPages(page.getPages());
pageResult.setRecords(responses);
return pageResult;
}
@Override
public AchievementResponse getAchievementResponseById(String id) {
Achievement achievement = this.getById(id);
if (achievement == null) {
return null;
}
return convertToResponse(achievement);
}
@Override
public AchievementResponse createAchievementWithResponse(AchievementCreateRequest request) {
Achievement achievement = new Achievement();
org.springframework.beans.BeanUtils.copyProperties(request, achievement);
achievement.setId(snowflakeIdGenerator.nextIdAsString());
boolean saved = this.save(achievement);
if (!saved) {
return null;
}
return convertToResponse(achievement);
}
@Override
public AchievementResponse updateAchievementWithResponse(AchievementUpdateRequest request) {
Achievement achievement = this.getById(request.getId());
if (achievement == null) {
return null;
}
// 更新非空字段
if (request.getTitle() != null) {
achievement.setTitle(request.getTitle());
}
if (request.getDescription() != null) {
achievement.setDescription(request.getDescription());
}
if (request.getCategory() != null) {
achievement.setCategory(request.getCategory());
}
if (request.getIcon() != null) {
achievement.setIcon(request.getIcon());
}
if (request.getRarity() != null) {
achievement.setRarity(request.getRarity());
}
if (request.getConditionType() != null) {
achievement.setConditionType(request.getConditionType());
}
if (request.getConditionValue() != null) {
achievement.setConditionValue(request.getConditionValue());
}
if (request.getRewards() != null) {
achievement.setRewards(request.getRewards());
}
if (request.getIsHidden() != null) {
achievement.setIsHidden(request.getIsHidden());
}
achievement.setUpdateTime(LocalDateTime.now());
boolean updated = this.updateById(achievement);
if (!updated) {
return null;
}
return convertToResponse(achievement);
}
@Override
public List<AchievementResponse> getByCategoryWithResponse(String category) {
List<Achievement> achievements = getByCategory(category);
return achievements.stream()
.map(this::convertToResponse)
.collect(Collectors.toList());
}
@Override
public List<AchievementResponse> getByRarityWithResponse(String rarity) {
List<Achievement> achievements = getByRarity(rarity);
return achievements.stream()
.map(this::convertToResponse)
.collect(Collectors.toList());
}
@Override
public List<AchievementResponse> getUnlockedAchievementsWithResponse() {
List<Achievement> achievements = getUnlockedAchievements();
return achievements.stream()
.map(this::convertToResponse)
.collect(Collectors.toList());
}
@Override
public List<AchievementResponse> getLockedAchievementsWithResponse() {
List<Achievement> achievements = getLockedAchievements();
return achievements.stream()
.map(this::convertToResponse)
.collect(Collectors.toList());
}
@Override
public List<AchievementResponse> getRecentlyUnlockedWithResponse(Integer limit) {
List<Achievement> achievements = getRecentlyUnlocked(limit);
return achievements.stream()
.map(this::convertToResponse)
.collect(Collectors.toList());
}
/**
* 转换为响应对象
*/
private AchievementResponse convertToResponse(Achievement achievement) {
AchievementResponse response = new AchievementResponse();
org.springframework.beans.BeanUtils.copyProperties(achievement, response);
response.setId(achievement.getId());
if (achievement.getCreateTime() != null) {
response.setCreateTime(achievement.getCreateTime().format(DATE_TIME_FORMATTER));
}
if (achievement.getUpdateTime() != null) {
response.setUpdateTime(achievement.getUpdateTime().format(DATE_TIME_FORMATTER));
}
if (achievement.getUnlockedTime() != null) {
response.setUnlockedTime(achievement.getUnlockedTime().format(DATE_TIME_FORMATTER));
}
return response;
}
}
@@ -0,0 +1,226 @@
package com.emotion.service.impl;
import com.emotion.dto.request.AdminChangePasswordRequest;
import com.emotion.dto.request.AdminLoginRequest;
import com.emotion.dto.response.AdminAuthResponse;
import com.emotion.dto.response.AdminInfoResponse;
import com.emotion.entity.Admin;
import com.emotion.exception.AuthException;
import com.emotion.service.AdminAuthService;
import com.emotion.service.AdminService;
import com.emotion.util.JwtUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import javax.servlet.http.HttpServletRequest;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.concurrent.TimeUnit;
/**
* 管理员认证服务实现类
*
* @author emotion-museum
* @date 2025-10-27
*/
@Slf4j
@Service
public class AdminAuthServiceImpl implements AdminAuthService {
@Autowired
private AdminService adminService;
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Autowired
private JwtUtil jwtUtil;
@Autowired
private org.springframework.security.crypto.password.PasswordEncoder passwordEncoder;
private static final String ADMIN_TOKEN_PREFIX = "admin_token:";
private static final String ADMIN_REFRESH_TOKEN_PREFIX = "admin_refresh_token:";
private static final int TOKEN_EXPIRE_HOURS = 24;
private static final int REFRESH_TOKEN_EXPIRE_DAYS = 7;
private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
@Override
public AdminAuthResponse login(AdminLoginRequest request) {
Admin admin = adminService.getByAccount(request.getAccount());
if (admin == null) {
throw new AuthException("账号或密码错误");
}
if (admin.getStatus() != 1) {
throw new AuthException("账号已被禁用");
}
if (!passwordEncoder.matches(request.getPassword(), admin.getPassword())) {
throw new AuthException("账号或密码错误");
}
log.info("管理员登录成功: account={}, adminId={}", request.getAccount(), admin.getId());
String accessToken = generateAccessToken(admin);
String refreshToken = generateRefreshToken(admin);
updateLoginInfo(admin);
AdminAuthResponse response = new AdminAuthResponse();
response.setAccessToken(accessToken);
response.setRefreshToken(refreshToken);
response.setExpiresIn((long) TOKEN_EXPIRE_HOURS * 3600);
response.setAdminInfo(convertToAdminInfoResponse(admin));
response.setLoginTime(LocalDateTime.now().format(DATE_TIME_FORMATTER));
return response;
}
@Override
public AdminInfoResponse getCurrentAdminInfo(String adminId) {
Admin admin = adminService.getById(adminId);
if (admin == null) {
throw new AuthException("管理员不存在");
}
return convertToAdminInfoResponse(admin);
}
@Override
public boolean logout(HttpServletRequest request) {
String authHeader = request.getHeader("Authorization");
if (authHeader != null && authHeader.startsWith("Bearer ")) {
String token = authHeader.substring(7);
String adminId = jwtUtil.getUserIdFromToken(token);
if (adminId != null) {
redisTemplate.delete(ADMIN_TOKEN_PREFIX + adminId);
log.info("管理员登出成功: adminId={}", adminId);
return true;
}
}
return false;
}
@Override
public AdminAuthResponse refreshToken(String refreshToken) {
if (!jwtUtil.validateToken(refreshToken)) {
throw new AuthException("刷新令牌无效或已过期");
}
String adminId = jwtUtil.getUserIdFromToken(refreshToken);
String userType = jwtUtil.getUserTypeFromToken(refreshToken);
if (!"admin".equals(userType)) {
throw new AuthException("无效的刷新令牌");
}
String cachedToken = (String) redisTemplate.opsForValue().get(ADMIN_REFRESH_TOKEN_PREFIX + adminId);
if (cachedToken == null || !cachedToken.equals(refreshToken)) {
throw new AuthException("刷新令牌已失效");
}
Admin admin = adminService.getById(adminId);
if (admin == null) {
throw new AuthException("管理员不存在");
}
if (admin.getStatus() != 1) {
throw new AuthException("账号已被禁用");
}
String newAccessToken = generateAccessToken(admin);
String newRefreshToken = generateRefreshToken(admin);
AdminAuthResponse response = new AdminAuthResponse();
response.setAccessToken(newAccessToken);
response.setRefreshToken(newRefreshToken);
response.setExpiresIn((long) TOKEN_EXPIRE_HOURS * 3600);
response.setAdminInfo(convertToAdminInfoResponse(admin));
response.setLoginTime(LocalDateTime.now().format(DATE_TIME_FORMATTER));
return response;
}
@Override
public boolean validateToken(HttpServletRequest request) {
String authHeader = request.getHeader("Authorization");
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
return false;
}
String token = authHeader.substring(7);
if (!jwtUtil.validateToken(token)) {
return false;
}
String userType = jwtUtil.getUserTypeFromToken(token);
return "admin".equals(userType);
}
@Override
public String getAdminIdFromToken(String token) {
return jwtUtil.getUserIdFromToken(token);
}
private String generateAccessToken(Admin admin) {
String token = jwtUtil.generateToken(admin.getId(), admin.getUsername(), "admin");
redisTemplate.opsForValue().set(
ADMIN_TOKEN_PREFIX + admin.getId(),
token,
TOKEN_EXPIRE_HOURS,
TimeUnit.HOURS
);
return token;
}
private String generateRefreshToken(Admin admin) {
String token = jwtUtil.generateRefreshToken(admin.getId(), admin.getUsername(), "admin");
redisTemplate.opsForValue().set(
ADMIN_REFRESH_TOKEN_PREFIX + admin.getId(),
token,
REFRESH_TOKEN_EXPIRE_DAYS,
TimeUnit.DAYS
);
return token;
}
private void updateLoginInfo(Admin admin) {
admin.setLastLoginTime(LocalDateTime.now());
admin.setLoginCount(admin.getLoginCount() == null ? 1 : admin.getLoginCount() + 1);
adminService.updateById(admin);
}
private AdminInfoResponse convertToAdminInfoResponse(Admin admin) {
AdminInfoResponse response = new AdminInfoResponse();
BeanUtils.copyProperties(admin, response);
return response;
}
@Override
public void changePassword(String adminId, AdminChangePasswordRequest request) {
Admin admin = adminService.getById(adminId);
if (admin == null) {
throw new AuthException("管理员不存在");
}
if (!passwordEncoder.matches(request.getOldPassword(), admin.getPassword())) {
throw new AuthException("原密码不正确");
}
admin.setPassword(passwordEncoder.encode(request.getNewPassword()));
adminService.updateById(admin);
// 清除该管理员的Redis token,强制重新登录
redisTemplate.delete(ADMIN_TOKEN_PREFIX + adminId);
redisTemplate.delete(ADMIN_REFRESH_TOKEN_PREFIX + adminId);
log.info("管理员修改密码成功: adminId={}", adminId);
}
}
@@ -0,0 +1,283 @@
package com.emotion.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.emotion.common.PageResult;
import com.emotion.dto.request.AdminCreateRequest;
import com.emotion.dto.request.AdminPageRequest;
import com.emotion.dto.request.AdminUpdateRequest;
import com.emotion.dto.response.AdminResponse;
import com.emotion.entity.Admin;
import com.emotion.exception.BusinessException;
import com.emotion.mapper.AdminMapper;
import com.emotion.service.AdminService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.stream.Collectors;
/**
* 管理员服务实现类
*
* @author huazhongmin
* @date 2025-10-27
*/
@Service
@Slf4j
public class AdminServiceImpl extends ServiceImpl<AdminMapper, Admin> implements AdminService {
private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
private final BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
private final RedisTemplate<String, Object> redisTemplate;
public AdminServiceImpl(RedisTemplate<String, Object> redisTemplate) {
this.redisTemplate = redisTemplate;
}
@Override
public PageResult<AdminResponse> getPageWithResponse(AdminPageRequest request) {
Page<Admin> page = new Page<>(request.getCurrent(), request.getSize());
LambdaQueryWrapper<Admin> wrapper = new LambdaQueryWrapper<>();
if (StringUtils.hasText(request.getKeyword())) {
wrapper.and(w -> w.like(Admin::getAccount, request.getKeyword())
.or().like(Admin::getUsername, request.getKeyword())
.or().like(Admin::getEmail, request.getKeyword())
.or().like(Admin::getPhone, request.getKeyword()));
}
if (StringUtils.hasText(request.getAccount())) {
wrapper.like(Admin::getAccount, request.getAccount());
}
if (StringUtils.hasText(request.getUsername())) {
wrapper.like(Admin::getUsername, request.getUsername());
}
if (StringUtils.hasText(request.getEmail())) {
wrapper.like(Admin::getEmail, request.getEmail());
}
if (StringUtils.hasText(request.getPhone())) {
wrapper.like(Admin::getPhone, request.getPhone());
}
if (StringUtils.hasText(request.getRole())) {
wrapper.eq(Admin::getRole, request.getRole());
}
if (request.getStatus() != null) {
wrapper.eq(Admin::getStatus, request.getStatus());
}
if (StringUtils.hasText(request.getDepartment())) {
wrapper.like(Admin::getDepartment, request.getDepartment());
}
wrapper.eq(Admin::getIsDeleted, 0);
if (StringUtils.hasText(request.getOrderBy())) {
if ("asc".equalsIgnoreCase(request.getOrderDirection())) {
wrapper.orderByAsc(Admin::getCreateTime);
} else {
wrapper.orderByDesc(Admin::getCreateTime);
}
} else {
wrapper.orderByDesc(Admin::getCreateTime);
}
IPage<Admin> adminPage = this.page(page, wrapper);
List<AdminResponse> responseList = adminPage.getRecords().stream()
.map(this::convertToResponse)
.collect(Collectors.toList());
PageResult<AdminResponse> result = new PageResult<>();
result.setRecords(responseList);
result.setTotal(adminPage.getTotal());
result.setCurrent(adminPage.getCurrent());
result.setSize(adminPage.getSize());
result.setPages(adminPage.getPages());
return result;
}
@Override
public AdminResponse getAdminResponseById(String id) {
Admin admin = this.getById(id);
if (admin == null) {
return null;
}
return convertToResponse(admin);
}
@Override
public AdminResponse createAdminWithResponse(AdminCreateRequest request) {
LambdaQueryWrapper<Admin> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(Admin::getAccount, request.getAccount())
.eq(Admin::getIsDeleted, 0);
if (this.count(wrapper) > 0) {
throw new BusinessException("账号已存在");
}
if (StringUtils.hasText(request.getEmail())) {
wrapper = new LambdaQueryWrapper<>();
wrapper.eq(Admin::getEmail, request.getEmail())
.eq(Admin::getIsDeleted, 0);
if (this.count(wrapper) > 0) {
throw new BusinessException("邮箱已存在");
}
}
if (StringUtils.hasText(request.getPhone())) {
wrapper = new LambdaQueryWrapper<>();
wrapper.eq(Admin::getPhone, request.getPhone())
.eq(Admin::getIsDeleted, 0);
if (this.count(wrapper) > 0) {
throw new BusinessException("手机号已存在");
}
}
Admin admin = new Admin();
BeanUtils.copyProperties(request, admin);
admin.setPassword(passwordEncoder.encode(request.getPassword()));
admin.setStatus(1);
admin.setLoginCount(0);
boolean saved = this.save(admin);
if (!saved) {
return null;
}
return convertToResponse(admin);
}
@Override
public AdminResponse updateAdminWithResponse(AdminUpdateRequest request) {
Admin admin = this.getById(request.getId());
if (admin == null) {
throw new BusinessException("管理员不存在");
}
if (StringUtils.hasText(request.getEmail()) && !request.getEmail().equals(admin.getEmail())) {
LambdaQueryWrapper<Admin> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(Admin::getEmail, request.getEmail())
.ne(Admin::getId, request.getId())
.eq(Admin::getIsDeleted, 0);
if (this.count(wrapper) > 0) {
throw new BusinessException("邮箱已存在");
}
}
if (StringUtils.hasText(request.getPhone()) && !request.getPhone().equals(admin.getPhone())) {
LambdaQueryWrapper<Admin> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(Admin::getPhone, request.getPhone())
.ne(Admin::getId, request.getId())
.eq(Admin::getIsDeleted, 0);
if (this.count(wrapper) > 0) {
throw new BusinessException("手机号已存在");
}
}
if (StringUtils.hasText(request.getUsername())) {
admin.setUsername(request.getUsername());
}
if (StringUtils.hasText(request.getEmail())) {
admin.setEmail(request.getEmail());
}
if (StringUtils.hasText(request.getPhone())) {
admin.setPhone(request.getPhone());
}
if (StringUtils.hasText(request.getAvatar())) {
admin.setAvatar(request.getAvatar());
}
if (StringUtils.hasText(request.getRole())) {
admin.setRole(request.getRole());
}
if (request.getPermissions() != null) {
admin.setPermissions(request.getPermissions());
}
if (request.getStatus() != null) {
admin.setStatus(request.getStatus());
}
if (StringUtils.hasText(request.getDepartment())) {
admin.setDepartment(request.getDepartment());
}
if (StringUtils.hasText(request.getPosition())) {
admin.setPosition(request.getPosition());
}
boolean updated = this.updateById(admin);
if (!updated) {
return null;
}
return convertToResponse(admin);
}
@Override
public Admin getByAccount(String account) {
LambdaQueryWrapper<Admin> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(Admin::getAccount, account)
.eq(Admin::getIsDeleted, 0);
return this.getOne(wrapper);
}
@Override
public Admin getByEmail(String email) {
LambdaQueryWrapper<Admin> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(Admin::getEmail, email)
.eq(Admin::getIsDeleted, 0);
return this.getOne(wrapper);
}
@Override
public Admin getByPhone(String phone) {
LambdaQueryWrapper<Admin> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(Admin::getPhone, phone)
.eq(Admin::getIsDeleted, 0);
return this.getOne(wrapper);
}
@Override
public void resetPassword(String adminId, String newPassword) {
Admin admin = this.getById(adminId);
if (admin == null) {
throw new BusinessException("管理员不存在");
}
admin.setPassword(passwordEncoder.encode(newPassword));
this.updateById(admin);
// 清除该管理员的Redis token,强制重新登录
redisTemplate.delete("admin_token:" + adminId);
redisTemplate.delete("admin_refresh_token:" + adminId);
log.info("管理员重置密码成功: adminId={}", adminId);
}
private AdminResponse convertToResponse(Admin admin) {
AdminResponse response = new AdminResponse();
BeanUtils.copyProperties(admin, response);
if (admin.getLastLoginTime() != null) {
response.setLastLoginTime(admin.getLastLoginTime().format(DATE_TIME_FORMATTER));
}
if (admin.getCreateTime() != null) {
response.setCreateTime(admin.getCreateTime().format(DATE_TIME_FORMATTER));
}
if (admin.getUpdateTime() != null) {
response.setUpdateTime(admin.getUpdateTime().format(DATE_TIME_FORMATTER));
}
return response;
}
}
@@ -0,0 +1,65 @@
package com.emotion.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.emotion.common.PageResult;
import com.emotion.dto.request.ai.AiCallLogQueryRequest;
import com.emotion.entity.AiCallLog;
import com.emotion.mapper.AiCallLogMapper;
import com.emotion.service.AiCallLogService;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class AiCallLogServiceImpl extends ServiceImpl<AiCallLogMapper, AiCallLog> implements AiCallLogService {
@Override
public List<AiCallLog> latest(Integer limit) {
int size = limit == null ? 50 : Math.max(1, Math.min(limit, 200));
return list(new LambdaQueryWrapper<AiCallLog>()
.eq(AiCallLog::getIsDeleted, 0)
.orderByDesc(AiCallLog::getCreateTime)
.last("limit " + size));
}
@Override
public PageResult<AiCallLog> query(AiCallLogQueryRequest request) {
Page<AiCallLog> pageParam = new Page<>(request.getPageNum(), request.getPageSize());
LambdaQueryWrapper<AiCallLog> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(AiCallLog::getIsDeleted, 0)
.eq(StringUtils.isNotBlank(request.getStatus()), AiCallLog::getStatus, request.getStatus())
.eq(StringUtils.isNotBlank(request.getSceneCode()), AiCallLog::getSceneCode, request.getSceneCode())
.eq(StringUtils.isNotBlank(request.getProviderCode()), AiCallLog::getProviderCode, request.getProviderCode())
.eq(StringUtils.isNotBlank(request.getEndpointCode()), AiCallLog::getEndpointCode, request.getEndpointCode())
.ge(request.getStartTime() != null, AiCallLog::getCreateTime, request.getStartTime())
.le(request.getEndTime() != null, AiCallLog::getCreateTime, request.getEndTime())
.orderByDesc(AiCallLog::getCreateTime);
if (StringUtils.isNotBlank(request.getKeyword())) {
wrapper.and(w -> w.like(AiCallLog::getInputText, request.getKeyword())
.or()
.like(AiCallLog::getOutputText, request.getKeyword()));
}
IPage<AiCallLog> page = page(pageParam, wrapper);
return PageResult.of(page);
}
@Override
public AiCallLog findByRequestId(String requestId, String userId) {
if (StringUtils.isBlank(requestId)) {
return null;
}
return getOne(new LambdaQueryWrapper<AiCallLog>()
.eq(AiCallLog::getIsDeleted, 0)
.eq(AiCallLog::getRequestId, requestId)
.eq(StringUtils.isNotBlank(userId), AiCallLog::getUserId, userId)
.orderByDesc(AiCallLog::getCreateTime)
.last("limit 1"));
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,479 @@
package com.emotion.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.emotion.common.PageResult;
import com.emotion.dto.request.aiconfig.AiConfigCreateRequest;
import com.emotion.dto.request.aiconfig.AiConfigPageRequest;
import com.emotion.dto.request.aiconfig.AiConfigTestUpdateRequest;
import com.emotion.dto.request.aiconfig.AiConfigUpdateRequest;
import com.emotion.dto.response.aiconfig.AiConfigResponse;
import com.emotion.entity.AiConfig;
import com.emotion.mapper.AiConfigMapper;
import com.emotion.service.AiConfigService;
import com.emotion.util.SnowflakeIdGenerator;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.stream.Collectors;
/**
* AI配置服务实现类
*
* @author system
* @date 2025-10-30
*/
@Service
public class AiConfigServiceImpl extends ServiceImpl<AiConfigMapper, AiConfig> implements AiConfigService {
@Autowired
private SnowflakeIdGenerator snowflakeIdGenerator;
private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
@Override
public IPage<AiConfig> getPage(AiConfigPageRequest request) {
Page<AiConfig> page = new Page<>(request.getCurrent(), request.getSize());
LambdaQueryWrapper<AiConfig> wrapper = new LambdaQueryWrapper<>();
// 关键词搜索
if (StringUtils.hasText(request.getKeyword())) {
wrapper.and(w -> w.like(AiConfig::getConfigName, request.getKeyword())
.or().like(AiConfig::getConfigKey, request.getKeyword())
.or().like(AiConfig::getDescription, request.getKeyword()));
}
// 配置类型过滤
if (StringUtils.hasText(request.getConfigType())) {
wrapper.eq(AiConfig::getConfigType, request.getConfigType());
}
// 服务提供商过滤
if (StringUtils.hasText(request.getProvider())) {
wrapper.eq(AiConfig::getProvider, request.getProvider());
}
// 使用场景过滤
if (StringUtils.hasText(request.getUsageScenario())) {
wrapper.eq(AiConfig::getUsageScenario, request.getUsageScenario());
}
// 是否启用过滤
if (request.getIsEnabled() != null) {
wrapper.eq(AiConfig::getIsEnabled, request.getIsEnabled());
}
// 是否为默认配置过滤
if (request.getIsDefault() != null) {
wrapper.eq(AiConfig::getIsDefault, request.getIsDefault());
}
// 环境过滤
if (StringUtils.hasText(request.getEnvironment())) {
wrapper.eq(AiConfig::getEnvironment, request.getEnvironment());
}
// 排序
if (StringUtils.hasText(request.getOrderBy())) {
if ("asc".equalsIgnoreCase(request.getOrderDirection())) {
wrapper.orderByAsc(getLambdaByField(request.getOrderBy()));
} else {
wrapper.orderByDesc(getLambdaByField(request.getOrderBy()));
}
} else {
wrapper.orderByDesc(AiConfig::getCreateTime);
}
return this.page(page, wrapper);
}
@Override
public PageResult<AiConfigResponse> getPageWithResponse(AiConfigPageRequest request) {
IPage<AiConfig> page = getPage(request);
List<AiConfigResponse> responses = page.getRecords().stream()
.map(this::convertToResponse)
.collect(Collectors.toList());
PageResult<AiConfigResponse> result = new PageResult<>();
result.setCurrent(page.getCurrent());
result.setSize(page.getSize());
result.setTotal(page.getTotal());
result.setPages(page.getPages());
result.setRecords(responses);
return result;
}
@Override
public AiConfigResponse getAiConfigResponseById(String id) {
AiConfig aiConfig = this.getById(id);
return aiConfig != null ? convertToResponse(aiConfig) : null;
}
@Override
public AiConfigResponse createAiConfigWithResponse(AiConfigCreateRequest request) {
AiConfig aiConfig = new AiConfig();
BeanUtils.copyProperties(request, aiConfig);
aiConfig.setId(String.valueOf(snowflakeIdGenerator.nextId()));
// 处理JSON字段,防止空字符串导致数据库报错
if (!StringUtils.hasText(aiConfig.getCustomHeaders())) {
aiConfig.setCustomHeaders("{}");
}
if (!StringUtils.hasText(aiConfig.getCustomParams())) {
aiConfig.setCustomParams("{}");
}
boolean saved = this.save(aiConfig);
return saved ? convertToResponse(aiConfig) : null;
}
@Override
public AiConfigResponse updateAiConfigWithResponse(AiConfigUpdateRequest request) {
AiConfig aiConfig = this.getById(request.getId());
if (aiConfig == null) {
return null;
}
BeanUtils.copyProperties(request, aiConfig);
// 处理JSON字段,防止空字符串导致数据库报错
if (request.getCustomHeaders() != null && !StringUtils.hasText(request.getCustomHeaders())) {
aiConfig.setCustomHeaders("{}");
}
if (request.getCustomParams() != null && !StringUtils.hasText(request.getCustomParams())) {
aiConfig.setCustomParams("{}");
}
boolean updated = this.updateById(aiConfig);
return updated ? convertToResponse(aiConfig) : null;
}
@Override
public List<AiConfig> getByConfigType(String configType) {
LambdaQueryWrapper<AiConfig> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(AiConfig::getConfigType, configType);
wrapper.orderByDesc(AiConfig::getPriority);
return this.list(wrapper);
}
@Override
public List<AiConfigResponse> getByConfigTypeWithResponse(String configType) {
return getByConfigType(configType).stream()
.map(this::convertToResponse)
.collect(Collectors.toList());
}
@Override
public List<AiConfig> getByProvider(String provider) {
LambdaQueryWrapper<AiConfig> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(AiConfig::getProvider, provider);
wrapper.orderByDesc(AiConfig::getPriority);
return this.list(wrapper);
}
@Override
public List<AiConfigResponse> getByProviderWithResponse(String provider) {
return getByProvider(provider).stream()
.map(this::convertToResponse)
.collect(Collectors.toList());
}
@Override
public List<AiConfig> getByUsageScenario(String usageScenario) {
LambdaQueryWrapper<AiConfig> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(AiConfig::getUsageScenario, usageScenario);
wrapper.eq(AiConfig::getIsEnabled, 1);
wrapper.orderByDesc(AiConfig::getPriority);
return this.list(wrapper);
}
@Override
public List<AiConfigResponse> getByUsageScenarioWithResponse(String usageScenario) {
return getByUsageScenario(usageScenario).stream()
.map(this::convertToResponse)
.collect(Collectors.toList());
}
@Override
public List<AiConfig> getByEnvironment(String environment) {
LambdaQueryWrapper<AiConfig> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(AiConfig::getEnvironment, environment);
wrapper.orderByDesc(AiConfig::getPriority);
return this.list(wrapper);
}
@Override
public List<AiConfigResponse> getByEnvironmentWithResponse(String environment) {
return getByEnvironment(environment).stream()
.map(this::convertToResponse)
.collect(Collectors.toList());
}
@Override
public List<AiConfig> getEnabledConfigs() {
LambdaQueryWrapper<AiConfig> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(AiConfig::getIsEnabled, 1);
wrapper.orderByDesc(AiConfig::getPriority);
return this.list(wrapper);
}
@Override
public List<AiConfigResponse> getEnabledConfigsWithResponse() {
return getEnabledConfigs().stream()
.map(this::convertToResponse)
.collect(Collectors.toList());
}
@Override
public List<AiConfig> getDisabledConfigs() {
LambdaQueryWrapper<AiConfig> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(AiConfig::getIsEnabled, 0);
wrapper.orderByDesc(AiConfig::getCreateTime);
return this.list(wrapper);
}
@Override
public List<AiConfigResponse> getDisabledConfigsWithResponse() {
return getDisabledConfigs().stream()
.map(this::convertToResponse)
.collect(Collectors.toList());
}
@Override
public List<AiConfig> getDefaultConfigs() {
LambdaQueryWrapper<AiConfig> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(AiConfig::getIsDefault, 1);
wrapper.orderByDesc(AiConfig::getPriority);
return this.list(wrapper);
}
@Override
public List<AiConfigResponse> getDefaultConfigsWithResponse() {
return getDefaultConfigs().stream()
.map(this::convertToResponse)
.collect(Collectors.toList());
}
@Override
public AiConfig getByConfigKey(String configKey) {
LambdaQueryWrapper<AiConfig> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(AiConfig::getConfigKey, configKey);
wrapper.eq(AiConfig::getIsEnabled, 1);
return this.getOne(wrapper);
}
@Override
public AiConfigResponse getByConfigKeyWithResponse(String configKey) {
AiConfig aiConfig = getByConfigKey(configKey);
return aiConfig != null ? convertToResponse(aiConfig) : null;
}
@Override
public boolean enableConfig(String id) {
LambdaUpdateWrapper<AiConfig> wrapper = new LambdaUpdateWrapper<>();
wrapper.eq(AiConfig::getId, id);
wrapper.set(AiConfig::getIsEnabled, 1);
return this.update(wrapper);
}
@Override
public boolean disableConfig(String id) {
LambdaUpdateWrapper<AiConfig> wrapper = new LambdaUpdateWrapper<>();
wrapper.eq(AiConfig::getId, id);
wrapper.set(AiConfig::getIsEnabled, 0);
return this.update(wrapper);
}
@Override
public boolean setAsDefault(String id) {
LambdaUpdateWrapper<AiConfig> wrapper = new LambdaUpdateWrapper<>();
wrapper.eq(AiConfig::getId, id);
wrapper.set(AiConfig::getIsDefault, 1);
return this.update(wrapper);
}
@Override
public boolean unsetDefault(String id) {
LambdaUpdateWrapper<AiConfig> wrapper = new LambdaUpdateWrapper<>();
wrapper.eq(AiConfig::getId, id);
wrapper.set(AiConfig::getIsDefault, 0);
return this.update(wrapper);
}
@Override
public AiConfig getBestConfig(String usageScenario, String environment) {
LambdaQueryWrapper<AiConfig> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(AiConfig::getUsageScenario, usageScenario);
wrapper.eq(AiConfig::getEnvironment, environment);
wrapper.eq(AiConfig::getIsEnabled, 1);
wrapper.orderByDesc(AiConfig::getIsDefault);
wrapper.orderByDesc(AiConfig::getPriority);
wrapper.last("LIMIT 1");
return this.getOne(wrapper);
}
@Override
public AiConfigResponse getBestConfigWithResponse(String usageScenario, String environment) {
AiConfig aiConfig = getBestConfig(usageScenario, environment);
return aiConfig != null ? convertToResponse(aiConfig) : null;
}
@Override
public Long countEnabledConfigs() {
LambdaQueryWrapper<AiConfig> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(AiConfig::getIsEnabled, 1);
return this.count(wrapper);
}
@Override
public Long countDisabledConfigs() {
LambdaQueryWrapper<AiConfig> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(AiConfig::getIsEnabled, 0);
return this.count(wrapper);
}
@Override
public Long countDefaultConfigs() {
LambdaQueryWrapper<AiConfig> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(AiConfig::getIsDefault, 1);
return this.count(wrapper);
}
@Override
public Long countByConfigType(String configType) {
LambdaQueryWrapper<AiConfig> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(AiConfig::getConfigType, configType);
return this.count(wrapper);
}
@Override
public Long countByProvider(String provider) {
LambdaQueryWrapper<AiConfig> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(AiConfig::getProvider, provider);
return this.count(wrapper);
}
/**
* 将实体转换为响应对象
*/
private AiConfigResponse convertToResponse(AiConfig aiConfig) {
AiConfigResponse response = new AiConfigResponse();
BeanUtils.copyProperties(aiConfig, response);
// 格式化时间
if (aiConfig.getCreateTime() != null) {
response.setCreateTime(aiConfig.getCreateTime().format(DATE_TIME_FORMATTER));
}
if (aiConfig.getUpdateTime() != null) {
response.setUpdateTime(aiConfig.getUpdateTime().format(DATE_TIME_FORMATTER));
}
// 管理端不需要脱敏,直接返回完整的API Token
response.setApiToken(aiConfig.getApiToken());
// 转换BigDecimal为Double
if (aiConfig.getTemperature() != null) {
response.setTemperature(aiConfig.getTemperature().doubleValue());
}
if (aiConfig.getTopP() != null) {
response.setTopP(aiConfig.getTopP().doubleValue());
}
if (aiConfig.getInputPricePer1k() != null) {
response.setInputPricePer1k(aiConfig.getInputPricePer1k().doubleValue());
}
if (aiConfig.getOutputPricePer1k() != null) {
response.setOutputPricePer1k(aiConfig.getOutputPricePer1k().doubleValue());
}
return response;
}
/**
* 根据字段名获取对应的Lambda表达式
*/
private com.baomidou.mybatisplus.core.toolkit.support.SFunction<AiConfig, ?> getLambdaByField(String field) {
switch (field) {
case "configName":
return AiConfig::getConfigName;
case "configKey":
return AiConfig::getConfigKey;
case "configType":
return AiConfig::getConfigType;
case "provider":
return AiConfig::getProvider;
case "usageScenario":
return AiConfig::getUsageScenario;
case "priority":
return AiConfig::getPriority;
case "createTime":
return AiConfig::getCreateTime;
case "updateTime":
return AiConfig::getUpdateTime;
default:
return AiConfig::getCreateTime;
}
}
@Override
public AiConfigResponse updateFromTestRequest(AiConfigTestUpdateRequest request) {
// 查询现有配置
AiConfig aiConfig = this.getById(request.getId());
if (aiConfig == null) {
return null;
}
// 更新字段
if (StringUtils.hasText(request.getApiBaseUrl())) {
aiConfig.setApiBaseUrl(request.getApiBaseUrl());
}
if (StringUtils.hasText(request.getApiToken())) {
aiConfig.setApiToken(request.getApiToken());
}
if (StringUtils.hasText(request.getClientId())) {
aiConfig.setClientId(request.getClientId());
}
if (StringUtils.hasText(request.getClientSecret())) {
aiConfig.setClientSecret(request.getClientSecret());
}
if (StringUtils.hasText(request.getGrantType())) {
aiConfig.setGrantType(request.getGrantType());
}
if (StringUtils.hasText(request.getBotId())) {
aiConfig.setBotId(request.getBotId());
}
if (StringUtils.hasText(request.getWorkflowId())) {
aiConfig.setWorkflowId(request.getWorkflowId());
}
if (StringUtils.hasText(request.getCustomHeaders())) {
aiConfig.setCustomHeaders(request.getCustomHeaders());
}
if (StringUtils.hasText(request.getCustomParams())) {
aiConfig.setCustomParams(request.getCustomParams());
}
if (request.getSupportStream() != null) {
aiConfig.setSupportStream(request.getSupportStream());
}
// 保存更新
this.updateById(aiConfig);
// 返回响应对象
return convertToResponse(aiConfig);
}
}
@@ -0,0 +1,59 @@
package com.emotion.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.emotion.entity.AiEndpointConfig;
import com.emotion.mapper.AiEndpointConfigMapper;
import com.emotion.service.AiEndpointConfigService;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.util.List;
@Service
public class AiEndpointConfigServiceImpl extends ServiceImpl<AiEndpointConfigMapper, AiEndpointConfig> implements AiEndpointConfigService {
@Override
public List<AiEndpointConfig> listVisible() {
return list(new LambdaQueryWrapper<AiEndpointConfig>()
.eq(AiEndpointConfig::getIsDeleted, 0)
.orderByDesc(AiEndpointConfig::getCreateTime));
}
@Override
public AiEndpointConfig saveEndpoint(AiEndpointConfig endpoint) {
applyDefaults(endpoint);
save(endpoint);
return endpoint;
}
@Override
public AiEndpointConfig updateEndpoint(AiEndpointConfig endpoint) {
updateById(endpoint);
return getById(endpoint.getId());
}
@Override
public AiEndpointConfig getEnabledById(String id) {
AiEndpointConfig endpoint = getById(id);
if (endpoint == null || endpoint.getIsEnabled() == null || endpoint.getIsEnabled() != 1) {
return null;
}
return endpoint;
}
private void applyDefaults(AiEndpointConfig endpoint) {
if (!StringUtils.hasText(endpoint.getResponseMode())) {
endpoint.setResponseMode("streaming");
}
if (endpoint.getSupportStream() == null) {
endpoint.setSupportStream(1);
}
if (endpoint.getIsEnabled() == null) {
endpoint.setIsEnabled(1);
}
if (endpoint.getTimeoutMs() == null) {
endpoint.setTimeoutMs(60000);
}
}
}
@@ -0,0 +1,72 @@
package com.emotion.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.emotion.entity.AiProvider;
import com.emotion.mapper.AiProviderMapper;
import com.emotion.service.AiProviderService;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.util.List;
import java.util.stream.Collectors;
@Service
public class AiProviderServiceImpl extends ServiceImpl<AiProviderMapper, AiProvider> implements AiProviderService {
@Override
public List<AiProvider> listVisible() {
return list(new LambdaQueryWrapper<AiProvider>()
.eq(AiProvider::getIsDeleted, 0)
.orderByDesc(AiProvider::getCreateTime))
.stream()
.map(this::maskSecret)
.collect(Collectors.toList());
}
@Override
public AiProvider saveProvider(AiProvider provider) {
if (provider.getIsEnabled() == null) {
provider.setIsEnabled(1);
}
if (!StringUtils.hasText(provider.getAuthType())) {
provider.setAuthType("bearer");
}
if (provider.getTimeoutMs() == null) {
provider.setTimeoutMs(60000);
}
save(provider);
return maskSecret(provider);
}
@Override
public AiProvider updateProvider(AiProvider provider) {
if (!StringUtils.hasText(provider.getApiKey()) || "******".equals(provider.getApiKey())) {
provider.setApiKey(null);
}
updateById(provider);
return maskSecret(getById(provider.getId()));
}
@Override
public AiProvider getEnabledById(String id) {
AiProvider provider = getById(id);
if (provider == null || provider.getIsEnabled() == null || provider.getIsEnabled() != 1) {
return null;
}
return provider;
}
private AiProvider maskSecret(AiProvider provider) {
if (provider == null) {
return null;
}
AiProvider copy = new AiProvider();
BeanUtils.copyProperties(provider, copy);
if (StringUtils.hasText(copy.getApiKey())) {
copy.setApiKey("******");
}
return copy;
}
}
@@ -0,0 +1,571 @@
package com.emotion.service.impl;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.emotion.dto.request.ai.AiRuntimeRequest;
import com.emotion.dto.response.ai.AiTestTemplateResponse;
import com.emotion.dto.response.ai.AiRuntimeTestResponse;
import com.emotion.dto.response.ai.AiStreamEvent;
import com.emotion.entity.AiCallLog;
import com.emotion.entity.AiEndpointConfig;
import com.emotion.entity.AiProvider;
import com.emotion.entity.AiSceneBinding;
import com.emotion.service.AiCallLogService;
import com.emotion.service.AiEndpointConfigService;
import com.emotion.service.AiProviderService;
import com.emotion.service.AiRuntimeService;
import com.emotion.service.AiSceneBindingService;
import com.emotion.service.ScriptContextService;
import com.emotion.service.ai.AiProviderAdapter;
import com.emotion.util.UserContextHolder;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Consumer;
@Slf4j
@Service
public class AiRuntimeServiceImpl implements AiRuntimeService {
private final AiSceneBindingService sceneBindingService;
private final AiEndpointConfigService endpointConfigService;
private final AiProviderService providerService;
private final AiCallLogService callLogService;
private final ScriptContextService scriptContextService;
private final List<AiProviderAdapter> adapters;
public AiRuntimeServiceImpl(AiSceneBindingService sceneBindingService,
AiEndpointConfigService endpointConfigService,
AiProviderService providerService,
AiCallLogService callLogService,
ScriptContextService scriptContextService,
List<AiProviderAdapter> adapters) {
this.sceneBindingService = sceneBindingService;
this.endpointConfigService = endpointConfigService;
this.providerService = providerService;
this.callLogService = callLogService;
this.scriptContextService = scriptContextService;
this.adapters = adapters;
}
@Override
public void invokeStream(AiRuntimeRequest request, Consumer<AiStreamEvent> consumer) {
String requestId = StringUtils.hasText(request.getRequestId()) ? request.getRequestId() : UUID.randomUUID().toString();
request.setRequestId(requestId);
enrichInputs(request);
long startedAt = System.currentTimeMillis();
AtomicLong firstTokenAt = new AtomicLong(0);
AtomicInteger chunks = new AtomicInteger(0);
StringBuilder output = new StringBuilder();
AiCallLog callLog = new AiCallLog();
callLog.setRequestId(requestId);
callLog.setSceneCode(request.getSceneCode());
callLog.setUserId(resolveUserId(request));
callLog.setUserName(request.getUserName());
callLog.setInputText(JSON.toJSONString(request.getInputs()));
callLog.setStatus("running");
try {
RuntimeTarget target = resolveTarget(request);
callLog.setProviderCode(target.provider.getProviderCode());
callLog.setEndpointCode(target.endpoint.getEndpointCode());
callLogService.save(callLog);
consumer.accept(AiStreamEvent.start(request.getSceneCode()));
target.adapter.stream(target.provider, target.endpoint, request, event -> {
if ("delta".equals(event.getType())) {
chunks.incrementAndGet();
if (firstTokenAt.compareAndSet(0, System.currentTimeMillis())) {
log.debug("AI first token emitted, scene={}, requestId={}", request.getSceneCode(), requestId);
}
if (event.getContent() != null) {
output.append(event.getContent());
}
}
consumer.accept(event);
});
callLog.setStatus("success");
callLog.setOutputText(output.toString());
callLog.setStreamChunks(chunks.get());
callLog.setFirstTokenMs(firstTokenAt.get() == 0 ? null : firstTokenAt.get() - startedAt);
callLog.setDurationMs(System.currentTimeMillis() - startedAt);
callLogService.updateById(callLog);
emitDone(consumer, Map.of(
"requestId", requestId,
"streamChunks", chunks.get(),
"durationMs", callLog.getDurationMs()
));
} catch (Exception e) {
String code = normalizeErrorCode(e);
callLog.setOutputText(output.toString());
callLog.setStreamChunks(chunks.get());
callLog.setFirstTokenMs(firstTokenAt.get() == 0 ? null : firstTokenAt.get() - startedAt);
callLog.setDurationMs(System.currentTimeMillis() - startedAt);
if (StringUtils.hasText(output.toString())) {
callLog.setStatus("success");
callLog.setErrorCode(null);
callLog.setErrorMessage(null);
saveOrUpdateLog(callLog);
emitDone(consumer, recoveredDoneMetadata(requestId, chunks.get(), callLog.getDurationMs(), code, e.getMessage()));
return;
}
callLog.setStatus("failed");
callLog.setErrorCode(code);
callLog.setErrorMessage(e.getMessage());
saveOrUpdateLog(callLog);
consumer.accept(AiStreamEvent.error(code, e.getMessage()));
}
}
@Override
public AiRuntimeTestResponse test(AiRuntimeRequest request) {
long startedAt = System.currentTimeMillis();
StringBuilder output = new StringBuilder();
AtomicInteger chunks = new AtomicInteger(0);
final String[] errorCode = new String[1];
final String[] errorMessage = new String[1];
invokeStream(request, event -> {
if ("delta".equals(event.getType()) && event.getContent() != null) {
chunks.incrementAndGet();
output.append(event.getContent());
} else if ("error".equals(event.getType())) {
errorCode[0] = event.getCode();
errorMessage[0] = event.getMessage();
}
});
return AiRuntimeTestResponse.builder()
.sceneCode(request.getSceneCode())
.status(errorCode[0] == null || StringUtils.hasText(output.toString()) ? "success" : "failed")
.output(output.toString())
.streamChunks(chunks.get())
.durationMs(System.currentTimeMillis() - startedAt)
.errorCode(StringUtils.hasText(output.toString()) ? null : errorCode[0])
.errorMessage(StringUtils.hasText(output.toString()) ? null : errorMessage[0])
.build();
}
@Override
public AiRuntimeTestResponse testEndpoint(String endpointId, Map<String, Object> inputs) {
long startedAt = System.currentTimeMillis();
StringBuilder output = new StringBuilder();
AtomicInteger chunks = new AtomicInteger(0);
final String[] errorCode = new String[1];
final String[] errorMessage = new String[1];
invokeEndpointStream(endpointId, inputs, event -> {
if ("delta".equals(event.getType()) && event.getContent() != null) {
chunks.incrementAndGet();
output.append(event.getContent());
} else if ("error".equals(event.getType())) {
errorCode[0] = event.getCode();
errorMessage[0] = event.getMessage();
}
});
return AiRuntimeTestResponse.builder()
.sceneCode("")
.status(errorCode[0] == null || StringUtils.hasText(output.toString()) ? "success" : "failed")
.output(output.toString())
.streamChunks(chunks.get())
.durationMs(System.currentTimeMillis() - startedAt)
.errorCode(StringUtils.hasText(output.toString()) ? null : errorCode[0])
.errorMessage(StringUtils.hasText(output.toString()) ? null : errorMessage[0])
.build();
}
@Override
public void invokeEndpointStream(String endpointId, Map<String, Object> inputs, Consumer<AiStreamEvent> consumer) {
long startedAt = System.currentTimeMillis();
AtomicLong firstTokenAt = new AtomicLong(0);
AtomicInteger chunks = new AtomicInteger(0);
Object inputRequestId = inputs == null ? null : inputs.get("requestId");
String requestId = inputRequestId != null && StringUtils.hasText(String.valueOf(inputRequestId))
? String.valueOf(inputRequestId)
: UUID.randomUUID().toString();
StringBuilder output = new StringBuilder();
AiEndpointConfig endpoint = endpointConfigService.getEnabledById(endpointId);
if (endpoint == null) {
throw new IllegalStateException("AI_ENDPOINT_DISABLED");
}
AiProvider provider = providerService.getEnabledById(endpoint.getProviderId());
if (provider == null) {
throw new IllegalStateException("AI_PROVIDER_DISABLED");
}
AiProviderAdapter adapter = adapters.stream()
.filter(item -> item.supports(provider.getProviderType()))
.findFirst()
.orElseThrow(() -> new IllegalStateException("AI_PROVIDER_ADAPTER_NOT_FOUND"));
AiRuntimeRequest request = new AiRuntimeRequest();
request.setInputs(inputs == null ? new com.alibaba.fastjson2.JSONObject() : new com.alibaba.fastjson2.JSONObject(inputs));
request.setEndpointId(endpointId);
request.setUserId(resolveUserId(request));
request.setUserName(UserContextHolder.getCurrentUsername());
request.setUserType(UserContextHolder.getCurrentUserType());
request.setRequestId(requestId);
enrichInputs(request);
AiCallLog callLog = new AiCallLog();
callLog.setRequestId(requestId);
callLog.setEndpointCode(endpoint.getEndpointCode());
callLog.setProviderCode(provider.getProviderCode());
callLog.setUserId(request.getUserId());
callLog.setUserName(request.getUserName());
callLog.setInputText(JSON.toJSONString(request.getInputs()));
callLog.setStatus("running");
callLogService.save(callLog);
consumer.accept(AiStreamEvent.start(endpoint.getEndpointCode()));
try {
adapter.stream(provider, endpoint, request, event -> {
if ("delta".equals(event.getType())) {
chunks.incrementAndGet();
if (firstTokenAt.compareAndSet(0, System.currentTimeMillis())) {
log.debug("AI first token emitted, endpoint={}, requestId={}", endpoint.getEndpointCode(), requestId);
}
if (event.getContent() != null) {
output.append(event.getContent());
}
}
consumer.accept(event);
});
callLog.setStatus("success");
callLog.setOutputText(output.toString());
callLog.setStreamChunks(chunks.get());
callLog.setFirstTokenMs(firstTokenAt.get() == 0 ? null : firstTokenAt.get() - startedAt);
callLog.setDurationMs(System.currentTimeMillis() - startedAt);
callLogService.updateById(callLog);
emitDone(consumer, Map.of(
"requestId", requestId,
"streamChunks", chunks.get(),
"durationMs", callLog.getDurationMs()
));
} catch (Exception e) {
String code = normalizeErrorCode(e);
callLog.setOutputText(output.toString());
callLog.setStreamChunks(chunks.get());
callLog.setFirstTokenMs(firstTokenAt.get() == 0 ? null : firstTokenAt.get() - startedAt);
callLog.setDurationMs(System.currentTimeMillis() - startedAt);
if (StringUtils.hasText(output.toString())) {
callLog.setStatus("success");
callLog.setErrorCode(null);
callLog.setErrorMessage(null);
saveOrUpdateLog(callLog);
emitDone(consumer, recoveredDoneMetadata(requestId, chunks.get(), callLog.getDurationMs(), code, e.getMessage()));
return;
}
callLog.setStatus("failed");
callLog.setErrorCode(code);
callLog.setErrorMessage(e.getMessage());
saveOrUpdateLog(callLog);
consumer.accept(AiStreamEvent.error(code, e.getMessage()));
}
}
@Override
public AiTestTemplateResponse buildEndpointTestTemplate(String endpointId) {
AiEndpointConfig endpoint = endpointConfigService.getById(endpointId);
if (endpoint == null || Integer.valueOf(1).equals(endpoint.getIsDeleted())) {
throw new IllegalArgumentException("AI_ENDPOINT_NOT_FOUND");
}
AiProvider provider = providerService.getById(endpoint.getProviderId());
if (provider == null || Integer.valueOf(1).equals(provider.getIsDeleted())) {
throw new IllegalArgumentException("AI_PROVIDER_NOT_FOUND");
}
return buildTemplate(null, endpoint, provider);
}
@Override
public AiTestTemplateResponse buildSceneTestTemplate(String sceneCode) {
AiSceneBinding scene = sceneBindingService.resolveScene(sceneCode);
if (scene == null) {
throw new IllegalArgumentException("AI_SCENE_NOT_BOUND");
}
AiEndpointConfig endpoint = endpointConfigService.getById(scene.getEndpointId());
if (endpoint == null || Integer.valueOf(1).equals(endpoint.getIsDeleted())) {
throw new IllegalArgumentException("AI_ENDPOINT_NOT_FOUND");
}
AiProvider provider = providerService.getById(endpoint.getProviderId());
if (provider == null || Integer.valueOf(1).equals(provider.getIsDeleted())) {
throw new IllegalArgumentException("AI_PROVIDER_NOT_FOUND");
}
return buildTemplate(sceneCode, endpoint, provider);
}
private AiTestTemplateResponse buildTemplate(String sceneCode, AiEndpointConfig endpoint, AiProvider provider) {
LinkedHashMap<String, Object> inputs = new LinkedHashMap<>();
applyDefaultInputs(inputs, endpoint.getDefaultInputs());
applyProviderSampleInputs(inputs, sceneCode, endpoint, provider);
List<AiTestTemplateResponse.ParamField> fields = new ArrayList<>();
inputs.forEach((key, value) -> fields.add(AiTestTemplateResponse.ParamField.builder()
.name(key)
.label(paramLabel(key))
.type(paramType(value))
.value(value)
.required(requiredParam(key, provider.getProviderType()))
.placeholder(paramPlaceholder(key))
.build()));
return AiTestTemplateResponse.builder()
.sceneCode(sceneCode)
.endpointId(endpoint.getId())
.endpointCode(endpoint.getEndpointCode())
.endpointName(endpoint.getEndpointName())
.providerType(provider.getProviderType())
.inputs(inputs)
.paramFields(fields)
.build();
}
private void applyDefaultInputs(LinkedHashMap<String, Object> inputs, String defaultInputs) {
if (!StringUtils.hasText(defaultInputs)) {
return;
}
try {
JSONObject parsed = JSON.parseObject(defaultInputs);
parsed.forEach((key, value) -> {
if (value instanceof JSONObject && ((JSONObject) value).containsKey("_meta")) {
inputs.put(key, ((JSONObject) value).get("value"));
} else {
inputs.put(key, value);
}
});
} catch (Exception ignored) {
}
}
private void applyProviderSampleInputs(LinkedHashMap<String, Object> inputs,
String sceneCode,
AiEndpointConfig endpoint,
AiProvider provider) {
String providerType = provider.getProviderType();
String sample = samplePrompt(sceneCode, endpoint);
if ("dify".equalsIgnoreCase(providerType)) {
inputs.putIfAbsent("query", sample);
inputs.putIfAbsent("inputs", Map.of("input", sample, "user_id", "admin-test-user"));
inputs.putIfAbsent("response_mode", "streaming");
inputs.putIfAbsent("conversation_id", "");
inputs.putIfAbsent("user", "admin-test-user");
inputs.putIfAbsent("user_id", "admin-test-user");
return;
}
inputs.putIfAbsent("input", sample);
inputs.putIfAbsent("prompt", sample);
inputs.putIfAbsent("message", sample);
inputs.putIfAbsent("user_id", "admin-test-user");
if ("chat".equalsIgnoreCase(endpoint.getEndpointType())) {
inputs.putIfAbsent("conversationId", "admin-test-conversation");
}
}
private String samplePrompt(String sceneCode, AiEndpointConfig endpoint) {
String code = StringUtils.hasText(sceneCode) ? sceneCode : endpoint.getEndpointCode();
if (code == null) {
return "请用一句中文回复测试成功。";
}
if (code.contains("script") || code.contains("life.generate")) {
return "请生成一个关于普通人重新找回生活热情的短剧本,要求结构清晰、中文输出。";
}
if (code.contains("short_story") || code.contains("story")) {
return "请生成一篇 300 字以内的治愈短篇小说,主题是雨后的新开始。";
}
if (code.contains("diary") || code.contains("healing") || code.contains("life")) {
return "今天工作压力很大,但傍晚散步时感觉心情慢慢平静下来,请给我一段温和的回应。";
}
if (code.contains("summary")) {
return "用户今天先表达焦虑,随后通过散步和朋友聊天逐渐恢复平静,请生成一段情绪总结。";
}
if (code.contains("chat")) {
return "你好,请用一句中文回复:AI 接口测试成功。";
}
return "请用一句中文回复测试成功。";
}
private String paramLabel(String key) {
Map<String, String> labels = Map.ofEntries(
Map.entry("input", "输入内容"),
Map.entry("prompt", "提示词"),
Map.entry("message", "消息内容"),
Map.entry("query", "用户问题"),
Map.entry("inputs", "Dify 变量"),
Map.entry("response_mode", "响应模式"),
Map.entry("conversation_id", "会话 ID"),
Map.entry("user", "用户标识"),
Map.entry("user_id", "用户标识"),
Map.entry("conversationId", "会话 ID")
);
return labels.getOrDefault(key, key);
}
private String paramType(Object value) {
if (value instanceof Number) {
return "number";
}
if (value instanceof Boolean) {
return "boolean";
}
if (value instanceof Map || value instanceof JSONObject) {
return "json";
}
String text = value == null ? "" : String.valueOf(value);
return text.length() > 60 ? "textarea" : "string";
}
private Boolean requiredParam(String key, String providerType) {
if ("dify".equalsIgnoreCase(providerType)) {
return "query".equals(key) || "response_mode".equals(key) || "user".equals(key);
}
return "input".equals(key) || "user_id".equals(key);
}
private String paramPlaceholder(String key) {
if ("inputs".equals(key)) {
return "{\"input\":\"测试内容\"}";
}
return "请输入" + paramLabel(key);
}
private RuntimeTarget resolveTarget(AiRuntimeRequest request) {
if (!StringUtils.hasText(request.getSceneCode())) {
throw new IllegalArgumentException("AI_SCENE_REQUIRED");
}
if (!StringUtils.hasText(request.getUserId())) {
request.setUserId(resolveUserId(request));
}
AiSceneBinding scene = sceneBindingService.resolveScene(request.getSceneCode());
if (scene == null) {
throw new IllegalStateException("AI_SCENE_NOT_BOUND");
}
AiEndpointConfig endpoint = endpointConfigService.getEnabledById(scene.getEndpointId());
if (endpoint == null) {
throw new IllegalStateException("AI_ENDPOINT_DISABLED");
}
if (Integer.valueOf(1).equals(scene.getRequiredStream()) && !Integer.valueOf(1).equals(endpoint.getSupportStream())) {
throw new IllegalStateException("AI_ENDPOINT_STREAM_REQUIRED");
}
AiProvider provider = providerService.getEnabledById(endpoint.getProviderId());
if (provider == null) {
throw new IllegalStateException("AI_PROVIDER_DISABLED");
}
AiProviderAdapter adapter = adapters.stream()
.filter(item -> item.supports(provider.getProviderType()))
.findFirst()
.orElseThrow(() -> new IllegalStateException("AI_PROVIDER_ADAPTER_NOT_FOUND"));
return new RuntimeTarget(scene, endpoint, provider, adapter);
}
private String resolveUserId(AiRuntimeRequest request) {
if (StringUtils.hasText(request.getUserId())) {
return request.getUserId();
}
String currentUserId = UserContextHolder.getCurrentUserId();
return StringUtils.hasText(currentUserId) ? currentUserId : "anonymous";
}
private void enrichInputs(AiRuntimeRequest request) {
if (request.getInputs() == null) {
request.setInputs(new com.alibaba.fastjson2.JSONObject());
}
String userId = resolveUserId(request);
request.setUserId(userId);
request.getInputs().put("userId", userId);
request.getInputs().put("currentUserId", userId);
request.getInputs().put("user_id", userId);
if (StringUtils.hasText(request.getUserName())) {
request.getInputs().put("userName", request.getUserName());
request.getInputs().put("username", request.getUserName());
}
if (StringUtils.hasText(request.getUserType())) {
request.getInputs().put("userType", request.getUserType());
}
if (StringUtils.hasText(request.getRequestId())) {
request.getInputs().put("requestId", request.getRequestId());
}
enrichSceneInputs(request, userId);
}
private void enrichSceneInputs(AiRuntimeRequest request, String userId) {
if (!StringUtils.hasText(request.getSceneCode())) {
return;
}
if ("script_generate".equals(request.getSceneCode()) || "short_story_generate".equals(request.getSceneCode())) {
Boolean useSocialInsights = request.getInputs().getBoolean("useSocialInsights");
String socialInsightContext = scriptContextService.buildSocialInsightContext(userId, useSocialInsights);
if (StringUtils.hasText(socialInsightContext)) {
request.getInputs().put("socialInsightContext", socialInsightContext);
}
}
}
private String normalizeErrorCode(Exception e) {
String message = e.getMessage();
if (message != null && message.startsWith("AI_")) {
return message;
}
if (message != null && message.contains("timed out")) {
return "AI_STREAM_TIMEOUT";
}
return "AI_STREAM_INTERRUPTED";
}
private Map<String, Object> recoveredDoneMetadata(String requestId, int streamChunks, Long durationMs, String code, String message) {
Map<String, Object> metadata = new LinkedHashMap<>();
metadata.put("requestId", requestId);
metadata.put("streamChunks", streamChunks);
metadata.put("durationMs", durationMs);
metadata.put("recovered", true);
metadata.put("warningCode", code);
if (StringUtils.hasText(message)) {
metadata.put("warningMessage", message);
}
return metadata;
}
private void emitDone(Consumer<AiStreamEvent> consumer, Map<String, Object> metadata) {
try {
consumer.accept(AiStreamEvent.done(metadata));
} catch (Exception e) {
log.debug("AI stream done event skipped after output persisted: {}", e.getMessage());
}
}
private void saveOrUpdateLog(AiCallLog callLog) {
if (StringUtils.hasText(callLog.getId())) {
callLogService.updateById(callLog);
} else {
callLogService.save(callLog);
}
}
private static class RuntimeTarget {
private final AiSceneBinding scene;
private final AiEndpointConfig endpoint;
private final AiProvider provider;
private final AiProviderAdapter adapter;
private RuntimeTarget(AiSceneBinding scene, AiEndpointConfig endpoint, AiProvider provider, AiProviderAdapter adapter) {
this.scene = scene;
this.endpoint = endpoint;
this.provider = provider;
this.adapter = adapter;
}
}
}
@@ -0,0 +1,32 @@
package com.emotion.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.emotion.entity.AiSceneBinding;
import com.emotion.mapper.AiSceneBindingMapper;
import com.emotion.service.AiSceneBindingService;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class AiSceneBindingServiceImpl extends ServiceImpl<AiSceneBindingMapper, AiSceneBinding> implements AiSceneBindingService {
@Override
public List<AiSceneBinding> listVisible() {
return list(new LambdaQueryWrapper<AiSceneBinding>()
.eq(AiSceneBinding::getIsDeleted, 0)
.orderByDesc(AiSceneBinding::getPriority)
.orderByDesc(AiSceneBinding::getCreateTime));
}
@Override
public AiSceneBinding resolveScene(String sceneCode) {
return getOne(new LambdaQueryWrapper<AiSceneBinding>()
.eq(AiSceneBinding::getSceneCode, sceneCode)
.eq(AiSceneBinding::getIsEnabled, 1)
.eq(AiSceneBinding::getIsDeleted, 0)
.orderByDesc(AiSceneBinding::getPriority)
.last("limit 1"));
}
}
@@ -0,0 +1,292 @@
package com.emotion.service.impl;
import com.alibaba.fastjson2.JSON;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.emotion.dto.request.analytics.AnalyticsEventBatchRequest;
import com.emotion.dto.request.analytics.AnalyticsEventRequest;
import com.emotion.dto.request.analytics.AnalyticsQueryRequest;
import com.emotion.dto.response.analytics.AnalyticsBatchResponse;
import com.emotion.dto.response.analytics.AnalyticsFunnelItem;
import com.emotion.dto.response.analytics.AnalyticsOverviewResponse;
import com.emotion.dto.response.analytics.AnalyticsPreferenceItem;
import com.emotion.dto.response.analytics.AnalyticsTopEventItem;
import com.emotion.dto.response.analytics.AnalyticsTrendItem;
import com.emotion.dto.response.analytics.AnalyticsUserItem;
import com.emotion.entity.AnalyticsEvent;
import com.emotion.mapper.AnalyticsEventMapper;
import com.emotion.service.AnalyticsService;
import com.emotion.service.analytics.AnalyticsDictionary;
import com.emotion.util.UserContextHolder;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import java.nio.charset.StandardCharsets;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
@Service
public class AnalyticsServiceImpl extends ServiceImpl<AnalyticsEventMapper, AnalyticsEvent>
implements AnalyticsService {
private static final Pattern SAFE_NAME = Pattern.compile("^[a-z][a-z0-9_]{1,99}$");
private static final int JSON_LIMIT_BYTES = 4096;
private static final int DEFAULT_LIMIT = 20;
private static final int MAX_LIMIT = 100;
private static final List<String> BLOCKED_PROPERTY_KEYS = List.of(
"token", "access_token", "refresh_token", "password", "phone", "smsCode", "content", "fullContent"
);
private static final List<String> FUNNEL_EVENTS = List.of(
"app_launch", "page_view", "script_inspiration_click", "script_generate_start",
"script_generate_success", "script_detail_view", "path_select", "script_tts_play"
);
private static final List<String> PREFERENCE_DIMENSIONS = List.of("style", "length", "source", "tab", "platform");
public static boolean isSafeEventName(String eventName) {
return StringUtils.hasText(eventName) && SAFE_NAME.matcher(eventName).matches();
}
@Override
public AnalyticsBatchResponse ingestBatch(AnalyticsEventBatchRequest request) {
if (request == null || CollectionUtils.isEmpty(request.getEvents())) {
return AnalyticsBatchResponse.builder().accepted(0).rejected(0).build();
}
List<AnalyticsEvent> events = new ArrayList<>();
int rejected = 0;
LocalDateTime serverTime = LocalDateTime.now();
String currentUserId = UserContextHolder.getCurrentUserId();
Map<String, Object> deviceInfo = sanitizeJsonMap(request.getDeviceInfo());
for (AnalyticsEventRequest eventRequest : request.getEvents()) {
if (!isValidEvent(eventRequest)) {
rejected++;
continue;
}
AnalyticsEvent event = new AnalyticsEvent();
event.setUserId(currentUserId);
event.setAnonymousId(trimToNull(request.getAnonymousId()));
event.setSessionId(request.getSessionId());
event.setEventName(eventRequest.getEventName());
event.setEventType(eventRequest.getEventType());
event.setPagePath(trimToNull(eventRequest.getPagePath()));
event.setReferrerPath(trimToNull(eventRequest.getReferrerPath()));
event.setProperties(sanitizeJsonMap(eventRequest.getProperties()));
event.setDeviceInfo(deviceInfo);
event.setDurationMs(sanitizeDuration(eventRequest.getDurationMs()));
event.setOccurredAt(sanitizeOccurredAt(eventRequest.getOccurredAt(), serverTime));
event.setServerTime(serverTime);
events.add(event);
}
if (!events.isEmpty()) {
saveBatch(events);
}
return AnalyticsBatchResponse.builder()
.accepted(events.size())
.rejected(rejected)
.build();
}
@Override
public AnalyticsOverviewResponse getOverview(AnalyticsQueryRequest request) {
DateRange range = resolveRange(request);
return AnalyticsOverviewResponse.builder()
.pv(defaultLong(baseMapper.countEvent("page_view", range.start, range.end)))
.uv(defaultLong(baseMapper.countUniqueVisitors(range.start, range.end)))
.eventCount(defaultLong(baseMapper.countAll(range.start, range.end)))
.activeUsers(defaultLong(baseMapper.countActiveUsers(range.start, range.end)))
.ttsRequests(defaultLong(baseMapper.countEvent("script_tts_request", range.start, range.end)))
.ttsPlays(defaultLong(baseMapper.countEvent("script_tts_play", range.start, range.end)))
.avgStayMs(defaultDouble(baseMapper.avgStayMs(range.start, range.end)))
.build();
}
@Override
public List<AnalyticsTrendItem> getTrend(AnalyticsQueryRequest request) {
DateRange range = resolveRange(request);
List<AnalyticsTrendItem> result = baseMapper.selectTrend(range.start, range.end, resolveDateFormat(request));
result.forEach(this::fillTrendLabels);
return result;
}
@Override
public List<AnalyticsFunnelItem> getFunnel(AnalyticsQueryRequest request) {
DateRange range = resolveRange(request);
Map<String, Long> usersByEvent = new HashMap<>();
for (AnalyticsTopEventItem item : baseMapper.selectFunnelUsers(range.start, range.end)) {
usersByEvent.put(item.getEventName(), item.getUsers());
}
List<AnalyticsFunnelItem> result = new ArrayList<>();
long firstUsers = 0L;
for (String eventName : FUNNEL_EVENTS) {
long users = usersByEvent.getOrDefault(eventName, 0L);
if (firstUsers == 0L && users > 0L) {
firstUsers = users;
}
double conversionRate = firstUsers == 0L ? 0D : (double) users / firstUsers;
result.add(AnalyticsFunnelItem.builder()
.eventName(eventName)
.label(AnalyticsDictionary.eventLabel(eventName))
.users(users)
.conversionRate(conversionRate)
.build());
}
return result;
}
@Override
public List<AnalyticsPreferenceItem> getPreferences(AnalyticsQueryRequest request) {
DateRange range = resolveRange(request);
int perDimensionLimit = Math.max(3, Math.min(10, resolveLimit(request)));
List<AnalyticsPreferenceItem> result = new ArrayList<>();
for (String dimension : PREFERENCE_DIMENSIONS) {
result.addAll(baseMapper.selectPreference(dimension, range.start, range.end, perDimensionLimit));
}
result.forEach(this::fillPreferenceLabels);
return result;
}
@Override
public List<AnalyticsTopEventItem> getTopEvents(AnalyticsQueryRequest request) {
DateRange range = resolveRange(request);
List<AnalyticsTopEventItem> result = baseMapper.selectTopEvents(range.start, range.end, resolveLimit(request));
result.forEach(this::fillTopEventLabels);
return result;
}
@Override
public List<AnalyticsUserItem> getUsers(AnalyticsQueryRequest request) {
DateRange range = resolveRange(request);
return baseMapper.selectUsers(range.start, range.end, resolveLimit(request));
}
private boolean isValidEvent(AnalyticsEventRequest request) {
return request != null
&& isSafeEventName(request.getEventName())
&& isSafeEventName(request.getEventType())
&& fitsJsonLimit(request.getProperties());
}
private Map<String, Object> sanitizeJsonMap(Map<String, Object> source) {
if (source == null || source.isEmpty()) {
return null;
}
Map<String, Object> sanitized = new LinkedHashMap<>();
for (Map.Entry<String, Object> entry : source.entrySet()) {
String key = entry.getKey();
if (!StringUtils.hasText(key) || BLOCKED_PROPERTY_KEYS.contains(key)) {
continue;
}
sanitized.put(key, entry.getValue());
}
if (sanitized.isEmpty() || !fitsJsonLimit(sanitized)) {
return null;
}
return sanitized;
}
private boolean fitsJsonLimit(Map<String, Object> value) {
if (value == null || value.isEmpty()) {
return true;
}
return JSON.toJSONString(value).getBytes(StandardCharsets.UTF_8).length <= JSON_LIMIT_BYTES;
}
private Long sanitizeDuration(Long durationMs) {
if (durationMs == null || durationMs < 0) {
return null;
}
return Math.min(durationMs, 24L * 60L * 60L * 1000L);
}
private LocalDateTime sanitizeOccurredAt(LocalDateTime occurredAt, LocalDateTime fallback) {
if (occurredAt == null) {
return fallback;
}
LocalDateTime earliest = fallback.minusDays(30);
LocalDateTime latest = fallback.plusMinutes(10);
if (occurredAt.isBefore(earliest) || occurredAt.isAfter(latest)) {
return fallback;
}
return occurredAt;
}
private String trimToNull(String value) {
if (!StringUtils.hasText(value)) {
return null;
}
return value.trim();
}
private DateRange resolveRange(AnalyticsQueryRequest request) {
LocalDate today = LocalDate.now();
LocalDate endDate = request != null && request.getEndDate() != null ? request.getEndDate() : today;
LocalDate startDate = request != null && request.getStartDate() != null ? request.getStartDate() : endDate.minusDays(6);
if (startDate.isAfter(endDate)) {
startDate = endDate;
}
if (startDate.isBefore(endDate.minusDays(89))) {
startDate = endDate.minusDays(89);
}
return new DateRange(startDate.atStartOfDay(), endDate.plusDays(1).atStartOfDay());
}
private String resolveDateFormat(AnalyticsQueryRequest request) {
if (request != null && "hour".equalsIgnoreCase(request.getGranularity())) {
return "%Y-%m-%d %H:00";
}
if (request != null && "month".equalsIgnoreCase(request.getGranularity())) {
return "%Y-%m";
}
return "%Y-%m-%d";
}
private int resolveLimit(AnalyticsQueryRequest request) {
int limit = request != null && request.getLimit() != null ? request.getLimit() : DEFAULT_LIMIT;
return Math.max(1, Math.min(MAX_LIMIT, limit));
}
private void fillTopEventLabels(AnalyticsTopEventItem item) {
item.setEventLabel(AnalyticsDictionary.eventLabel(item.getEventName()));
item.setEventTypeLabel(AnalyticsDictionary.eventTypeLabel(item.getEventType()));
item.setPageLabel(AnalyticsDictionary.pageLabel(item.getPagePath()));
item.setApiLabel(AnalyticsDictionary.apiLabel(item.getApiPath()));
}
private void fillTrendLabels(AnalyticsTrendItem item) {
item.setEventLabel(AnalyticsDictionary.eventLabel(item.getEventName()));
}
private void fillPreferenceLabels(AnalyticsPreferenceItem item) {
item.setDimensionLabel(AnalyticsDictionary.dimensionLabel(item.getDimension()));
item.setValueLabel(AnalyticsDictionary.valueLabel(item.getDimension(), item.getValue()));
}
private long defaultLong(Long value) {
return value == null ? 0L : value;
}
private double defaultDouble(Double value) {
return value == null ? 0D : value;
}
private static class DateRange {
private final LocalDateTime start;
private final LocalDateTime end;
private DateRange(LocalDateTime start, LocalDateTime end) {
this.start = start;
this.end = end;
}
}
}
@@ -0,0 +1,339 @@
package com.emotion.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.emotion.dto.request.ApiEndpointListRequest;
import com.emotion.dto.response.ApiEndpointDetailResponse;
import com.emotion.dto.response.ApiEndpointItemResponse;
import com.emotion.dto.response.ApiParamItemResponse;
import com.emotion.entity.ApiEndpoint;
import com.emotion.entity.ApiParam;
import com.emotion.mapper.ApiEndpointMapper;
import com.emotion.mapper.ApiParamMapper;
import com.emotion.service.ApiEndpointService;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import org.springframework.web.client.RestTemplate;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
/**
* 接口端点服务实现
*
* @author Peanut
* @date 2026-05-23
*/
@Service
public class ApiEndpointServiceImpl implements ApiEndpointService {
private static final Logger log = LoggerFactory.getLogger(ApiEndpointServiceImpl.class);
private static final int MAX_REF_DEPTH = 10;
@Autowired
private ApiEndpointMapper endpointMapper;
@Autowired
private ApiParamMapper paramMapper;
@Value("${server.port:19089}")
private int serverPort;
@Autowired
private RestTemplate restTemplate;
@Autowired
private ObjectMapper objectMapper;
@Override
public IPage<ApiEndpointItemResponse> getPage(ApiEndpointListRequest request) {
Page<ApiEndpoint> page = new Page<>(request.getCurrent(), request.getSize());
LambdaQueryWrapper<ApiEndpoint> wrapper = new LambdaQueryWrapper<>();
if (StringUtils.hasText(request.getKeyword())) {
wrapper.and(w -> w.like(ApiEndpoint::getPath, request.getKeyword())
.or().like(ApiEndpoint::getSummary, request.getKeyword())
.or().like(ApiEndpoint::getOperationId, request.getKeyword()));
}
if (StringUtils.hasText(request.getMethod())) {
wrapper.eq(ApiEndpoint::getMethod, request.getMethod());
}
if (StringUtils.hasText(request.getTags())) {
wrapper.like(ApiEndpoint::getTags, request.getTags());
}
if (request.getDeprecated() != null) {
wrapper.eq(ApiEndpoint::getDeprecated, request.getDeprecated());
}
wrapper.orderByDesc(ApiEndpoint::getCreateTime);
IPage<ApiEndpoint> result = endpointMapper.selectPage(page, wrapper);
return result.convert(this::toItemResponse);
}
@Override
public ApiEndpointDetailResponse getDetail(String operationId) {
LambdaQueryWrapper<ApiEndpoint> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(ApiEndpoint::getOperationId, operationId);
ApiEndpoint endpoint = endpointMapper.selectOne(wrapper);
if (endpoint == null) return null;
ApiEndpointDetailResponse response = toDetailResponse(endpoint);
LambdaQueryWrapper<ApiParam> paramWrapper = new LambdaQueryWrapper<>();
paramWrapper.eq(ApiParam::getEndpointId, endpoint.getId());
paramWrapper.orderByAsc(ApiParam::getName);
List<ApiParam> params = paramMapper.selectList(paramWrapper);
response.setParams(params.stream().map(this::toParamItemResponse).toList());
return response;
}
@Override
@Transactional
public void syncFromOpenApi() {
String url = "http://127.0.0.1:" + serverPort + "/api/v3/api-docs";
log.info("开始同步接口数据,URL: {}", url);
String json;
try {
json = restTemplate.getForObject(url, String.class);
} catch (Exception e) {
log.warn("获取 OpenAPI spec 失败: {}", e.getMessage());
return;
}
if (json == null || json.isBlank()) {
log.warn("OpenAPI spec 为空");
return;
}
try {
JsonNode root = objectMapper.readTree(json);
JsonNode components = root.path("components");
JsonNode schemas = components.path("schemas");
paramMapper.delete(null);
endpointMapper.delete(null);
log.info("已清空旧接口数据");
JsonNode paths = root.path("paths");
Iterator<Map.Entry<String, JsonNode>> pathEntries = paths.fields();
int count = 0;
java.util.Set<String> seenOperationIds = new java.util.HashSet<>();
while (pathEntries.hasNext()) {
Map.Entry<String, JsonNode> pathEntry = pathEntries.next();
String path = pathEntry.getKey();
JsonNode methods = pathEntry.getValue();
for (Iterator<String> it = methods.fieldNames(); it.hasNext(); ) {
String methodKey = it.next();
String method = methodKey.toUpperCase();
// Skip non-HTTP-method keys (e.g., summary, description at path level)
if (!isHttpMethod(method)) continue;
JsonNode endpointNode = methods.get(methodKey);
String operationId = endpointNode.path("operationId").asText();
if (operationId.isEmpty()) continue;
if (seenOperationIds.contains(operationId)) {
log.warn("跳过重复 operationId: {} ({} {})", operationId, path, method);
continue;
}
seenOperationIds.add(operationId);
ApiEndpoint apiEndpoint = ApiEndpoint.builder()
.path(path)
.method(method)
.operationId(operationId)
.summary(endpointNode.path("summary").asText(null))
.description(endpointNode.path("description").asText(null))
.deprecated(endpointNode.path("deprecated").asBoolean(false) ? 1 : 0)
.build();
JsonNode tagsNode = endpointNode.path("tags");
if (tagsNode.isArray() && tagsNode.size() > 0) {
List<String> tagList = new ArrayList<>();
for (JsonNode t : tagsNode) tagList.add(t.asText());
apiEndpoint.setTags(String.join(",", tagList));
}
// Parse parameters
List<ApiParam> paramList = new ArrayList<>();
JsonNode parameters = endpointNode.path("parameters");
if (parameters.isArray()) {
for (JsonNode param : parameters) {
ApiParam apiParam = parseParam(param, schemas, 0);
if (apiParam != null) {
paramList.add(apiParam);
}
}
}
// Parse requestBody schema
JsonNode requestBody = endpointNode.path("requestBody");
if (!requestBody.isMissingNode()) {
JsonNode content = requestBody.path("content");
if (!content.isMissingNode()) {
apiEndpoint.setRequestSchema(resolveSchema(content, schemas, 0));
}
}
// Parse responses schema
JsonNode responses = endpointNode.path("responses");
if (!responses.isMissingNode()) {
apiEndpoint.setResponseSchema(responses.toString());
}
endpointMapper.insert(apiEndpoint);
for (ApiParam p : paramList) {
p.setEndpointId(apiEndpoint.getId());
paramMapper.insert(p);
}
count++;
}
}
log.info("同步完成,共同步 {} 个接口", count);
} catch (Exception e) {
log.error("同步接口数据失败", e);
throw new RuntimeException("同步接口数据失败", e);
}
}
private boolean isHttpMethod(String method) {
return method.equals("GET") || method.equals("POST") || method.equals("PUT")
|| method.equals("DELETE") || method.equals("PATCH")
|| method.equals("HEAD") || method.equals("OPTIONS");
}
private ApiParam parseParam(JsonNode paramNode, JsonNode schemas, int depth) {
if (depth > MAX_REF_DEPTH) return null;
String name = paramNode.path("name").asText(null);
if (name == null) return null;
JsonNode schemaNode = paramNode.path("schema");
String typeDef = schemaNode.path("type").asText(null);
// Parse enum values if present
String enumValues = null;
JsonNode enumArray = schemaNode.path("enum");
if (enumArray.isArray() && enumArray.size() > 0) {
List<String> values = new ArrayList<>();
for (JsonNode v : enumArray) values.add(v.asText());
enumValues = String.join(",", values);
}
String defaultValue = schemaNode.path("default").asText(null);
return ApiParam.builder()
.paramType(paramNode.path("in").asText(null))
.name(name)
.required(paramNode.path("required").asBoolean(false) ? 1 : 0)
.paramTypeDef(typeDef)
.description(paramNode.path("description").asText(null))
.defaultValue(defaultValue)
.enumValues(enumValues)
.example(paramNode.path("example").asText(null))
.build();
}
private String resolveSchema(JsonNode content, JsonNode schemas, int depth) {
if (depth > MAX_REF_DEPTH) return "{}";
JsonNode appJson = content.path("application/json");
if (appJson.isMissingNode()) return "{}";
JsonNode schema = appJson.path("schema");
return expandRef(schema, schemas, depth);
}
private String expandRef(JsonNode node, JsonNode schemas, int depth) {
if (depth > MAX_REF_DEPTH) return "{\"$ref\": \"max depth exceeded\"}";
if (node.has("$ref")) {
String ref = node.path("$ref").asText();
if (ref.startsWith("#/components/schemas/")) {
String schemaName = ref.substring("#/components/schemas/".length());
JsonNode schemaNode = schemas.path(schemaName);
if (!schemaNode.isMissingNode()) {
return expandRef(schemaNode, schemas, depth + 1);
}
}
return "{\"$ref\": \"" + ref + "\"}";
}
if (node.has("properties")) {
JsonNode props = node.path("properties");
StringBuilder sb = new StringBuilder("{");
boolean first = true;
for (Iterator<Map.Entry<String, JsonNode>> it = props.fields(); it.hasNext(); ) {
Map.Entry<String, JsonNode> entry = it.next();
if (!first) sb.append(",");
first = false;
sb.append("\"").append(entry.getKey()).append("\":")
.append(expandRef(entry.getValue(), schemas, depth + 1));
}
sb.append("}");
return sb.toString();
}
return node.toString();
}
private ApiEndpointItemResponse toItemResponse(ApiEndpoint e) {
ApiEndpointItemResponse r = new ApiEndpointItemResponse();
r.setId(e.getId());
r.setPath(e.getPath());
r.setMethod(e.getMethod());
r.setOperationId(e.getOperationId());
r.setSummary(e.getSummary());
r.setTags(e.getTags());
r.setDeprecated(e.getDeprecated());
r.setCreateTime(e.getCreateTime() != null ? e.getCreateTime().toString() : null);
return r;
}
private ApiEndpointDetailResponse toDetailResponse(ApiEndpoint e) {
ApiEndpointDetailResponse r = new ApiEndpointDetailResponse();
r.setId(e.getId());
r.setPath(e.getPath());
r.setMethod(e.getMethod());
r.setOperationId(e.getOperationId());
r.setSummary(e.getSummary());
r.setDescription(e.getDescription());
r.setTags(e.getTags());
r.setDeprecated(e.getDeprecated());
r.setRequestSchema(e.getRequestSchema());
r.setResponseSchema(e.getResponseSchema());
r.setCreateTime(e.getCreateTime() != null ? e.getCreateTime().toString() : null);
return r;
}
private ApiParamItemResponse toParamItemResponse(ApiParam p) {
ApiParamItemResponse r = new ApiParamItemResponse();
r.setParamType(p.getParamType());
r.setName(p.getName());
r.setRequired(p.getRequired());
r.setParamTypeDef(p.getParamTypeDef());
r.setDescription(p.getDescription());
r.setDefaultValue(p.getDefaultValue());
r.setEnumValues(p.getEnumValues());
r.setExample(p.getExample());
return r;
}
}
@@ -0,0 +1,117 @@
package com.emotion.service.impl;
import com.emotion.dto.response.asr.AsrTranscribeResponse;
import com.emotion.service.AsrService;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.multipart.MultipartFile;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
@Service
public class AsrServiceImpl implements AsrService {
private final RestTemplate restTemplate;
@Value("${emotion.asr.enabled:true}")
private boolean enabled;
@Value("${emotion.asr.engine-url:http://127.0.0.1:19120}")
private String engineUrl;
@Value("${emotion.asr.max-file-size:10485760}")
private long maxFileSize;
@Value("${emotion.asr.allowed-types:wav,mp3,m4a,mp4,aac,amr}")
private String allowedTypes;
public AsrServiceImpl(RestTemplate restTemplate) {
this.restTemplate = restTemplate;
}
@Override
public AsrTranscribeResponse transcribe(MultipartFile file) {
validate(file);
try {
ByteArrayResource resource = new ByteArrayResource(file.getBytes()) {
@Override
public String getFilename() {
return StringUtils.hasText(file.getOriginalFilename()) ? file.getOriginalFilename() : "voice.wav";
}
};
MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
body.add("file", resource);
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.MULTIPART_FORM_DATA);
HttpEntity<MultiValueMap<String, Object>> request = new HttpEntity<>(body, headers);
ResponseEntity<Map> response = restTemplate.postForEntity(engineUrl + "/transcribe", request, Map.class);
Map<?, ?> data = response.getBody();
boolean success = data != null && Boolean.TRUE.equals(data.get("success"));
if (!success) {
String message = data == null ? "ASR service returned empty response" : String.valueOf(data.get("errorMessage"));
throw new IllegalStateException(message);
}
return AsrTranscribeResponse.builder()
.text(stringValue(data.get("text")))
.language(stringValue(data.get("language")))
.durationMs(longValue(data.get("durationMs")))
.engine(stringValue(data.get("engine")))
.model(stringValue(data.get("model")))
.build();
} catch (IllegalStateException e) {
throw e;
} catch (Exception e) {
throw new IllegalStateException("语音识别服务暂时不可用: " + e.getMessage(), e);
}
}
private void validate(MultipartFile file) {
if (!enabled) {
throw new IllegalStateException("语音识别功能未启用");
}
if (file == null || file.isEmpty()) {
throw new IllegalArgumentException("请上传语音文件");
}
if (file.getSize() > maxFileSize) {
throw new IllegalArgumentException("语音文件过大,请控制在10MB以内");
}
String filename = file.getOriginalFilename();
String extension = "";
if (StringUtils.hasText(filename) && filename.contains(".")) {
extension = filename.substring(filename.lastIndexOf('.') + 1).toLowerCase(Locale.ROOT);
}
Set<String> allowed = Set.of(allowedTypes.toLowerCase(Locale.ROOT).split(","));
if (StringUtils.hasText(extension) && !allowed.contains(extension)) {
throw new IllegalArgumentException("不支持的语音格式: " + extension);
}
}
private String stringValue(Object value) {
if (value == null) return null;
String text = String.valueOf(value);
return "null".equals(text) ? null : text;
}
private Long longValue(Object value) {
if (value instanceof Number) {
return ((Number) value).longValue();
}
return null;
}
}
@@ -0,0 +1,692 @@
package com.emotion.service.impl;
import com.emotion.dto.request.LoginRequest;
import com.emotion.dto.request.RegisterRequest;
import com.emotion.dto.request.ResetPasswordRequest;
import com.emotion.dto.request.WechatLoginRequest;
import com.emotion.dto.response.AuthResponse;
import com.emotion.dto.response.CaptchaResponse;
import com.emotion.dto.response.ResetPasswordResponse;
import com.emotion.dto.response.SmsCodeResponse;
import com.emotion.dto.response.UserInfoResponse;
import com.emotion.dto.wechat.WechatCodeSessionResponse;
import com.emotion.entity.User;
import com.emotion.exception.AuthException;
import com.emotion.exception.BusinessException;
import com.emotion.exception.CaptchaException;
import com.emotion.exception.TokenException;
import com.emotion.service.AuthService;
import com.emotion.service.UserService;
import com.emotion.service.WechatMiniProgramService;
import com.emotion.util.JwtUtil;
import com.emotion.util.TokenUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import javax.imageio.ImageIO;
import javax.servlet.http.HttpServletRequest;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Base64;
import java.util.Random;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
/**
* 认证服务实现类
*
* @author huazhongmin
* @date 2025-07-23
*/
@Slf4j
@Service
public class AuthServiceImpl implements AuthService {
@Autowired
private UserService userService;
@Autowired
private WechatMiniProgramService wechatMiniProgramService;
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private JwtUtil jwtUtil;
@Autowired
private TokenUtil tokenUtil;
private static final String CAPTCHA_PREFIX = "captcha:";
private static final String SMS_CODE_PREFIX = "sms_code:";
private static final String TOKEN_PREFIX = "token:";
private static final String REFRESH_TOKEN_PREFIX = "refresh_token:";
private static final int CAPTCHA_EXPIRE_MINUTES = 5;
private static final int SMS_CODE_EXPIRE_MINUTES = 5;
private static final int TOKEN_EXPIRE_HOURS = 24;
private static final int REFRESH_TOKEN_EXPIRE_DAYS = 7;
private static final String DEFAULT_SMS_CODE = "123456";
private static final String WECHAT_MP_TYPE = "wechat-mp";
private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
@Override
public AuthResponse login(LoginRequest request) {
// 验证短信验证码
if (!validateSmsCode(request.getPhone(), request.getSmsCode())) {
throw new CaptchaException("验证码错误或已过期");
}
// 根据手机号查询用户
User user = userService.getByPhone(request.getPhone());
// 如果用户不存在,则自动注册
if (user == null) {
log.info("用户不存在,自动注册: phone={}", request.getPhone());
user = autoRegisterUser(request.getPhone());
} else {
// 检查用户状态
if (user.getStatus() != 1) {
throw new AuthException("账号已被禁用");
}
log.info("用户登录: phone={}, userId={}", request.getPhone(), user.getId());
}
// 生成令牌
String accessToken = generateAccessToken(user);
String refreshToken = generateRefreshToken(user);
// 更新用户最后活跃时间
userService.updateLastActiveTime(user.getId(), LocalDateTime.now());
// 构建响应
AuthResponse response = new AuthResponse();
response.setAccessToken(accessToken);
response.setRefreshToken(refreshToken);
response.setExpiresIn((long) TOKEN_EXPIRE_HOURS * 3600);
response.setUserInfo(convertToUserInfoResponse(user));
response.setLoginTime(LocalDateTime.now().format(DATE_TIME_FORMATTER));
return response;
}
@Override
public AuthResponse wechatLogin(WechatLoginRequest request) {
WechatCodeSessionResponse session = wechatMiniProgramService.code2Session(request.getCode());
String openid = session.getOpenid();
User user = userService.getByThirdParty(WECHAT_MP_TYPE, openid);
if (user == null) {
user = createWechatUser(openid, request);
log.info("Wechat mini program user created: userId={}", user.getId());
} else if (user.getStatus() != 1) {
throw new AuthException("账号已被禁用");
}
String accessToken = generateAccessToken(user);
String refreshToken = generateRefreshToken(user);
userService.updateLastActiveTime(user.getId(), LocalDateTime.now());
AuthResponse response = new AuthResponse();
response.setAccessToken(accessToken);
response.setRefreshToken(refreshToken);
response.setExpiresIn((long) TOKEN_EXPIRE_HOURS * 3600);
response.setUserInfo(convertToUserInfoResponse(user));
response.setLoginTime(LocalDateTime.now().format(DATE_TIME_FORMATTER));
return response;
}
@Override
public AuthResponse register(RegisterRequest request) {
// 验证短信验证码
if (!validateSmsCode(request.getPhone(), request.getSmsCode())) {
throw new CaptchaException("验证码错误或已过期");
}
// 检查手机号是否已存在
if (userService.getByPhone(request.getPhone()) != null) {
throw new BusinessException("手机号已被注册");
}
// 生成随机用户名:开心 + 6位随机大小写字母和数字
String username = generateRandomUsername();
// 使用手机号作为账号
String account = request.getPhone();
// 创建用户(密码在UserService中加密,这里不需要预先加密)
User user = userService.createUser(
account,
username,
request.getPassword(),
null, // 邮箱为空
request.getPhone()
);
// 生成令牌
String accessToken = generateAccessToken(user);
String refreshToken = generateRefreshToken(user);
// 构建响应
AuthResponse response = new AuthResponse();
response.setAccessToken(accessToken);
response.setRefreshToken(refreshToken);
response.setExpiresIn((long) TOKEN_EXPIRE_HOURS * 3600);
response.setUserInfo(convertToUserInfoResponse(user));
response.setLoginTime(LocalDateTime.now().format(DATE_TIME_FORMATTER));
log.info("用户注册成功: phone={}, username={}", request.getPhone(), username);
return response;
}
@Override
public ResetPasswordResponse resetPassword(ResetPasswordRequest request) {
// 验证码本期固定为123456
if (request.getCaptcha() == null || !"123456".equals(request.getCaptcha().trim())) {
throw new CaptchaException("验证码错误或已过期");
}
// 根据手机号查询用户
User user = userService.getByPhone(request.getPhone());
if (user == null) {
throw new BusinessException("用户不存在");
}
// 使用统一的 PasswordEncoder 进行加密,保持与登录/注册一致
String encoded = passwordEncoder.encode(request.getNewPassword());
user.setPassword(encoded);
userService.updateById(user);
ResetPasswordResponse resp = new ResetPasswordResponse();
resp.setSuccess(true);
resp.setMessage("重置密码成功");
return resp;
}
@Override
public UserInfoResponse getCurrentUserInfo(String userId) {
User user = userService.getById(userId);
if (user == null) {
throw new AuthException("用户不存在");
}
return convertToUserInfoResponse(user);
}
@Override
public CaptchaResponse generateCaptcha() {
String captchaKey = UUID.randomUUID().toString();
String captchaCode = generateCaptchaCode();
// 生成验证码图片
String captchaImage = generateCaptchaImage(captchaCode);
// 存储验证码到Redis
redisTemplate.opsForValue().set(
CAPTCHA_PREFIX + captchaKey,
captchaCode.toLowerCase(),
CAPTCHA_EXPIRE_MINUTES,
TimeUnit.MINUTES
);
CaptchaResponse response = new CaptchaResponse();
response.setCaptchaKey(captchaKey);
response.setCaptchaImage(captchaImage);
response.setExpiresIn((long) CAPTCHA_EXPIRE_MINUTES * 60);
return response;
}
@Override
public boolean validateCaptcha(String captchaKey, String captcha) {
if (!StringUtils.hasText(captchaKey) || !StringUtils.hasText(captcha)) {
return false;
}
String storedCaptcha = (String) redisTemplate.opsForValue().get(CAPTCHA_PREFIX + captchaKey);
if (storedCaptcha == null) {
return false;
}
// 验证成功后删除验证码
redisTemplate.delete(CAPTCHA_PREFIX + captchaKey);
return storedCaptcha.equalsIgnoreCase(captcha.trim());
}
@Override
public boolean logout(String userId, String token) {
// 删除访问令牌
redisTemplate.delete(TOKEN_PREFIX + token);
// 删除刷新令牌(如果存在)
String refreshTokenKey = REFRESH_TOKEN_PREFIX + userId;
redisTemplate.delete(refreshTokenKey);
return true;
}
@Override
public boolean logoutByToken(HttpServletRequest request) {
String token = tokenUtil.extractToken(request);
String userId = validateTokenAndGetUserId(token);
return logout(userId, token);
}
/**
* 验证令牌并获取用户ID
*/
private String validateTokenAndGetUserId(String token) {
if (!tokenUtil.isValidToken(token)) {
throw new TokenException("未提供访问令牌");
}
if (!validateToken(token)) {
throw new TokenException("访问令牌无效或已过期");
}
String userId = getUserIdFromToken(token);
if (userId == null) {
throw new TokenException("访问令牌无效");
}
return userId;
}
@Override
public AuthResponse refreshToken(String refreshToken) {
String userId = (String) redisTemplate.opsForValue().get(REFRESH_TOKEN_PREFIX + refreshToken);
if (userId == null) {
throw new TokenException("刷新令牌无效或已过期");
}
User user = userService.getById(userId);
if (user == null) {
throw new AuthException("用户不存在");
}
// 生成新的访问令牌
String newAccessToken = generateAccessToken(user);
String newRefreshToken = generateRefreshToken(user);
// 删除旧的刷新令牌
redisTemplate.delete(REFRESH_TOKEN_PREFIX + refreshToken);
AuthResponse response = new AuthResponse();
response.setAccessToken(newAccessToken);
response.setRefreshToken(newRefreshToken);
response.setExpiresIn((long) TOKEN_EXPIRE_HOURS * 3600);
response.setUserInfo(convertToUserInfoResponse(user));
response.setLoginTime(LocalDateTime.now().format(DATE_TIME_FORMATTER));
return response;
}
@Override
public boolean validateToken(HttpServletRequest request) {
String token = tokenUtil.extractToken(request);
return validateToken(token);
}
@Override
public boolean validateToken(String token) {
if (!tokenUtil.isValidToken(token)) {
return false;
}
try {
// 首先尝试JWT验证
if (jwtUtil.validateToken(token)) {
// JWT验证成功,再检查Redis中是否存在(用于登出功能)
return redisTemplate.hasKey(TOKEN_PREFIX + token);
}
return false;
} catch (Exception e) {
log.warn("Token验证失败: {}", e.getMessage());
return false;
}
}
@Override
public String getUserIdFromToken(String token) {
if (!tokenUtil.isValidToken(token)) {
return null;
}
try {
// 首先尝试从JWT中获取用户ID
String userId = jwtUtil.getUserIdFromToken(token);
if (userId != null) {
// 验证Redis中是否存在该token(确保未被登出)
if (redisTemplate.hasKey(TOKEN_PREFIX + token)) {
return userId;
}
}
return null;
} catch (Exception e) {
log.warn("从Token获取用户ID失败: {}", e.getMessage());
return null;
}
}
@Override
public String getUsernameFromToken(String token) {
if (!tokenUtil.isValidToken(token)) {
return null;
}
try {
// 直接从JWT中获取用户名
return jwtUtil.getUsernameFromToken(token);
} catch (Exception e) {
log.warn("从Token获取用户名失败: {}", e.getMessage());
return null;
}
}
/**
* 生成访问令牌
*/
private String generateAccessToken(User user) {
// 使用JWT生成token
String token = jwtUtil.generateToken(user.getId(), user.getUsername());
// 同时在Redis中存储token信息,用于快速验证和登出功能
redisTemplate.opsForValue().set(
TOKEN_PREFIX + token,
user.getId(),
TOKEN_EXPIRE_HOURS,
TimeUnit.HOURS
);
return token;
}
/**
* 生成刷新令牌
*/
private String generateRefreshToken(User user) {
// 使用JWT生成刷新token
String refreshToken = jwtUtil.generateRefreshToken(user.getId(), user.getUsername());
// 在Redis中存储刷新token信息
redisTemplate.opsForValue().set(
REFRESH_TOKEN_PREFIX + refreshToken,
user.getId(),
REFRESH_TOKEN_EXPIRE_DAYS,
TimeUnit.DAYS
);
return refreshToken;
}
/**
* 生成验证码
*/
private String generateCaptchaCode() {
Random random = new Random();
StringBuilder code = new StringBuilder();
for (int i = 0; i < 4; i++) {
code.append(random.nextInt(10));
}
return code.toString();
}
/**
* 生成验证码图片
*/
private String generateCaptchaImage(String code) {
int width = 120;
int height = 40;
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
Graphics2D g = image.createGraphics();
// 设置背景色
g.setColor(Color.WHITE);
g.fillRect(0, 0, width, height);
// 设置字体
g.setFont(new Font("Arial", Font.BOLD, 20));
g.setColor(Color.BLACK);
// 绘制验证码
for (int i = 0; i < code.length(); i++) {
g.drawString(String.valueOf(code.charAt(i)), 20 + i * 20, 25);
}
// 添加干扰线
Random random = new Random();
g.setColor(Color.GRAY);
for (int i = 0; i < 5; i++) {
int x1 = random.nextInt(width);
int y1 = random.nextInt(height);
int x2 = random.nextInt(width);
int y2 = random.nextInt(height);
g.drawLine(x1, y1, x2, y2);
}
g.dispose();
// 转换为Base64
try {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ImageIO.write(image, "png", baos);
byte[] imageBytes = baos.toByteArray();
return "data:image/png;base64," + Base64.getEncoder().encodeToString(imageBytes);
} catch (IOException e) {
throw new CaptchaException("生成验证码图片失败");
}
}
/**
* 验证密码
*/
private boolean verifyPassword(String rawPassword, String encodedPassword) {
// 使用BCrypt进行密码验证
return passwordEncoder.matches(rawPassword, encodedPassword);
}
/**
* 加密密码
*/
private String encryptPassword(String rawPassword) {
// 使用BCrypt进行密码加密
return passwordEncoder.encode(rawPassword);
}
/**
* 转换为用户信息响应
*/
private UserInfoResponse convertToUserInfoResponse(User user) {
UserInfoResponse response = new UserInfoResponse();
BeanUtils.copyProperties(user, response);
if (user.getCreateTime() != null) {
response.setCreateTime(user.getCreateTime().format(DATE_TIME_FORMATTER));
}
if (user.getLastActiveTime() != null) {
response.setLastActiveTime(user.getLastActiveTime().format(DATE_TIME_FORMATTER));
}
return response;
}
@Override
public boolean existsByAccount(String account) {
if (!StringUtils.hasText(account)) {
return false;
}
User user = userService.getByAccount(account);
return user != null;
}
@Override
public boolean existsByEmail(String email) {
if (!StringUtils.hasText(email)) {
return false;
}
User user = userService.getByEmail(email);
return user != null;
}
@Override
public boolean existsByPhone(String phone) {
if (!StringUtils.hasText(phone)) {
return false;
}
User user = userService.getByPhone(phone);
return user != null;
}
@Override
public SmsCodeResponse sendSmsCode(String phone) {
// 验证手机号格式
if (!StringUtils.hasText(phone) || !phone.matches("^1[3-9]\\d{9}$")) {
throw new BusinessException("手机号格式不正确");
}
// 检查手机号是否已注册,用于提示信息
boolean isRegistered = existsByPhone(phone);
String message;
if (isRegistered) {
message = "验证码已发送,用于登录验证,有效期" + SMS_CODE_EXPIRE_MINUTES + "分钟";
} else {
message = "验证码已发送,用于注册验证,有效期" + SMS_CODE_EXPIRE_MINUTES + "分钟";
}
// TODO: 接入真实的短信服务商(阿里云、腾讯云等)
// 目前使用固定验证码 123456
String code = DEFAULT_SMS_CODE;
// 将验证码存储到Redis,有效期5分钟
String key = SMS_CODE_PREFIX + phone;
redisTemplate.opsForValue().set(key, code, SMS_CODE_EXPIRE_MINUTES, TimeUnit.MINUTES);
log.info("发送短信验证码: phone={}, code={}, isRegistered={}", phone, code, isRegistered);
// 构建响应
return SmsCodeResponse.builder()
.code(code) // 开发环境返回验证码,生产环境应该删除此行
.expiresIn((long) SMS_CODE_EXPIRE_MINUTES * 60)
.message(message)
.build();
}
@Override
public boolean validateSmsCode(String phone, String code) {
if (!StringUtils.hasText(phone) || !StringUtils.hasText(code)) {
return false;
}
String key = SMS_CODE_PREFIX + phone;
String storedCode = (String) redisTemplate.opsForValue().get(key);
if (storedCode == null) {
log.warn("短信验证码不存在或已过期: phone={}", phone);
return false;
}
boolean isValid = storedCode.equals(code);
// 验证成功后删除验证码(一次性使用)
if (isValid) {
redisTemplate.delete(key);
log.info("短信验证码验证成功: phone={}", phone);
} else {
log.warn("短信验证码错误: phone={}, expected={}, actual={}", phone, storedCode, code);
}
return isValid;
}
/**
* 生成随机用户名
* 格式:开心 + 6位随机大小写字母和数字
*
* @return 随机用户名
*/
private String generateRandomUsername() {
String chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
Random random = new Random();
StringBuilder sb = new StringBuilder("开心");
for (int i = 0; i < 6; i++) {
int index = random.nextInt(chars.length());
sb.append(chars.charAt(index));
}
return sb.toString();
}
/**
* 自动注册用户(用于登录时用户不存在的情况)
*
* @param phone 手机号
* @return 新创建的用户
*/
private User autoRegisterUser(String phone) {
// 生成随机用户名:开心 + 6位随机大小写字母和数字
String username = generateRandomUsername();
// 使用手机号作为账号
String account = phone;
// 生成随机密码(用户可以后续修改)
String randomPassword = generateRandomPassword();
// 创建用户
User user = userService.createUser(
account,
username,
randomPassword,
null, // 邮箱为空
phone
);
log.info("自动注册用户成功: phone={}, username={}, userId={}", phone, username, user.getId());
return user;
}
/**
* 生成随机密码
* 格式:8位随机大小写字母和数字
*
* @return 随机密码
*/
private User createWechatUser(String openid, WechatLoginRequest request) {
String username = generateRandomUsername();
User user = new User();
user.setAccount("wx_" + openid);
user.setUsername(username);
user.setNickname(StringUtils.hasText(request.getNickname()) ? request.getNickname() : username);
user.setAvatar(StringUtils.hasText(request.getAvatar()) ? request.getAvatar() : null);
user.setPassword(passwordEncoder.encode(UUID.randomUUID().toString()));
user.setMemberLevel("free");
user.setStatus(1);
user.setIsVerified(1);
user.setLastActiveTime(LocalDateTime.now());
user.setThirdPartyType(WECHAT_MP_TYPE);
user.setThirdPartyId(openid);
userService.save(user);
return user;
}
private String generateRandomPassword() {
String chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
Random random = new Random();
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 8; i++) {
int index = random.nextInt(chars.length());
sb.append(chars.charAt(index));
}
return sb.toString();
}
}
@@ -0,0 +1,155 @@
package com.emotion.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.emotion.common.PageResult;
import com.emotion.dto.request.comment.CommentCreateRequest;
import com.emotion.dto.request.comment.CommentUpdateRequest;
import com.emotion.dto.request.comment.CommentPageRequest;
import com.emotion.dto.response.comment.CommentResponse;
import com.emotion.entity.Comment;
import com.emotion.mapper.CommentMapper;
import com.emotion.service.CommentService;
import com.emotion.util.SnowflakeIdGenerator;
import com.emotion.util.UserContextUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.stream.Collectors;
/**
* 评论服务实现类
*
* @author huazhongmin
* @date 2025-07-24
*/
@Service
public class CommentServiceImpl extends ServiceImpl<CommentMapper, Comment> implements CommentService {
@Autowired
private SnowflakeIdGenerator snowflakeIdGenerator;
private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
@Override
public PageResult<CommentResponse> getPage(CommentPageRequest request) {
Page<Comment> page = new Page<>(request.getCurrent(), request.getSize());
LambdaQueryWrapper<Comment> wrapper = new LambdaQueryWrapper<>();
// 根据请求参数构建查询条件
if (StringUtils.hasText(request.getPostId())) {
wrapper.eq(Comment::getPostId, request.getPostId());
}
if (StringUtils.hasText(request.getUserId())) {
wrapper.eq(Comment::getUserId, request.getUserId());
}
if (StringUtils.hasText(request.getKeyword())) {
wrapper.like(Comment::getContent, request.getKeyword());
}
wrapper.eq(Comment::getIsDeleted, 0).orderByDesc(Comment::getCreateTime);
page = this.page(page, wrapper);
return convertPageToPageResult(page);
}
@Override
public CommentResponse getById(String id) {
Comment comment = this.getBaseMapper().selectById(id);
if (comment == null) {
return null;
}
return convertToResponse(comment);
}
@Override
public CommentResponse create(CommentCreateRequest request) {
Comment comment = new Comment();
comment.setId(snowflakeIdGenerator.nextIdAsString()); // 使用雪花算法生成ID
comment.setPostId(request.getPostId());
// 从上下文中获取当前用户ID
String currentUserId = UserContextUtils.getCurrentUserId();
if (currentUserId != null) {
comment.setUserId(currentUserId);
} else if (request.getUserId() != null) {
// 如果上下文中没有用户ID,则使用请求中的用户ID(向后兼容)
comment.setUserId(request.getUserId());
} else {
throw new IllegalArgumentException("用户ID不能为空");
}
comment.setContent(request.getContent());
comment.setReplyToId(request.getReplyToId());
comment.setLikes(0);
this.save(comment);
return convertToResponse(comment);
}
@Override
public CommentResponse update(CommentUpdateRequest request) {
Comment comment = this.getBaseMapper().selectById(request.getId());
if (comment == null) {
return null;
}
// 只更新非空字段
if (StringUtils.hasText(request.getContent())) {
comment.setContent(request.getContent());
}
this.updateById(comment);
return convertToResponse(comment);
}
@Override
public boolean delete(String id) {
Comment comment = this.getBaseMapper().selectById(id);
if (comment == null) {
return false;
}
// 使用 MyBatis-Plus 的 removeById 方法,自动处理逻辑删除
return this.removeById(id);
}
/**
* 转换为响应对象
*/
private CommentResponse convertToResponse(Comment comment) {
CommentResponse response = new CommentResponse();
BeanUtils.copyProperties(comment, response);
response.setId(comment.getId());
if (comment.getCreateTime() != null) {
response.setCreateTime(comment.getCreateTime().format(DATE_TIME_FORMATTER));
}
if (comment.getUpdateTime() != null) {
response.setUpdateTime(comment.getUpdateTime().format(DATE_TIME_FORMATTER));
}
return response;
}
/**
* 转换分页对象为PageResult对象
*/
private PageResult<CommentResponse> convertPageToPageResult(Page<Comment> page) {
PageResult<CommentResponse> pageResult = new PageResult<>();
pageResult.setCurrent(page.getCurrent());
pageResult.setSize(page.getSize());
pageResult.setTotal(page.getTotal());
pageResult.setPages(page.getPages());
pageResult.setRecords(page.getRecords().stream()
.map(this::convertToResponse)
.collect(Collectors.toList()));
return pageResult;
}
}
@@ -0,0 +1,178 @@
package com.emotion.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.emotion.common.PageResult;
import com.emotion.dto.request.community.CommunityPostCreateRequest;
import com.emotion.dto.request.community.CommunityPostUpdateRequest;
import com.emotion.dto.request.community.CommunityPostPageRequest;
import com.emotion.dto.response.community.CommunityPostResponse;
import com.emotion.entity.CommunityPost;
import com.emotion.mapper.CommunityPostMapper;
import com.emotion.service.CommunityPostService;
import com.emotion.util.SnowflakeIdGenerator;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.stream.Collectors;
/**
* 社区帖子服务实现类
*
* @author huazhongmin
* @date 2025-07-24
*/
@Service
public class CommunityPostServiceImpl extends ServiceImpl<CommunityPostMapper, CommunityPost> implements CommunityPostService {
@Autowired
private SnowflakeIdGenerator snowflakeIdGenerator;
private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
@Override
public PageResult<CommunityPostResponse> getPage(CommunityPostPageRequest request) {
Page<CommunityPost> page = new Page<>(request.getCurrent(), request.getSize());
LambdaQueryWrapper<CommunityPost> wrapper = new LambdaQueryWrapper<>();
// 根据请求参数构建查询条件
if (StringUtils.hasText(request.getUserId())) {
wrapper.eq(CommunityPost::getUserId, request.getUserId());
}
if (StringUtils.hasText(request.getType())) {
wrapper.eq(CommunityPost::getType, request.getType());
}
if (StringUtils.hasText(request.getLocationId())) {
wrapper.eq(CommunityPost::getLocationId, request.getLocationId());
}
if (request.getPublicOnly() != null && request.getPublicOnly()) {
wrapper.eq(CommunityPost::getIsPrivate, 0);
}
if (StringUtils.hasText(request.getKeyword())) {
wrapper.and(w -> w.like(CommunityPost::getTitle, request.getKeyword())
.or().like(CommunityPost::getContent, request.getKeyword()));
}
wrapper.eq(CommunityPost::getIsDeleted, 0).orderByDesc(CommunityPost::getCreateTime);
page = this.page(page, wrapper);
return convertPageToResponse(page);
}
@Override
public CommunityPostResponse getById(String id) {
CommunityPost post = this.getBaseMapper().selectById(id);
if (post == null) {
return null;
}
// 增加浏览数
Integer viewCount = post.getViewCount() == null ? 1 : post.getViewCount() + 1;
post.setViewCount(viewCount);
this.updateById(post);
return convertToResponse(post);
}
@Override
public CommunityPostResponse create(CommunityPostCreateRequest request) {
CommunityPost post = new CommunityPost();
post.setId(snowflakeIdGenerator.nextIdAsString()); // 使用雪花算法生成ID
post.setUserId(request.getUserId());
post.setTitle(request.getTitle());
post.setContent(request.getContent());
post.setType(request.getType());
post.setLocationId(request.getLocationId());
post.setTags(request.getTags());
post.setIsPrivate(request.getIsPrivate() != null ? request.getIsPrivate() : 0); // 默认公开
post.setLikes(0);
post.setViewCount(0);
post.setCommentCount(0);
this.save(post);
return convertToResponse(post);
}
@Override
public CommunityPostResponse update(CommunityPostUpdateRequest request) {
CommunityPost post = this.getBaseMapper().selectById(request.getId());
if (post == null) {
return null;
}
// 只更新非空字段
if (request.getTitle() != null) {
post.setTitle(request.getTitle());
}
if (request.getContent() != null) {
post.setContent(request.getContent());
}
if (request.getType() != null) {
post.setType(request.getType());
}
if (request.getLocationId() != null) {
post.setLocationId(request.getLocationId());
}
if (request.getTags() != null) {
post.setTags(request.getTags());
}
if (request.getIsPrivate() != null) {
post.setIsPrivate(request.getIsPrivate());
}
this.updateById(post);
return convertToResponse(post);
}
@Override
public boolean delete(String id) {
CommunityPost post = this.getBaseMapper().selectById(id);
if (post == null) {
return false;
}
// 使用 MyBatis-Plus 的 removeById 方法,自动处理逻辑删除
return this.removeById(id);
}
/**
* 转换为响应对象
*/
private CommunityPostResponse convertToResponse(CommunityPost post) {
CommunityPostResponse response = new CommunityPostResponse();
BeanUtils.copyProperties(post, response);
response.setId(post.getId());
if (post.getCreateTime() != null) {
response.setCreateTime(post.getCreateTime().format(DATE_TIME_FORMATTER));
}
if (post.getUpdateTime() != null) {
response.setUpdateTime(post.getUpdateTime().format(DATE_TIME_FORMATTER));
}
return response;
}
/**
* 转换分页对象为响应对象
*/
private PageResult<CommunityPostResponse> convertPageToResponse(Page<CommunityPost> page) {
PageResult<CommunityPostResponse> responsePage = new PageResult<>();
responsePage.setCurrent(page.getCurrent());
responsePage.setSize(page.getSize());
responsePage.setTotal(page.getTotal());
responsePage.setPages(page.getPages());
responsePage.setRecords(
page.getRecords().stream()
.map(this::convertToResponse)
.collect(Collectors.toList()));
return responsePage;
}
}
@@ -0,0 +1,363 @@
package com.emotion.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.emotion.common.PageResult;
import com.emotion.dto.request.ConversationCreateRequest;
import com.emotion.dto.request.ConversationPageRequest;
import com.emotion.dto.response.ConversationResponse;
import com.emotion.entity.Conversation;
import com.emotion.mapper.ConversationMapper;
import com.emotion.service.ConversationService;
import com.emotion.util.SnowflakeIdGenerator;
import com.emotion.util.UserContextUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import javax.servlet.http.HttpServletRequest;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.stream.Collectors;
/**
* 会话服务实现类
*
* @author huazhongmin
* @date 2025-07-24
*/
@Service
public class ConversationServiceImpl extends ServiceImpl<ConversationMapper, Conversation> implements ConversationService {
@Autowired
private SnowflakeIdGenerator snowflakeIdGenerator;
private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
@Override
public IPage<Conversation> getPage(ConversationPageRequest request) {
Page<Conversation> page = new Page<>(request.getCurrent(), request.getSize());
LambdaQueryWrapper<Conversation> wrapper = new LambdaQueryWrapper<>();
// 根据请求参数构建查询条件
if (StringUtils.hasText(request.getUserId())) {
wrapper.eq(Conversation::getUserId, request.getUserId());
}
if (StringUtils.hasText(request.getStatus())) {
wrapper.eq(Conversation::getConversationStatus, request.getStatus());
}
if (StringUtils.hasText(request.getType())) {
wrapper.eq(Conversation::getType, request.getType());
}
if (StringUtils.hasText(request.getKeyword())) {
wrapper.and(w -> w.like(Conversation::getTitle, request.getKeyword())
.or().like(Conversation::getSummary, request.getKeyword()));
}
wrapper.eq(Conversation::getIsDeleted, 0).orderByDesc(Conversation::getCreateTime);
return this.page(page, wrapper);
}
@Override
public IPage<Conversation> getPageByUserId(ConversationPageRequest request) {
Page<Conversation> page = new Page<>(request.getCurrent(), request.getSize());
LambdaQueryWrapper<Conversation> wrapper = new LambdaQueryWrapper<>();
// 必须提供userId参数
if (!StringUtils.hasText(request.getUserId())) {
throw new IllegalArgumentException("userId不能为空");
}
wrapper.eq(Conversation::getUserId, request.getUserId())
.eq(Conversation::getIsDeleted, 0)
.orderByDesc(Conversation::getCreateTime);
return this.page(page, wrapper);
}
@Override
public List<Conversation> getByUserId(String userId) {
LambdaQueryWrapper<Conversation> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(Conversation::getUserId, userId)
.eq(Conversation::getIsDeleted, 0)
.orderByDesc(Conversation::getCreateTime);
return this.list(wrapper);
}
@Override
public List<Conversation> getActiveByUserId(String userId) {
LambdaQueryWrapper<Conversation> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(Conversation::getUserId, userId)
.eq(Conversation::getConversationStatus, "active")
.eq(Conversation::getIsDeleted, 0)
.orderByDesc(Conversation::getLastActiveTime);
return this.list(wrapper);
}
@Override
public Conversation getByCozeConversationId(String cozeConversationId) {
LambdaQueryWrapper<Conversation> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(Conversation::getCozeConversationId, cozeConversationId)
.eq(Conversation::getIsDeleted, 0);
return this.getOne(wrapper);
}
@Override
public boolean updateMessageCount(String conversationId, Integer messageCount) {
Conversation conversation = new Conversation();
conversation.setId(conversationId);
conversation.setMessageCount(messageCount);
return this.updateById(conversation);
}
@Override
public boolean updateStatus(String conversationId, Integer status) {
Conversation conversation = new Conversation();
conversation.setId(conversationId);
// 根据status值设置对应的状态字符串
String statusStr = "active";
if (status == 1) {
statusStr = "ended";
} else if (status == 2) {
statusStr = "archived";
}
conversation.setConversationStatus(statusStr);
return this.updateById(conversation);
}
@Override
public boolean updateEndTime(String conversationId, LocalDateTime endTime) {
Conversation conversation = new Conversation();
conversation.setId(conversationId);
conversation.setEndTime(endTime);
return this.updateById(conversation);
}
@Override
public Long countByUserId(String userId) {
LambdaQueryWrapper<Conversation> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(Conversation::getUserId, userId)
.eq(Conversation::getIsDeleted, 0);
return this.count(wrapper);
}
@Override
public Long countActiveByUserId(String userId) {
LambdaQueryWrapper<Conversation> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(Conversation::getUserId, userId)
.eq(Conversation::getConversationStatus, "active")
.eq(Conversation::getIsDeleted, 0);
return this.count(wrapper);
}
@Override
public List<Conversation> getForArchive(Integer days) {
LocalDateTime archiveTime = LocalDateTime.now().minusDays(days);
LambdaQueryWrapper<Conversation> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(Conversation::getConversationStatus, "active")
.lt(Conversation::getLastActiveTime, archiveTime)
.eq(Conversation::getIsDeleted, 0)
.orderByAsc(Conversation::getLastActiveTime);
return this.list(wrapper);
}
@Override
public boolean batchArchive(List<String> conversationIds) {
for (String conversationId : conversationIds) {
Conversation conversation = new Conversation();
conversation.setId(conversationId);
conversation.setConversationStatus("archived");
this.updateById(conversation);
}
return true;
}
@Override
public Conversation createConversation(String userId, String title, String cozeConversationId) {
Conversation conversation = new Conversation();
conversation.setId(snowflakeIdGenerator.nextIdAsString()); // 使用雪花算法生成ID
conversation.setUserId(userId);
conversation.setTitle(title);
conversation.setCozeConversationId(cozeConversationId);
conversation.setUserType("registered");
conversation.setType("chat");
conversation.setConversationStatus("active");
conversation.setStartTime(LocalDateTime.now());
conversation.setLastActiveTime(LocalDateTime.now());
conversation.setMessageCount(0);
conversation.setTotalTokens(0);
conversation.setTotalCost(java.math.BigDecimal.ZERO);
this.save(conversation);
return conversation;
}
@Override
public boolean endConversation(String conversationId) {
Conversation conversation = new Conversation();
conversation.setId(conversationId);
conversation.setConversationStatus("ended");
conversation.setEndTime(LocalDateTime.now());
return this.updateById(conversation);
}
@Override
public PageResult<ConversationResponse> getPageWithResponse(ConversationPageRequest request) {
IPage<Conversation> page = this.getPage(request);
return convertPageToResponse(page);
}
@Override
public PageResult<ConversationResponse> getPageByUserIdWithResponse(ConversationPageRequest request) {
IPage<Conversation> page = this.getPageByUserId(request);
return convertPageToResponse(page);
}
@Override
public ConversationResponse getConversationResponseById(String id) {
Conversation conversation = this.getById(id);
if (conversation == null) {
return null;
}
return convertToResponse(conversation);
}
@Override
public List<ConversationResponse> getByUserIdWithResponse(String userId) {
List<Conversation> conversations = this.getByUserId(userId);
return conversations.stream()
.map(this::convertToResponse)
.collect(Collectors.toList());
}
@Override
public List<ConversationResponse> getActiveConversationsWithResponse() {
// 实现获取活跃对话的逻辑
LambdaQueryWrapper<Conversation> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(Conversation::getConversationStatus, "active")
.eq(Conversation::getIsDeleted, 0)
.orderByDesc(Conversation::getLastActiveTime);
List<Conversation> conversations = this.list(wrapper);
return conversations.stream()
.map(this::convertToResponse)
.collect(Collectors.toList());
}
@Override
public List<ConversationResponse> getArchivedConversationsWithResponse() {
// 实现获取归档对话的逻辑
LambdaQueryWrapper<Conversation> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(Conversation::getConversationStatus, "archived")
.eq(Conversation::getIsDeleted, 0)
.orderByDesc(Conversation::getCreateTime);
List<Conversation> conversations = this.list(wrapper);
return conversations.stream()
.map(this::convertToResponse)
.collect(Collectors.toList());
}
@Override
public boolean archiveConversation(String id) {
Conversation conversation = new Conversation();
conversation.setId(id);
conversation.setConversationStatus("archived");
return this.updateById(conversation);
}
@Override
public boolean activateConversation(String id) {
Conversation conversation = new Conversation();
conversation.setId(id);
conversation.setConversationStatus("active");
return this.updateById(conversation);
}
@Override
public ConversationResponse createConversationWithResponse(ConversationCreateRequest request,
HttpServletRequest httpRequest) {
// 获取客户端IP地址
String clientIp = UserContextUtils.getClientIpAddress(httpRequest);
Conversation conversation = this.createConversation(
request.getUserId(),
request.getTitle(),
null // cozeConversationId
);
// 设置客户端IP
conversation.setClientIp(clientIp);
this.updateById(conversation);
return convertToResponse(conversation);
}
@Override
public ConversationResponse updateConversationWithResponse(ConversationCreateRequest request) {
Conversation conversation = this.getById(request.getId()); // 修复:使用ID而不是userId
if (conversation == null) {
return null;
}
// 更新字段
if (request.getTitle() != null) {
conversation.setTitle(request.getTitle());
}
if (request.getType() != null) {
conversation.setType(request.getType());
}
this.updateById(conversation);
return convertToResponse(conversation);
}
@Override
public boolean updateConversationStatus(String id, String status) {
Conversation conversation = new Conversation();
conversation.setId(id);
conversation.setConversationStatus(status);
return this.updateById(conversation);
}
/**
* 转换为响应对象
*/
private ConversationResponse convertToResponse(Conversation conversation) {
ConversationResponse response = new ConversationResponse();
BeanUtils.copyProperties(conversation, response);
response.setId(conversation.getId());
response.setStatus(conversation.getConversationStatus());
if (conversation.getCreateTime() != null) {
response.setCreateTime(conversation.getCreateTime().format(DATE_TIME_FORMATTER));
}
if (conversation.getUpdateTime() != null) {
response.setUpdateTime(conversation.getUpdateTime().format(DATE_TIME_FORMATTER));
}
if (conversation.getLastActiveTime() != null) {
response.setLastMessageTime(conversation.getLastActiveTime().format(DATE_TIME_FORMATTER));
}
return response;
}
/**
* 转换分页对象为响应对象
*/
private PageResult<ConversationResponse> convertPageToResponse(IPage<Conversation> page) {
PageResult<ConversationResponse> responsePage = new PageResult<>();
responsePage.setCurrent(page.getCurrent());
responsePage.setSize(page.getSize());
responsePage.setTotal(page.getTotal());
responsePage.setPages(page.getPages());
responsePage.setRecords(
page.getRecords().stream()
.map(this::convertToResponse)
.collect(Collectors.toList()));
return responsePage;
}
}
@@ -0,0 +1,365 @@
package com.emotion.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.emotion.common.PageResult;
import com.emotion.dto.request.coze.CozeApiCallPageRequest;
import com.emotion.dto.request.coze.CozeApiCallCreateRequest;
import com.emotion.dto.request.coze.CozeApiCallUpdateRequest;
import com.emotion.dto.response.coze.CozeApiCallResponse;
import com.emotion.entity.CozeApiCall;
import com.emotion.mapper.CozeApiCallMapper;
import com.emotion.service.CozeApiCallService;
import com.emotion.util.SnowflakeIdGenerator;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.stream.Collectors;
/**
* Coze API调用记录服务实现类
*
* @author huazhongmin
* @date 2025-07-23
*/
@Service
public class CozeApiCallServiceImpl extends ServiceImpl<CozeApiCallMapper, CozeApiCall> implements CozeApiCallService {
@Autowired
private SnowflakeIdGenerator snowflakeIdGenerator;
private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
@Override
public PageResult<CozeApiCallResponse> getPage(CozeApiCallPageRequest request) {
Page<CozeApiCall> page = new Page<>(request.getCurrent(), request.getSize());
LambdaQueryWrapper<CozeApiCall> wrapper = new LambdaQueryWrapper<>();
// 根据请求参数构建查询条件
if (StringUtils.hasText(request.getConversationId())) {
wrapper.eq(CozeApiCall::getConversationId, request.getConversationId());
}
if (StringUtils.hasText(request.getUserId())) {
wrapper.eq(CozeApiCall::getUserId, request.getUserId());
}
if (StringUtils.hasText(request.getBotId())) {
wrapper.eq(CozeApiCall::getBotId, request.getBotId());
}
if (StringUtils.hasText(request.getStatus())) {
wrapper.eq(CozeApiCall::getStatus, request.getStatus());
}
if (StringUtils.hasText(request.getRequestType())) {
wrapper.eq(CozeApiCall::getRequestType, request.getRequestType());
}
if (StringUtils.hasText(request.getTraceId())) {
wrapper.eq(CozeApiCall::getTraceId, request.getTraceId());
}
// 关键词搜索
if (StringUtils.hasText(request.getKeyword())) {
wrapper.and(w -> w.like(CozeApiCall::getRequestUrl, request.getKeyword())
.or().like(CozeApiCall::getAiReply, request.getKeyword())
.or().like(CozeApiCall::getUserMessage, request.getKeyword()));
}
wrapper.eq(CozeApiCall::getIsDeleted, 0);
// 排序
if (StringUtils.hasText(request.getOrderBy())) {
if ("asc".equalsIgnoreCase(request.getOrderDirection())) {
wrapper.orderByAsc(CozeApiCall::getStartTime);
} else {
wrapper.orderByDesc(CozeApiCall::getStartTime);
}
} else {
wrapper.orderByDesc(CozeApiCall::getStartTime);
}
page = this.page(page, wrapper);
return convertPageToPageResult(page);
}
@Override
public CozeApiCallResponse getById(String id) {
CozeApiCall apiCall = this.getBaseMapper().selectById(id);
if (apiCall == null || apiCall.getIsDeleted() == 1) {
return null;
}
return convertToResponse(apiCall);
}
@Override
public CozeApiCallResponse create(CozeApiCallCreateRequest request) {
CozeApiCall apiCall = new CozeApiCall();
apiCall.setId(snowflakeIdGenerator.nextIdAsString()); // 使用雪花算法生成ID
// 复制属性
apiCall.setConversationId(request.getConversationId());
apiCall.setMessageId(request.getMessageId());
apiCall.setCozeChatId(request.getCozeChatId());
apiCall.setCozeConversationId(request.getCozeConversationId());
apiCall.setBotId(request.getBotId());
apiCall.setWorkflowId(request.getWorkflowId());
apiCall.setUserId(request.getUserId());
apiCall.setRequestType(request.getRequestType());
apiCall.setRequestUrl(request.getRequestUrl());
apiCall.setRequestBody(request.getRequestBody());
apiCall.setRequestHeaders(request.getRequestHeaders());
apiCall.setUserMessage(request.getUserMessage());
apiCall.setUserMessageType(request.getUserMessageType());
apiCall.setAiReply(request.getAiReply());
apiCall.setAiReplyType(request.getAiReplyType());
apiCall.setResponseStatus(request.getResponseStatus());
apiCall.setResponseBody(request.getResponseBody());
apiCall.setResponseHeaders(request.getResponseHeaders());
apiCall.setPollCount(request.getPollCount());
apiCall.setFinalStatus(request.getFinalStatus());
apiCall.setStatus(request.getStatus());
apiCall.setDurationMs(request.getDurationMs());
apiCall.setPromptTokens(request.getPromptTokens());
apiCall.setCompletionTokens(request.getCompletionTokens());
apiCall.setTotalTokens(request.getTotalTokens());
apiCall.setCost(request.getCost());
apiCall.setFunctionCalls(request.getFunctionCalls());
apiCall.setFunctionResults(request.getFunctionResults());
apiCall.setErrorCode(request.getErrorCode());
apiCall.setErrorMessage(request.getErrorMessage());
apiCall.setClientIp(request.getClientIp());
apiCall.setUserAgent(request.getUserAgent());
apiCall.setSessionId(request.getSessionId());
apiCall.setTraceId(request.getTraceId());
apiCall.setMetadata(request.getMetadata());
this.save(apiCall);
return convertToResponse(apiCall);
}
@Override
public CozeApiCallResponse update(CozeApiCallUpdateRequest request) {
CozeApiCall apiCall = this.getBaseMapper().selectById(request.getId());
if (apiCall == null || apiCall.getIsDeleted() == 1) {
return null;
}
// 只更新非空字段
if (request.getConversationId() != null) {
apiCall.setConversationId(request.getConversationId());
}
if (request.getMessageId() != null) {
apiCall.setMessageId(request.getMessageId());
}
if (request.getCozeChatId() != null) {
apiCall.setCozeChatId(request.getCozeChatId());
}
if (request.getCozeConversationId() != null) {
apiCall.setCozeConversationId(request.getCozeConversationId());
}
if (request.getBotId() != null) {
apiCall.setBotId(request.getBotId());
}
if (request.getWorkflowId() != null) {
apiCall.setWorkflowId(request.getWorkflowId());
}
if (request.getUserId() != null) {
apiCall.setUserId(request.getUserId());
}
if (request.getRequestType() != null) {
apiCall.setRequestType(request.getRequestType());
}
if (request.getRequestUrl() != null) {
apiCall.setRequestUrl(request.getRequestUrl());
}
if (request.getRequestBody() != null) {
apiCall.setRequestBody(request.getRequestBody());
}
if (request.getRequestHeaders() != null) {
apiCall.setRequestHeaders(request.getRequestHeaders());
}
if (request.getUserMessage() != null) {
apiCall.setUserMessage(request.getUserMessage());
}
if (request.getUserMessageType() != null) {
apiCall.setUserMessageType(request.getUserMessageType());
}
if (request.getAiReply() != null) {
apiCall.setAiReply(request.getAiReply());
}
if (request.getAiReplyType() != null) {
apiCall.setAiReplyType(request.getAiReplyType());
}
if (request.getResponseStatus() != null) {
apiCall.setResponseStatus(request.getResponseStatus());
}
if (request.getResponseBody() != null) {
apiCall.setResponseBody(request.getResponseBody());
}
if (request.getResponseHeaders() != null) {
apiCall.setResponseHeaders(request.getResponseHeaders());
}
if (request.getPollCount() != null) {
apiCall.setPollCount(request.getPollCount());
}
if (request.getFinalStatus() != null) {
apiCall.setFinalStatus(request.getFinalStatus());
}
if (request.getStatus() != null) {
apiCall.setStatus(request.getStatus());
}
if (request.getDurationMs() != null) {
apiCall.setDurationMs(request.getDurationMs());
}
if (request.getPromptTokens() != null) {
apiCall.setPromptTokens(request.getPromptTokens());
}
if (request.getCompletionTokens() != null) {
apiCall.setCompletionTokens(request.getCompletionTokens());
}
if (request.getTotalTokens() != null) {
apiCall.setTotalTokens(request.getTotalTokens());
}
if (request.getCost() != null) {
apiCall.setCost(request.getCost());
}
if (request.getFunctionCalls() != null) {
apiCall.setFunctionCalls(request.getFunctionCalls());
}
if (request.getFunctionResults() != null) {
apiCall.setFunctionResults(request.getFunctionResults());
}
if (request.getErrorCode() != null) {
apiCall.setErrorCode(request.getErrorCode());
}
if (request.getErrorMessage() != null) {
apiCall.setErrorMessage(request.getErrorMessage());
}
if (request.getClientIp() != null) {
apiCall.setClientIp(request.getClientIp());
}
if (request.getUserAgent() != null) {
apiCall.setUserAgent(request.getUserAgent());
}
if (request.getSessionId() != null) {
apiCall.setSessionId(request.getSessionId());
}
if (request.getTraceId() != null) {
apiCall.setTraceId(request.getTraceId());
}
if (request.getMetadata() != null) {
apiCall.setMetadata(request.getMetadata());
}
this.updateById(apiCall);
return convertToResponse(apiCall);
}
@Override
public boolean delete(String id) {
CozeApiCall apiCall = this.getBaseMapper().selectById(id);
if (apiCall == null) {
return false;
}
// 使用 MyBatis-Plus 的 removeById 方法,自动处理逻辑删除
return this.removeById(id);
}
@Override
public Long countByUserId(String userId) {
LambdaQueryWrapper<CozeApiCall> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(CozeApiCall::getUserId, userId)
.eq(CozeApiCall::getIsDeleted, 0);
return this.count(wrapper);
}
@Override
public Long countByBotId(String botId) {
LambdaQueryWrapper<CozeApiCall> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(CozeApiCall::getBotId, botId)
.eq(CozeApiCall::getIsDeleted, 0);
return this.count(wrapper);
}
@Override
public Long countByStatus(String status) {
LambdaQueryWrapper<CozeApiCall> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(CozeApiCall::getStatus, status)
.eq(CozeApiCall::getIsDeleted, 0);
return this.count(wrapper);
}
@Override
public Long sumTokensByUserId(String userId) {
// 使用原生SQL或者查询后计算
List<CozeApiCall> calls = this.list(new LambdaQueryWrapper<CozeApiCall>()
.eq(CozeApiCall::getUserId, userId)
.eq(CozeApiCall::getIsDeleted, 0)
.isNotNull(CozeApiCall::getTotalTokens));
return calls.stream().mapToLong(call -> call.getTotalTokens() != null ? call.getTotalTokens() : 0).sum();
}
@Override
public BigDecimal sumCostByUserId(String userId) {
List<CozeApiCall> calls = this.list(new LambdaQueryWrapper<CozeApiCall>()
.eq(CozeApiCall::getUserId, userId)
.eq(CozeApiCall::getIsDeleted, 0)
.isNotNull(CozeApiCall::getCost));
return calls.stream()
.map(call -> call.getCost() != null ? call.getCost() : BigDecimal.ZERO)
.reduce(BigDecimal.ZERO, BigDecimal::add);
}
/**
* 转换为响应对象
*/
private CozeApiCallResponse convertToResponse(CozeApiCall apiCall) {
CozeApiCallResponse response = new CozeApiCallResponse();
BeanUtils.copyProperties(apiCall, response);
response.setId(apiCall.getId());
if (apiCall.getCreateTime() != null) {
response.setCreateTime(apiCall.getCreateTime().format(DATE_TIME_FORMATTER));
}
if (apiCall.getUpdateTime() != null) {
response.setUpdateTime(apiCall.getUpdateTime().format(DATE_TIME_FORMATTER));
}
if (apiCall.getStartTime() != null) {
response.setStartTime(apiCall.getStartTime().format(DATE_TIME_FORMATTER));
}
if (apiCall.getEndTime() != null) {
response.setEndTime(apiCall.getEndTime().format(DATE_TIME_FORMATTER));
}
if (apiCall.getPollStartTime() != null) {
response.setPollStartTime(apiCall.getPollStartTime().format(DATE_TIME_FORMATTER));
}
if (apiCall.getPollEndTime() != null) {
response.setPollEndTime(apiCall.getPollEndTime().format(DATE_TIME_FORMATTER));
}
return response;
}
/**
* 转换分页对象为PageResult对象
*/
private PageResult<CozeApiCallResponse> convertPageToPageResult(Page<CozeApiCall> page) {
PageResult<CozeApiCallResponse> pageResult = new PageResult<>();
pageResult.setCurrent(page.getCurrent());
pageResult.setSize(page.getSize());
pageResult.setTotal(page.getTotal());
pageResult.setPages(page.getPages());
pageResult.setRecords(page.getRecords().stream()
.map(this::convertToResponse)
.collect(Collectors.toList()));
return pageResult;
}
}
@@ -0,0 +1,523 @@
package com.emotion.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.emotion.dto.request.AiConfigCallStatsRequest;
import com.emotion.dto.response.AiConfigCallStatsResponse;
import com.emotion.dto.response.AiConfigCallStatsItem;
import com.emotion.dto.response.DashboardStatsResponse;
import com.emotion.entity.*;
import com.emotion.mapper.AiConfigMapper;
import com.emotion.service.*;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.lang.management.ManagementFactory;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 仪表盘服务实现类
*
* @author system
* @date 2025-10-31
*/
@Slf4j
@Service
public class DashboardServiceImpl implements DashboardService {
@Autowired
private UserService userService;
@Autowired
private GuestUserService guestUserService;
@Autowired
private ConversationService conversationService;
@Autowired
private MessageService messageService;
@Autowired
private DiaryPostService diaryPostService;
@Autowired
private CommunityPostService communityPostService;
@Autowired
private EmotionRecordService emotionRecordService;
@Autowired
private CozeApiCallService cozeApiCallService;
@Autowired
private AiConfigService aiConfigService;
@Autowired
private AdminService adminService;
@Autowired
private AiConfigMapper aiConfigMapper;
@Autowired
private AchievementService achievementService;
@Autowired
private RewardService rewardService;
@Override
public DashboardStatsResponse getDashboardStats() {
log.info("获取仪表盘统计数据");
try {
return DashboardStatsResponse.builder()
.userStats(getUserStats())
.contentStats(getContentStats())
.aiServiceStats(getAiServiceStats())
.systemStats(getSystemStats())
.recentActivities(getRecentActivities())
.userGrowthTrends(getUserGrowthTrends(7))
.recentLogins(getRecentLogins(10))
.updateTime(LocalDateTime.now())
.build();
} catch (Exception e) {
log.error("获取仪表盘统计数据失败", e);
// 返回默认数据,避免前端报错
return getDefaultDashboardStats();
}
}
@Override
public DashboardStatsResponse.UserStats getUserStats() {
try {
// 获取用户统计数据
Long totalUsers = userService.count(new LambdaQueryWrapper<User>()
.eq(User::getIsDeleted, 0));
Long guestUsers = guestUserService.count(new LambdaQueryWrapper<GuestUser>()
.eq(GuestUser::getIsDeleted, 0));
// 获取今日新增用户
LocalDateTime todayStart = LocalDate.now().atStartOfDay();
LocalDateTime todayEnd = todayStart.plusDays(1);
Long todayNewUsers = userService.count(new LambdaQueryWrapper<User>()
.eq(User::getIsDeleted, 0)
.ge(User::getCreateTime, todayStart)
.lt(User::getCreateTime, todayEnd));
// 活跃用户数(最近7天有活动的用户)
LocalDateTime sevenDaysAgo = LocalDateTime.now().minusDays(7);
Long activeUsers = userService.count(new LambdaQueryWrapper<User>()
.eq(User::getIsDeleted, 0)
.ge(User::getLastActiveTime, sevenDaysAgo));
return DashboardStatsResponse.UserStats.builder()
.totalUsers(totalUsers)
.todayNewUsers(todayNewUsers)
.activeUsers(activeUsers)
.guestUsers(guestUsers)
.build();
} catch (Exception e) {
log.error("获取用户统计数据失败", e);
return DashboardStatsResponse.UserStats.builder()
.totalUsers(0L)
.todayNewUsers(0L)
.activeUsers(0L)
.guestUsers(0L)
.build();
}
}
@Override
public DashboardStatsResponse.ContentStats getContentStats() {
try {
Long totalConversations = conversationService.count(new LambdaQueryWrapper<Conversation>()
.eq(Conversation::getIsDeleted, 0));
Long totalMessages = messageService.count(new LambdaQueryWrapper<Message>()
.eq(Message::getIsDeleted, 0));
Long diaryPosts = diaryPostService.count(new LambdaQueryWrapper<DiaryPost>()
.eq(DiaryPost::getIsDeleted, 0));
Long communityPosts = communityPostService.count(new LambdaQueryWrapper<CommunityPost>()
.eq(CommunityPost::getIsDeleted, 0));
Long emotionRecords = emotionRecordService.count(new LambdaQueryWrapper<EmotionRecord>()
.eq(EmotionRecord::getIsDeleted, 0));
return DashboardStatsResponse.ContentStats.builder()
.totalConversations(totalConversations)
.totalMessages(totalMessages)
.diaryPosts(diaryPosts)
.communityPosts(communityPosts)
.emotionRecords(emotionRecords)
.build();
} catch (Exception e) {
log.error("获取内容统计数据失败", e);
return DashboardStatsResponse.ContentStats.builder()
.totalConversations(0L)
.totalMessages(0L)
.diaryPosts(0L)
.communityPosts(0L)
.emotionRecords(0L)
.build();
}
}
@Override
public DashboardStatsResponse.AiServiceStats getAiServiceStats() {
try {
Long totalApiCalls = cozeApiCallService.count(new LambdaQueryWrapper<CozeApiCall>()
.eq(CozeApiCall::getIsDeleted, 0));
Long aiConfigCount = aiConfigService.count(new LambdaQueryWrapper<AiConfig>()
.eq(AiConfig::getIsDeleted, 0));
// 获取今日API调用次数
LocalDateTime todayStart = LocalDate.now().atStartOfDay();
LocalDateTime todayEnd = todayStart.plusDays(1);
Long todayApiCalls = cozeApiCallService.count(new LambdaQueryWrapper<CozeApiCall>()
.eq(CozeApiCall::getIsDeleted, 0)
.ge(CozeApiCall::getCreateTime, todayStart)
.lt(CozeApiCall::getCreateTime, todayEnd));
// 获取成功和失败的调用次数
Long successfulCalls = cozeApiCallService.count(new LambdaQueryWrapper<CozeApiCall>()
.eq(CozeApiCall::getIsDeleted, 0)
.eq(CozeApiCall::getStatus, "success"));
Long failedCalls = cozeApiCallService.count(new LambdaQueryWrapper<CozeApiCall>()
.eq(CozeApiCall::getIsDeleted, 0)
.eq(CozeApiCall::getStatus, "failed"));
// 计算平均响应时间(从duration_ms字段)
List<CozeApiCall> apiCalls = cozeApiCallService.list(new LambdaQueryWrapper<CozeApiCall>()
.eq(CozeApiCall::getIsDeleted, 0)
.isNotNull(CozeApiCall::getDurationMs)
.last("LIMIT 1000")); // 取最近1000条记录计算平均值
Double avgResponseTime = apiCalls.stream()
.filter(call -> call.getDurationMs() != null)
.mapToInt(CozeApiCall::getDurationMs)
.average()
.orElse(0.0);
return DashboardStatsResponse.AiServiceStats.builder()
.totalApiCalls(totalApiCalls)
.todayApiCalls(todayApiCalls)
.successfulCalls(successfulCalls)
.failedCalls(failedCalls)
.avgResponseTime(avgResponseTime)
.aiConfigCount(aiConfigCount)
.build();
} catch (Exception e) {
log.error("获取AI服务统计数据失败", e);
return DashboardStatsResponse.AiServiceStats.builder()
.totalApiCalls(0L)
.todayApiCalls(0L)
.successfulCalls(0L)
.failedCalls(0L)
.avgResponseTime(0.0)
.aiConfigCount(0L)
.build();
}
}
@Override
public DashboardStatsResponse.SystemStats getSystemStats() {
try {
Long adminCount = adminService.count(new LambdaQueryWrapper<Admin>()
.eq(Admin::getIsDeleted, 0)
.eq(Admin::getStatus, 1)); // 只统计正常状态的管理员
Long achievementCount = achievementService.count(new LambdaQueryWrapper<Achievement>()
.eq(Achievement::getIsDeleted, 0));
Long rewardCount = rewardService.count(new LambdaQueryWrapper<Reward>()
.eq(Reward::getIsDeleted, 0));
// 获取系统运行时间
long uptimeMs = ManagementFactory.getRuntimeMXBean().getUptime();
String uptime = formatUptime(uptimeMs);
return DashboardStatsResponse.SystemStats.builder()
.adminCount(adminCount)
.achievementCount(achievementCount)
.rewardCount(rewardCount)
.uptime(uptime)
.build();
} catch (Exception e) {
log.error("获取系统统计数据失败", e);
return DashboardStatsResponse.SystemStats.builder()
.adminCount(0L)
.achievementCount(0L)
.rewardCount(0L)
.uptime("未知")
.build();
}
}
/**
* 获取最近活动
*/
private List<DashboardStatsResponse.RecentActivity> getRecentActivities() {
List<DashboardStatsResponse.RecentActivity> activities = new ArrayList<>();
try {
// 获取最近注册的用户
List<User> recentUsers = userService.list(new LambdaQueryWrapper<User>()
.eq(User::getIsDeleted, 0)
.orderByDesc(User::getCreateTime)
.last("LIMIT 3"));
for (User user : recentUsers) {
activities.add(DashboardStatsResponse.RecentActivity.builder()
.type("user_register")
.description("新用户注册: " + (user.getNickname() != null ? user.getNickname() : user.getUsername()))
.userId(user.getId())
.username(user.getNickname() != null ? user.getNickname() : user.getUsername())
.activityTime(user.getCreateTime())
.extraData(new HashMap<>())
.build());
}
// 获取最近的对话
List<Conversation> recentConversations = conversationService.list(new LambdaQueryWrapper<Conversation>()
.eq(Conversation::getIsDeleted, 0)
.orderByDesc(Conversation::getCreateTime)
.last("LIMIT 3"));
for (Conversation conversation : recentConversations) {
Map<String, Object> extraData = new HashMap<>();
extraData.put("conversationId", conversation.getId());
extraData.put("title", conversation.getTitle());
activities.add(DashboardStatsResponse.RecentActivity.builder()
.type("ai_chat")
.description("新的AI对话: " + (conversation.getTitle() != null ? conversation.getTitle() : "未命名对话"))
.userId(conversation.getUserId())
.username("用户")
.activityTime(conversation.getCreateTime())
.extraData(extraData)
.build());
}
// 获取最近的日记帖子
List<DiaryPost> recentDiaryPosts = diaryPostService.list(new LambdaQueryWrapper<DiaryPost>()
.eq(DiaryPost::getIsDeleted, 0)
.orderByDesc(DiaryPost::getCreateTime)
.last("LIMIT 2"));
for (DiaryPost post : recentDiaryPosts) {
Map<String, Object> extraData = new HashMap<>();
extraData.put("postId", post.getId());
extraData.put("title", post.getTitle());
activities.add(DashboardStatsResponse.RecentActivity.builder()
.type("diary_post")
.description("新的日记: " + (post.getTitle() != null ? post.getTitle() : "无标题"))
.userId(post.getUserId())
.username("用户")
.activityTime(post.getCreateTime())
.extraData(extraData)
.build());
}
// 按时间排序,取最新的10条
activities.sort((a, b) -> b.getActivityTime().compareTo(a.getActivityTime()));
if (activities.size() > 10) {
activities = activities.subList(0, 10);
}
} catch (Exception e) {
log.error("获取最近活动失败", e);
}
return activities;
}
/**
* 格式化运行时间
*/
private String formatUptime(long uptimeMs) {
long seconds = uptimeMs / 1000;
long minutes = seconds / 60;
long hours = minutes / 60;
long days = hours / 24;
if (days > 0) {
return String.format("%d天%d小时%d分钟", days, hours % 24, minutes % 60);
} else if (hours > 0) {
return String.format("%d小时%d分钟", hours, minutes % 60);
} else if (minutes > 0) {
return String.format("%d分钟", minutes);
} else {
return String.format("%d秒", seconds);
}
}
/**
* 获取默认仪表盘统计数据(用于异常情况)
*/
private DashboardStatsResponse getDefaultDashboardStats() {
return DashboardStatsResponse.builder()
.userStats(DashboardStatsResponse.UserStats.builder()
.totalUsers(0L)
.todayNewUsers(0L)
.activeUsers(0L)
.guestUsers(0L)
.build())
.contentStats(DashboardStatsResponse.ContentStats.builder()
.totalConversations(0L)
.totalMessages(0L)
.diaryPosts(0L)
.communityPosts(0L)
.emotionRecords(0L)
.build())
.aiServiceStats(DashboardStatsResponse.AiServiceStats.builder()
.totalApiCalls(0L)
.todayApiCalls(0L)
.successfulCalls(0L)
.failedCalls(0L)
.avgResponseTime(0.0)
.aiConfigCount(0L)
.build())
.systemStats(DashboardStatsResponse.SystemStats.builder()
.adminCount(0L)
.achievementCount(0L)
.rewardCount(0L)
.uptime("未知")
.build())
.recentActivities(new ArrayList<>())
.userGrowthTrends(new ArrayList<>())
.recentLogins(new ArrayList<>())
.updateTime(LocalDateTime.now())
.build();
}
@Override
public List<DashboardStatsResponse.UserGrowthTrend> getUserGrowthTrends(int days) {
List<DashboardStatsResponse.UserGrowthTrend> trends = new ArrayList<>();
try {
LocalDate endDate = LocalDate.now();
LocalDate startDate = endDate.minusDays(days - 1);
// 获取指定日期范围内每天的用户注册数据
for (int i = 0; i < days; i++) {
LocalDate currentDate = startDate.plusDays(i);
LocalDateTime dayStart = currentDate.atStartOfDay();
LocalDateTime dayEnd = dayStart.plusDays(1);
// 统计当天新增用户数
Long newUsers = userService.count(new LambdaQueryWrapper<User>()
.eq(User::getIsDeleted, 0)
.ge(User::getCreateTime, dayStart)
.lt(User::getCreateTime, dayEnd));
// 统计截止到当天的总用户数
Long totalUsers = userService.count(new LambdaQueryWrapper<User>()
.eq(User::getIsDeleted, 0)
.lt(User::getCreateTime, dayEnd));
trends.add(DashboardStatsResponse.UserGrowthTrend.builder()
.date(currentDate.toString())
.newUsers(newUsers)
.totalUsers(totalUsers)
.build());
}
} catch (Exception e) {
log.error("获取用户增长趋势数据失败", e);
}
return trends;
}
@Override
public List<DashboardStatsResponse.RecentLogin> getRecentLogins(int limit) {
List<DashboardStatsResponse.RecentLogin> recentLogins = new ArrayList<>();
try {
// 获取最近活跃的用户(按最后活跃时间排序)
List<User> recentUsers = userService.list(new LambdaQueryWrapper<User>()
.eq(User::getIsDeleted, 0)
.isNotNull(User::getLastActiveTime)
.orderByDesc(User::getLastActiveTime)
.last("LIMIT " + limit));
for (User user : recentUsers) {
String timeDescription = formatTimeDescription(user.getLastActiveTime());
recentLogins.add(DashboardStatsResponse.RecentLogin.builder()
.userId(user.getId())
.username(user.getUsername())
.nickname(user.getNickname())
.avatar(user.getAvatar())
.loginTime(user.getLastActiveTime())
.timeDescription(timeDescription)
.build());
}
} catch (Exception e) {
log.error("获取最近登录用户失败", e);
}
return recentLogins;
}
@Override
public AiConfigCallStatsResponse getAiConfigCallStats(AiConfigCallStatsRequest request) {
Integer limit = request != null ? request.getLimit() : null;
if (limit == null) {
limit = 20;
}
// 防御性限制,避免过大查询
if (limit > 200) {
limit = 200;
}
if (limit < 1) {
limit = 1;
}
List<AiConfigCallStatsItem> items = aiConfigMapper.selectAiConfigCallStats(limit);
return AiConfigCallStatsResponse.builder()
.items(items)
.build();
}
/**
* 格式化时间描述
*/
private String formatTimeDescription(LocalDateTime dateTime) {
if (dateTime == null) {
return "未知";
}
LocalDateTime now = LocalDateTime.now();
long minutes = java.time.Duration.between(dateTime, now).toMinutes();
if (minutes < 1) {
return "刚刚";
} else if (minutes < 60) {
return minutes + "分钟前";
} else if (minutes < 1440) { // 24小时
long hours = minutes / 60;
return hours + "小时前";
} else {
long days = minutes / 1440;
if (days == 1) {
return "昨天";
} else if (days < 7) {
return days + "天前";
} else {
return dateTime.toLocalDate().toString();
}
}
}
}
@@ -0,0 +1,322 @@
package com.emotion.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.emotion.common.PageResult;
import com.emotion.dto.request.DiaryCommentCreateRequest;
import com.emotion.dto.request.DiaryCommentPageRequest;
import com.emotion.dto.response.DiaryCommentResponse;
import com.emotion.entity.DiaryComment;
import com.emotion.mapper.DiaryCommentMapper;
import com.emotion.service.DiaryCommentService;
import com.emotion.util.SnowflakeIdGenerator;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.stream.Collectors;
/**
* 日记评论服务实现类
*
* @author huazhongmin
* @date 2025-07-23
*/
@Service
public class DiaryCommentServiceImpl extends ServiceImpl<DiaryCommentMapper, DiaryComment> implements DiaryCommentService {
@Autowired
private SnowflakeIdGenerator snowflakeIdGenerator;
@Autowired
private ObjectMapper objectMapper;
private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
@Override
public PageResult<DiaryCommentResponse> getPageWithResponse(DiaryCommentPageRequest request) {
Page<DiaryComment> page = new Page<>(request.getCurrent(), request.getSize());
LambdaQueryWrapper<DiaryComment> wrapper = new LambdaQueryWrapper<>();
// 基础查询条件
wrapper.eq(DiaryComment::getIsDeleted, 0);
// 根据请求参数添加查询条件
if (StringUtils.hasText(request.getDiaryId())) {
wrapper.eq(DiaryComment::getDiaryId, request.getDiaryId());
}
if (StringUtils.hasText(request.getUserId())) {
wrapper.eq(DiaryComment::getUserId, request.getUserId());
}
if (StringUtils.hasText(request.getCommentType())) {
wrapper.eq(DiaryComment::getCommentType, request.getCommentType());
}
if (request.getTopLevelOnly() != null && request.getTopLevelOnly()) {
wrapper.isNull(DiaryComment::getParentCommentId);
}
// 排序
wrapper.orderByDesc(DiaryComment::getIsTop)
.orderByDesc(DiaryComment::getPublishTime);
IPage<DiaryComment> resultPage = this.page(page, wrapper);
return convertPageToResponse(resultPage);
}
@Override
public DiaryCommentResponse getCommentResponseById(String id) {
DiaryComment comment = this.getById(id);
if (comment == null) {
return null;
}
return convertToResponse(comment);
}
@Override
public List<DiaryCommentResponse> getCommentTreeWithResponse(String diaryId) {
List<DiaryComment> comments = this.getCommentTree(diaryId);
return comments.stream()
.map(this::convertToResponse)
.collect(Collectors.toList());
}
@Override
public DiaryCommentResponse createCommentWithResponse(DiaryCommentCreateRequest request) {
DiaryComment comment = DiaryComment.builder()
.id(snowflakeIdGenerator.nextIdAsString()) // 使用雪花算法生成ID
.diaryId(request.getDiaryId())
.userId(request.getUserId())
.content(request.getContent())
.images(convertListToJson(request.getImages()))
.parentCommentId(request.getParentCommentId())
.commentType("user")
.likeCount(0)
.replyCount(0)
.isAnonymous(request.getIsAnonymous())
.isTop(0)
.status("published")
.publishTime(LocalDateTime.now())
.build();
this.save(comment);
// 如果有父评论,更新父评论的回复数
if (StringUtils.hasText(request.getParentCommentId())) {
this.incrementReplyCount(request.getParentCommentId());
this.updateLastReplyTime(request.getParentCommentId());
}
return convertToResponse(comment);
}
@Override
public DiaryCommentResponse updateCommentWithResponse(DiaryCommentCreateRequest request) {
LambdaUpdateWrapper<DiaryComment> wrapper = new LambdaUpdateWrapper<>();
wrapper.eq(DiaryComment::getId, request.getId())
.set(StringUtils.hasText(request.getContent()), DiaryComment::getContent, request.getContent())
.set(request.getImages() != null, DiaryComment::getImages, convertListToJson(request.getImages()))
.set(DiaryComment::getUpdateTime, LocalDateTime.now());
boolean updated = this.update(wrapper);
if (!updated) {
return null;
}
DiaryComment updatedComment = this.getById(request.getId());
return updatedComment != null ? convertToResponse(updatedComment) : null;
}
@Override
public boolean deleteComment(String commentId) {
return this.removeById(commentId);
}
@Override
public boolean softDeleteComment(String commentId) {
LambdaUpdateWrapper<DiaryComment> wrapper = new LambdaUpdateWrapper<>();
wrapper.eq(DiaryComment::getId, commentId)
.set(DiaryComment::getIsDeleted, 1)
.set(DiaryComment::getUpdateTime, LocalDateTime.now());
return this.update(wrapper);
}
@Override
public boolean restoreComment(String commentId) {
LambdaUpdateWrapper<DiaryComment> wrapper = new LambdaUpdateWrapper<>();
wrapper.eq(DiaryComment::getId, commentId)
.set(DiaryComment::getIsDeleted, 0)
.set(DiaryComment::getUpdateTime, LocalDateTime.now());
return this.update(wrapper);
}
@Override
public boolean incrementLikeCount(String commentId) {
LambdaUpdateWrapper<DiaryComment> wrapper = new LambdaUpdateWrapper<>();
wrapper.eq(DiaryComment::getId, commentId)
.setSql("like_count = like_count + 1")
.set(DiaryComment::getUpdateTime, LocalDateTime.now());
return this.update(wrapper);
}
@Override
public boolean decrementLikeCount(String commentId) {
LambdaUpdateWrapper<DiaryComment> wrapper = new LambdaUpdateWrapper<>();
wrapper.eq(DiaryComment::getId, commentId)
.setSql("like_count = GREATEST(like_count - 1, 0)")
.set(DiaryComment::getUpdateTime, LocalDateTime.now());
return this.update(wrapper);
}
@Override
public boolean setTop(String commentId, Integer isTop) {
LambdaUpdateWrapper<DiaryComment> wrapper = new LambdaUpdateWrapper<>();
wrapper.eq(DiaryComment::getId, commentId)
.set(DiaryComment::getIsTop, isTop)
.set(DiaryComment::getUpdateTime, LocalDateTime.now());
return this.update(wrapper);
}
/**
* 获取评论树结构
*/
private List<DiaryComment> getCommentTree(String diaryId) {
// 获取所有顶级评论(没有父评论的评论)
LambdaQueryWrapper<DiaryComment> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(DiaryComment::getDiaryId, diaryId)
.isNull(DiaryComment::getParentCommentId)
.eq(DiaryComment::getIsDeleted, 0)
.orderByDesc(DiaryComment::getIsTop)
.orderByDesc(DiaryComment::getPublishTime);
List<DiaryComment> topComments = this.list(wrapper);
// 为每个顶级评论加载回复
return topComments.stream()
.peek(comment -> {
List<DiaryComment> replies = getRepliesByParentId(comment.getId());
// 这里可以递归加载更深层的回复,但为了性能考虑,通常只加载一层
})
.collect(Collectors.toList());
}
/**
* 根据父评论ID查询回复列表
*/
private List<DiaryComment> getRepliesByParentId(String parentCommentId) {
LambdaQueryWrapper<DiaryComment> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(DiaryComment::getParentCommentId, parentCommentId)
.eq(DiaryComment::getIsDeleted, 0)
.orderByAsc(DiaryComment::getPublishTime);
return this.list(wrapper);
}
/**
* 增加回复数
*/
private boolean incrementReplyCount(String commentId) {
LambdaUpdateWrapper<DiaryComment> wrapper = new LambdaUpdateWrapper<>();
wrapper.eq(DiaryComment::getId, commentId)
.setSql("reply_count = reply_count + 1")
.set(DiaryComment::getUpdateTime, LocalDateTime.now());
return this.update(wrapper);
}
/**
* 减少回复数
*/
private boolean decrementReplyCount(String commentId) {
LambdaUpdateWrapper<DiaryComment> wrapper = new LambdaUpdateWrapper<>();
wrapper.eq(DiaryComment::getId, commentId)
.setSql("reply_count = GREATEST(reply_count - 1, 0)")
.set(DiaryComment::getUpdateTime, LocalDateTime.now());
return this.update(wrapper);
}
/**
* 更新最后回复时间
*/
private boolean updateLastReplyTime(String commentId) {
LambdaUpdateWrapper<DiaryComment> wrapper = new LambdaUpdateWrapper<>();
wrapper.eq(DiaryComment::getId, commentId)
.set(DiaryComment::getLastReplyTime, LocalDateTime.now())
.set(DiaryComment::getUpdateTime, LocalDateTime.now());
return this.update(wrapper);
}
/**
* 转换为响应对象
*/
private DiaryCommentResponse convertToResponse(DiaryComment comment) {
DiaryCommentResponse response = new DiaryCommentResponse();
BeanUtils.copyProperties(comment, response);
// 转换时间格式
if (comment.getPublishTime() != null) {
response.setPublishTime(comment.getPublishTime().format(DATE_TIME_FORMATTER));
}
if (comment.getLastReplyTime() != null) {
response.setLastReplyTime(comment.getLastReplyTime().format(DATE_TIME_FORMATTER));
}
if (comment.getCreateTime() != null) {
response.setCreateTime(comment.getCreateTime().format(DATE_TIME_FORMATTER));
}
if (comment.getUpdateTime() != null) {
response.setUpdateTime(comment.getUpdateTime().format(DATE_TIME_FORMATTER));
}
// 转换JSON字段
try {
if (comment.getImages() != null) {
response.setImages(objectMapper.readValue(comment.getImages(), new TypeReference<List<String>>() {
}));
}
if (comment.getMetadata() != null) {
response.setMetadata(objectMapper.readValue(comment.getMetadata(), Object.class));
}
} catch (JsonProcessingException e) {
// 忽略JSON解析错误
}
return response;
}
/**
* 转换分页对象为响应对象
*/
private PageResult<DiaryCommentResponse> convertPageToResponse(IPage<DiaryComment> page) {
PageResult<DiaryCommentResponse> responsePage = new PageResult<>();
responsePage.setCurrent(page.getCurrent());
responsePage.setSize(page.getSize());
responsePage.setTotal(page.getTotal());
responsePage.setPages(page.getPages());
responsePage.setRecords(
page.getRecords().stream()
.map(this::convertToResponse)
.collect(Collectors.toList()));
return responsePage;
}
/**
* 将List转换为JSON字符串
*/
private String convertListToJson(List<?> list) {
if (list == null || list.isEmpty()) {
return null;
}
try {
return objectMapper.writeValueAsString(list);
} catch (JsonProcessingException e) {
return null;
}
}
}
@@ -0,0 +1,467 @@
package com.emotion.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.emotion.common.PageResult;
import com.emotion.dto.request.DiaryPostCreateRequest;
import com.emotion.dto.request.DiaryPostPageRequest;
import com.emotion.dto.request.DiaryPostUpdateRequest;
import com.emotion.dto.response.DiaryPostResponse;
import com.emotion.entity.DiaryPost;
import com.emotion.mapper.DiaryPostMapper;
import com.emotion.service.DiaryPostService;
import com.emotion.service.DiaryCommentService;
import com.emotion.service.AiChatService;
import com.emotion.util.SnowflakeIdGenerator;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.stream.Collectors;
/**
* 用户日记服务实现类
*
* @author huazhongmin
* @date 2025-07-23
*/
@Service
public class DiaryPostServiceImpl extends ServiceImpl<DiaryPostMapper, DiaryPost> implements DiaryPostService {
@Autowired
private AiChatService aiChatService;
@Autowired
private DiaryCommentService diaryCommentService;
@Autowired
private ObjectMapper objectMapper;
@Autowired
private SnowflakeIdGenerator snowflakeIdGenerator;
private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
@Override
public PageResult<DiaryPostResponse> getPageWithResponse(DiaryPostPageRequest request) {
Page<DiaryPost> page = new Page<>(request.getCurrent(), request.getSize());
LambdaQueryWrapper<DiaryPost> wrapper = new LambdaQueryWrapper<>();
// 基础查询条件
wrapper.eq(DiaryPost::getIsDeleted, 0);
// 根据请求参数添加查询条件
if (StringUtils.hasText(request.getUserId())) {
wrapper.eq(DiaryPost::getUserId, request.getUserId());
}
if (Boolean.TRUE.equals(request.getPublicOnly())) {
wrapper.eq(DiaryPost::getIsPublic, 1)
.eq(DiaryPost::getStatus, "published");
}
if (Boolean.TRUE.equals(request.getFeaturedOnly())) {
wrapper.eq(DiaryPost::getFeatured, 1)
.eq(DiaryPost::getIsPublic, 1)
.eq(DiaryPost::getStatus, "published");
}
if (StringUtils.hasText(request.getMood())) {
wrapper.eq(DiaryPost::getMood, request.getMood());
}
if (StringUtils.hasText(request.getTag())) {
// 标签查询可能需要特殊处理,因为tags字段是JSON格式
wrapper.like(DiaryPost::getTags, request.getTag());
}
// 排序
wrapper.orderByDesc(DiaryPost::getPriority)
.orderByDesc(DiaryPost::getPublishTime);
IPage<DiaryPost> resultPage = this.page(page, wrapper);
List<DiaryPostResponse> responses = resultPage.getRecords().stream()
.map(this::convertToResponse)
.collect(Collectors.toList());
return createPageResult(resultPage, responses);
}
@Override
public DiaryPostResponse getDiaryPostResponseById(String id) {
DiaryPost diaryPost = this.getById(id);
if (diaryPost == null) {
return null;
}
// 增加浏览数
incrementViewCount(id);
return convertToResponse(diaryPost);
}
@Override
public DiaryPostResponse createDiaryPostWithResponse(DiaryPostCreateRequest request) {
// 处理标题:如果为空,则设置为null或生成默认标题
String title = request.getTitle();
if (title == null || title.trim().isEmpty()) {
// 可以选择设置为null,或者生成一个默认标题
// 这里我们设置为null,让数据库使用默认值
title = null;
}
DiaryPost diaryPost = DiaryPost.builder()
.id(snowflakeIdGenerator.nextIdAsString()) // 使用雪花算法生成ID
.userId(request.getUserId())
.title(title)
.content(request.getContent())
.images(convertListToJson(request.getImages()))
.videos(convertListToJson(request.getVideos()))
.location(request.getLocation())
.weather(request.getWeather())
.mood(request.getMood())
.tags(convertListToJson(request.getTags()))
.isPublic(request.getIsPublic())
.isAnonymous(request.getIsAnonymous())
.viewCount(0)
.likeCount(0)
.commentCount(0)
.shareCount(0)
.publishTime(LocalDateTime.now())
.status("published")
.priority(0)
.featured(0)
.build();
this.save(diaryPost);
return convertToResponse(diaryPost);
}
@Override
public DiaryPostResponse updateDiaryPostWithResponse(DiaryPostUpdateRequest request) {
LambdaUpdateWrapper<DiaryPost> wrapper = new LambdaUpdateWrapper<>();
wrapper.eq(DiaryPost::getId, request.getId())
.set(StringUtils.hasText(request.getTitle()), DiaryPost::getTitle, request.getTitle())
.set(StringUtils.hasText(request.getContent()), DiaryPost::getContent, request.getContent())
.set(request.getImages() != null, DiaryPost::getImages, convertListToJson(request.getImages()))
.set(request.getVideos() != null, DiaryPost::getVideos, convertListToJson(request.getVideos()))
.set(StringUtils.hasText(request.getLocation()), DiaryPost::getLocation, request.getLocation())
.set(StringUtils.hasText(request.getWeather()), DiaryPost::getWeather, request.getWeather())
.set(StringUtils.hasText(request.getMood()), DiaryPost::getMood, request.getMood())
.set(request.getTags() != null, DiaryPost::getTags, convertListToJson(request.getTags()))
.set(request.getIsPublic() != null, DiaryPost::getIsPublic, request.getIsPublic())
.set(request.getIsAnonymous() != null, DiaryPost::getIsAnonymous, request.getIsAnonymous())
.set(StringUtils.hasText(request.getStatus()), DiaryPost::getStatus, request.getStatus())
.set(DiaryPost::getUpdateTime, LocalDateTime.now());
boolean updated = this.update(wrapper);
if (!updated) {
return null;
}
DiaryPost updatedDiaryPost = this.getById(request.getId());
return convertToResponse(updatedDiaryPost);
}
@Override
public boolean deleteDiaryPost(String diaryId) {
return this.removeById(diaryId);
}
@Override
public boolean softDeleteDiaryPost(String diaryId) {
LambdaUpdateWrapper<DiaryPost> wrapper = new LambdaUpdateWrapper<>();
wrapper.eq(DiaryPost::getId, diaryId)
.set(DiaryPost::getIsDeleted, 1)
.set(DiaryPost::getUpdateTime, LocalDateTime.now());
return this.update(wrapper);
}
@Override
public boolean restoreDiaryPost(String diaryId) {
LambdaUpdateWrapper<DiaryPost> wrapper = new LambdaUpdateWrapper<>();
wrapper.eq(DiaryPost::getId, diaryId)
.set(DiaryPost::getIsDeleted, 0)
.set(DiaryPost::getUpdateTime, LocalDateTime.now());
return this.update(wrapper);
}
@Override
public boolean incrementViewCount(String diaryId) {
LambdaUpdateWrapper<DiaryPost> wrapper = new LambdaUpdateWrapper<>();
wrapper.eq(DiaryPost::getId, diaryId)
.setSql("view_count = view_count + 1")
.set(DiaryPost::getUpdateTime, LocalDateTime.now());
return this.update(wrapper);
}
@Override
public boolean incrementLikeCount(String diaryId) {
LambdaUpdateWrapper<DiaryPost> wrapper = new LambdaUpdateWrapper<>();
wrapper.eq(DiaryPost::getId, diaryId)
.setSql("like_count = like_count + 1")
.set(DiaryPost::getUpdateTime, LocalDateTime.now());
return this.update(wrapper);
}
@Override
public boolean decrementLikeCount(String diaryId) {
LambdaUpdateWrapper<DiaryPost> wrapper = new LambdaUpdateWrapper<>();
wrapper.eq(DiaryPost::getId, diaryId)
.setSql("like_count = GREATEST(like_count - 1, 0)")
.set(DiaryPost::getUpdateTime, LocalDateTime.now());
return this.update(wrapper);
}
@Override
public boolean incrementCommentCount(String diaryId) {
LambdaUpdateWrapper<DiaryPost> wrapper = new LambdaUpdateWrapper<>();
wrapper.eq(DiaryPost::getId, diaryId)
.setSql("comment_count = comment_count + 1")
.set(DiaryPost::getUpdateTime, LocalDateTime.now());
return this.update(wrapper);
}
@Override
public boolean decrementCommentCount(String diaryId) {
LambdaUpdateWrapper<DiaryPost> wrapper = new LambdaUpdateWrapper<>();
wrapper.eq(DiaryPost::getId, diaryId)
.setSql("comment_count = GREATEST(comment_count - 1, 0)")
.set(DiaryPost::getUpdateTime, LocalDateTime.now());
return this.update(wrapper);
}
@Override
public boolean incrementShareCount(String diaryId) {
LambdaUpdateWrapper<DiaryPost> wrapper = new LambdaUpdateWrapper<>();
wrapper.eq(DiaryPost::getId, diaryId)
.setSql("share_count = share_count + 1")
.set(DiaryPost::getUpdateTime, LocalDateTime.now());
return this.update(wrapper);
}
@Override
public boolean updateLastCommentTime(String diaryId) {
LambdaUpdateWrapper<DiaryPost> wrapper = new LambdaUpdateWrapper<>();
wrapper.eq(DiaryPost::getId, diaryId)
.set(DiaryPost::getLastCommentTime, LocalDateTime.now())
.set(DiaryPost::getUpdateTime, LocalDateTime.now());
return this.update(wrapper);
}
@Override
public boolean setFeatured(String diaryId, Integer featured) {
LambdaUpdateWrapper<DiaryPost> wrapper = new LambdaUpdateWrapper<>();
wrapper.eq(DiaryPost::getId, diaryId)
.set(DiaryPost::getFeatured, featured)
.set(DiaryPost::getUpdateTime, LocalDateTime.now());
return this.update(wrapper);
}
@Override
public boolean setPriority(String diaryId, Integer priority) {
LambdaUpdateWrapper<DiaryPost> wrapper = new LambdaUpdateWrapper<>();
wrapper.eq(DiaryPost::getId, diaryId)
.set(DiaryPost::getPriority, priority)
.set(DiaryPost::getUpdateTime, LocalDateTime.now());
return this.update(wrapper);
}
@Override
public Long countByUserId(String userId) {
LambdaQueryWrapper<DiaryPost> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(DiaryPost::getUserId, userId)
.eq(DiaryPost::getIsDeleted, 0);
return this.count(wrapper);
}
@Override
public Long countPublicByUserId(String userId) {
LambdaQueryWrapper<DiaryPost> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(DiaryPost::getUserId, userId)
.eq(DiaryPost::getIsPublic, 1)
.eq(DiaryPost::getStatus, "published")
.eq(DiaryPost::getIsDeleted, 0);
return this.count(wrapper);
}
@Override
public Long countFeatured() {
LambdaQueryWrapper<DiaryPost> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(DiaryPost::getFeatured, 1)
.eq(DiaryPost::getIsDeleted, 0);
return this.count(wrapper);
}
@Override
public DiaryPostResponse publishDiaryWithAiComment(DiaryPostCreateRequest request) {
// 1. 保存日记
DiaryPost diaryPost = DiaryPost.builder()
.id(snowflakeIdGenerator.nextIdAsString()) // 使用雪花算法生成ID
.userId(request.getUserId())
.title(request.getTitle())
.content(request.getContent())
.images(convertListToJson(request.getImages()))
.videos(convertListToJson(request.getVideos()))
.location(request.getLocation())
.weather(request.getWeather())
.mood(request.getMood())
.tags(convertListToJson(request.getTags()))
.isPublic(request.getIsPublic())
.isAnonymous(request.getIsAnonymous())
.viewCount(0)
.likeCount(0)
.commentCount(0)
.shareCount(0)
.publishTime(LocalDateTime.now())
.status("published")
.priority(0)
.featured(0)
.build();
this.save(diaryPost);
// 2. 生成AI评论
String aiComment = null;
try {
String conversationId = "diary-" + diaryPost.getId();
aiComment = aiChatService.sendSummaryMessage(conversationId, diaryPost.getContent(), request.getUserId());
} catch (Exception e) {
aiComment = "开开:AI评论生成失败";
}
// 3. 写入AI评论到diary_comment表
if (aiComment != null) {
// 使用createCommentWithResponse方法创建AI评论
com.emotion.dto.request.DiaryCommentCreateRequest commentRequest = new com.emotion.dto.request.DiaryCommentCreateRequest();
commentRequest.setDiaryId(diaryPost.getId());
commentRequest.setUserId("system"); // AI评论使用system用户ID
commentRequest.setContent(aiComment);
commentRequest.setIsAnonymous(0);
diaryCommentService.createCommentWithResponse(commentRequest);
addAiComment(
diaryPost.getId(),
aiComment,
null,
null,
null,
null
);
}
// 4. 返回日记详情(含AI评论)
return convertToResponse(diaryPost);
}
/**
* 添加AI评论
*/
private boolean addAiComment(String diaryId, String aiComment, Object aiEmotionAnalysis,
BigDecimal aiSentimentScore, List<String> aiKeywords, String aiSuggestions) {
LambdaUpdateWrapper<DiaryPost> wrapper = new LambdaUpdateWrapper<>();
wrapper.eq(DiaryPost::getId, diaryId)
.set(DiaryPost::getAiComment, aiComment)
.set(DiaryPost::getAiCommentTime, LocalDateTime.now())
.set(DiaryPost::getAiEmotionAnalysis, convertObjectToJson(aiEmotionAnalysis))
.set(DiaryPost::getAiSentimentScore, aiSentimentScore)
.set(DiaryPost::getAiKeywords, convertListToJson(aiKeywords))
.set(DiaryPost::getAiSuggestions, aiSuggestions)
.set(DiaryPost::getUpdateTime, LocalDateTime.now());
return this.update(wrapper);
}
/**
* 创建PageResult对象
*/
private <T> PageResult<T> createPageResult(IPage<?> page, List<T> records) {
PageResult<T> pageResult = new PageResult<>();
pageResult.setCurrent(page.getCurrent());
pageResult.setSize(page.getSize());
pageResult.setTotal(page.getTotal());
pageResult.setPages(page.getPages());
pageResult.setRecords(records);
return pageResult;
}
/**
* 将List转换为JSON字符串
*/
private String convertListToJson(List<?> list) {
if (list == null || list.isEmpty()) {
return null;
}
try {
return objectMapper.writeValueAsString(list);
} catch (JsonProcessingException e) {
return null;
}
}
/**
* 将Object转换为JSON字符串
*/
private String convertObjectToJson(Object obj) {
if (obj == null) {
return null;
}
try {
return objectMapper.writeValueAsString(obj);
} catch (JsonProcessingException e) {
return null;
}
}
/**
* 转换实体为响应DTO
*/
private DiaryPostResponse convertToResponse(DiaryPost diaryPost) {
DiaryPostResponse response = new DiaryPostResponse();
BeanUtils.copyProperties(diaryPost, response);
// 转换时间格式
if (diaryPost.getPublishTime() != null) {
response.setPublishTime(diaryPost.getPublishTime().format(DATE_TIME_FORMATTER));
}
if (diaryPost.getLastCommentTime() != null) {
response.setLastCommentTime(diaryPost.getLastCommentTime().format(DATE_TIME_FORMATTER));
}
if (diaryPost.getAiCommentTime() != null) {
response.setAiCommentTime(diaryPost.getAiCommentTime().format(DATE_TIME_FORMATTER));
}
if (diaryPost.getCreateTime() != null) {
response.setCreateTime(diaryPost.getCreateTime().format(DATE_TIME_FORMATTER));
}
if (diaryPost.getUpdateTime() != null) {
response.setUpdateTime(diaryPost.getUpdateTime().format(DATE_TIME_FORMATTER));
}
// 转换JSON字段
try {
if (diaryPost.getImages() != null) {
response.setImages(objectMapper.readValue(diaryPost.getImages(), new TypeReference<List<String>>() {
}));
}
if (diaryPost.getVideos() != null) {
response.setVideos(objectMapper.readValue(diaryPost.getVideos(), new TypeReference<List<String>>() {
}));
}
if (diaryPost.getTags() != null) {
response.setTags(objectMapper.readValue(diaryPost.getTags(), new TypeReference<List<String>>() {
}));
}
if (diaryPost.getAiKeywords() != null) {
response.setAiKeywords(
objectMapper.readValue(diaryPost.getAiKeywords(), new TypeReference<List<String>>() {
}));
}
if (diaryPost.getAiEmotionAnalysis() != null) {
response.setAiEmotionAnalysis(objectMapper.readValue(diaryPost.getAiEmotionAnalysis(), Object.class));
}
if (diaryPost.getMetadata() != null) {
response.setMetadata(objectMapper.readValue(diaryPost.getMetadata(), Object.class));
}
} catch (JsonProcessingException e) {
// 忽略JSON解析错误
}
return response;
}
}
@@ -0,0 +1,266 @@
package com.emotion.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.emotion.entity.Dictionary;
import com.emotion.mapper.DictionaryMapper;
import com.emotion.service.DictionaryService;
import com.emotion.dto.request.dictionary.DictionaryCreateRequest;
import com.emotion.dto.request.dictionary.DictionaryPageRequest;
import com.emotion.dto.request.dictionary.DictionaryUpdateRequest;
import com.emotion.dto.response.dictionary.DictionaryResponse;
import com.emotion.common.PageResult;
import com.emotion.common.Result;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.stream.Collectors;
/**
* 字典Service实现类
*
* @author huazhongmin
* @date 2025-12-22
*/
@Service
public class DictionaryServiceImpl extends ServiceImpl<DictionaryMapper, Dictionary> implements DictionaryService {
/**
* 创建字典
*
* @param request 创建请求
* @return 创建结果
*/
@Override
public Result<DictionaryResponse> createDictionary(DictionaryCreateRequest request) {
// 检查字典类型+字典编码是否已存在
LambdaQueryWrapper<Dictionary> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(Dictionary::getDictType, request.getDictType())
.eq(Dictionary::getDictCode, request.getDictCode())
.eq(Dictionary::getIsDeleted, 0);
if (this.count(queryWrapper) > 0) {
return Result.error(400, "同一字典类型下字典编码已存在");
}
// 创建字典实体
Dictionary dictionary = new Dictionary();
BeanUtils.copyProperties(request, dictionary);
dictionary.setSortOrder(request.getSortOrder() != null ? request.getSortOrder() : 0);
// 保存字典
if (this.save(dictionary)) {
DictionaryResponse response = convertToResponse(dictionary);
return Result.success(response);
}
return Result.error(500, "创建字典失败");
}
/**
* 更新字典
*
* @param request 更新请求
* @return 更新结果
*/
@Override
public Result<DictionaryResponse> updateDictionary(DictionaryUpdateRequest request) {
// 检查字典是否存在
Dictionary dictionary = this.getById(request.getId());
if (dictionary == null || dictionary.getIsDeleted() == 1) {
return Result.error(400, "字典不存在");
}
// 检查字典类型+字典编码是否已存在(排除当前字典)
LambdaQueryWrapper<Dictionary> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(Dictionary::getDictType, request.getDictType())
.eq(Dictionary::getDictCode, request.getDictCode())
.ne(Dictionary::getId, request.getId())
.eq(Dictionary::getIsDeleted, 0);
if (this.count(queryWrapper) > 0) {
return Result.error(400, "同一字典类型下字典编码已存在");
}
// 更新字典实体
BeanUtils.copyProperties(request, dictionary);
// 保存更新
if (this.updateById(dictionary)) {
DictionaryResponse response = convertToResponse(dictionary);
return Result.success(response);
}
return Result.error(500, "更新字典失败");
}
/**
* 删除字典
*
* @param id 字典ID
* @return 删除结果
*/
@Override
public Result<Void> deleteDictionary(String id) {
// 检查字典是否存在
Dictionary dictionary = this.getById(id);
if (dictionary == null || dictionary.getIsDeleted() == 1) {
return Result.error(400, "字典不存在");
}
// 删除字典(逻辑删除)
if (this.removeById(id)) {
return Result.success();
}
return Result.error(500, "删除字典失败");
}
/**
* 获取字典详情
*
* @param id 字典ID
* @return 字典详情
*/
@Override
public Result<DictionaryResponse> getDictionary(String id) {
// 获取字典
Dictionary dictionary = this.getById(id);
if (dictionary == null || dictionary.getIsDeleted() == 1) {
return Result.error(400, "字典不存在");
}
// 转换为响应对象
DictionaryResponse response = convertToResponse(dictionary);
return Result.success(response);
}
/**
* 分页查询字典
*
* @param request 分页请求
* @return 分页结果
*/
@Override
public Result<PageResult<DictionaryResponse>> listDictionaries(DictionaryPageRequest request) {
// 构建查询条件
LambdaQueryWrapper<Dictionary> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(Dictionary::getIsDeleted, 0);
// 添加查询条件
if (request.getDictType() != null && !request.getDictType().isEmpty()) {
queryWrapper.eq(Dictionary::getDictType, request.getDictType());
}
if (request.getDictCode() != null && !request.getDictCode().isEmpty()) {
queryWrapper.like(Dictionary::getDictCode, request.getDictCode());
}
if (request.getDictName() != null && !request.getDictName().isEmpty()) {
queryWrapper.like(Dictionary::getDictName, request.getDictName());
}
if (request.getStatus() != null) {
queryWrapper.eq(Dictionary::getStatus, request.getStatus());
}
// 排序
queryWrapper.orderByAsc(Dictionary::getDictType).orderByAsc(Dictionary::getSortOrder).orderByAsc(Dictionary::getCreateTime);
// 分页查询
Page<Dictionary> page = this.page(new Page<>(request.getCurrent(), request.getSize()), queryWrapper);
// 转换为响应对象
List<DictionaryResponse> responseList = page.getRecords().stream()
.map(this::convertToResponse)
.collect(Collectors.toList());
// 构建分页结果
PageResult<DictionaryResponse> pageResult = new PageResult<>();
pageResult.setRecords(responseList);
pageResult.setCurrent(page.getCurrent());
pageResult.setSize(page.getSize());
pageResult.setTotal(page.getTotal());
pageResult.setPages(page.getPages());
return Result.success(pageResult);
}
/**
* 根据字典类型查询字典集合
*
* @param dictType 字典类型
* @return 字典集合
*/
@Override
public Result<List<DictionaryResponse>> getDictionariesByType(String dictType) {
// 构建查询条件
LambdaQueryWrapper<Dictionary> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(Dictionary::getDictType, dictType)
.eq(Dictionary::getIsDeleted, 0)
.orderByAsc(Dictionary::getSortOrder)
.orderByAsc(Dictionary::getCreateTime);
// 查询字典集合
List<Dictionary> dictionaryList = this.list(queryWrapper);
// 转换为响应对象
List<DictionaryResponse> responseList = dictionaryList.stream()
.map(this::convertToResponse)
.collect(Collectors.toList());
return Result.success(responseList);
}
/**
* 根据字典类型查询启用的字典集合
*
* @param dictType 字典类型
* @return 启用的字典集合
*/
@Override
public Result<List<DictionaryResponse>> getEnabledDictionariesByType(String dictType) {
// 构建查询条件
LambdaQueryWrapper<Dictionary> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(Dictionary::getDictType, dictType)
.eq(Dictionary::getStatus, 1)
.eq(Dictionary::getIsDeleted, 0)
.orderByAsc(Dictionary::getSortOrder)
.orderByAsc(Dictionary::getCreateTime);
// 查询字典集合
List<Dictionary> dictionaryList = this.list(queryWrapper);
// 转换为响应对象
List<DictionaryResponse> responseList = dictionaryList.stream()
.map(this::convertToResponse)
.collect(Collectors.toList());
return Result.success(responseList);
}
/**
* 实体转换为响应对象
*
* @param dictionary 字典实体
* @return 字典响应对象
*/
private DictionaryResponse convertToResponse(Dictionary dictionary) {
DictionaryResponse response = new DictionaryResponse();
// 基础字段映射
response.setId(dictionary.getId());
response.setDictType(dictionary.getDictType());
response.setDictCode(dictionary.getDictCode());
response.setDictName(dictionary.getDictName());
response.setDictValue(dictionary.getDictValue());
response.setSortOrder(dictionary.getSortOrder());
response.setStatus(dictionary.getStatus());
response.setCreateBy(dictionary.getCreateBy());
response.setUpdateBy(dictionary.getUpdateBy());
response.setRemarks(dictionary.getRemarks());
// 转换时间类型
if (dictionary.getCreateTime() != null) {
response.setCreateTime(dictionary.getCreateTime().toString());
}
if (dictionary.getUpdateTime() != null) {
response.setUpdateTime(dictionary.getUpdateTime().toString());
}
return response;
}
}
@@ -0,0 +1,225 @@
package com.emotion.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.emotion.common.BasePageRequest;
import com.emotion.common.PageResult;
import com.emotion.dto.request.EmotionAnalysisCreateRequest;
import com.emotion.dto.request.EmotionAnalysisPageRequest;
import com.emotion.dto.request.EmotionAnalysisUpdateRequest;
import com.emotion.dto.response.EmotionAnalysisResponse;
import com.emotion.entity.EmotionAnalysis;
import com.emotion.mapper.EmotionAnalysisMapper;
import com.emotion.service.EmotionAnalysisService;
import com.emotion.util.SnowflakeIdGenerator;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.stream.Collectors;
/**
* 情绪分析服务实现类
*
* @author huazhongmin
* @date 2025-07-24
*/
@Service
public class EmotionAnalysisServiceImpl extends ServiceImpl<EmotionAnalysisMapper, EmotionAnalysis> implements EmotionAnalysisService {
@Autowired
private SnowflakeIdGenerator snowflakeIdGenerator;
private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
@Override
public IPage<EmotionAnalysis> getPage(BasePageRequest request) {
Page<EmotionAnalysis> page = new Page<>(request.getCurrent(), request.getSize());
LambdaQueryWrapper<EmotionAnalysis> wrapper = new LambdaQueryWrapper<>();
// 关键词搜索
if (StringUtils.hasText(request.getKeyword())) {
wrapper.and(w -> w.like(EmotionAnalysis::getPrimaryEmotion, request.getKeyword())
.or().like(EmotionAnalysis::getPolarity, request.getKeyword()));
}
wrapper.eq(EmotionAnalysis::getIsDeleted, 0);
// 排序
if (StringUtils.hasText(request.getOrderBy())) {
if ("asc".equalsIgnoreCase(request.getOrderDirection())) {
wrapper.orderByAsc(EmotionAnalysis::getCreateTime);
} else {
wrapper.orderByDesc(EmotionAnalysis::getCreateTime);
}
} else {
wrapper.orderByDesc(EmotionAnalysis::getCreateTime);
}
return this.page(page, wrapper);
}
@Override
public IPage<EmotionAnalysis> getPageByUserId(BasePageRequest request, String userId) {
Page<EmotionAnalysis> page = new Page<>(request.getCurrent(), request.getSize());
LambdaQueryWrapper<EmotionAnalysis> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(EmotionAnalysis::getCreateBy, userId)
.eq(EmotionAnalysis::getIsDeleted, 0)
.orderByDesc(EmotionAnalysis::getCreateTime);
return this.page(page, wrapper);
}
@Override
public EmotionAnalysis getByMessageId(String messageId) {
LambdaQueryWrapper<EmotionAnalysis> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(EmotionAnalysis::getMessageId, messageId)
.eq(EmotionAnalysis::getIsDeleted, 0);
return this.getOne(wrapper);
}
@Override
public EmotionAnalysis createEmotionAnalysis(String messageId, String userId, String primaryEmotion,
String polarity, Double intensity, Double confidence) {
EmotionAnalysis analysis = new EmotionAnalysis();
analysis.setId(snowflakeIdGenerator.nextIdAsString()); // 使用雪花算法生成ID
analysis.setMessageId(messageId);
analysis.setCreateBy(userId);
analysis.setPrimaryEmotion(primaryEmotion);
analysis.setPolarity(polarity);
analysis.setIntensity(BigDecimal.valueOf(intensity));
analysis.setConfidence(BigDecimal.valueOf(confidence));
this.save(analysis);
return analysis;
}
// 新增的方法实现
@Override
public PageResult<EmotionAnalysisResponse> getPageWithResponse(EmotionAnalysisPageRequest request) {
IPage<EmotionAnalysis> page = getPage(request);
List<EmotionAnalysisResponse> responses = page.getRecords().stream()
.map(this::convertToResponse)
.collect(Collectors.toList());
return createPageResult(page, responses);
}
@Override
public PageResult<EmotionAnalysisResponse> getPageByUserIdWithResponse(String userId,
EmotionAnalysisPageRequest request) {
IPage<EmotionAnalysis> page = getPageByUserId(request, userId);
List<EmotionAnalysisResponse> responses = page.getRecords().stream()
.map(this::convertToResponse)
.collect(Collectors.toList());
return createPageResult(page, responses);
}
@Override
public EmotionAnalysisResponse getEmotionAnalysisResponseById(String id) {
EmotionAnalysis analysis = this.getById(id);
if (analysis == null) {
return null;
}
return convertToResponse(analysis);
}
@Override
public EmotionAnalysisResponse getEmotionAnalysisResponseByMessageId(String messageId) {
EmotionAnalysis analysis = getByMessageId(messageId);
if (analysis == null) {
return null;
}
return convertToResponse(analysis);
}
@Override
public EmotionAnalysisResponse createEmotionAnalysisWithResponse(EmotionAnalysisCreateRequest request) {
EmotionAnalysis analysis = createEmotionAnalysis(
request.getMessageId(),
request.getUserId(),
request.getPrimaryEmotion(),
request.getPolarity(),
request.getIntensity(),
request.getConfidence());
return convertToResponse(analysis);
}
@Override
public EmotionAnalysisResponse updateEmotionAnalysisWithResponse(EmotionAnalysisUpdateRequest request) {
EmotionAnalysis analysis = this.getById(request.getId());
if (analysis == null) {
return null;
}
// 更新字段
if (request.getMessageId() != null) {
analysis.setMessageId(request.getMessageId());
}
if (request.getUserId() != null) {
analysis.setCreateBy(request.getUserId());
}
if (request.getPrimaryEmotion() != null) {
analysis.setPrimaryEmotion(request.getPrimaryEmotion());
}
if (request.getPolarity() != null) {
analysis.setPolarity(request.getPolarity());
}
if (request.getIntensity() != null) {
analysis.setIntensity(BigDecimal.valueOf(request.getIntensity()));
}
if (request.getConfidence() != null) {
analysis.setConfidence(BigDecimal.valueOf(request.getConfidence()));
}
this.updateById(analysis);
return convertToResponse(analysis);
}
@Override
public boolean deleteEmotionAnalysis(String id) {
return this.removeById(id);
}
/**
* 创建PageResult对象
*/
private <T> PageResult<T> createPageResult(IPage<?> page, List<T> records) {
PageResult<T> pageResult = new PageResult<>();
pageResult.setCurrent(page.getCurrent());
pageResult.setSize(page.getSize());
pageResult.setTotal(page.getTotal());
pageResult.setPages(page.getPages());
pageResult.setRecords(records);
return pageResult;
}
/**
* 转换为响应对象
*/
private EmotionAnalysisResponse convertToResponse(EmotionAnalysis analysis) {
EmotionAnalysisResponse response = new EmotionAnalysisResponse();
BeanUtils.copyProperties(analysis, response);
response.setId(analysis.getId());
if (analysis.getCreateTime() != null) {
response.setCreateTime(analysis.getCreateTime().format(DATE_TIME_FORMATTER));
}
if (analysis.getUpdateTime() != null) {
response.setUpdateTime(analysis.getUpdateTime().format(DATE_TIME_FORMATTER));
}
// 处理Double类型转换
if (analysis.getIntensity() != null) {
response.setIntensity(analysis.getIntensity().doubleValue());
}
if (analysis.getConfidence() != null) {
response.setConfidence(analysis.getConfidence().doubleValue());
}
return response;
}
}
@@ -0,0 +1,297 @@
package com.emotion.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.emotion.common.BasePageRequest;
import com.emotion.common.PageResult;
import com.emotion.dto.request.EmotionRecordCreateRequest;
import com.emotion.dto.request.EmotionRecordPageRequest;
import com.emotion.dto.request.EmotionRecordUpdateRequest;
import com.emotion.dto.response.EmotionRecordResponse;
import com.emotion.entity.EmotionRecord;
import com.emotion.mapper.EmotionRecordMapper;
import com.emotion.service.EmotionRecordService;
import com.emotion.util.SnowflakeIdGenerator;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.Map;
import java.util.HashMap;
import java.util.Collections;
import java.util.stream.Collectors;
/**
* 情绪记录服务实现类
*
* @author huazhongmin
* @date 2025-07-24
*/
@Service
public class EmotionRecordServiceImpl extends ServiceImpl<EmotionRecordMapper, EmotionRecord> implements EmotionRecordService {
@Autowired
private SnowflakeIdGenerator snowflakeIdGenerator;
private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
@Override
public IPage<EmotionRecord> getPage(BasePageRequest request) {
Page<EmotionRecord> page = new Page<>(request.getCurrent(), request.getSize());
LambdaQueryWrapper<EmotionRecord> wrapper = new LambdaQueryWrapper<>();
if (StringUtils.hasText(request.getKeyword())) {
wrapper.and(w -> w.like(EmotionRecord::getDescription, request.getKeyword())
.or().like(EmotionRecord::getNotes, request.getKeyword()));
}
wrapper.eq(EmotionRecord::getIsDeleted, 0).orderByDesc(EmotionRecord::getCreateTime);
return this.page(page, wrapper);
}
@Override
public IPage<EmotionRecord> getPageByUserId(BasePageRequest request, String userId) {
Page<EmotionRecord> page = new Page<>(request.getCurrent(), request.getSize());
LambdaQueryWrapper<EmotionRecord> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(EmotionRecord::getUserId, userId)
.eq(EmotionRecord::getIsDeleted, 0)
.orderByDesc(EmotionRecord::getCreateTime);
return this.page(page, wrapper);
}
@Override
public EmotionRecord createEmotionRecord(String userId, String emotionType, Double intensity,
String trigger, String location, String notes) {
EmotionRecord record = new EmotionRecord();
record.setId(snowflakeIdGenerator.nextIdAsString()); // 使用雪花算法生成ID
record.setUserId(userId);
record.setEmotionType(emotionType);
record.setIntensity(BigDecimal.valueOf(intensity));
record.setTriggers(trigger);
record.setLocation(location);
record.setNotes(notes);
record.setRecordDate(LocalDate.now());
this.save(record);
return record;
}
// 新增的方法实现
@Override
public PageResult<EmotionRecordResponse> getPageWithResponse(EmotionRecordPageRequest request) {
IPage<EmotionRecord> page = getPage(request);
List<EmotionRecordResponse> responses = page.getRecords().stream()
.map(this::convertToResponse)
.collect(Collectors.toList());
PageResult<EmotionRecordResponse> pageResult = new PageResult<>();
pageResult.setCurrent(page.getCurrent());
pageResult.setSize(page.getSize());
pageResult.setTotal(page.getTotal());
pageResult.setPages(page.getPages());
pageResult.setRecords(responses);
return pageResult;
}
@Override
public PageResult<EmotionRecordResponse> getPageByUserIdWithResponse(String userId, EmotionRecordPageRequest request) {
IPage<EmotionRecord> page = getPageByUserId(request, userId);
List<EmotionRecordResponse> responses = page.getRecords().stream()
.map(this::convertToResponse)
.collect(Collectors.toList());
PageResult<EmotionRecordResponse> pageResult = new PageResult<>();
pageResult.setCurrent(page.getCurrent());
pageResult.setSize(page.getSize());
pageResult.setTotal(page.getTotal());
pageResult.setPages(page.getPages());
pageResult.setRecords(responses);
return pageResult;
}
@Override
public EmotionRecordResponse getEmotionRecordResponseById(String id) {
EmotionRecord record = this.getById(id);
if (record == null) {
return null;
}
return convertToResponse(record);
}
@Override
public EmotionRecordResponse createEmotionRecordWithResponse(EmotionRecordCreateRequest request) {
EmotionRecord record = new EmotionRecord();
record.setId(snowflakeIdGenerator.nextIdAsString()); // 使用雪花算法生成ID
record.setUserId(request.getUserId());
record.setRecordDate(request.getRecordDate());
record.setEmotionType(request.getEmotionType());
record.setIntensity(request.getIntensity());
record.setTriggers(request.getTriggers());
record.setDescription(request.getDescription());
// 注意:tags字段在实体类中是String类型,需要转换
record.setTags(request.getTags() != null ? request.getTags().toString() : null);
record.setWeather(request.getWeather());
record.setLocation(request.getLocation());
record.setActivity(request.getActivity());
record.setPeople(request.getPeople());
record.setNotes(request.getNotes());
this.save(record);
return convertToResponse(record);
}
@Override
public EmotionRecordResponse updateEmotionRecordWithResponse(EmotionRecordUpdateRequest request) {
EmotionRecord record = this.getById(request.getId());
if (record == null) {
return null;
}
// 更新字段
if (request.getRecordDate() != null) {
record.setRecordDate(request.getRecordDate());
}
if (request.getEmotionType() != null) {
record.setEmotionType(request.getEmotionType());
}
if (request.getIntensity() != null) {
record.setIntensity(request.getIntensity());
}
if (request.getTriggers() != null) {
record.setTriggers(request.getTriggers());
}
if (request.getDescription() != null) {
record.setDescription(request.getDescription());
}
// 注意:tags字段在实体类中是String类型,需要转换
if (request.getTags() != null) {
record.setTags(request.getTags().toString());
}
if (request.getWeather() != null) {
record.setWeather(request.getWeather());
}
if (request.getLocation() != null) {
record.setLocation(request.getLocation());
}
if (request.getActivity() != null) {
record.setActivity(request.getActivity());
}
if (request.getPeople() != null) {
record.setPeople(request.getPeople());
}
if (request.getNotes() != null) {
record.setNotes(request.getNotes());
}
this.updateById(record);
return convertToResponse(record);
}
@Override
public boolean deleteEmotionRecord(String id) {
return this.removeById(id);
}
@Override
public Map<String, Object> getEmotionStats(String userId, String startDate, String endDate) {
Map<String, Object> stats = new HashMap<>();
// 构建查询条件
LambdaQueryWrapper<EmotionRecord> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(EmotionRecord::getUserId, userId)
.eq(EmotionRecord::getIsDeleted, 0);
// 如果提供了日期范围,则添加时间条件
if (StringUtils.hasText(startDate) && StringUtils.hasText(endDate)) {
try {
LocalDateTime start = LocalDate.parse(startDate).atStartOfDay();
LocalDateTime end = LocalDate.parse(endDate).atTime(23, 59, 59);
wrapper.between(EmotionRecord::getCreateTime, start, end);
} catch (Exception e) {
// 如果日期解析失败,忽略日期条件
}
}
// 查询用户的所有情绪记录
List<EmotionRecord> records = this.list(wrapper);
if (records.isEmpty()) {
stats.put("emotionDistribution", new HashMap<>());
stats.put("totalRecords", 0);
stats.put("averageIntensity", 0.0);
stats.put("mostFrequentEmotion", null);
stats.put("emotionTrend", "no_data");
return stats;
}
// 情绪类型分布
Map<String, Long> emotionDistribution = records.stream()
.collect(Collectors.groupingBy(EmotionRecord::getEmotionType, Collectors.counting()));
stats.put("emotionDistribution", emotionDistribution);
stats.put("totalRecords", records.size());
// 平均情绪强度
double avgIntensity = records.stream()
.mapToDouble(record -> record.getIntensity().doubleValue())
.average()
.orElse(0.0);
stats.put("averageIntensity", avgIntensity);
// 最常见的情绪类型
String mostFrequentEmotion = emotionDistribution.entrySet().stream()
.max(Map.Entry.comparingByValue())
.map(Map.Entry::getKey)
.orElse(null);
stats.put("mostFrequentEmotion", mostFrequentEmotion);
// 情绪趋势(简单实现:比较前半段和后半段的平均强度)
if (records.size() > 1) {
int mid = records.size() / 2;
double firstHalfAvg = records.subList(0, mid).stream()
.mapToDouble(record -> record.getIntensity().doubleValue())
.average()
.orElse(0.0);
double secondHalfAvg = records.subList(mid, records.size()).stream()
.mapToDouble(record -> record.getIntensity().doubleValue())
.average()
.orElse(0.0);
if (secondHalfAvg > firstHalfAvg) {
stats.put("emotionTrend", "improving");
} else if (secondHalfAvg < firstHalfAvg) {
stats.put("emotionTrend", "declining");
} else {
stats.put("emotionTrend", "stable");
}
} else {
stats.put("emotionTrend", "insufficient_data");
}
return stats;
}
/**
* 转换为响应对象
*/
private EmotionRecordResponse convertToResponse(EmotionRecord record) {
EmotionRecordResponse response = new EmotionRecordResponse();
BeanUtils.copyProperties(record, response);
response.setId(record.getId());
if (record.getCreateTime() != null) {
response.setCreateTime(record.getCreateTime().format(DATE_TIME_FORMATTER));
}
if (record.getUpdateTime() != null) {
response.setUpdateTime(record.getUpdateTime().format(DATE_TIME_FORMATTER));
}
return response;
}
}
@@ -0,0 +1,714 @@
package com.emotion.service.impl;
import com.alibaba.fastjson2.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.emotion.common.PageResult;
import com.emotion.dto.request.ai.AiRuntimeRequest;
import com.emotion.dto.request.EpicScriptCreateRequest;
import com.emotion.dto.request.EpicScriptInspirationRequest;
import com.emotion.dto.request.EpicScriptPageRequest;
import com.emotion.dto.request.EpicScriptUpdateRequest;
import com.emotion.dto.response.ai.AiRuntimeTestResponse;
import com.emotion.dto.response.EpicScriptInspirationResponse;
import com.emotion.dto.response.EpicScriptResponse;
import com.emotion.dto.response.InspirationSuggestionResponse;
import com.emotion.entity.EpicScript;
import com.emotion.mapper.EpicScriptMapper;
import com.emotion.service.AiRuntimeService;
import com.emotion.service.EpicScriptService;
import com.emotion.service.LifePathService;
import com.emotion.service.ScriptContextService;
import com.emotion.service.TtsTaskService;
import com.emotion.util.UserContextHolder;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* 爽文剧本服务实现类
*
* @author huazhongmin
* @date 2025-12-22
*/
@Slf4j
@Service
public class EpicScriptServiceImpl extends ServiceImpl<EpicScriptMapper, EpicScript>
implements EpicScriptService {
private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
private static final int DAILY_INSPIRATION_LIMIT = 3;
private static final List<InspirationSuggestionResponse> INSPIRATION_SUGGESTIONS = List.of(
new InspirationSuggestionResponse("我想把最近一次低谷,改写成主角觉醒的开端。", "觉醒", "转折"),
new InspirationSuggestionResponse("如果我在最遗憾的选择里勇敢了一次,人生会怎样展开?", "遗憾", "重启"),
new InspirationSuggestionResponse("把一次普通的职场挑战,写成逆风翻盘的高光篇章。", "职场", "成长"),
new InspirationSuggestionResponse("我想见到十年后的自己,让 TA 给现在的我一封信。", "未来", "对话"),
new InspirationSuggestionResponse("把一段关系里的告别,写成重新认识自己的旅程。", "关系", "治愈"),
new InspirationSuggestionResponse("让我的童年记忆成为故事里的隐藏力量。", "童年", "力量"),
new InspirationSuggestionResponse("把一次失败的面试、考试或竞赛,改写成命运伏笔。", "挑战", "伏笔"),
new InspirationSuggestionResponse("写一个我终于不再讨好别人,开始选择自己的平行人生。", "自我", "选择")
);
/**
* Coze工作流配置键 - 爽文剧本生成
*/
@Autowired
@Lazy
private LifePathService lifePathService;
@Autowired
private AiRuntimeService aiRuntimeService;
@Autowired
private ScriptContextService scriptContextService;
@Autowired
private TtsTaskService ttsTaskService;
@Override
public PageResult<EpicScriptResponse> getPageByCurrentUser(EpicScriptPageRequest request) {
String currentUserId = UserContextHolder.getCurrentUserId();
if (currentUserId == null) {
return new PageResult<>();
}
Page<EpicScript> page = new Page<>(request.getCurrent(), request.getSize());
LambdaQueryWrapper<EpicScript> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(EpicScript::getUserId, currentUserId)
.eq(EpicScript::getIsDeleted, 0);
// 风格筛选
if (StringUtils.hasText(request.getStyle())) {
wrapper.eq(EpicScript::getStyle, request.getStyle());
}
// 篇幅筛选
if (StringUtils.hasText(request.getLength())) {
wrapper.eq(EpicScript::getLength, request.getLength());
}
// 关键词搜索
if (StringUtils.hasText(request.getKeyword())) {
wrapper.and(w -> w.like(EpicScript::getTitle, request.getKeyword())
.or().like(EpicScript::getTheme, request.getKeyword()));
}
// 按创建时间倒序排列
wrapper.orderByDesc(EpicScript::getCreateTime);
Page<EpicScript> resultPage = this.page(page, wrapper);
List<EpicScriptResponse> responses = resultPage.getRecords().stream()
.map(this::convertToResponse)
.collect(Collectors.toList());
PageResult<EpicScriptResponse> pageResult = new PageResult<>();
pageResult.setCurrent(resultPage.getCurrent());
pageResult.setSize(resultPage.getSize());
pageResult.setTotal(resultPage.getTotal());
pageResult.setPages(resultPage.getPages());
pageResult.setRecords(responses);
return pageResult;
}
@Override
public List<EpicScriptResponse> getListByCurrentUser() {
String currentUserId = UserContextHolder.getCurrentUserId();
if (currentUserId == null) {
return List.of();
}
LambdaQueryWrapper<EpicScript> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(EpicScript::getUserId, currentUserId)
.eq(EpicScript::getIsDeleted, 0)
.orderByDesc(EpicScript::getCreateTime);
return this.list(wrapper).stream()
.map(this::convertToResponse)
.collect(Collectors.toList());
}
@Override
public EpicScriptResponse getScriptById(String id) {
EpicScript script = this.getById(id);
if (script == null || script.getIsDeleted() == 1) {
return null;
}
// 验证权限
String currentUserId = UserContextHolder.getCurrentUserId();
if (!script.getUserId().equals(currentUserId)) {
return null;
}
return convertToResponse(script);
}
@Override
public EpicScriptResponse createScript(EpicScriptCreateRequest request) {
String currentUserId = UserContextHolder.getCurrentUserId();
if (currentUserId == null) {
return null;
}
EpicScript script = new EpicScript();
script.setUserId(currentUserId);
script.setTitle(request.getTitle());
script.setTheme(request.getTheme());
script.setStyle(StringUtils.hasText(request.getStyle()) ? request.getStyle() : "career");
script.setLength(StringUtils.hasText(request.getLength()) ? request.getLength() : "medium");
script.setPlotIntro(request.getPlotIntro());
script.setPlotTurning(request.getPlotTurning());
script.setPlotClimax(request.getPlotClimax());
script.setPlotEnding(request.getPlotEnding());
script.setPlotJson(request.getPlotJson());
script.setIsSelected(request.getIsSelected() != null && request.getIsSelected() ? 1 : 0);
// 调用Coze AI生成剧本内容
String existingContent = extractExistingGeneratedContent(script.getPlotJson());
String aiGeneratedContent = StringUtils.hasText(existingContent)
? existingContent
: generateScriptByAi(request, currentUserId);
if (aiGeneratedContent != null) {
// 将AI生成的内容存储到plotJson中
Map<String, Object> plotJson = script.getPlotJson();
if (plotJson == null) {
plotJson = new java.util.HashMap<>();
}
plotJson.put("fullContent", aiGeneratedContent);
List<String> socialInsightLabels = scriptContextService.getConfirmedInsightLabels(currentUserId);
if (!Boolean.FALSE.equals(request.getUseSocialInsights()) && !socialInsightLabels.isEmpty()) {
plotJson.put("socialInsightLabels", socialInsightLabels);
}
script.setPlotJson(plotJson);
log.info("AI生成剧本内容成功,用户ID: {}, 内容长度: {}", currentUserId, aiGeneratedContent.length());
}
this.save(script);
prewarmScriptTts(script);
return convertToResponse(script);
}
@Override
public List<InspirationSuggestionResponse> getInspirationRecommendations() {
return INSPIRATION_SUGGESTIONS;
}
@Override
public List<InspirationSuggestionResponse> getRandomInspirations(Integer size) {
int limit = size == null ? 3 : Math.max(1, Math.min(size, INSPIRATION_SUGGESTIONS.size()));
List<InspirationSuggestionResponse> suggestions = new ArrayList<>(INSPIRATION_SUGGESTIONS);
Collections.shuffle(suggestions);
return suggestions.subList(0, limit);
}
@Override
public EpicScriptInspirationResponse generateFromInspiration(EpicScriptInspirationRequest request) {
String currentUserId = UserContextHolder.getCurrentUserId();
if (currentUserId == null) {
return null;
}
int usedToday = countTodayScripts(currentUserId);
if (usedToday >= DAILY_INSPIRATION_LIMIT) {
throw new IllegalStateException("今日灵感生成次数已用完");
}
String prompt = request.getPrompt().trim();
EpicScriptCreateRequest createRequest = new EpicScriptCreateRequest();
createRequest.setTitle(buildInspirationTitle(prompt));
createRequest.setTheme(prompt);
createRequest.setStyle(StringUtils.hasText(request.getStyle()) ? request.getStyle() : "career");
createRequest.setLength(StringUtils.hasText(request.getLength()) ? request.getLength() : "medium");
createRequest.setCharacterInfo(request.getCharacterInfo());
createRequest.setLifeEventsSummary(request.getLifeEventsSummary());
createRequest.setUseSocialInsights(request.getUseSocialInsights());
Map<String, Object> plotJson = new HashMap<>();
plotJson.put("mode", "inspiration");
plotJson.put("prompt", prompt);
plotJson.put("source", StringUtils.hasText(request.getSource()) ? request.getSource() : "mini-program");
createRequest.setPlotJson(plotJson);
EpicScriptResponse script = createScript(createRequest);
EpicScriptInspirationResponse response = new EpicScriptInspirationResponse();
response.setScript(script);
response.setPrompt(prompt);
response.setRemainingCount(Math.max(0, DAILY_INSPIRATION_LIMIT - usedToday - 1));
response.setSuggestions(getRandomInspirations(3));
return response;
}
private int countTodayScripts(String userId) {
LocalDate today = LocalDate.now();
LocalDateTime start = today.atStartOfDay();
LocalDateTime end = today.plusDays(1).atStartOfDay();
LambdaQueryWrapper<EpicScript> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(EpicScript::getUserId, userId)
.eq(EpicScript::getIsDeleted, 0)
.ge(EpicScript::getCreateTime, start)
.lt(EpicScript::getCreateTime, end);
return Math.toIntExact(this.count(wrapper));
}
private String buildInspirationTitle(String prompt) {
String normalized = prompt.replaceAll("\\s+", " ").trim();
if (normalized.length() <= 22) {
return normalized;
}
return normalized.substring(0, 22) + "...";
}
/**
* 调用Coze AI生成爽文剧本内容
*
* @param request 剧本创建请求
* @param userId 用户ID
* @return AI生成的剧本内容,失败时返回null
*/
private String generateScriptByAi(EpicScriptCreateRequest request, String userId) {
try {
// 组装AI输入
String input = assembleScriptInput(request, userId);
log.info("开始调用AI生成剧本,用户ID: {}, 输入长度: {}", userId, input.length());
String result = invokeScriptRuntime(request, input, userId);
log.info("AI生成剧本完成,用户ID: {}, 结果长度: {}", userId, result != null ? result.length() : 0);
return result;
} catch (Exception e) {
log.error("AI生成剧本失败,用户ID: {}, 错误: {}", userId, e.getMessage(), e);
// AI调用失败不影响剧本创建,返回null
return null;
}
}
private String invokeScriptRuntime(EpicScriptCreateRequest request, String input, String userId) {
JSONObject inputs = new JSONObject();
inputs.put("input", input);
inputs.put("prompt", StringUtils.hasText(request.getTheme()) ? request.getTheme() : input);
inputs.put("theme", request.getTheme());
inputs.put("style", request.getStyle());
inputs.put("length", request.getLength());
inputs.put("useSocialInsights", request.getUseSocialInsights());
return invokeRuntime("script_generate", inputs, userId);
}
private String invokeScriptRuntime(EpicScriptUpdateRequest request, String input, String userId) {
JSONObject inputs = new JSONObject();
inputs.put("input", input);
inputs.put("prompt", StringUtils.hasText(request.getTheme()) ? request.getTheme() : input);
inputs.put("theme", request.getTheme());
inputs.put("style", request.getStyle());
inputs.put("length", request.getLength());
inputs.put("useSocialInsights", request.getUseSocialInsights());
return invokeRuntime("script_generate", inputs, userId);
}
private String invokeRuntime(String sceneCode, JSONObject inputs, String userId) {
AiRuntimeRequest runtimeRequest = new AiRuntimeRequest();
runtimeRequest.setSceneCode(sceneCode);
runtimeRequest.setUserId(userId);
runtimeRequest.setInputs(inputs);
AiRuntimeTestResponse response = aiRuntimeService.test(runtimeRequest);
if (response == null || !"success".equals(response.getStatus()) || !StringUtils.hasText(response.getOutput())) {
String message = response == null ? "AI_RUNTIME_EMPTY_RESPONSE" : response.getErrorMessage();
throw new IllegalStateException(StringUtils.hasText(message) ? message : "AI_RUNTIME_EMPTY_RESPONSE");
}
return response.getOutput();
}
private String extractExistingGeneratedContent(Map<String, Object> plotJson) {
if (plotJson == null) {
return null;
}
Object fullContent = plotJson.get("fullContent");
return fullContent == null ? null : String.valueOf(fullContent);
}
/**
* 组装AI输入内容
* 将EpicScriptCreateRequest的字段组装为格式化字符串
*
* @param request 剧本创建请求
* @return 格式化的输入字符串
*/
private String assembleScriptInput(EpicScriptCreateRequest request, String userId) {
StringBuilder sb = new StringBuilder();
// 角色信息
if (StringUtils.hasText(request.getCharacterInfo())) {
sb.append("【角色信息】").append(request.getCharacterInfo()).append("\n");
}
// 过往经历
if (StringUtils.hasText(request.getLifeEventsSummary())) {
sb.append("【过往经历】").append(request.getLifeEventsSummary()).append("\n");
}
String socialContext = scriptContextService == null
? null
: scriptContextService.buildSocialInsightContext(userId, request.getUseSocialInsights());
if (StringUtils.hasText(socialContext)) {
sb.append(socialContext).append("\n");
}
// 标题
if (StringUtils.hasText(request.getTitle())) {
sb.append("【剧本标题】").append(request.getTitle()).append("\n");
}
// 主题/渴望
if (StringUtils.hasText(request.getTheme())) {
sb.append("【主题渴望】").append(request.getTheme()).append("\n");
}
// 风格
if (StringUtils.hasText(request.getStyle())) {
String styleDesc = getStyleDescription(request.getStyle());
sb.append("【剧本风格】").append(styleDesc).append("\n");
}
// 篇幅
if (StringUtils.hasText(request.getLength())) {
String lengthDesc = getLengthDescription(request.getLength());
sb.append("【篇幅长度】").append(lengthDesc).append("\n");
}
// 序幕:低谷回响
if (StringUtils.hasText(request.getPlotIntro())) {
sb.append("【序幕-低谷回响】").append(request.getPlotIntro()).append("\n");
}
// 转折:契机出现
if (StringUtils.hasText(request.getPlotTurning())) {
sb.append("【转折-契机出现】").append(request.getPlotTurning()).append("\n");
}
// 高潮:命运抉择
if (StringUtils.hasText(request.getPlotClimax())) {
sb.append("【高潮-命运抉择】").append(request.getPlotClimax()).append("\n");
}
// 结局:新的开始
if (StringUtils.hasText(request.getPlotEnding())) {
sb.append("【结局-新的开始】").append(request.getPlotEnding()).append("\n");
}
return sb.toString().trim();
}
private String assembleScriptInput(EpicScriptCreateRequest request) {
return assembleScriptInput(request, null);
}
/**
* 获取风格描述
*
* @param style 风格代码
* @return 风格描述
*/
private String getStyleDescription(String style) {
switch (style) {
case "career":
return "职场逆袭";
case "love":
return "情感圆满";
case "fantasy":
return "玄幻觉醒";
default:
return style;
}
}
/**
* 获取篇幅描述
*
* @param length 篇幅代码
* @return 篇幅描述
*/
private String getLengthDescription(String length) {
switch (length) {
case "medium":
return "标准篇";
case "long":
return "长篇";
default:
return length;
}
}
@Override
public EpicScriptResponse updateScript(EpicScriptUpdateRequest request) {
EpicScript script = this.getById(request.getId());
if (script == null || script.getIsDeleted() == 1) {
return null;
}
// 验证权限
String currentUserId = UserContextHolder.getCurrentUserId();
if (!script.getUserId().equals(currentUserId)) {
return null;
}
// 更新字段
if (StringUtils.hasText(request.getTitle())) {
script.setTitle(request.getTitle());
}
if (request.getTheme() != null) {
script.setTheme(request.getTheme());
}
if (StringUtils.hasText(request.getStyle())) {
script.setStyle(request.getStyle());
}
if (StringUtils.hasText(request.getLength())) {
script.setLength(request.getLength());
}
if (request.getPlotIntro() != null) {
script.setPlotIntro(request.getPlotIntro());
}
if (request.getPlotTurning() != null) {
script.setPlotTurning(request.getPlotTurning());
}
if (request.getPlotClimax() != null) {
script.setPlotClimax(request.getPlotClimax());
}
if (request.getPlotEnding() != null) {
script.setPlotEnding(request.getPlotEnding());
}
if (request.getPlotJson() != null) {
script.setPlotJson(request.getPlotJson());
}
if (request.getIsSelected() != null) {
script.setIsSelected(request.getIsSelected() ? 1 : 0);
}
// 如果需要重新生成AI内容
if (Boolean.TRUE.equals(request.getRegenerateContent())) {
String aiGeneratedContent = regenerateScriptByAi(request, script, currentUserId);
if (aiGeneratedContent != null) {
Map<String, Object> plotJson = script.getPlotJson();
if (plotJson == null) {
plotJson = new java.util.HashMap<>();
}
plotJson.put("fullContent", aiGeneratedContent);
List<String> socialInsightLabels = scriptContextService.getConfirmedInsightLabels(currentUserId);
if (!Boolean.FALSE.equals(request.getUseSocialInsights()) && !socialInsightLabels.isEmpty()) {
plotJson.put("socialInsightLabels", socialInsightLabels);
}
script.setPlotJson(plotJson);
log.info("AI重新生成剧本内容成功,用户ID: {}, 剧本ID: {}", currentUserId, script.getId());
}
}
this.updateById(script);
prewarmScriptTts(script);
return convertToResponse(script);
}
private void prewarmScriptTts(EpicScript script) {
if (script == null || !StringUtils.hasText(script.getId()) || !StringUtils.hasText(script.getUserId())) {
return;
}
try {
ttsTaskService.prewarmEpicScript(script.getUserId(), script.getId());
} catch (Exception e) {
log.warn("Epic script TTS prewarm failed, scriptId={}, userId={}, error={}",
script.getId(), script.getUserId(), e.getMessage());
}
}
/**
* 调用Coze AI重新生成爽文剧本内容
*
* @param request 剧本更新请求
* @param script 原剧本实体
* @param userId 用户ID
* @return AI生成的剧本内容,失败时返回null
*/
private String regenerateScriptByAi(EpicScriptUpdateRequest request, EpicScript script, String userId) {
try {
// 组装AI输入
String input = assembleUpdateScriptInput(request, script, userId);
log.info("开始调用AI重新生成剧本,用户ID: {}, 剧本ID: {}", userId, script.getId());
String result = invokeScriptRuntime(request, input, userId);
log.info("AI重新生成剧本完成,用户ID: {}, 结果长度: {}", userId, result != null ? result.length() : 0);
return result;
} catch (Exception e) {
log.error("AI重新生成剧本失败,用户ID: {}, 剧本ID: {}, 错误: {}", userId, script.getId(), e.getMessage(), e);
return null;
}
}
/**
* 组装更新时的AI输入内容
*
* @param request 更新请求
* @param script 原剧本实体
* @return 格式化的输入字符串
*/
private String assembleUpdateScriptInput(EpicScriptUpdateRequest request, EpicScript script, String userId) {
StringBuilder sb = new StringBuilder();
// 角色信息(优先使用请求中的,否则使用原有的)
String characterInfo = StringUtils.hasText(request.getCharacterInfo())
? request.getCharacterInfo() : null;
if (characterInfo != null) {
sb.append("【角色信息】").append(characterInfo).append("\n");
}
// 过往经历
String lifeEventsSummary = StringUtils.hasText(request.getLifeEventsSummary())
? request.getLifeEventsSummary() : null;
if (lifeEventsSummary != null) {
sb.append("【过往经历】").append(lifeEventsSummary).append("\n");
}
String socialContext = scriptContextService == null
? null
: scriptContextService.buildSocialInsightContext(userId, request.getUseSocialInsights());
if (StringUtils.hasText(socialContext)) {
sb.append(socialContext).append("\n");
}
// 标题
String title = StringUtils.hasText(request.getTitle()) ? request.getTitle() : script.getTitle();
if (StringUtils.hasText(title)) {
sb.append("【剧本标题】").append(title).append("\n");
}
// 主题/渴望
String theme = request.getTheme() != null ? request.getTheme() : script.getTheme();
if (StringUtils.hasText(theme)) {
sb.append("【主题渴望】").append(theme).append("\n");
}
// 风格
String style = StringUtils.hasText(request.getStyle()) ? request.getStyle() : script.getStyle();
if (StringUtils.hasText(style)) {
String styleDesc = getStyleDescription(style);
sb.append("【剧本风格】").append(styleDesc).append("\n");
}
// 篇幅
String length = StringUtils.hasText(request.getLength()) ? request.getLength() : script.getLength();
if (StringUtils.hasText(length)) {
String lengthDesc = getLengthDescription(length);
sb.append("【篇幅长度】").append(lengthDesc).append("\n");
}
// 序幕
String plotIntro = request.getPlotIntro() != null ? request.getPlotIntro() : script.getPlotIntro();
if (StringUtils.hasText(plotIntro)) {
sb.append("【序幕-低谷回响】").append(plotIntro).append("\n");
}
// 转折
String plotTurning = request.getPlotTurning() != null ? request.getPlotTurning() : script.getPlotTurning();
if (StringUtils.hasText(plotTurning)) {
sb.append("【转折-契机出现】").append(plotTurning).append("\n");
}
// 高潮
String plotClimax = request.getPlotClimax() != null ? request.getPlotClimax() : script.getPlotClimax();
if (StringUtils.hasText(plotClimax)) {
sb.append("【高潮-命运抉择】").append(plotClimax).append("\n");
}
// 结局
String plotEnding = request.getPlotEnding() != null ? request.getPlotEnding() : script.getPlotEnding();
if (StringUtils.hasText(plotEnding)) {
sb.append("【结局-新的开始】").append(plotEnding).append("\n");
}
return sb.toString().trim();
}
@Override
public EpicScriptResponse selectScript(String id) {
String currentUserId = UserContextHolder.getCurrentUserId();
if (currentUserId == null) {
return null;
}
// 先取消所有选中
LambdaQueryWrapper<EpicScript> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(EpicScript::getUserId, currentUserId)
.eq(EpicScript::getIsSelected, 1)
.eq(EpicScript::getIsDeleted, 0);
List<EpicScript> selectedScripts = this.list(wrapper);
for (EpicScript s : selectedScripts) {
s.setIsSelected(0);
this.updateById(s);
}
// 选中指定剧本
EpicScript script = this.getById(id);
if (script == null || script.getIsDeleted() == 1) {
return null;
}
if (!script.getUserId().equals(currentUserId)) {
return null;
}
script.setIsSelected(1);
this.updateById(script);
return convertToResponse(script);
}
@Override
public boolean deleteScript(String id) {
EpicScript script = this.getById(id);
if (script == null) {
return false;
}
// 验证权限
String currentUserId = UserContextHolder.getCurrentUserId();
if (!script.getUserId().equals(currentUserId)) {
return false;
}
// 删除关联的路径
lifePathService.deleteByScriptId(id);
// 使用 MyBatis-Plus 的 removeById 方法,自动处理逻辑删除
return this.removeById(id);
}
/**
* 转换为响应对象
*/
private EpicScriptResponse convertToResponse(EpicScript script) {
EpicScriptResponse response = new EpicScriptResponse();
BeanUtils.copyProperties(script, response);
response.setId(script.getId());
response.setIsSelected(script.getIsSelected() != null && script.getIsSelected() == 1);
if (script.getCreateTime() != null) {
response.setCreateTime(script.getCreateTime().format(DATE_TIME_FORMATTER));
}
if (script.getUpdateTime() != null) {
response.setUpdateTime(script.getUpdateTime().format(DATE_TIME_FORMATTER));
}
return response;
}
}
@@ -0,0 +1,177 @@
package com.emotion.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.emotion.common.BasePageRequest;
import com.emotion.common.PageResult;
import com.emotion.dto.request.growth.GrowthTopicCreateRequest;
import com.emotion.dto.request.growth.GrowthTopicUpdateRequest;
import com.emotion.dto.response.growth.GrowthTopicResponse;
import com.emotion.entity.GrowthTopic;
import com.emotion.mapper.GrowthTopicMapper;
import com.emotion.service.GrowthTopicService;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
/**
* 成长话题服务实现类
*
* @author huazhongmin
* @date 2025-09-08
*/
@Service
public class GrowthTopicServiceImpl extends ServiceImpl<GrowthTopicMapper, GrowthTopic> implements GrowthTopicService {
@Override
public IPage<GrowthTopic> getPage(BasePageRequest request) {
Page<GrowthTopic> page = new Page<>(request.getCurrent(), request.getSize());
LambdaQueryWrapper<GrowthTopic> wrapper = new LambdaQueryWrapper<>();
// 关键词搜索
if (StringUtils.hasText(request.getKeyword())) {
wrapper.and(w -> w.like(GrowthTopic::getTitle, request.getKeyword())
.or().like(GrowthTopic::getDescription, request.getKeyword()));
}
wrapper.eq(GrowthTopic::getIsDeleted, 0);
// 排序
if (StringUtils.hasText(request.getOrderBy())) {
if ("asc".equalsIgnoreCase(request.getOrderDirection())) {
wrapper.orderByAsc(GrowthTopic::getCreateTime);
} else {
wrapper.orderByDesc(GrowthTopic::getCreateTime);
}
} else {
wrapper.orderByDesc(GrowthTopic::getCreateTime);
}
return this.page(page, wrapper);
}
@Override
public PageResult<GrowthTopicResponse> getPageWithResponse(BasePageRequest request) {
IPage<GrowthTopic> page = getPage(request);
List<GrowthTopicResponse> responses = page.getRecords().stream()
.map(this::convertToResponse)
.collect(Collectors.toList());
PageResult<GrowthTopicResponse> pageResult = new PageResult<>();
pageResult.setRecords(responses);
pageResult.setCurrent(page.getCurrent());
pageResult.setSize(page.getSize());
pageResult.setTotal(page.getTotal());
pageResult.setPages(page.getPages());
return pageResult;
}
@Override
public GrowthTopicResponse getGrowthTopicResponseById(String id) {
GrowthTopic topic = this.getById(id);
if (topic == null || topic.getIsDeleted() == 1) {
return null;
}
return convertToResponse(topic);
}
@Override
public GrowthTopic createGrowthTopic(String title, String description, String category,
String difficultyLevel, String tags, LocalDateTime endTime) {
GrowthTopic topic = new GrowthTopic();
topic.setTitle(title);
topic.setDescription(description);
topic.setCategory(category);
topic.setDifficulty(difficultyLevel);
topic.setContent(description);
topic.setDurationDays(30); // 默认30天
topic.setIsUnlocked(1);
topic.setProgress(BigDecimal.ZERO);
topic.setRewards("成长积分");
this.save(topic);
return topic;
}
@Override
public GrowthTopicResponse createGrowthTopicWithResponse(GrowthTopicCreateRequest request) {
GrowthTopic topic = createGrowthTopic(
request.getTitle(),
request.getDescription(),
request.getCategory(),
request.getDifficultyLevel(),
request.getTags(),
request.getEndTime());
return convertToResponse(topic);
}
@Override
public GrowthTopicResponse updateGrowthTopicWithResponse(GrowthTopicUpdateRequest request) {
GrowthTopic topic = this.getById(request.getId());
if (topic == null || topic.getIsDeleted() == 1) {
return null;
}
// 只更新非空字段
if (StringUtils.hasText(request.getTitle())) {
topic.setTitle(request.getTitle());
}
if (StringUtils.hasText(request.getDescription())) {
topic.setDescription(request.getDescription());
topic.setContent(request.getDescription());
}
if (StringUtils.hasText(request.getCategory())) {
topic.setCategory(request.getCategory());
}
if (StringUtils.hasText(request.getDifficultyLevel())) {
topic.setDifficulty(request.getDifficultyLevel());
}
if (StringUtils.hasText(request.getTags())) {
// GrowthTopic实体中没有tags字段,暂时忽略
}
if (request.getEndTime() != null) {
// GrowthTopic实体中没有endTime字段,暂时忽略
}
this.updateById(topic);
return convertToResponse(topic);
}
@Override
public boolean deleteGrowthTopic(String id) {
GrowthTopic topic = this.getById(id);
if (topic == null) {
return false;
}
// 使用 MyBatis-Plus 的 removeById 方法,自动处理逻辑删除
return this.removeById(id);
}
/**
* 将实体转换为响应对象
*/
private GrowthTopicResponse convertToResponse(GrowthTopic topic) {
if (topic == null) {
return null;
}
GrowthTopicResponse response = new GrowthTopicResponse();
BeanUtils.copyProperties(topic, response);
response.setId(topic.getId());
if (topic.getCreateTime() != null) {
response.setCreateTime(topic.getCreateTime().toString());
}
if (topic.getUpdateTime() != null) {
response.setUpdateTime(topic.getUpdateTime().toString());
}
return response;
}
}
@@ -0,0 +1,312 @@
package com.emotion.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.emotion.common.BasePageRequest;
import com.emotion.common.PageResult;
import com.emotion.dto.request.guest.GuestUserCreateRequest;
import com.emotion.dto.request.guest.GuestUserUpdateRequest;
import com.emotion.dto.response.guest.GuestUserResponse;
import com.emotion.entity.GuestUser;
import com.emotion.mapper.GuestUserMapper;
import com.emotion.service.GuestUserService;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.time.LocalDateTime;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
/**
* 访客用户服务实现类
*
* @author huazhongmin
* @date 2025-09-08
*/
@Service
public class GuestUserServiceImpl extends ServiceImpl<GuestUserMapper, GuestUser> implements GuestUserService {
@Override
public IPage<GuestUser> getPage(BasePageRequest request) {
Page<GuestUser> page = new Page<>(request.getCurrent(), request.getSize());
LambdaQueryWrapper<GuestUser> wrapper = new LambdaQueryWrapper<>();
if (StringUtils.hasText(request.getKeyword())) {
wrapper.like(GuestUser::getNickname, request.getKeyword());
}
wrapper.eq(GuestUser::getIsDeleted, 0).orderByDesc(GuestUser::getCreateTime);
return this.page(page, wrapper);
}
@Override
public PageResult<GuestUserResponse> getPageWithResponse(BasePageRequest request) {
IPage<GuestUser> page = getPage(request);
List<GuestUserResponse> responses = page.getRecords().stream()
.map(this::convertToResponse)
.collect(Collectors.toList());
PageResult<GuestUserResponse> pageResult = new PageResult<>();
pageResult.setRecords(responses);
pageResult.setCurrent(page.getCurrent());
pageResult.setSize(page.getSize());
pageResult.setTotal(page.getTotal());
pageResult.setPages(page.getPages());
return pageResult;
}
@Override
public GuestUserResponse getGuestUserResponseById(String id) {
GuestUser guestUser = this.getById(id);
if (guestUser == null || guestUser.getIsDeleted() == 1) {
return null;
}
return convertToResponse(guestUser);
}
@Override
public GuestUser getByDeviceId(String deviceId) {
LambdaQueryWrapper<GuestUser> wrapper = new LambdaQueryWrapper<>();
wrapper.like(GuestUser::getDeviceInfo, deviceId)
.eq(GuestUser::getIsDeleted, 0);
return this.getOne(wrapper);
}
@Override
public List<GuestUser> getByIpAddress(String ipAddress) {
LambdaQueryWrapper<GuestUser> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(GuestUser::getIpAddress, ipAddress)
.eq(GuestUser::getIsDeleted, 0)
.orderByDesc(GuestUser::getCreateTime);
return this.list(wrapper);
}
@Override
public List<GuestUser> getByUserAgent(String userAgent) {
LambdaQueryWrapper<GuestUser> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(GuestUser::getUserAgent, userAgent)
.eq(GuestUser::getIsDeleted, 0)
.orderByDesc(GuestUser::getCreateTime);
return this.list(wrapper);
}
@Override
public List<GuestUser> getByStatus(String status) {
// GuestUser实体中没有status字段,暂时返回空列表
return Collections.emptyList();
}
@Override
public List<GuestUser> getByTimeRange(LocalDateTime startTime, LocalDateTime endTime) {
LambdaQueryWrapper<GuestUser> wrapper = new LambdaQueryWrapper<>();
wrapper.between(GuestUser::getCreateTime, startTime, endTime)
.eq(GuestUser::getIsDeleted, 0)
.orderByDesc(GuestUser::getCreateTime);
return this.list(wrapper);
}
@Override
public List<GuestUser> getByLastActiveTimeRange(LocalDateTime startTime, LocalDateTime endTime) {
LambdaQueryWrapper<GuestUser> wrapper = new LambdaQueryWrapper<>();
wrapper.between(GuestUser::getLastActiveTime, startTime, endTime)
.eq(GuestUser::getIsDeleted, 0)
.orderByDesc(GuestUser::getLastActiveTime);
return this.list(wrapper);
}
@Override
public Long countByStatus(String status) {
// GuestUser实体中没有status字段,暂时返回0
return 0L;
}
@Override
public Long countByIpAddress(String ipAddress) {
LambdaQueryWrapper<GuestUser> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(GuestUser::getIpAddress, ipAddress)
.eq(GuestUser::getIsDeleted, 0);
return this.count(wrapper);
}
@Override
public Long countTodayNewGuests() {
LocalDateTime today = LocalDateTime.now().withHour(0).withMinute(0).withSecond(0).withNano(0);
LocalDateTime tomorrow = today.plusDays(1);
LambdaQueryWrapper<GuestUser> wrapper = new LambdaQueryWrapper<>();
wrapper.between(GuestUser::getCreateTime, today, tomorrow)
.eq(GuestUser::getIsDeleted, 0);
return this.count(wrapper);
}
@Override
public Long countActiveGuests(Integer days) {
LocalDateTime activeTime = LocalDateTime.now().minusDays(days);
LambdaQueryWrapper<GuestUser> wrapper = new LambdaQueryWrapper<>();
wrapper.ge(GuestUser::getLastActiveTime, activeTime)
.eq(GuestUser::getIsDeleted, 0);
return this.count(wrapper);
}
@Override
public List<GuestUser> getRecentVisitors(Integer limit) {
LambdaQueryWrapper<GuestUser> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(GuestUser::getIsDeleted, 0)
.orderByDesc(GuestUser::getLastActiveTime)
.last("LIMIT " + limit);
return this.list(wrapper);
}
@Override
public List<GuestUser> getInactiveGuests(Integer days) {
LocalDateTime inactiveTime = LocalDateTime.now().minusDays(days);
LambdaQueryWrapper<GuestUser> wrapper = new LambdaQueryWrapper<>();
wrapper.lt(GuestUser::getLastActiveTime, inactiveTime)
.eq(GuestUser::getIsDeleted, 0)
.orderByAsc(GuestUser::getLastActiveTime);
return this.list(wrapper);
}
@Override
public List<GuestUser> getByVisitCountRange(Integer minVisits, Integer maxVisits) {
// GuestUser实体中没有visitCount字段,暂时返回空列表
return Collections.emptyList();
}
@Override
public Double getAvgVisitCount() {
// 这里需要自定义SQL查询平均值,暂时返回0
return 0.0;
}
@Override
public boolean updateLastActiveTime(String id, LocalDateTime lastActiveTime) {
GuestUser guestUser = new GuestUser();
guestUser.setId(id);
guestUser.setLastActiveTime(lastActiveTime);
return this.updateById(guestUser);
}
@Override
public boolean incrementVisitCount(String id) {
// GuestUser实体中没有visitCount字段,暂时返回false
return false;
}
@Override
public boolean updateStatus(String id, String status) {
// GuestUser实体中没有status字段,暂时返回false
return false;
}
@Override
public GuestUser getOrCreateByDeviceInfo(String deviceId, String ipAddress, String userAgent) {
// 先尝试根据设备信息查找
GuestUser existingUser = getByDeviceId(deviceId);
if (existingUser != null) {
// 更新最后活跃时间
updateLastActiveTime(existingUser.getId(), LocalDateTime.now());
return existingUser;
}
// 如果不存在,创建新的访客用户
return createGuestUser(deviceId, ipAddress, userAgent, null);
}
@Override
public boolean cleanExpiredGuests(Integer days) {
LocalDateTime expireTime = LocalDateTime.now().minusDays(days);
LambdaQueryWrapper<GuestUser> wrapper = new LambdaQueryWrapper<>();
wrapper.lt(GuestUser::getLastActiveTime, expireTime);
// 使用 MyBatis-Plus 的 remove 方法,自动处理逻辑删除
return this.remove(wrapper);
}
@Override
public GuestUser createGuestUser(String deviceId, String ipAddress, String userAgent, String location) {
GuestUser guestUser = new GuestUser();
guestUser.setGuestUserId("guest_" + System.currentTimeMillis());
guestUser.setIpAddress(ipAddress);
guestUser.setUserAgent(userAgent);
guestUser.setNickname("访客用户");
guestUser.setAvatar("default_avatar.png");
guestUser.setLastActiveTime(LocalDateTime.now());
guestUser.setConversationCount(0);
guestUser.setMessageCount(0);
guestUser.setLocation(location);
guestUser.setDeviceInfo(deviceId);
this.save(guestUser);
return guestUser;
}
@Override
public GuestUserResponse createGuestUserWithResponse(GuestUserCreateRequest request) {
GuestUser guestUser = createGuestUser(
request.getDeviceId(),
request.getIpAddress(),
request.getUserAgent(),
request.getLocation());
return convertToResponse(guestUser);
}
@Override
public GuestUserResponse updateGuestUserWithResponse(GuestUserUpdateRequest request) {
GuestUser guestUser = this.getById(request.getId());
if (guestUser == null || guestUser.getIsDeleted() == 1) {
return null;
}
// 只更新非空字段
if (StringUtils.hasText(request.getNickname())) {
guestUser.setNickname(request.getNickname());
}
if (StringUtils.hasText(request.getAvatar())) {
guestUser.setAvatar(request.getAvatar());
}
if (StringUtils.hasText(request.getLocation())) {
guestUser.setLocation(request.getLocation());
}
this.updateById(guestUser);
return convertToResponse(guestUser);
}
@Override
public boolean deleteGuestUser(String id) {
GuestUser guestUser = this.getById(id);
if (guestUser == null) {
return false;
}
// 使用 MyBatis-Plus 的 removeById 方法,自动处理逻辑删除
return this.removeById(id);
}
/**
* 将实体转换为响应对象
*/
private GuestUserResponse convertToResponse(GuestUser guestUser) {
if (guestUser == null) {
return null;
}
GuestUserResponse response = new GuestUserResponse();
BeanUtils.copyProperties(guestUser, response);
response.setId(guestUser.getId());
if (guestUser.getCreateTime() != null) {
response.setCreateTime(guestUser.getCreateTime().toString());
}
if (guestUser.getUpdateTime() != null) {
response.setUpdateTime(guestUser.getUpdateTime().toString());
}
return response;
}
}
@@ -0,0 +1,90 @@
package com.emotion.service.impl;
import com.emotion.service.TtsEngineClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import org.springframework.web.client.RestTemplate;
import java.io.File;
import java.time.Duration;
import java.util.HashMap;
import java.util.Map;
@Service
public class HttpTtsEngineClient implements TtsEngineClient {
private final RestTemplate restTemplate;
private final String engineUrl;
public HttpTtsEngineClient(RestTemplateBuilder restTemplateBuilder,
@Value("${emotion.tts.engine-url:http://127.0.0.1:19110}") String engineUrl,
@Value("${emotion.tts.engine-connect-timeout-ms:5000}") long connectTimeoutMs,
@Value("${emotion.tts.engine-read-timeout-ms:240000}") long readTimeoutMs) {
this.engineUrl = normalizeEngineUrl(engineUrl);
this.restTemplate = restTemplateBuilder
.setConnectTimeout(Duration.ofMillis(connectTimeoutMs))
.setReadTimeout(Duration.ofMillis(readTimeoutMs))
.defaultHeader("User-Agent", "EmotionMuseum-TTS/1.0")
.defaultHeader("Accept", "application/json, text/plain, */*")
.build();
}
@Override
public TtsEngineResult synthesize(String text, String voice, String outputPath, SynthesisOptions options) {
try {
Map<String, Object> body = new HashMap<>();
body.put("text", text);
body.put("voice", voice);
body.put("outputPath", outputPath);
if (options != null) {
if (options.getSpeechRate() != null) {
body.put("speechRate", options.getSpeechRate());
}
if (options.getPitch() != null) {
body.put("pitch", options.getPitch());
}
if (StringUtils.hasText(options.getEmotion())) {
body.put("emotion", options.getEmotion());
}
}
ResponseEntity<Map> response = restTemplate.postForEntity(engineUrl + "/synthesize", body, Map.class);
Map<?, ?> data = response.getBody();
boolean success = data != null && Boolean.TRUE.equals(data.get("success"));
if (!success) {
String message = data == null ? "empty response" : String.valueOf(data.get("errorMessage"));
if (hasGeneratedAudio(outputPath)) {
return new TtsEngineResult(true, outputPath, null, null);
}
return new TtsEngineResult(false, null, null, message);
}
Long durationMs = data.get("durationMs") instanceof Number
? ((Number) data.get("durationMs")).longValue()
: null;
return new TtsEngineResult(true, String.valueOf(data.get("audioPath")), durationMs, null);
} catch (Exception e) {
if (hasGeneratedAudio(outputPath)) {
return new TtsEngineResult(true, outputPath, null, null);
}
return new TtsEngineResult(false, null, null, e.getMessage());
}
}
private static String normalizeEngineUrl(String value) {
if (!StringUtils.hasText(value)) {
return "http://127.0.0.1:19110";
}
String trimmed = value.trim();
return trimmed.endsWith("/") ? trimmed.substring(0, trimmed.length() - 1) : trimmed;
}
private static boolean hasGeneratedAudio(String outputPath) {
if (!StringUtils.hasText(outputPath)) {
return false;
}
File file = new File(outputPath);
return file.isFile() && file.length() > 0L;
}
}
@@ -0,0 +1,427 @@
package com.emotion.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.alibaba.fastjson2.JSONObject;
import com.emotion.common.PageResult;
import com.emotion.dto.request.LifeEventCreateRequest;
import com.emotion.dto.request.LifeEventPageRequest;
import com.emotion.dto.request.LifeEventUpdateRequest;
import com.emotion.dto.request.ai.AiRuntimeRequest;
import com.emotion.dto.response.ai.AiRuntimeTestResponse;
import com.emotion.dto.response.LifeEventResponse;
import com.emotion.entity.LifeEvent;
import com.emotion.mapper.LifeEventMapper;
import com.emotion.service.AiRuntimeService;
import com.emotion.service.LifeEventService;
import com.emotion.util.UserContextHolder;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.YearMonth;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.stream.Collectors;
/**
* 生命事件服务实现类
*
* @author huazhongmin
* @date 2025-12-22
*/
@Service
@Slf4j
public class LifeEventServiceImpl extends ServiceImpl<LifeEventMapper, LifeEvent>
implements LifeEventService {
private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
private static final DateTimeFormatter ISO_FORMATTER = DateTimeFormatter.ISO_DATE_TIME;
private static final DateTimeFormatter DATE_ONLY_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd");
private static final DateTimeFormatter YEAR_MONTH_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM");
@Autowired
private AiRuntimeService aiRuntimeService;
@Override
public PageResult<LifeEventResponse> getPageByCurrentUser(LifeEventPageRequest request) {
String currentUserId = UserContextHolder.getCurrentUserId();
if (currentUserId == null) {
return new PageResult<>();
}
Page<LifeEvent> page = new Page<>(request.getCurrent(), request.getSize());
LambdaQueryWrapper<LifeEvent> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(LifeEvent::getUserId, currentUserId)
.eq(LifeEvent::getIsDeleted, 0);
// 事件类型筛选
if (StringUtils.hasText(request.getEventType())) {
wrapper.eq(LifeEvent::getEventType, request.getEventType());
}
// 情绪类型筛选
if (StringUtils.hasText(request.getEmotionType())) {
wrapper.eq(LifeEvent::getEmotionType, request.getEmotionType());
}
// 关键词搜索
if (StringUtils.hasText(request.getKeyword())) {
wrapper.and(w -> w.like(LifeEvent::getTitle, request.getKeyword())
.or().like(LifeEvent::getContent, request.getKeyword()));
}
// 按事件日期倒序排列
wrapper.orderByDesc(LifeEvent::getEventDate);
Page<LifeEvent> resultPage = this.page(page, wrapper);
List<LifeEventResponse> responses = resultPage.getRecords().stream()
.map(this::convertToResponse)
.collect(Collectors.toList());
PageResult<LifeEventResponse> pageResult = new PageResult<>();
pageResult.setCurrent(resultPage.getCurrent());
pageResult.setSize(resultPage.getSize());
pageResult.setTotal(resultPage.getTotal());
pageResult.setPages(resultPage.getPages());
pageResult.setRecords(responses);
return pageResult;
}
@Override
public List<LifeEventResponse> getListByCurrentUser() {
String currentUserId = UserContextHolder.getCurrentUserId();
if (currentUserId == null) {
return List.of();
}
LambdaQueryWrapper<LifeEvent> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(LifeEvent::getUserId, currentUserId)
.eq(LifeEvent::getIsDeleted, 0)
.orderByDesc(LifeEvent::getEventDate);
return this.list(wrapper).stream()
.map(this::convertToResponse)
.collect(Collectors.toList());
}
@Override
public LifeEventResponse getEventById(String id) {
LifeEvent event = this.getById(id);
if (event == null || event.getIsDeleted() == 1) {
return null;
}
// 验证权限
String currentUserId = UserContextHolder.getCurrentUserId();
if (!event.getUserId().equals(currentUserId)) {
return null;
}
return convertToResponse(event);
}
@Override
public LifeEventResponse createEvent(LifeEventCreateRequest request) {
String currentUserId = UserContextHolder.getCurrentUserId();
if (currentUserId == null) {
return null;
}
LifeEvent event = new LifeEvent();
event.setUserId(currentUserId);
event.setEventType(StringUtils.hasText(request.getEventType()) ? request.getEventType() : "daily_log");
event.setTitle(request.getTitle());
event.setContent(request.getContent());
event.setAiReply(request.getAiReply());
event.setEmotionType(request.getEmotionType());
event.setTags(request.getTags());
event.setTimeMode(StringUtils.hasText(request.getTimeMode()) ? request.getTimeMode() : "date");
event.setEventDateText(StringUtils.hasText(request.getEventDateText()) ? request.getEventDateText() : request.getEventDate());
// 解析事件日期,支持多种格式
event.setEventDate(parseEventDate(request.getEventDate(), event.getTimeMode(), event.getEventDateText()));
event.setEventEndDate(parseEventEndDate(request.getEventEndDate(), event.getTimeMode()));
// 情绪评分
if (request.getEmotionScore() != null) {
event.setEmotionScore(BigDecimal.valueOf(request.getEmotionScore()));
}
if (!StringUtils.hasText(event.getAiReply())) {
String aiGeneratedContent = generateHealingByAi(event);
if (StringUtils.hasText(aiGeneratedContent)) {
event.setAiReply(aiGeneratedContent);
}
}
this.save(event);
return convertToResponse(event);
}
/**
* 调用Coze AI生成疗愈内容
*
* @param event 人生事件
* @return AI生成的疗愈内容,失败时返回null
*/
private String generateHealingByAi(LifeEvent event) {
try {
// 组装AI输入
String input = assembleHealingInput(event);
log.info("开始调用AI生成疗愈回复,用户ID: {}, 输入长度: {}", event.getUserId(), input.length());
JSONObject inputs = new JSONObject();
inputs.put("mode", "life_event_analysis");
inputs.put("input", input);
inputs.put("message", input);
inputs.put("content", event.getContent());
inputs.put("title", event.getTitle());
inputs.put("emotionType", event.getEmotionType());
inputs.put("eventType", event.getEventType());
inputs.put("eventDate", event.getEventDateText());
AiRuntimeRequest runtimeRequest = new AiRuntimeRequest();
runtimeRequest.setSceneCode("life_healing");
runtimeRequest.setUserId(event.getUserId());
runtimeRequest.setInputs(inputs);
AiRuntimeTestResponse response = aiRuntimeService.test(runtimeRequest);
String result = response != null ? response.getOutput() : null;
log.info("AI生成疗愈回复完成,用户ID: {}, 结果长度: {}", event.getUserId(), result != null ? result.length() : 0);
return result;
} catch (Exception e) {
log.error("AI生成疗愈回复失败,用户ID: {}, 错误: {}", event.getUserId(), e.getMessage(), e);
// AI调用失败不影响事件创建,返回null
return null;
}
}
/**
* 组装AI疗愈输入内容
*
* @param event 人生事件
* @return 格式化的输入字符串
*/
private String assembleHealingInput(LifeEvent event) {
StringBuilder sb = new StringBuilder();
// 标题
if (StringUtils.hasText(event.getTitle())) {
sb.append("【事件标题】").append(event.getTitle()).append("\n");
}
// 发生时间
if (!ObjectUtils.isEmpty(event.getEventDate())) {
sb.append("【发生时间】").append(event.getEventDate()).append("\n");
}
// 经历详解
if (StringUtils.hasText(event.getContent())) {
sb.append("【经历详解】").append(event.getContent()).append("\n");
}
// 情绪类型
if (StringUtils.hasText(event.getEmotionType())) {
sb.append("【情绪类型】").append(event.getEmotionType()).append("\n");
}
return sb.toString().trim();
}
@Override
public LifeEventResponse updateEvent(LifeEventUpdateRequest request) {
LifeEvent event = this.getById(request.getId());
if (event == null || event.getIsDeleted() == 1) {
return null;
}
// 验证权限
String currentUserId = UserContextHolder.getCurrentUserId();
if (!event.getUserId().equals(currentUserId)) {
return null;
}
boolean hasClientAiReply = request.getAiReply() != null;
// 更新字段
if (StringUtils.hasText(request.getEventType())) {
event.setEventType(request.getEventType());
}
if (StringUtils.hasText(request.getTitle())) {
event.setTitle(request.getTitle());
}
if (StringUtils.hasText(request.getContent())) {
event.setContent(request.getContent());
}
if (request.getAiReply() != null) {
event.setAiReply(request.getAiReply());
}
if (request.getEmotionType() != null) {
event.setEmotionType(request.getEmotionType());
}
if (request.getTags() != null) {
event.setTags(request.getTags());
}
if (StringUtils.hasText(request.getTimeMode())) {
event.setTimeMode(request.getTimeMode());
}
if (request.getEventDateText() != null) {
event.setEventDateText(request.getEventDateText());
}
if (StringUtils.hasText(request.getEventDate())) {
event.setEventDate(parseEventDate(request.getEventDate(), event.getTimeMode(), event.getEventDateText()));
}
if (request.getEventEndDate() != null) {
event.setEventEndDate(parseEventEndDate(request.getEventEndDate(), event.getTimeMode()));
}
if (request.getEmotionScore() != null) {
event.setEmotionScore(BigDecimal.valueOf(request.getEmotionScore()));
}
if (!hasClientAiReply) {
String aiGeneratedContent = generateHealingByAi(event);
if (StringUtils.hasText(aiGeneratedContent)) {
event.setAiReply(aiGeneratedContent);
}
}
this.updateById(event);
return convertToResponse(event);
}
@Override
public boolean deleteEvent(String id) {
LifeEvent event = this.getById(id);
if (event == null) {
return false;
}
// 验证权限
String currentUserId = UserContextHolder.getCurrentUserId();
if (!event.getUserId().equals(currentUserId)) {
return false;
}
// 使用 MyBatis-Plus 的 removeById 方法,自动处理逻辑删除
return this.removeById(id);
}
/**
* 转换为响应对象
*/
private LifeEventResponse convertToResponse(LifeEvent event) {
LifeEventResponse response = new LifeEventResponse();
BeanUtils.copyProperties(event, response);
response.setId(event.getId());
if (event.getEventDate() != null) {
response.setEventDate(event.getEventDate().format(ISO_FORMATTER));
}
if (event.getEventEndDate() != null) {
response.setEventEndDate(event.getEventEndDate().format(ISO_FORMATTER));
}
if (event.getEmotionScore() != null) {
response.setEmotionScore(event.getEmotionScore().doubleValue());
}
if (event.getCreateTime() != null) {
response.setCreateTime(event.getCreateTime().format(DATE_TIME_FORMATTER));
}
if (event.getUpdateTime() != null) {
response.setUpdateTime(event.getUpdateTime().format(DATE_TIME_FORMATTER));
}
return response;
}
/**
* 解析事件日期,支持多种格式
* 支持格式:yyyy-MM-dd、yyyy-MM-ddTHH:mm:ss、ISO_DATE_TIME
*
* @param dateStr 日期字符串
* @return 解析后的LocalDateTime,解析失败返回当前时间
*/
private LocalDateTime parseEventDate(String dateStr, String timeMode, String eventDateText) {
String source = StringUtils.hasText(dateStr) ? dateStr : eventDateText;
if (!StringUtils.hasText(source)) {
return LocalDateTime.now();
}
if ("month".equals(timeMode)) {
try {
return YearMonth.parse(source.substring(0, 7), YEAR_MONTH_FORMATTER).atDay(1).atStartOfDay();
} catch (Exception ignored) {
}
}
if ("season".equals(timeMode)) {
LocalDate seasonDate = parseSeasonStart(source);
if (seasonDate != null) {
return seasonDate.atStartOfDay();
}
}
// 尝试ISO格式 (yyyy-MM-ddTHH:mm:ss.SSSZ)
try {
return LocalDateTime.parse(source, ISO_FORMATTER);
} catch (Exception ignored) {
}
// 尝试日期时间格式 (yyyy-MM-dd HH:mm:ss)
try {
return LocalDateTime.parse(source, DATE_TIME_FORMATTER);
} catch (Exception ignored) {
}
// 尝试纯日期格式 (yyyy-MM-dd),时间设为当天开始
try {
return LocalDate.parse(source, DATE_ONLY_FORMATTER).atStartOfDay();
} catch (Exception ignored) {
}
// 所有格式都失败,返回当前时间
return LocalDateTime.now();
}
private LocalDateTime parseEventEndDate(String dateStr, String timeMode) {
if (!"range".equals(timeMode) || !StringUtils.hasText(dateStr)) {
return null;
}
return parseEventDate(dateStr, "date", dateStr);
}
private LocalDate parseSeasonStart(String value) {
try {
String[] parts = value.split("-");
int year = Integer.parseInt(parts[0]);
String season = parts.length > 1 ? parts[1] : "spring";
int month;
switch (season) {
case "summer":
month = 6;
break;
case "autumn":
month = 9;
break;
case "winter":
month = 12;
break;
case "spring":
default:
month = 3;
break;
}
return LocalDate.of(year, month, 1);
} catch (Exception ignored) {
return null;
}
}
}
@@ -0,0 +1,230 @@
package com.emotion.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.emotion.common.PageResult;
import com.emotion.dto.request.LifePathCreateRequest;
import com.emotion.dto.request.LifePathPageRequest;
import com.emotion.dto.request.LifePathUpdateRequest;
import com.emotion.dto.response.LifePathResponse;
import com.emotion.entity.LifePath;
import com.emotion.mapper.LifePathMapper;
import com.emotion.service.LifePathService;
import com.emotion.util.UserContextHolder;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.math.BigDecimal;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.stream.Collectors;
/**
* 实现路径服务实现类
*
* @author huazhongmin
* @date 2025-12-22
*/
@Service
public class LifePathServiceImpl extends ServiceImpl<LifePathMapper, LifePath>
implements LifePathService {
private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
@Override
public PageResult<LifePathResponse> getPageByCurrentUser(LifePathPageRequest request) {
String currentUserId = UserContextHolder.getCurrentUserId();
if (currentUserId == null) {
return new PageResult<>();
}
Page<LifePath> page = new Page<>(request.getCurrent(), request.getSize());
LambdaQueryWrapper<LifePath> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(LifePath::getUserId, currentUserId)
.eq(LifePath::getIsDeleted, 0);
// 剧本ID筛选
if (StringUtils.hasText(request.getScriptId())) {
wrapper.eq(LifePath::getScriptId, request.getScriptId());
}
// 状态筛选
if (StringUtils.hasText(request.getStatus())) {
wrapper.eq(LifePath::getStatus, request.getStatus());
}
// 按创建时间倒序排列
wrapper.orderByDesc(LifePath::getCreateTime);
Page<LifePath> resultPage = this.page(page, wrapper);
List<LifePathResponse> responses = resultPage.getRecords().stream()
.map(this::convertToResponse)
.collect(Collectors.toList());
PageResult<LifePathResponse> pageResult = new PageResult<>();
pageResult.setCurrent(resultPage.getCurrent());
pageResult.setSize(resultPage.getSize());
pageResult.setTotal(resultPage.getTotal());
pageResult.setPages(resultPage.getPages());
pageResult.setRecords(responses);
return pageResult;
}
@Override
public List<LifePathResponse> getListByCurrentUser() {
String currentUserId = UserContextHolder.getCurrentUserId();
if (currentUserId == null) {
return List.of();
}
LambdaQueryWrapper<LifePath> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(LifePath::getUserId, currentUserId)
.eq(LifePath::getIsDeleted, 0)
.orderByDesc(LifePath::getCreateTime);
return this.list(wrapper).stream()
.map(this::convertToResponse)
.collect(Collectors.toList());
}
@Override
public LifePathResponse getByScriptId(String scriptId) {
String currentUserId = UserContextHolder.getCurrentUserId();
if (currentUserId == null) {
return null;
}
LambdaQueryWrapper<LifePath> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(LifePath::getUserId, currentUserId)
.eq(LifePath::getScriptId, scriptId)
.eq(LifePath::getIsDeleted, 0);
LifePath path = this.getOne(wrapper);
return path != null ? convertToResponse(path) : null;
}
@Override
public LifePathResponse getPathById(String id) {
LifePath path = this.getById(id);
if (path == null || path.getIsDeleted() == 1) {
return null;
}
// 验证权限
String currentUserId = UserContextHolder.getCurrentUserId();
if (!path.getUserId().equals(currentUserId)) {
return null;
}
return convertToResponse(path);
}
@Override
public LifePathResponse createPath(LifePathCreateRequest request) {
String currentUserId = UserContextHolder.getCurrentUserId();
if (currentUserId == null) {
return null;
}
LifePath path = new LifePath();
path.setUserId(currentUserId);
path.setScriptId(request.getScriptId());
path.setTitle(request.getTitle());
path.setDescription(request.getDescription());
path.setSteps(request.getSteps());
path.setStatus(StringUtils.hasText(request.getStatus()) ? request.getStatus() : "active");
path.setProgress(request.getProgress() != null ? BigDecimal.valueOf(request.getProgress()) : BigDecimal.ZERO);
this.save(path);
return convertToResponse(path);
}
@Override
public LifePathResponse updatePath(LifePathUpdateRequest request) {
LifePath path = this.getById(request.getId());
if (path == null || path.getIsDeleted() == 1) {
return null;
}
// 验证权限
String currentUserId = UserContextHolder.getCurrentUserId();
if (!path.getUserId().equals(currentUserId)) {
return null;
}
// 更新字段
if (StringUtils.hasText(request.getTitle())) {
path.setTitle(request.getTitle());
}
if (request.getDescription() != null) {
path.setDescription(request.getDescription());
}
if (request.getSteps() != null) {
path.setSteps(request.getSteps());
}
if (StringUtils.hasText(request.getStatus())) {
path.setStatus(request.getStatus());
}
if (request.getProgress() != null) {
path.setProgress(BigDecimal.valueOf(request.getProgress()));
}
this.updateById(path);
return convertToResponse(path);
}
@Override
public boolean deletePath(String id) {
LifePath path = this.getById(id);
if (path == null) {
return false;
}
// 验证权限
String currentUserId = UserContextHolder.getCurrentUserId();
if (!path.getUserId().equals(currentUserId)) {
return false;
}
// 使用 MyBatis-Plus 的 removeById 方法,自动处理逻辑删除
return this.removeById(id);
}
@Override
public boolean deleteByScriptId(String scriptId) {
String currentUserId = UserContextHolder.getCurrentUserId();
if (currentUserId == null) {
return false;
}
LambdaQueryWrapper<LifePath> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(LifePath::getUserId, currentUserId)
.eq(LifePath::getScriptId, scriptId);
// 使用 MyBatis-Plus 的 remove 方法,自动处理逻辑删除
return this.remove(wrapper);
}
/**
* 转换为响应对象
*/
private LifePathResponse convertToResponse(LifePath path) {
LifePathResponse response = new LifePathResponse();
BeanUtils.copyProperties(path, response);
response.setId(path.getId());
if (path.getProgress() != null) {
response.setProgress(path.getProgress().doubleValue());
}
if (path.getCreateTime() != null) {
response.setCreateTime(path.getCreateTime().format(DATE_TIME_FORMATTER));
}
if (path.getUpdateTime() != null) {
response.setUpdateTime(path.getUpdateTime().format(DATE_TIME_FORMATTER));
}
return response;
}
}
@@ -0,0 +1,456 @@
package com.emotion.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.emotion.common.BasePageRequest;
import com.emotion.common.PageResult;
import com.emotion.dto.request.MessagePageRequest;
import com.emotion.dto.request.MessageSearchRequest;
import com.emotion.dto.request.MessageRecentRequest;
import com.emotion.dto.request.MessageCreateRequest;
import com.emotion.dto.response.MessageResponse;
import com.emotion.entity.Message;
import com.emotion.mapper.MessageMapper;
import com.emotion.service.MessageService;
import com.emotion.util.UserContextUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.stream.Collectors;
/**
* 消息服务实现类
*
* @author huazhongmin
* @date 2025-09-08
*/
@Slf4j
@Service
public class MessageServiceImpl extends ServiceImpl<MessageMapper, Message> implements MessageService {
private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
@Override
public IPage<Message> getPage(BasePageRequest request) {
Page<Message> page = new Page<>(request.getCurrent(), request.getSize());
LambdaQueryWrapper<Message> wrapper = new LambdaQueryWrapper<>();
if (StringUtils.hasText(request.getKeyword())) {
wrapper.like(Message::getContent, request.getKeyword());
}
wrapper.eq(Message::getIsDeleted, 0).orderByDesc(Message::getCreateTime);
return this.page(page, wrapper);
}
@Override
public PageResult<MessageResponse> getPageWithResponse(MessagePageRequest request) {
// 从上下文中获取当前用户ID
String userId = UserContextUtils.requireCurrentUserId();
// 调用用户消息分页查询方法
PageResult<MessageResponse> pageResult = getUserMessagesWithPage(request);
return pageResult;
}
@Override
public IPage<Message> getPageByConversationId(BasePageRequest request, String conversationId) {
Page<Message> page = new Page<>(request.getCurrent(), request.getSize());
LambdaQueryWrapper<Message> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(Message::getConversationId, conversationId)
.eq(Message::getIsDeleted, 0)
.orderByAsc(Message::getMessageOrder);
return this.page(page, wrapper);
}
@Override
public List<Message> getByConversationId(String conversationId) {
LambdaQueryWrapper<Message> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(Message::getConversationId, conversationId)
.eq(Message::getIsDeleted, 0)
.orderByAsc(Message::getMessageOrder);
return this.list(wrapper);
}
@Override
public List<Message> getBySender(String sender) {
LambdaQueryWrapper<Message> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(Message::getSender, sender)
.eq(Message::getIsDeleted, 0)
.orderByDesc(Message::getCreateTime);
return this.list(wrapper);
}
@Override
public List<Message> getByTimeRange(String conversationId, LocalDateTime startTime, LocalDateTime endTime) {
LambdaQueryWrapper<Message> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(Message::getConversationId, conversationId)
.between(Message::getCreateTime, startTime, endTime)
.eq(Message::getIsDeleted, 0)
.orderByAsc(Message::getMessageOrder);
return this.list(wrapper);
}
@Override
public Message getLastMessageByConversationId(String conversationId) {
LambdaQueryWrapper<Message> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(Message::getConversationId, conversationId)
.eq(Message::getIsDeleted, 0)
.orderByDesc(Message::getMessageOrder)
.last("LIMIT 1");
return this.getOne(wrapper);
}
@Override
public List<Message> getRepliesByParentId(String parentMessageId) {
LambdaQueryWrapper<Message> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(Message::getParentMessageId, parentMessageId)
.eq(Message::getIsDeleted, 0)
.orderByAsc(Message::getMessageOrder);
return this.list(wrapper);
}
@Override
public Long countByConversationId(String conversationId) {
LambdaQueryWrapper<Message> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(Message::getConversationId, conversationId)
.eq(Message::getIsDeleted, 0);
return this.count(wrapper);
}
@Override
public Long countBySender(String sender) {
LambdaQueryWrapper<Message> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(Message::getSender, sender)
.eq(Message::getIsDeleted, 0);
return this.count(wrapper);
}
@Override
public Long countUnreadMessages(String conversationId) {
LambdaQueryWrapper<Message> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(Message::getConversationId, conversationId)
.eq(Message::getIsRead, 0)
.eq(Message::getIsDeleted, 0);
return this.count(wrapper);
}
@Override
public boolean updateStatus(String messageId, String status) {
Message message = new Message();
message.setId(messageId);
message.setStatus(status);
return this.updateById(message);
}
@Override
public boolean updateReadStatus(String messageId, Integer isRead) {
Message message = new Message();
message.setId(messageId);
message.setIsRead(isRead);
return this.updateById(message);
}
@Override
public boolean markConversationMessagesAsRead(String conversationId) {
LambdaQueryWrapper<Message> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(Message::getConversationId, conversationId)
.eq(Message::getIsRead, 0)
.eq(Message::getIsDeleted, 0);
Message updateMessage = new Message();
updateMessage.setIsRead(1);
return this.update(updateMessage, wrapper);
}
@Override
public Message createMessage(Message message) {
// 设置默认值
if (message.getTimestamp() == null) {
message.setTimestamp(LocalDateTime.now());
}
if (message.getStatus() == null) {
message.setStatus("sent");
}
if (message.getIsRead() == null) {
message.setIsRead(0);
}
// 设置消息顺序 - 在同一个会话中递增
if (message.getMessageOrder() == null) {
Long nextOrder = getNextMessageOrder(message.getConversationId());
message.setMessageOrder(nextOrder);
}
this.save(message);
return message;
}
/**
* 获取会话中下一个消息顺序
*
* @param conversationId 会话ID
* @return 下一个消息顺序
*/
private Long getNextMessageOrder(String conversationId) {
LambdaQueryWrapper<Message> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(Message::getConversationId, conversationId)
.eq(Message::getIsDeleted, 0)
.orderByDesc(Message::getMessageOrder)
.last("LIMIT 1");
Message lastMessage = this.getOne(wrapper);
if (lastMessage != null && lastMessage.getMessageOrder() != null) {
return lastMessage.getMessageOrder() + 1;
}
return 1L;
}
@Override
public boolean markAsRead(String messageId) {
return updateReadStatus(messageId, 1);
}
@Override
public List<Message> getByUserIdAndTimeRange(String userId, LocalDateTime startTime, LocalDateTime endTime) {
// 使用 MyBatis-Plus 条件构造器,直接根据消息表的 user_id 字段查询
LambdaQueryWrapper<Message> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(Message::getUserId, userId)
.between(Message::getCreateTime, startTime, endTime)
.eq(Message::getIsDeleted, 0)
.orderByAsc(Message::getMessageOrder);
return this.list(wrapper);
}
@Override
public IPage<Message> getByUserIdWithPage(String userId, Integer current, Integer size) {
// 使用 MyBatis-Plus 分页 + 条件构造器
Page<Message> page = new Page<>(current, size);
LambdaQueryWrapper<Message> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(Message::getUserId, userId)
.eq(Message::getIsDeleted, 0)
.orderByAsc(Message::getMessageOrder);
return this.page(page, wrapper);
}
@Override
public List<Message> searchByUserIdAndKeyword(String userId, String keyword, Integer limit) {
// 使用 MyBatis-Plus 分页 + 条件构造器,避免硬编码 SQL
Page<Message> page = new Page<>(1, limit);
LambdaQueryWrapper<Message> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(Message::getUserId, userId)
.eq(Message::getIsDeleted, 0)
.like(StringUtils.hasText(keyword), Message::getContent, keyword)
.orderByAsc(Message::getMessageOrder);
IPage<Message> result = this.page(page, wrapper);
return result.getRecords();
}
@Override
public List<Message> getRecentByUserId(String userId, Integer limit) {
// 使用 MyBatis-Plus 分页查询最近消息
Page<Message> page = new Page<>(1, limit);
LambdaQueryWrapper<Message> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(Message::getUserId, userId)
.eq(Message::getIsDeleted, 0)
.orderByDesc(Message::getMessageOrder);
IPage<Message> result = this.page(page, wrapper);
return result.getRecords();
}
@Override
public PageResult<MessageResponse> getUserMessagesWithPage(MessagePageRequest request) {
// 从上下文中获取当前用户ID
String userId = UserContextUtils.requireCurrentUserId();
// 调用原有的分页查询方法
IPage<Message> page = getByUserIdWithPage(userId, Math.toIntExact(request.getCurrent()),
Math.toIntExact(request.getSize()));
// 转换为响应对象
List<MessageResponse> responses = page.getRecords().stream()
.map(this::convertToResponse)
.collect(Collectors.toList());
// 构建分页结果
PageResult<MessageResponse> pageResult = new PageResult<>();
pageResult.setCurrent(page.getCurrent());
pageResult.setSize(page.getSize());
pageResult.setTotal(page.getTotal());
pageResult.setPages(page.getPages());
pageResult.setRecords(responses);
return pageResult;
}
@Override
public List<MessageResponse> searchUserMessages(MessageSearchRequest request) {
// 从上下文中获取当前用户ID
String userId = UserContextUtils.requireCurrentUserId();
// 调用原有的搜索方法
List<Message> messages = searchByUserIdAndKeyword(userId, request.getKeyword(), request.getLimit());
// 转换为响应对象
return messages.stream()
.map(this::convertToResponse)
.collect(Collectors.toList());
}
@Override
public List<MessageResponse> getUserRecentMessages(MessageRecentRequest request) {
// 从上下文中获取当前用户ID
String userId = UserContextUtils.requireCurrentUserId();
// 调用原有的获取最近消息方法
List<Message> messages = getRecentByUserId(userId, request.getLimit());
// 转换为响应对象
return messages.stream()
.map(this::convertToResponse)
.collect(Collectors.toList());
}
@Override
public MessageResponse createMessageFromRequest(MessageCreateRequest request) {
// 从上下文中获取当前用户ID
String userId = UserContextUtils.requireCurrentUserId();
// 构建消息对象
Message message = new Message();
message.setConversationId(request.getConversationId());
message.setCreateBy(userId);
message.setContent(request.getContent());
message.setType(request.getContentType());
message.setSender(request.getSenderType());
// 调用原有的创建方法
Message savedMessage = createMessage(message);
// 转换为响应对象
return convertToResponse(savedMessage);
}
@Override
public MessageResponse getMessageById(String id) {
Message message = getById(id);
if (message == null) {
return null;
}
// 转换为响应对象
return convertToResponse(message);
}
@Override
public PageResult<MessageResponse> searchWithResponse(MessageSearchRequest request) {
// 从上下文中获取当前用户ID
String userId = UserContextUtils.requireCurrentUserId();
// 构建分页对象
Page<Message> page = new Page<>(1L, request.getLimit().longValue());
// 调用搜索方法
List<Message> messages = searchByUserIdAndKeyword(userId, request.getKeyword(), request.getLimit());
// 设置分页信息
page.setRecords(messages);
page.setTotal(messages.size());
// 转换为响应对象
List<MessageResponse> responses = messages.stream()
.map(this::convertToResponse)
.collect(Collectors.toList());
// 构建分页结果
PageResult<MessageResponse> pageResult = new PageResult<>();
pageResult.setRecords(responses);
pageResult.setCurrent(page.getCurrent());
pageResult.setSize(page.getSize());
pageResult.setTotal(page.getTotal());
pageResult.setPages(page.getPages());
return pageResult;
}
@Override
public PageResult<MessageResponse> getRecentWithResponse(MessageRecentRequest request) {
// 从上下文中获取当前用户ID
String userId = UserContextUtils.requireCurrentUserId();
// 构建分页对象
Page<Message> page = new Page<>(1L, request.getLimit().longValue());
// 调用获取最近消息方法
List<Message> messages = getRecentByUserId(userId, request.getLimit());
// 设置分页信息
page.setRecords(messages);
page.setTotal(messages.size());
// 转换为响应对象
List<MessageResponse> responses = messages.stream()
.map(this::convertToResponse)
.collect(Collectors.toList());
// 构建分页结果
PageResult<MessageResponse> pageResult = new PageResult<>();
pageResult.setRecords(responses);
pageResult.setCurrent(page.getCurrent());
pageResult.setSize(page.getSize());
pageResult.setTotal(page.getTotal());
pageResult.setPages(page.getPages());
return pageResult;
}
@Override
public MessageResponse updateMessage(String id, String content) {
Message message = this.getById(id);
if (message == null || message.getIsDeleted() == 1) {
return null;
}
// 更新内容
message.setContent(content);
this.updateById(message);
// 转换为响应对象
return convertToResponse(message);
}
@Override
public boolean deleteMessage(String id) {
Message message = this.getById(id);
if (message == null) {
return false;
}
// 使用 MyBatis-Plus 的 removeById 方法,自动处理逻辑删除
return this.removeById(id);
}
/**
* 转换为响应对象
*/
private MessageResponse convertToResponse(Message message) {
MessageResponse response = new MessageResponse();
BeanUtils.copyProperties(message, response);
response.setId(message.getId());
if (message.getCreateTime() != null) {
response.setCreateTime(message.getCreateTime().format(DATE_TIME_FORMATTER));
}
if (message.getUpdateTime() != null) {
response.setUpdateTime(message.getUpdateTime().format(DATE_TIME_FORMATTER));
}
return response;
}
}
@@ -0,0 +1,356 @@
package com.emotion.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.emotion.common.BasePageRequest;
import com.emotion.common.PageResult;
import com.emotion.dto.request.reward.RewardCreateRequest;
import com.emotion.dto.request.reward.RewardUpdateRequest;
import com.emotion.dto.response.reward.RewardResponse;
import com.emotion.entity.Reward;
import com.emotion.mapper.RewardMapper;
import com.emotion.service.RewardService;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.time.LocalDateTime;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
/**
* 奖励服务实现类
*
* @author huazhongmin
* @date 2025-09-08
*/
@Service
public class RewardServiceImpl extends ServiceImpl<RewardMapper, Reward> implements RewardService {
@Override
public IPage<Reward> getPage(BasePageRequest request) {
Page<Reward> page = new Page<>(request.getCurrent(), request.getSize());
LambdaQueryWrapper<Reward> wrapper = new LambdaQueryWrapper<>();
// 关键词搜索
if (StringUtils.hasText(request.getKeyword())) {
wrapper.and(w -> w.like(Reward::getName, request.getKeyword())
.or().like(Reward::getDescription, request.getKeyword()));
}
wrapper.eq(Reward::getIsDeleted, 0);
// 排序
if (StringUtils.hasText(request.getOrderBy())) {
if ("asc".equalsIgnoreCase(request.getOrderDirection())) {
wrapper.orderByAsc(Reward::getCreateTime);
} else {
wrapper.orderByDesc(Reward::getCreateTime);
}
} else {
wrapper.orderByDesc(Reward::getCreateTime);
}
return this.page(page, wrapper);
}
@Override
public PageResult<RewardResponse> getPageWithResponse(BasePageRequest request) {
IPage<Reward> page = getPage(request);
List<RewardResponse> responses = page.getRecords().stream()
.map(this::convertToResponse)
.collect(Collectors.toList());
PageResult<RewardResponse> pageResult = new PageResult<>();
pageResult.setRecords(responses);
pageResult.setCurrent(page.getCurrent());
pageResult.setSize(page.getSize());
pageResult.setTotal(page.getTotal());
pageResult.setPages(page.getPages());
return pageResult;
}
@Override
public IPage<Reward> getPageByUserId(BasePageRequest request, String userId) {
Page<Reward> page = new Page<>(request.getCurrent(), request.getSize());
LambdaQueryWrapper<Reward> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(Reward::getCreateBy, userId)
.eq(Reward::getIsDeleted, 0)
.orderByDesc(Reward::getCreateTime);
return this.page(page, wrapper);
}
@Override
public RewardResponse getRewardResponseById(String id) {
Reward reward = this.getById(id);
if (reward == null || reward.getIsDeleted() == 1) {
return null;
}
return convertToResponse(reward);
}
@Override
public List<Reward> getByUserId(String userId) {
LambdaQueryWrapper<Reward> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(Reward::getCreateBy, userId)
.eq(Reward::getIsDeleted, 0)
.orderByDesc(Reward::getCreateTime);
return this.list(wrapper);
}
@Override
public List<Reward> getByRewardType(String rewardType) {
LambdaQueryWrapper<Reward> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(Reward::getType, rewardType)
.eq(Reward::getIsDeleted, 0)
.orderByDesc(Reward::getCreateTime);
return this.list(wrapper);
}
@Override
public List<Reward> getByUserIdAndRewardType(String userId, String rewardType) {
LambdaQueryWrapper<Reward> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(Reward::getCreateBy, userId)
.eq(Reward::getType, rewardType)
.eq(Reward::getIsDeleted, 0)
.orderByDesc(Reward::getCreateTime);
return this.list(wrapper);
}
@Override
public List<Reward> getByStatus(String status) {
// 这里需要根据实际的status字段来实现
// 由于Reward实体中没有明确的status字段,这里返回空列表
return Collections.emptyList();
}
@Override
public List<Reward> getByUserIdAndStatus(String userId, String status) {
// 这里需要根据实际的status字段来实现
return Collections.emptyList();
}
@Override
public List<Reward> getByPointsRange(Integer minPoints, Integer maxPoints) {
// 这里需要根据实际的points字段来实现
return Collections.emptyList();
}
@Override
public List<Reward> getByTimeRange(LocalDateTime startTime, LocalDateTime endTime) {
LambdaQueryWrapper<Reward> wrapper = new LambdaQueryWrapper<>();
wrapper.between(Reward::getCreateTime, startTime, endTime)
.eq(Reward::getIsDeleted, 0)
.orderByDesc(Reward::getCreateTime);
return this.list(wrapper);
}
@Override
public List<Reward> getByEarnedTimeRange(LocalDateTime startTime, LocalDateTime endTime) {
LambdaQueryWrapper<Reward> wrapper = new LambdaQueryWrapper<>();
wrapper.between(Reward::getEarnedTime, startTime, endTime)
.eq(Reward::getIsDeleted, 0)
.orderByDesc(Reward::getEarnedTime);
return this.list(wrapper);
}
@Override
public Long countByUserId(String userId) {
LambdaQueryWrapper<Reward> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(Reward::getCreateBy, userId)
.eq(Reward::getIsDeleted, 0);
return this.count(wrapper);
}
@Override
public Long countByRewardType(String rewardType) {
LambdaQueryWrapper<Reward> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(Reward::getType, rewardType)
.eq(Reward::getIsDeleted, 0);
return this.count(wrapper);
}
@Override
public Long countByUserIdAndRewardType(String userId, String rewardType) {
LambdaQueryWrapper<Reward> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(Reward::getCreateBy, userId)
.eq(Reward::getType, rewardType)
.eq(Reward::getIsDeleted, 0);
return this.count(wrapper);
}
@Override
public Long countByStatus(String status) {
// 这里需要根据实际的status字段来实现
return 0L;
}
@Override
public Integer sumPointsByUserId(String userId) {
// 这里需要根据实际的points字段来实现
return 0;
}
@Override
public Integer sumPointsByUserIdAndRewardType(String userId, String rewardType) {
// 这里需要根据实际的points字段来实现
return 0;
}
@Override
public List<Reward> getRecentByUserId(String userId, Integer limit) {
LambdaQueryWrapper<Reward> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(Reward::getCreateBy, userId)
.eq(Reward::getIsDeleted, 0)
.orderByDesc(Reward::getCreateTime)
.last("LIMIT " + limit);
return this.list(wrapper);
}
@Override
public List<Reward> getHighPointsRewards(Integer minPoints) {
// 这里需要根据实际的points字段来实现
return Collections.emptyList();
}
@Override
public List<Reward> getPendingRewardsByUserId(String userId) {
// 这里需要根据实际的status字段来实现
return Collections.emptyList();
}
@Override
public List<Reward> getClaimedRewardsByUserId(String userId) {
// 这里需要根据实际的status字段来实现
return Collections.emptyList();
}
@Override
public List<Reward> getExpiredRewards() {
// 这里需要根据实际的过期时间字段来实现
return Collections.emptyList();
}
@Override
public boolean updateStatus(String id, String status, LocalDateTime claimedTime) {
// 这里需要根据实际的status字段来实现
return false;
}
@Override
public boolean updateExpiredRewards() {
// 这里需要根据实际的过期时间字段来实现
return false;
}
@Override
public List<Reward> getBySource(String source) {
// 这里需要根据实际的source字段来实现
return Collections.emptyList();
}
@Override
public Reward createReward(String userId, String rewardType, String value,
String description, String topicId, String achievementId, String icon, String rarity) {
Reward reward = new Reward();
reward.setType(rewardType);
reward.setName(rewardType + "奖励");
reward.setValue(value);
reward.setDescription(description);
reward.setTopicId(topicId);
reward.setAchievementId(achievementId);
reward.setIcon(icon);
reward.setRarity(rarity);
reward.setCreateBy(userId);
reward.setEarnedTime(LocalDateTime.now());
reward.setIsNew(1);
this.save(reward);
return reward;
}
@Override
public RewardResponse createRewardWithResponse(RewardCreateRequest request) {
Reward reward = createReward(
request.getUserId(),
request.getRewardType(),
request.getValue(),
request.getDescription(),
request.getTopicId(),
request.getAchievementId(),
request.getIcon(),
request.getRarity());
return convertToResponse(reward);
}
@Override
public RewardResponse updateRewardWithResponse(RewardUpdateRequest request) {
Reward reward = this.getById(request.getId());
if (reward == null || reward.getIsDeleted() == 1) {
return null;
}
// 只更新非空字段
if (StringUtils.hasText(request.getRewardType())) {
reward.setType(request.getRewardType());
}
if (StringUtils.hasText(request.getValue())) {
reward.setValue(request.getValue());
}
if (StringUtils.hasText(request.getDescription())) {
reward.setDescription(request.getDescription());
}
if (StringUtils.hasText(request.getTopicId())) {
reward.setTopicId(request.getTopicId());
}
if (StringUtils.hasText(request.getAchievementId())) {
reward.setAchievementId(request.getAchievementId());
}
if (StringUtils.hasText(request.getIcon())) {
reward.setIcon(request.getIcon());
}
if (StringUtils.hasText(request.getRarity())) {
reward.setRarity(request.getRarity());
}
if (request.getIsNew() != null) {
reward.setIsNew(request.getIsNew());
}
this.updateById(reward);
return convertToResponse(reward);
}
@Override
public boolean deleteReward(String id) {
Reward reward = this.getById(id);
if (reward == null) {
return false;
}
// 使用 MyBatis-Plus 的 removeById 方法,自动处理逻辑删除
return this.removeById(id);
}
/**
* 将实体转换为响应对象
*/
private RewardResponse convertToResponse(Reward reward) {
if (reward == null) {
return null;
}
RewardResponse response = new RewardResponse();
BeanUtils.copyProperties(reward, response);
response.setId(reward.getId());
if (reward.getCreateTime() != null) {
response.setCreateTime(reward.getCreateTime().toString());
}
if (reward.getUpdateTime() != null) {
response.setUpdateTime(reward.getUpdateTime().toString());
}
return response;
}
}
@@ -0,0 +1,66 @@
package com.emotion.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.emotion.entity.SocialProfileInsight;
import com.emotion.mapper.SocialProfileInsightMapper;
import com.emotion.service.ScriptContextService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.util.List;
import java.util.stream.Collectors;
@Service
public class ScriptContextServiceImpl implements ScriptContextService {
@Autowired
private SocialProfileInsightMapper socialProfileInsightMapper;
@Override
public String buildSocialInsightContext(String userId, Boolean useSocialInsights) {
if (Boolean.FALSE.equals(useSocialInsights) || !StringUtils.hasText(userId)) {
return "";
}
List<SocialProfileInsight> insights = listConfirmedInsights(userId, 8);
if (insights.isEmpty()) {
return "";
}
StringBuilder sb = new StringBuilder("【用户确认的人生素材画像】\n");
for (SocialProfileInsight insight : insights) {
sb.append("- ")
.append(insight.getInsightType()).append("")
.append(insight.getLabel());
if (StringUtils.hasText(insight.getSummary())) {
sb.append("").append(insight.getSummary());
}
sb.append("\n");
}
sb.append("请仅将这些画像作为用户已确认的创作偏好和人生素材,不要编造未确认的社交平台事实。");
return sb.toString();
}
@Override
public List<String> getConfirmedInsightLabels(String userId) {
if (!StringUtils.hasText(userId)) {
return List.of();
}
return listConfirmedInsights(userId, 8).stream()
.map(SocialProfileInsight::getLabel)
.filter(StringUtils::hasText)
.distinct()
.collect(Collectors.toList());
}
private List<SocialProfileInsight> listConfirmedInsights(String userId, int limit) {
LambdaQueryWrapper<SocialProfileInsight> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(SocialProfileInsight::getUserId, userId)
.eq(SocialProfileInsight::getIsDeleted, 0)
.eq(SocialProfileInsight::getStatus, "confirmed")
.orderByDesc(SocialProfileInsight::getConfirmedAt)
.orderByDesc(SocialProfileInsight::getUpdateTime)
.last("LIMIT " + Math.max(1, limit));
return socialProfileInsightMapper.selectList(wrapper);
}
}
@@ -0,0 +1,253 @@
package com.emotion.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.emotion.dto.request.social.SocialContentLinkImportRequest;
import com.emotion.dto.request.social.SocialContentManualImportRequest;
import com.emotion.dto.response.social.SocialContentItemResponse;
import com.emotion.entity.SocialContentItem;
import com.emotion.entity.SocialProfileInsight;
import com.emotion.entity.UserConsentLog;
import com.emotion.mapper.SocialContentItemMapper;
import com.emotion.mapper.SocialProfileInsightMapper;
import com.emotion.mapper.UserConsentLogMapper;
import com.emotion.service.SocialContentService;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.DigestUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.multipart.MultipartFile;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.stream.Collectors;
@Service
public class SocialContentServiceImpl extends ServiceImpl<SocialContentItemMapper, SocialContentItem>
implements SocialContentService {
private static final Set<String> PLATFORMS = Set.of("xiaohongshu", "weibo", "wechat", "other");
private static final int MAX_CONTENT_LENGTH = 20000;
private static final long MAX_SCREENSHOT_SIZE = 5L * 1024L * 1024L;
private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
@Autowired
private UserConsentLogMapper userConsentLogMapper;
@Autowired
private SocialProfileInsightMapper socialProfileInsightMapper;
@Override
public SocialContentItemResponse manualImport(String userId, SocialContentManualImportRequest request) {
validateUser(userId);
String platform = normalizePlatform(request.getPlatform());
String content = normalizeContent(request.getContent());
SocialContentItem item = findDuplicate(userId, platform, content);
if (item == null) {
item = new SocialContentItem();
item.setUserId(userId);
item.setPlatform(platform);
item.setSourceType("manual_text");
item.setTitle(trimToNull(request.getTitle()));
item.setContent(content);
item.setImportStatus("parsed");
item.setApprovedForAi(Boolean.TRUE.equals(request.getApprovedForAi()) ? 1 : 0);
item.setContentHash(hashContent(content));
this.save(item);
} else if (Boolean.TRUE.equals(request.getApprovedForAi()) && !isApproved(item)) {
item.setApprovedForAi(1);
this.updateById(item);
}
logConsentIfApproved(userId, platform, item.getApprovedForAi());
return convertToResponse(item);
}
@Override
public SocialContentItemResponse linkImport(String userId, SocialContentLinkImportRequest request) {
validateUser(userId);
String platform = normalizePlatform(request.getPlatform());
if (!StringUtils.hasText(request.getSourceUrl())) {
throw new IllegalArgumentException("来源链接不能为空");
}
String content = StringUtils.hasText(request.getContent())
? normalizeContent(request.getContent())
: normalizeContent(request.getSourceUrl());
SocialContentItem item = findDuplicate(userId, platform, content);
if (item == null) {
item = new SocialContentItem();
item.setUserId(userId);
item.setPlatform(platform);
item.setSourceType("public_link");
item.setSourceUrl(request.getSourceUrl().trim());
item.setTitle(trimToNull(request.getTitle()));
item.setContent(content);
item.setImportStatus("parsed");
item.setApprovedForAi(Boolean.TRUE.equals(request.getApprovedForAi()) ? 1 : 0);
item.setContentHash(hashContent(content));
this.save(item);
} else if (Boolean.TRUE.equals(request.getApprovedForAi()) && !isApproved(item)) {
item.setApprovedForAi(1);
this.updateById(item);
}
logConsentIfApproved(userId, platform, item.getApprovedForAi());
return convertToResponse(item);
}
@Override
public SocialContentItemResponse screenshotImport(String userId, String platform, MultipartFile file) {
validateUser(userId);
normalizePlatform(platform);
if (file == null || file.isEmpty()) {
throw new IllegalArgumentException("截图文件不能为空");
}
if (file.getSize() > MAX_SCREENSHOT_SIZE) {
throw new IllegalArgumentException("截图不能超过5MB");
}
String name = file.getOriginalFilename() == null ? "" : file.getOriginalFilename().toLowerCase(Locale.ROOT);
if (!(name.endsWith(".jpg") || name.endsWith(".jpeg") || name.endsWith(".png") || name.endsWith(".webp"))) {
throw new IllegalArgumentException("仅支持jpg、png、webp截图");
}
throw new IllegalStateException("OCR暂未启用,请先使用粘贴文本导入");
}
@Override
public List<SocialContentItemResponse> listByUser(String userId) {
validateUser(userId);
LambdaQueryWrapper<SocialContentItem> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(SocialContentItem::getUserId, userId)
.eq(SocialContentItem::getIsDeleted, 0)
.orderByDesc(SocialContentItem::getCreateTime);
return this.list(wrapper).stream().map(this::convertToResponse).collect(Collectors.toList());
}
@Override
public boolean deleteByUser(String userId, String id, Boolean keepConfirmedInsights) {
validateUser(userId);
SocialContentItem item = getOwned(userId, id);
if (item == null) {
return false;
}
item.setIsDeleted(1);
item.setDeletedAt(LocalDateTime.now());
this.updateById(item);
LambdaUpdateWrapper<SocialProfileInsight> wrapper = new LambdaUpdateWrapper<>();
wrapper.eq(SocialProfileInsight::getUserId, userId)
.eq(SocialProfileInsight::getSourceItemId, id)
.eq(SocialProfileInsight::getIsDeleted, 0);
if (Boolean.TRUE.equals(keepConfirmedInsights)) {
wrapper.ne(SocialProfileInsight::getStatus, "confirmed");
}
wrapper.set(SocialProfileInsight::getStatus, "deleted")
.set(SocialProfileInsight::getIsDeleted, 1)
.set(SocialProfileInsight::getDeletedAt, LocalDateTime.now());
socialProfileInsightMapper.update(null, wrapper);
return true;
}
@Override
public SocialContentItemResponse updateApproval(String userId, String id, Boolean approvedForAi) {
validateUser(userId);
SocialContentItem item = getOwned(userId, id);
if (item == null) {
return null;
}
item.setApprovedForAi(Boolean.TRUE.equals(approvedForAi) ? 1 : 0);
this.updateById(item);
logConsentIfApproved(userId, item.getPlatform(), item.getApprovedForAi());
return convertToResponse(item);
}
private SocialContentItem getOwned(String userId, String id) {
if (!StringUtils.hasText(id)) {
return null;
}
LambdaQueryWrapper<SocialContentItem> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(SocialContentItem::getId, id)
.eq(SocialContentItem::getUserId, userId)
.eq(SocialContentItem::getIsDeleted, 0);
return this.getOne(wrapper, false);
}
private SocialContentItem findDuplicate(String userId, String platform, String content) {
LambdaQueryWrapper<SocialContentItem> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(SocialContentItem::getUserId, userId)
.eq(SocialContentItem::getPlatform, platform)
.eq(SocialContentItem::getContentHash, hashContent(content))
.eq(SocialContentItem::getIsDeleted, 0);
return this.getOne(wrapper, false);
}
private void logConsentIfApproved(String userId, String platform, Integer approvedForAi) {
if (approvedForAi == null || approvedForAi != 1) {
return;
}
UserConsentLog log = new UserConsentLog();
log.setUserId(userId);
log.setPlatform(platform);
log.setConsentType("ai_profile_analysis");
log.setConsentVersion("v1");
log.setScope("imported_social_content");
log.setPurpose("用于生成可编辑的人生画像,并增强人生剧本生成");
log.setStatus("granted");
log.setGrantedAt(LocalDateTime.now());
userConsentLogMapper.insert(log);
}
private String normalizePlatform(String platform) {
String value = platform == null ? "" : platform.trim().toLowerCase(Locale.ROOT);
if (!PLATFORMS.contains(value)) {
throw new IllegalArgumentException("不支持的平台");
}
return value;
}
private String normalizeContent(String content) {
if (!StringUtils.hasText(content)) {
throw new IllegalArgumentException("导入内容不能为空");
}
String normalized = content.replaceAll("\\s+", " ").trim();
if (normalized.length() > MAX_CONTENT_LENGTH) {
throw new IllegalArgumentException("导入内容不能超过20000个字符");
}
return normalized;
}
private String hashContent(String content) {
return DigestUtils.md5DigestAsHex(content.getBytes(StandardCharsets.UTF_8));
}
private boolean isApproved(SocialContentItem item) {
return item.getApprovedForAi() != null && item.getApprovedForAi() == 1;
}
private String trimToNull(String value) {
return StringUtils.hasText(value) ? value.trim() : null;
}
private void validateUser(String userId) {
if (!StringUtils.hasText(userId)) {
throw new IllegalArgumentException("用户未登录");
}
}
private SocialContentItemResponse convertToResponse(SocialContentItem item) {
SocialContentItemResponse response = new SocialContentItemResponse();
BeanUtils.copyProperties(item, response);
response.setApprovedForAi(isApproved(item));
response.setSourceDeleted(item.getIsDeleted() != null && item.getIsDeleted() == 1);
if (item.getCreateTime() != null) {
response.setCreateTime(item.getCreateTime().format(DATE_TIME_FORMATTER));
}
if (item.getUpdateTime() != null) {
response.setUpdateTime(item.getUpdateTime().format(DATE_TIME_FORMATTER));
}
return response;
}
}
@@ -0,0 +1,253 @@
package com.emotion.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.emotion.dto.request.social.SocialInsightGenerateRequest;
import com.emotion.dto.request.social.SocialInsightUpdateRequest;
import com.emotion.dto.response.social.SocialProfileInsightResponse;
import com.emotion.entity.SocialContentItem;
import com.emotion.entity.SocialProfileInsight;
import com.emotion.mapper.SocialContentItemMapper;
import com.emotion.mapper.SocialProfileInsightMapper;
import com.emotion.service.SocialInsightService;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.stream.Collectors;
@Service
public class SocialInsightServiceImpl extends ServiceImpl<SocialProfileInsightMapper, SocialProfileInsight>
implements SocialInsightService {
private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
private static final Set<String> STATUS_VALUES = Set.of("suggested", "confirmed", "rejected", "deleted");
@Autowired
private SocialContentItemMapper contentItemMapper;
@Override
public List<SocialProfileInsightResponse> generateInsights(String userId, SocialInsightGenerateRequest request) {
if (!StringUtils.hasText(userId)) {
return List.of();
}
LambdaQueryWrapper<SocialContentItem> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(SocialContentItem::getUserId, userId)
.eq(SocialContentItem::getIsDeleted, 0)
.eq(SocialContentItem::getApprovedForAi, 1)
.isNotNull(SocialContentItem::getContent)
.orderByDesc(SocialContentItem::getCreateTime);
if (request != null && request.getSourceItemIds() != null && !request.getSourceItemIds().isEmpty()) {
wrapper.in(SocialContentItem::getId, request.getSourceItemIds());
}
List<SocialContentItem> contentItems = contentItemMapper.selectList(wrapper);
List<SocialProfileInsight> saved = new ArrayList<>();
for (SocialContentItem item : contentItems) {
for (SocialProfileInsight insight : extractInsights(userId, item)) {
if (existsActiveInsight(userId, item.getId(), insight.getInsightType(), insight.getLabel())) {
continue;
}
this.save(insight);
saved.add(insight);
}
}
return saved.stream()
.sorted(Comparator.comparing(SocialProfileInsight::getCreateTime, Comparator.nullsLast(Comparator.reverseOrder())))
.map(this::convertToResponse)
.collect(Collectors.toList());
}
@Override
public List<SocialProfileInsightResponse> listByUser(String userId, String status) {
if (!StringUtils.hasText(userId)) {
return List.of();
}
LambdaQueryWrapper<SocialProfileInsight> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(SocialProfileInsight::getUserId, userId)
.eq(SocialProfileInsight::getIsDeleted, 0);
if (StringUtils.hasText(status)) {
wrapper.eq(SocialProfileInsight::getStatus, status);
}
wrapper.orderByDesc(SocialProfileInsight::getUpdateTime)
.orderByDesc(SocialProfileInsight::getCreateTime);
return this.list(wrapper).stream()
.map(this::convertToResponse)
.collect(Collectors.toList());
}
@Override
public SocialProfileInsightResponse updateByUser(String userId, String id, SocialInsightUpdateRequest request) {
SocialProfileInsight insight = getOwnedInsight(userId, id);
if (insight == null || request == null) {
return null;
}
if (StringUtils.hasText(request.getLabel())) {
insight.setLabel(request.getLabel().trim());
insight.setUserEdited(1);
}
if (request.getSummary() != null) {
insight.setSummary(request.getSummary().trim());
insight.setUserEdited(1);
}
if (StringUtils.hasText(request.getStatus())) {
String nextStatus = request.getStatus().trim().toLowerCase(Locale.ROOT);
if (!STATUS_VALUES.contains(nextStatus)) {
throw new IllegalArgumentException("不支持的画像状态");
}
insight.setStatus(nextStatus);
if ("confirmed".equals(nextStatus)) {
insight.setConfirmedAt(LocalDateTime.now());
}
if ("deleted".equals(nextStatus)) {
insight.setIsDeleted(1);
insight.setDeletedAt(LocalDateTime.now());
}
}
this.updateById(insight);
return convertToResponse(insight);
}
@Override
public boolean deleteByUser(String userId, String id) {
SocialProfileInsight insight = getOwnedInsight(userId, id);
if (insight == null) {
return false;
}
insight.setStatus("deleted");
insight.setIsDeleted(1);
insight.setDeletedAt(LocalDateTime.now());
return this.updateById(insight);
}
private List<SocialProfileInsight> extractInsights(String userId, SocialContentItem item) {
String content = item.getContent() == null ? "" : item.getContent().trim();
if (content.isEmpty()) {
return List.of();
}
List<SocialProfileInsight> insights = new ArrayList<>();
addIfMatched(insights, userId, item, "value", "职场成长", "用户内容中多次出现工作、职场或创业相关表达,适合转化为事业成长线。",
content, BigDecimal.valueOf(0.78), "工作", "职场", "老板", "产品", "创业", "公司", "项目");
addIfMatched(insights, userId, item, "value", "被认可", "用户对被看见、被肯定或证明自己有较强叙事诉求。",
content, BigDecimal.valueOf(0.74), "认可", "看见", "", "肯定", "证明", "成绩", "价值");
addIfMatched(insights, userId, item, "interest", "旅行探索", "用户表达了对远方、城市或旅行场景的兴趣,可用于开放式人生分支。",
content, BigDecimal.valueOf(0.70), "旅行", "旅游", "城市", "远方", "大理", "海边", "出发");
addIfMatched(insights, userId, item, "interest", "创作表达", "用户内容中有写作、音乐、设计或内容创作倾向,适合作为主角天赋线。",
content, BigDecimal.valueOf(0.72), "写作", "音乐", "创作", "设计", "内容", "", "摄影");
addIfMatched(insights, userId, item, "emotion_pattern", "关系修复", "用户对关系、告别或重新理解自己有明显表达,可用于情感修复线。",
content, BigDecimal.valueOf(0.69), "关系", "分手", "告别", "喜欢", "", "朋友", "家人");
addIfMatched(insights, userId, item, "script_theme", "低谷逆袭", "用户提到压力、失败或低谷,可转化为爽文开端和反转动力。",
content, BigDecimal.valueOf(0.73), "低谷", "失败", "焦虑", "难过", "压力", "崩溃", "迷茫");
if (insights.isEmpty()) {
insights.add(buildInsight(userId, item, "script_theme", "自我成长",
"这段内容适合作为自我探索和人生选择的素材。",
excerpt(content), BigDecimal.valueOf(0.58)));
}
return insights;
}
private void addIfMatched(List<SocialProfileInsight> insights, String userId, SocialContentItem item,
String type, String label, String summary, String content,
BigDecimal confidence, String... keywords) {
String lower = content.toLowerCase(Locale.ROOT);
for (String keyword : keywords) {
if (lower.contains(keyword.toLowerCase(Locale.ROOT))) {
insights.add(buildInsight(userId, item, type, label, summary, excerptAround(content, keyword), confidence));
return;
}
}
}
private SocialProfileInsight buildInsight(String userId, SocialContentItem item, String type, String label,
String summary, String evidence, BigDecimal confidence) {
SocialProfileInsight insight = new SocialProfileInsight();
insight.setUserId(userId);
insight.setSourceItemId(item.getId());
insight.setInsightType(type);
insight.setLabel(label);
insight.setSummary(summary);
insight.setEvidenceExcerpt(evidence);
insight.setConfidence(confidence);
insight.setStatus("suggested");
insight.setUserEdited(0);
insight.setIsDeleted(0);
return insight;
}
private boolean existsActiveInsight(String userId, String sourceItemId, String type, String label) {
LambdaQueryWrapper<SocialProfileInsight> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(SocialProfileInsight::getUserId, userId)
.eq(SocialProfileInsight::getSourceItemId, sourceItemId)
.eq(SocialProfileInsight::getInsightType, type)
.eq(SocialProfileInsight::getLabel, label)
.eq(SocialProfileInsight::getIsDeleted, 0);
return this.count(wrapper) > 0;
}
private SocialProfileInsight getOwnedInsight(String userId, String id) {
if (!StringUtils.hasText(userId) || !StringUtils.hasText(id)) {
return null;
}
LambdaQueryWrapper<SocialProfileInsight> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(SocialProfileInsight::getUserId, userId)
.eq(SocialProfileInsight::getId, id)
.eq(SocialProfileInsight::getIsDeleted, 0);
return this.getOne(wrapper, false);
}
private String excerptAround(String content, String keyword) {
int index = content.indexOf(keyword);
if (index < 0) {
return excerpt(content);
}
int start = Math.max(0, index - 45);
int end = Math.min(content.length(), index + keyword.length() + 65);
return excerpt(content.substring(start, end));
}
private String excerpt(String text) {
String normalized = text.replaceAll("\\s+", " ").trim();
if (normalized.length() <= 120) {
return normalized;
}
return normalized.substring(0, 120) + "...";
}
private SocialProfileInsightResponse convertToResponse(SocialProfileInsight insight) {
SocialProfileInsightResponse response = new SocialProfileInsightResponse();
BeanUtils.copyProperties(insight, response);
response.setId(insight.getId());
response.setUserEdited(insight.getUserEdited() != null && insight.getUserEdited() == 1);
response.setSourceDeleted(isSourceDeleted(insight.getSourceItemId()));
if (insight.getCreateTime() != null) {
response.setCreateTime(insight.getCreateTime().format(DATE_TIME_FORMATTER));
}
if (insight.getUpdateTime() != null) {
response.setUpdateTime(insight.getUpdateTime().format(DATE_TIME_FORMATTER));
}
return response;
}
private Boolean isSourceDeleted(String sourceItemId) {
if (!StringUtils.hasText(sourceItemId)) {
return true;
}
SocialContentItem item = contentItemMapper.selectById(sourceItemId);
return item == null || (item.getIsDeleted() != null && item.getIsDeleted() == 1);
}
}
@@ -0,0 +1,108 @@
package com.emotion.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.emotion.dto.request.SystemConfigUpdateRequest;
import com.emotion.dto.response.LoginConfigResponse;
import com.emotion.dto.response.SystemConfigResponse;
import com.emotion.entity.SystemConfig;
import com.emotion.mapper.SystemConfigMapper;
import com.emotion.service.SystemConfigService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
/**
* 系统配置服务实现类
*
* @author system
* @date 2026-06-27
*/
@Slf4j
@Service
public class SystemConfigServiceImpl extends ServiceImpl<SystemConfigMapper, SystemConfig> implements SystemConfigService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
private static final String CACHE_PREFIX = "system_config:";
private static final long CACHE_EXPIRE_HOURS = 24;
private static final String SMS_LOGIN_ENABLED_KEY = "sms_login_enabled";
@Override
public List<SystemConfigResponse> getVisibleConfigList() {
LambdaQueryWrapper<SystemConfig> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(SystemConfig::getIsVisible, 1)
.orderByAsc(SystemConfig::getSortOrder);
List<SystemConfig> configs = list(wrapper);
return configs.stream().map(this::convertToResponse).collect(Collectors.toList());
}
@Override
public void batchUpdateConfigs(List<SystemConfigUpdateRequest> requests) {
for (SystemConfigUpdateRequest request : requests) {
LambdaUpdateWrapper<SystemConfig> updateWrapper = new LambdaUpdateWrapper<>();
updateWrapper.eq(SystemConfig::getConfigKey, request.getConfigKey())
.set(SystemConfig::getConfigValue, request.getConfigValue());
update(updateWrapper);
// 清除缓存
redisTemplate.delete(CACHE_PREFIX + request.getConfigKey());
log.info("更新系统配置: key={}, value={}", request.getConfigKey(), request.getConfigValue());
}
}
@Override
public String getConfigValue(String configKey) {
// 先查 Redis 缓存
String cacheKey = CACHE_PREFIX + configKey;
String cached = (String) redisTemplate.opsForValue().get(cacheKey);
if (cached != null) {
return cached;
}
// 查数据库
LambdaQueryWrapper<SystemConfig> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(SystemConfig::getConfigKey, configKey);
SystemConfig config = getOne(wrapper);
if (config == null) {
return null;
}
// 写入缓存
redisTemplate.opsForValue().set(cacheKey, config.getConfigValue(), CACHE_EXPIRE_HOURS, TimeUnit.HOURS);
return config.getConfigValue();
}
@Override
public LoginConfigResponse getLoginConfig() {
LoginConfigResponse response = new LoginConfigResponse();
response.setWechatLoginEnabled(true);
String smsEnabled = getConfigValue(SMS_LOGIN_ENABLED_KEY);
response.setSmsLoginEnabled("true".equalsIgnoreCase(smsEnabled));
return response;
}
/**
* 实体转响应对象
*/
private SystemConfigResponse convertToResponse(SystemConfig config) {
SystemConfigResponse response = new SystemConfigResponse();
response.setConfigKey(config.getConfigKey());
response.setConfigValue(config.getConfigValue());
response.setValueType(config.getValueType());
response.setConfigGroup(config.getConfigGroup());
response.setConfigName(config.getConfigName());
response.setDescription(config.getDescription());
response.setSortOrder(config.getSortOrder());
return response;
}
}
@@ -0,0 +1,72 @@
package com.emotion.service.impl;
import com.emotion.dto.response.UserInfoResponse;
import com.emotion.exception.TokenException;
import com.emotion.service.AuthService;
import com.emotion.service.TokenService;
import com.emotion.util.TokenUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import javax.servlet.http.HttpServletRequest;
/**
* 令牌服务实现类
*
* @author huazhongmin
* @date 2025-07-23
*/
@Service
public class TokenServiceImpl implements TokenService {
@Autowired
private AuthService authService;
@Autowired
private TokenUtil tokenUtil;
@Override
public UserInfoResponse getUserInfoByToken(HttpServletRequest request) {
String userId = validateTokenAndGetUserId(request);
return authService.getCurrentUserInfo(userId);
}
@Override
public String getUsernameByToken(HttpServletRequest request) {
String token = tokenUtil.extractToken(request);
if (!tokenUtil.isValidToken(token)) {
throw new TokenException("未提供访问令牌");
}
if (!authService.validateToken(token)) {
throw new TokenException("访问令牌无效或已过期");
}
String username = authService.getUsernameFromToken(token);
if (username == null) {
throw new TokenException("访问令牌无效");
}
return username;
}
@Override
public String validateTokenAndGetUserId(HttpServletRequest request) {
String token = tokenUtil.extractToken(request);
if (!tokenUtil.isValidToken(token)) {
throw new TokenException("未提供访问令牌");
}
if (!authService.validateToken(token)) {
throw new TokenException("访问令牌无效或已过期");
}
String userId = authService.getUserIdFromToken(token);
if (userId == null) {
throw new TokenException("访问令牌无效");
}
return userId;
}
}
@@ -0,0 +1,163 @@
package com.emotion.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.emotion.common.PageResult;
import com.emotion.dto.request.TopicInteractionCreateRequest;
import com.emotion.dto.request.TopicInteractionPageRequest;
import com.emotion.dto.request.TopicInteractionUpdateRequest;
import com.emotion.dto.response.TopicInteractionResponse;
import com.emotion.entity.TopicInteraction;
import com.emotion.mapper.TopicInteractionMapper;
import com.emotion.service.TopicInteractionService;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.time.format.DateTimeFormatter;
/**
* 话题互动服务实现类
*
* @author huazhongmin
* @date 2025-07-24
*/
@Service
public class TopicInteractionServiceImpl extends ServiceImpl<TopicInteractionMapper, TopicInteraction> implements TopicInteractionService {
private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
@Override
public PageResult<TopicInteractionResponse> getPageWithResponse(TopicInteractionPageRequest request) {
Page<TopicInteraction> page = new Page<>(request.getCurrent(), request.getSize());
LambdaQueryWrapper<TopicInteraction> wrapper = new LambdaQueryWrapper<>();
// 根据关键词查询
if (StringUtils.hasText(request.getKeyword())) {
wrapper.and(w -> w.like(TopicInteraction::getContent, request.getKeyword())
.or().like(TopicInteraction::getUserInput, request.getKeyword())
.or().like(TopicInteraction::getAiResponse, request.getKeyword()));
}
// 根据话题ID查询
if (StringUtils.hasText(request.getTopicId())) {
wrapper.eq(TopicInteraction::getTopicId, request.getTopicId());
}
// 根据用户ID查询
if (StringUtils.hasText(request.getUserId())) {
wrapper.eq(TopicInteraction::getCreateBy, request.getUserId());
}
// 根据互动类型查询
if (StringUtils.hasText(request.getInteractionType())) {
wrapper.eq(TopicInteraction::getType, request.getInteractionType());
}
wrapper.eq(TopicInteraction::getIsDeleted, 0).orderByDesc(TopicInteraction::getCreateTime);
Page<TopicInteraction> resultPage = this.page(page, wrapper);
// 转换为响应对象
PageResult<TopicInteractionResponse> pageResult = new PageResult<>();
pageResult.setCurrent(resultPage.getCurrent());
pageResult.setSize(resultPage.getSize());
pageResult.setTotal(resultPage.getTotal());
pageResult.setPages(resultPage.getPages());
pageResult.setRecords(resultPage.getRecords().stream()
.map(this::convertToResponse)
.toList());
return pageResult;
}
@Override
public TopicInteractionResponse getTopicInteractionResponseById(String id) {
TopicInteraction topicInteraction = this.getById(id);
if (topicInteraction == null || topicInteraction.getIsDeleted() == 1) {
return null;
}
return convertToResponse(topicInteraction);
}
@Override
public TopicInteractionResponse createTopicInteractionWithResponse(TopicInteractionCreateRequest request) {
TopicInteraction topicInteraction = new TopicInteraction();
BeanUtils.copyProperties(request, topicInteraction);
// 设置用户ID为当前登录用户
topicInteraction.setCreateBy(getCurrentUserId());
this.save(topicInteraction);
return convertToResponse(topicInteraction);
}
@Override
public TopicInteractionResponse updateTopicInteractionWithResponse(TopicInteractionUpdateRequest request) {
TopicInteraction topicInteraction = this.getById(request.getId());
if (topicInteraction == null || topicInteraction.getIsDeleted() == 1) {
return null;
}
// 只更新非空字段
if (StringUtils.hasText(request.getTopicId())) {
topicInteraction.setTopicId(request.getTopicId());
}
if (StringUtils.hasText(request.getInteractionType())) {
topicInteraction.setType(request.getInteractionType());
}
if (StringUtils.hasText(request.getContent())) {
topicInteraction.setContent(request.getContent());
}
if (StringUtils.hasText(request.getUserInput())) {
topicInteraction.setUserInput(request.getUserInput());
}
if (StringUtils.hasText(request.getAiResponse())) {
topicInteraction.setAiResponse(request.getAiResponse());
}
if (request.getRating() != null) {
topicInteraction.setRating(request.getRating());
}
if (StringUtils.hasText(request.getFeedback())) {
topicInteraction.setFeedback(request.getFeedback());
}
this.updateById(topicInteraction);
return convertToResponse(topicInteraction);
}
@Override
public boolean deleteTopicInteraction(String id) {
TopicInteraction topicInteraction = this.getById(id);
if (topicInteraction == null) {
return false;
}
// 使用 MyBatis-Plus 的 removeById 方法,自动处理逻辑删除
return this.removeById(id);
}
/**
* 将实体对象转换为响应对象
*/
private TopicInteractionResponse convertToResponse(TopicInteraction topicInteraction) {
if (topicInteraction == null) {
return null;
}
TopicInteractionResponse response = new TopicInteractionResponse();
BeanUtils.copyProperties(topicInteraction, response);
response.setId(topicInteraction.getId());
if (topicInteraction.getCreateTime() != null) {
response.setCreateTime(topicInteraction.getCreateTime().format(FORMATTER));
}
if (topicInteraction.getUpdateTime() != null) {
response.setUpdateTime(topicInteraction.getUpdateTime().format(FORMATTER));
}
return response;
}
/**
* 获取当前登录用户ID(模拟实现)
*/
private String getCurrentUserId() {
// 实际项目中应从token中获取用户ID
return "current_user_id";
}
}
@@ -0,0 +1,493 @@
package com.emotion.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.emotion.dto.request.tts.TtsTaskCreateRequest;
import com.emotion.dto.response.tts.TtsTaskResponse;
import com.emotion.entity.EpicScript;
import com.emotion.entity.TtsTask;
import com.emotion.mapper.EpicScriptMapper;
import com.emotion.mapper.TtsTaskMapper;
import com.emotion.service.TtsEngineClient;
import com.emotion.service.TtsTaskService;
import com.emotion.util.UserContextHolder;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.util.DigestUtils;
import org.springframework.util.StringUtils;
import java.io.File;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
@Slf4j
@Service
public class TtsTaskServiceImpl extends ServiceImpl<TtsTaskMapper, TtsTask> implements TtsTaskService {
private static final String SOURCE_TYPE_EPIC_SCRIPT = "epic_script";
private static final String STATUS_PENDING = "pending";
private static final String STATUS_PROCESSING = "processing";
private static final String STATUS_SUCCESS = "success";
private static final String STATUS_FAILED = "failed";
private static final double FALLBACK_SPEECH_RATE = 0.92D;
private static final double FALLBACK_PITCH = 0D;
private static final String FALLBACK_EMOTION = "story";
private static final int NATURAL_PARAGRAPH_LIMIT = 140;
private final EpicScriptMapper epicScriptMapper;
private final TtsEngineClient ttsEngineClient;
private final Executor taskExecutor;
@Value("${emotion.tts.enabled:true}")
private boolean enabled;
@Value("${emotion.tts.output-dir:/data/uploads/emotion-museum/tts}")
private String outputDir;
@Value("${emotion.tts.public-url-prefix:/uploads/emotion-museum/tts}")
private String publicUrlPrefix;
@Value("${emotion.tts.max-text-length:5000}")
private int maxTextLength;
@Value("${emotion.tts.default-voice:default_zh_female}")
private String defaultVoice;
@Value("${emotion.tts.default-speech-rate:0.92}")
private double defaultSpeechRate;
@Value("${emotion.tts.default-pitch:0}")
private double defaultPitch;
@Value("${emotion.tts.default-emotion:story}")
private String defaultEmotion;
public TtsTaskServiceImpl(EpicScriptMapper epicScriptMapper,
TtsEngineClient ttsEngineClient,
@Qualifier("taskExecutor") Executor taskExecutor) {
this.epicScriptMapper = epicScriptMapper;
this.ttsEngineClient = ttsEngineClient;
this.taskExecutor = taskExecutor;
}
@Override
public TtsTaskResponse createOrReuse(TtsTaskCreateRequest request) {
if (!enabled) {
throw new IllegalStateException("TTS service is disabled");
}
String userId = currentUserId();
return createOrReuseForUser(userId, request);
}
@Override
public void prewarmEpicScript(String userId, String sourceId) {
if (!enabled || !StringUtils.hasText(userId) || !StringUtils.hasText(sourceId)) {
return;
}
try {
TtsTaskCreateRequest request = new TtsTaskCreateRequest();
request.setSourceType(SOURCE_TYPE_EPIC_SCRIPT);
request.setSourceId(sourceId);
createOrReuseForUser(userId, request);
} catch (Exception e) {
log.warn("TTS prewarm skipped, userId={}, sourceId={}, error={}", userId, sourceId, e.getMessage());
}
}
private TtsTaskResponse createOrReuseForUser(String userId, TtsTaskCreateRequest request) {
String sourceType = normalizeSourceType(request.getSourceType());
String sourceId = request.getSourceId().trim();
String voice = resolveVoice(request.getVoice());
TtsEngineClient.SynthesisOptions options = resolveOptions(request);
String cleaned = cleanText(loadSourceText(userId, sourceType, sourceId));
if (!StringUtils.hasText(cleaned)) {
throw new IllegalArgumentException("Source text is empty");
}
if (cleaned.length() > maxTextLength) {
cleaned = limitReadableText(cleaned, maxTextLength);
}
String hash = DigestUtils.md5DigestAsHex((voice + "\n" + options.cacheKey() + "\n" + cleaned).getBytes(StandardCharsets.UTF_8));
TtsTask owned = findOwnedTask(userId, sourceType, sourceId, voice, hash);
if (owned != null) {
if (STATUS_FAILED.equals(owned.getStatus()) && !recoverGeneratedAudio(owned)) {
log.info("Retrying failed TTS task, oldTaskId={}, sourceType={}, sourceId={}",
owned.getId(), sourceType, sourceId);
} else {
incrementRequestCount(owned);
return toResponse(owned);
}
}
TtsTask cachedSuccess = findSuccessfulCache(voice, hash);
if (cachedSuccess != null && StringUtils.hasText(cachedSuccess.getAudioUrl())) {
TtsTask task = buildTask(userId, sourceType, sourceId, voice, hash, cleaned.length());
task.setStatus(STATUS_SUCCESS);
task.setAudioPath(cachedSuccess.getAudioPath());
task.setAudioUrl(cachedSuccess.getAudioUrl());
task.setDurationMs(cachedSuccess.getDurationMs());
save(task);
incrementRequestCount(cachedSuccess);
return toResponse(task);
}
TtsTask task = buildTask(userId, sourceType, sourceId, voice, hash, cleaned.length());
save(task);
String synthesisText = cleaned;
CompletableFuture.runAsync(() -> process(task.getId(), synthesisText, voice, task.getAudioPath(), options), taskExecutor);
return toResponse(task);
}
@Override
public TtsTaskResponse getTask(String id) {
String userId = currentUserId();
TtsTask task = getById(id);
if (task == null || !userId.equals(task.getUserId())) {
return null;
}
if (STATUS_FAILED.equals(task.getStatus())) {
recoverGeneratedAudio(task);
}
return toResponse(task);
}
@Override
public TtsTaskResponse getBySource(String sourceType, String sourceId, String voice,
Double speechRate, Double pitch, String emotion) {
String userId = currentUserId();
String normalizedSourceType = normalizeSourceType(sourceType);
String normalizedVoice = resolveVoice(voice);
TtsEngineClient.SynthesisOptions options = resolveOptions(speechRate, pitch, emotion);
String cleaned = cleanText(loadSourceText(userId, normalizedSourceType, sourceId));
if (cleaned.length() > maxTextLength) {
cleaned = limitReadableText(cleaned, maxTextLength);
}
String hash = DigestUtils.md5DigestAsHex((normalizedVoice + "\n" + options.cacheKey() + "\n" + cleaned).getBytes(StandardCharsets.UTF_8));
TtsTask task = findOwnedTask(userId, normalizedSourceType, sourceId, normalizedVoice, hash);
if (task != null && STATUS_FAILED.equals(task.getStatus())) {
recoverGeneratedAudio(task);
}
return task == null ? null : toResponse(task);
}
private boolean recoverGeneratedAudio(TtsTask task) {
if (task == null || !StringUtils.hasText(task.getAudioPath())) {
return false;
}
File file = new File(task.getAudioPath());
if (!file.isFile() || file.length() <= 0L) {
return false;
}
task.setStatus(STATUS_SUCCESS);
task.setErrorMessage(null);
updateById(task);
log.info("Recovered generated TTS audio, taskId={}, audioPath={}", task.getId(), task.getAudioPath());
return true;
}
private void process(String taskId, String text, String voice, String outputPath, TtsEngineClient.SynthesisOptions options) {
try {
TtsTask task = getById(taskId);
if (task == null) {
return;
}
task.setStatus(STATUS_PROCESSING);
task.setErrorMessage(null);
updateById(task);
TtsEngineClient.TtsEngineResult result = ttsEngineClient.synthesize(text, voice, outputPath, options);
task = getById(taskId);
if (task == null) {
return;
}
if (result.isSuccess()) {
task.setStatus(STATUS_SUCCESS);
task.setDurationMs(result.getDurationMs());
if (StringUtils.hasText(result.getAudioPath())) {
task.setAudioPath(result.getAudioPath());
}
task.setErrorMessage(null);
} else {
task.setStatus(STATUS_FAILED);
task.setErrorMessage(limitError(result.getErrorMessage()));
}
updateById(task);
} catch (Exception e) {
log.warn("TTS task processing failed, taskId={}", taskId, e);
TtsTask task = getById(taskId);
if (task != null) {
if (recoverGeneratedAudio(task)) {
return;
}
task.setStatus(STATUS_FAILED);
task.setErrorMessage(limitError(e.getMessage()));
updateById(task);
}
}
}
private TtsTask buildTask(String userId, String sourceType, String sourceId, String voice, String hash, int textLength) {
String filename = hash + ".wav";
return TtsTask.builder()
.userId(userId)
.sourceType(sourceType)
.sourceId(sourceId)
.textHash(hash)
.textLength(textLength)
.voice(voice)
.status(STATUS_PENDING)
.audioPath(joinPath(outputDir, filename))
.audioUrl(joinPath(publicUrlPrefix, filename))
.requestCount(1)
.build();
}
private TtsTask findOwnedTask(String userId, String sourceType, String sourceId, String voice, String hash) {
return getOne(new LambdaQueryWrapper<TtsTask>()
.eq(TtsTask::getUserId, userId)
.eq(TtsTask::getSourceType, sourceType)
.eq(TtsTask::getSourceId, sourceId)
.eq(TtsTask::getVoice, voice)
.eq(TtsTask::getTextHash, hash)
.eq(TtsTask::getIsDeleted, 0)
.orderByDesc(TtsTask::getCreateTime)
.last("LIMIT 1"));
}
private TtsTask findSuccessfulCache(String voice, String hash) {
return getOne(new LambdaQueryWrapper<TtsTask>()
.eq(TtsTask::getTextHash, hash)
.eq(TtsTask::getVoice, voice)
.eq(TtsTask::getStatus, STATUS_SUCCESS)
.eq(TtsTask::getIsDeleted, 0)
.orderByDesc(TtsTask::getCreateTime)
.last("LIMIT 1"));
}
private void incrementRequestCount(TtsTask task) {
task.setRequestCount((task.getRequestCount() == null ? 0 : task.getRequestCount()) + 1);
updateById(task);
}
private String loadSourceText(String userId, String sourceType, String sourceId) {
if (!SOURCE_TYPE_EPIC_SCRIPT.equals(sourceType)) {
throw new IllegalArgumentException("Unsupported sourceType");
}
EpicScript script = epicScriptMapper.selectById(sourceId);
if (script == null || !userId.equals(script.getUserId())) {
throw new IllegalArgumentException("Script not found");
}
StringBuilder text = new StringBuilder();
append(text, script.getTitle());
Map<String, Object> plotJson = script.getPlotJson();
Object fullContent = plotJson == null ? null : plotJson.get("fullContent");
if (fullContent != null && StringUtils.hasText(String.valueOf(fullContent))) {
append(text, String.valueOf(fullContent));
} else {
append(text, script.getPlotIntro());
append(text, script.getPlotTurning());
append(text, script.getPlotClimax());
append(text, script.getPlotEnding());
}
return text.toString();
}
public static String cleanText(String text) {
if (text == null) {
return "";
}
String normalized = text.replace("\r\n", "\n")
.replace('\r', '\n')
.replaceAll("!\\[[^\\]]*]\\([^)]*\\)", "")
.replaceAll("\\[([^\\]]+)]\\([^)]*\\)", "$1")
.replaceAll("(?m)^\\s{0,3}#{1,6}\\s*", "")
.replaceAll("(?m)^\\s*>\\s?", "")
.replaceAll("(?m)^\\s*[-*+]\\s+", "")
.replaceAll("(?m)^\\s*\\d+[.)、]\\s+", "")
.replaceAll("<[^>]+>", "")
.replaceAll("[*_`~]", "")
.replaceAll("[“”]", "\"")
.replaceAll("[‘’]", "'")
.replaceAll("\\.{3,}", "……")
.replaceAll("-{2,}", "")
.replaceAll("[\\t\\u00A0]+", " ")
.replaceAll(" {2,}", " ")
.replaceAll("(?<=[\\p{IsHan}])[ \\t\\u00A0]+(?=[\\p{IsHan}])", "")
.replaceAll("[ \\t\\u00A0]*([,。!?;:、,.!?;:])[ \\t\\u00A0]*", "$1")
.replaceAll(",", "")
.replaceAll("!", "")
.replaceAll("\\?", "")
.replaceAll(";", "")
.replaceAll(":", "");
List<String> paragraphs = new ArrayList<>();
for (String paragraph : normalized.split("\\n+")) {
String trimmed = paragraph.trim();
if (!StringUtils.hasText(trimmed)) {
continue;
}
paragraphs.addAll(toReadableParagraphs(trimmed));
}
return String.join("\n\n", paragraphs).trim();
}
private TtsTaskResponse toResponse(TtsTask task) {
return TtsTaskResponse.builder()
.id(task.getId())
.sourceType(task.getSourceType())
.sourceId(task.getSourceId())
.status(task.getStatus())
.voice(task.getVoice())
.audioUrl(STATUS_SUCCESS.equals(task.getStatus()) ? task.getAudioUrl() : null)
.durationMs(task.getDurationMs())
.errorMessage(task.getErrorMessage())
.build();
}
private TtsEngineClient.SynthesisOptions resolveOptions(TtsTaskCreateRequest request) {
return resolveOptions(request.getSpeechRate(), request.getPitch(), request.getEmotion());
}
private TtsEngineClient.SynthesisOptions resolveOptions(Double requestSpeechRate, Double requestPitch, String requestEmotion) {
double speechRate = requestSpeechRate == null ? defaultOrFallback(defaultSpeechRate, FALLBACK_SPEECH_RATE) : requestSpeechRate;
double pitch = requestPitch == null ? defaultPitch : requestPitch;
String emotion = StringUtils.hasText(requestEmotion)
? requestEmotion.trim()
: (StringUtils.hasText(defaultEmotion) ? defaultEmotion.trim() : FALLBACK_EMOTION);
return new TtsEngineClient.SynthesisOptions(
round(clamp(speechRate, 0.60D, 1.40D)),
round(clamp(pitch, -20D, 20D)),
emotion.toLowerCase(Locale.ROOT)
);
}
private String currentUserId() {
String userId = UserContextHolder.getCurrentUserId();
if (!StringUtils.hasText(userId)) {
throw new IllegalArgumentException("User not logged in");
}
return userId;
}
private String normalizeSourceType(String sourceType) {
return StringUtils.hasText(sourceType) ? sourceType.trim() : SOURCE_TYPE_EPIC_SCRIPT;
}
private String resolveVoice(String voice) {
return StringUtils.hasText(voice) ? voice.trim() : defaultVoice;
}
private static void append(StringBuilder text, String value) {
if (StringUtils.hasText(value)) {
text.append(value).append("\n\n");
}
}
private static List<String> toReadableParagraphs(String paragraph) {
List<String> parts = new ArrayList<>();
StringBuilder current = new StringBuilder();
for (int index = 0; index < paragraph.length(); index++) {
char ch = paragraph.charAt(index);
current.append(ch);
if (isHardSentenceEnd(ch) || (current.length() >= NATURAL_PARAGRAPH_LIMIT && isSoftPause(ch))) {
addReadablePart(parts, current);
}
}
addReadablePart(parts, current);
return parts;
}
private static void addReadablePart(List<String> parts, StringBuilder current) {
String value = current.toString().trim();
current.setLength(0);
if (!StringUtils.hasText(value)) {
return;
}
if (value.length() > NATURAL_PARAGRAPH_LIMIT + 40) {
splitLongText(parts, value);
return;
}
parts.add(ensureSentenceEnding(value));
}
private static void splitLongText(List<String> parts, String value) {
StringBuilder chunk = new StringBuilder();
for (int index = 0; index < value.length(); index++) {
char ch = value.charAt(index);
chunk.append(ch);
if (chunk.length() >= NATURAL_PARAGRAPH_LIMIT) {
parts.add(ensureSentenceEnding(chunk.toString().trim()));
chunk.setLength(0);
}
}
if (chunk.length() > 0) {
parts.add(ensureSentenceEnding(chunk.toString().trim()));
}
}
private static String ensureSentenceEnding(String value) {
if (!StringUtils.hasText(value)) {
return "";
}
char last = value.charAt(value.length() - 1);
return isHardSentenceEnd(last) || isSoftPause(last) ? value : value + "";
}
private static boolean isHardSentenceEnd(char ch) {
return ch == '。' || ch == '' || ch == '' || ch == '' || ch == '…';
}
private static boolean isSoftPause(char ch) {
return ch == '' || ch == '、' || ch == '';
}
private static String limitReadableText(String text, int limit) {
if (text.length() <= limit) {
return text;
}
String truncated = text.substring(0, limit);
int cut = Math.max(
Math.max(truncated.lastIndexOf('。'), truncated.lastIndexOf('')),
Math.max(truncated.lastIndexOf(''), truncated.lastIndexOf('\n'))
);
if (cut > limit * 0.75) {
return truncated.substring(0, cut + 1).trim();
}
return ensureSentenceEnding(truncated.trim());
}
private static double clamp(double value, double min, double max) {
return Math.max(min, Math.min(max, value));
}
private static double round(double value) {
return Math.round(value * 100D) / 100D;
}
private static double defaultOrFallback(double value, double fallback) {
return value <= 0D ? fallback : value;
}
private static String joinPath(String prefix, String filename) {
if (prefix.endsWith("/")) {
return prefix + filename;
}
return prefix + "/" + filename;
}
private static String limitError(String message) {
if (message == null) {
return "TTS synthesis failed";
}
return message.length() > 1000 ? message.substring(0, 1000) : message;
}
}
@@ -0,0 +1,348 @@
package com.emotion.service.impl;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.bean.copier.CopyOptions;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.emotion.common.PageResult;
import com.emotion.dto.request.userprofile.UserProfileCreateRequest;
import com.emotion.dto.request.userprofile.UserProfilePageRequest;
import com.emotion.dto.request.userprofile.UserProfileUpdateRequest;
import com.emotion.dto.response.userprofile.UserProfileResponse;
import com.emotion.entity.LifeEvent;
import com.emotion.entity.User;
import com.emotion.entity.UserProfile;
import com.emotion.mapper.UserProfileMapper;
import com.emotion.service.LifeEventService;
import com.emotion.service.UserProfileService;
import com.emotion.service.UserService;
import com.emotion.util.UserContextHolder;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
/**
* 用户档案服务实现类
*
* @author huazhongmin
* @date 2025-12-21
*/
@Slf4j
@Service
public class UserProfileServiceImpl extends ServiceImpl<UserProfileMapper, UserProfile> implements UserProfileService {
@Autowired
private UserService userService;
@Lazy
@Autowired
private LifeEventService lifeEventService;
@Override
public UserProfileResponse createProfile(UserProfileCreateRequest request) {
log.info("Creating user profile: {}", request);
// 获取当前登录用户ID
String currentUserId = UserContextHolder.getCurrentUserId();
if (!StringUtils.hasText(currentUserId)) {
throw new RuntimeException("用户未登录");
}
// 检查是否已存在档案
LambdaQueryWrapper<UserProfile> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(UserProfile::getUserId, currentUserId);
if (count(queryWrapper) > 0) {
throw new RuntimeException("当前用户已存在档案信息");
}
UserProfile userProfile = new UserProfile();
BeanUtils.copyProperties(request, userProfile);
userProfile.setUserId(currentUserId);
userProfile.setStatus(1); // 默认正常
save(userProfile);
log.info("User profile created with ID: {}", userProfile.getId());
return convertToResponse(userProfile);
}
@Override
public UserProfileResponse updateProfile(UserProfileUpdateRequest request) {
log.info("Updating user profile: {}", request);
// 获取当前登录用户ID
String currentUserId = UserContextHolder.getCurrentUserId();
if (!StringUtils.hasText(currentUserId)) {
throw new RuntimeException("用户未登录");
}
UserProfile userProfile = null;
// 先根据请求ID查询
if (StringUtils.hasText(request.getId())) {
userProfile = getById(request.getId());
}
// 如果没有查到,根据当前用户ID查询
if (userProfile == null) {
LambdaQueryWrapper<UserProfile> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(UserProfile::getUserId, currentUserId);
userProfile = getOne(queryWrapper);
}
// 如果还是没有,则创建新档案
if (userProfile == null) {
log.info("User profile not found, creating new profile for user: {}", currentUserId);
UserProfileCreateRequest createRequest = new UserProfileCreateRequest();
BeanUtils.copyProperties(request, createRequest);
return createProfile(createRequest);
}
// 权限校验:只能修改自己的档案
if (!userProfile.getUserId().equals(currentUserId)) {
throw new RuntimeException("无权修改他人档案");
}
// 使用Hutool的BeanUtil进行部分更新,忽略null值
BeanUtil.copyProperties(request, userProfile, CopyOptions.create().setIgnoreNullValue(true));
updateById(userProfile);
log.info("User profile updated: {}", userProfile.getId());
return convertToResponse(userProfile);
}
@Override
public UserProfileResponse getCurrentUserProfile() {
String currentUserId = UserContextHolder.getCurrentUserId();
if (!StringUtils.hasText(currentUserId)) {
throw new RuntimeException("用户未登录");
}
LambdaQueryWrapper<UserProfile> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(UserProfile::getUserId, currentUserId);
// 获取最新的一条(理论上应该只有一条)
List<UserProfile> list = list(queryWrapper);
if (list.isEmpty()) {
return null;
}
return convertToResponse(list.get(0));
}
@Override
public UserProfileResponse getProfileById(String id) {
UserProfile userProfile = getById(id);
if (userProfile == null) {
return null;
}
return convertToResponse(userProfile);
}
@Override
public PageResult<UserProfileResponse> getProfilePage(UserProfilePageRequest request) {
Page<UserProfile> page = new Page<>(request.getCurrent(), request.getSize());
LambdaQueryWrapper<UserProfile> queryWrapper = new LambdaQueryWrapper<>();
// 构建查询条件
if (StringUtils.hasText(request.getUserId())) {
queryWrapper.eq(UserProfile::getUserId, request.getUserId());
}
if (StringUtils.hasText(request.getNickname())) {
queryWrapper.like(UserProfile::getNickname, request.getNickname());
}
if (StringUtils.hasText(request.getMbti())) {
queryWrapper.eq(UserProfile::getMbti, request.getMbti());
}
if (request.getStatus() != null) {
queryWrapper.eq(UserProfile::getStatus, request.getStatus());
}
queryWrapper.orderByDesc(UserProfile::getCreateTime);
IPage<UserProfile> userProfilePage = page(page, queryWrapper);
List<UserProfileResponse> list = userProfilePage.getRecords().stream()
.map(this::convertToResponse)
.collect(Collectors.toList());
PageResult<UserProfileResponse> result = new PageResult<>();
result.setCurrent(userProfilePage.getCurrent());
result.setSize(userProfilePage.getSize());
result.setTotal(userProfilePage.getTotal());
result.setPages(userProfilePage.getPages());
result.setRecords(list);
return result;
}
@Override
public List<UserProfileResponse> getProfileList(UserProfilePageRequest request) {
LambdaQueryWrapper<UserProfile> queryWrapper = new LambdaQueryWrapper<>();
// 构建查询条件
if (StringUtils.hasText(request.getUserId())) {
queryWrapper.eq(UserProfile::getUserId, request.getUserId());
}
if (StringUtils.hasText(request.getNickname())) {
queryWrapper.like(UserProfile::getNickname, request.getNickname());
}
if (StringUtils.hasText(request.getMbti())) {
queryWrapper.eq(UserProfile::getMbti, request.getMbti());
}
if (request.getStatus() != null) {
queryWrapper.eq(UserProfile::getStatus, request.getStatus());
}
queryWrapper.orderByDesc(UserProfile::getCreateTime);
List<UserProfile> list = list(queryWrapper);
return list.stream()
.map(this::convertToResponse)
.collect(Collectors.toList());
}
@Override
public boolean deleteProfile(String id) {
log.info("Deleting user profile: {}", id);
return removeById(id);
}
@Override
@Transactional(rollbackFor = Exception.class)
public int migrateLifeEventsFromProfiles() {
log.info("开始迁移用户档案中的生命事件数据到t_life_event表");
// 查询所有有生命事件数据的用户档案
LambdaQueryWrapper<UserProfile> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.and(w -> w
.isNotNull(UserProfile::getChildhoodContent).ne(UserProfile::getChildhoodContent, "")
.or().isNotNull(UserProfile::getPeakContent).ne(UserProfile::getPeakContent, "")
.or().isNotNull(UserProfile::getValleyContent).ne(UserProfile::getValleyContent, "")
);
List<UserProfile> profiles = list(queryWrapper);
log.info("找到 {} 个有生命事件数据的用户档案", profiles.size());
int migratedCount = 0;
for (UserProfile profile : profiles) {
String userId = profile.getUserId();
List<LifeEvent> eventsToSave = new ArrayList<>();
// 迁移童年记忆
if (StringUtils.hasText(profile.getChildhoodContent())) {
if (!isEventExists(userId, "childhood")) {
LifeEvent childhoodEvent = createLifeEventFromProfile(
userId,
"童年记忆",
profile.getChildhoodContent(),
profile.getChildhoodDate(),
"childhood"
);
eventsToSave.add(childhoodEvent);
}
}
// 迁移高光时刻 (peak -> joy)
if (StringUtils.hasText(profile.getPeakContent())) {
if (!isEventExists(userId, "joy")) {
LifeEvent peakEvent = createLifeEventFromProfile(
userId,
"光芒闪耀的时刻",
profile.getPeakContent(),
profile.getPeakDate(),
"joy"
);
eventsToSave.add(peakEvent);
}
}
// 迁移低谷时期 (valley -> low)
if (StringUtils.hasText(profile.getValleyContent())) {
if (!isEventExists(userId, "low")) {
LifeEvent valleyEvent = createLifeEventFromProfile(
userId,
"在暗夜中潜行",
profile.getValleyContent(),
profile.getValleyDate(),
"low"
);
eventsToSave.add(valleyEvent);
}
}
// 批量保存
if (!eventsToSave.isEmpty()) {
lifeEventService.saveBatch(eventsToSave);
migratedCount += eventsToSave.size();
log.info("用户 {} 迁移了 {} 条生命事件", userId, eventsToSave.size());
}
}
log.info("生命事件数据迁移完成,共迁移 {} 条记录", migratedCount);
return migratedCount;
}
/**
* 检查用户是否已存在指定标签的生命事件
*
* @param userId 用户ID
* @param tag 标签
* @return 是否存在
*/
private boolean isEventExists(String userId, String tag) {
LambdaQueryWrapper<LifeEvent> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(LifeEvent::getUserId, userId)
.eq(LifeEvent::getIsDeleted, 0)
.like(LifeEvent::getTags, tag);
return lifeEventService.count(wrapper) > 0;
}
/**
* 从用户档案数据创建生命事件
*
* @param userId 用户ID
* @param title 事件标题
* @param content 事件内容
* @param eventDate 事件日期
* @param tag 事件标签
* @return 生命事件实体
*/
private LifeEvent createLifeEventFromProfile(String userId, String title, String content,
LocalDate eventDate, String tag) {
LifeEvent event = new LifeEvent();
event.setUserId(userId);
event.setTitle(title);
event.setContent(content);
event.setEventType("milestone");
event.setTags(List.of(tag));
// 设置事件日期,如果没有则使用当前时间
if (eventDate != null) {
event.setEventDate(eventDate.atStartOfDay());
} else {
event.setEventDate(java.time.LocalDateTime.now());
}
return event;
}
private UserProfileResponse convertToResponse(UserProfile userProfile) {
if (userProfile == null) {
return null;
}
UserProfileResponse response = new UserProfileResponse();
BeanUtils.copyProperties(userProfile, response);
return response;
}
}
@@ -0,0 +1,341 @@
package com.emotion.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.emotion.common.PageResult;
import com.emotion.dto.request.UserCreateRequest;
import com.emotion.dto.request.UserPageRequest;
import com.emotion.dto.request.UserUpdateRequest;
import com.emotion.dto.request.UserProfileUpdateRequest;
import com.emotion.dto.response.UserResponse;
import com.emotion.entity.User;
import com.emotion.mapper.UserMapper;
import com.emotion.service.UserService;
import com.emotion.util.UserContextHolder;
import org.springframework.beans.BeanUtils;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.stream.Collectors;
/**
* 用户服务实现类
*
* @author huazhongmin
* @date 2025-07-24
*/
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
private final PasswordEncoder passwordEncoder;
private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
public UserServiceImpl(PasswordEncoder passwordEncoder) {
this.passwordEncoder = passwordEncoder;
}
@Override
public PageResult<UserResponse> getPageWithResponse(UserPageRequest request) {
Page<User> page = new Page<>(request.getCurrent(), request.getSize());
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
// 关键词搜索
if (StringUtils.hasText(request.getKeyword())) {
wrapper.and(w -> w.like(User::getUsername, request.getKeyword())
.or().like(User::getNickname, request.getKeyword())
.or().like(User::getEmail, request.getKeyword()));
}
// 账号查询
if (StringUtils.hasText(request.getAccount())) {
wrapper.eq(User::getAccount, request.getAccount());
}
// 用户名查询
if (StringUtils.hasText(request.getUsername())) {
wrapper.like(User::getUsername, request.getUsername());
}
// 邮箱查询
if (StringUtils.hasText(request.getEmail())) {
wrapper.eq(User::getEmail, request.getEmail());
}
// 手机号查询
if (StringUtils.hasText(request.getPhone())) {
wrapper.eq(User::getPhone, request.getPhone());
}
// 状态查询
if (request.getStatus() != null) {
wrapper.eq(User::getStatus, request.getStatus());
}
// 会员等级查询
if (StringUtils.hasText(request.getMemberLevel())) {
wrapper.eq(User::getMemberLevel, request.getMemberLevel());
}
wrapper.eq(User::getIsDeleted, 0);
// 排序
if (StringUtils.hasText(request.getOrderBy())) {
if ("asc".equalsIgnoreCase(request.getOrderDirection())) {
wrapper.orderByAsc(User::getCreateTime);
} else {
wrapper.orderByDesc(User::getCreateTime);
}
} else {
wrapper.orderByDesc(User::getCreateTime);
}
Page<User> resultPage = this.page(page, wrapper);
List<UserResponse> userResponses = resultPage.getRecords().stream()
.map(this::convertToResponse)
.collect(Collectors.toList());
PageResult<UserResponse> pageResult = new PageResult<>();
pageResult.setCurrent(resultPage.getCurrent());
pageResult.setSize(resultPage.getSize());
pageResult.setTotal(resultPage.getTotal());
pageResult.setPages(resultPage.getPages());
pageResult.setRecords(userResponses);
return pageResult;
}
@Override
public UserResponse getUserResponseById(String id) {
User user = this.getById(id);
if (user == null || user.getIsDeleted() == 1) {
return null;
}
return convertToResponse(user);
}
@Override
public UserResponse createUserWithResponse(UserCreateRequest request) {
User user = new User();
user.setAccount(request.getAccount());
user.setUsername(request.getUsername());
user.setPassword(passwordEncoder.encode(request.getPassword()));
user.setEmail(request.getEmail());
user.setPhone(request.getPhone());
user.setNickname(request.getUsername());
user.setMemberLevel("free");
user.setStatus(1);
user.setIsVerified(0);
user.setLastActiveTime(LocalDateTime.now());
this.save(user);
return convertToResponse(user);
}
@Override
public UserResponse updateUserWithResponse(UserUpdateRequest request) {
User user = this.getById(request.getId());
if (user == null || user.getIsDeleted() == 1) {
return null;
}
// 只更新非空字段
if (StringUtils.hasText(request.getUsername())) {
user.setUsername(request.getUsername());
}
if (StringUtils.hasText(request.getNickname())) {
user.setNickname(request.getNickname());
}
if (StringUtils.hasText(request.getEmail())) {
user.setEmail(request.getEmail());
}
if (StringUtils.hasText(request.getPhone())) {
user.setPhone(request.getPhone());
}
if (StringUtils.hasText(request.getAvatar())) {
user.setAvatar(request.getAvatar());
}
if (request.getBirthDate() != null) {
user.setBirthDate(request.getBirthDate());
}
if (StringUtils.hasText(request.getLocation())) {
user.setLocation(request.getLocation());
}
if (StringUtils.hasText(request.getBio())) {
user.setBio(request.getBio());
}
if (request.getStatus() != null) {
user.setStatus(request.getStatus());
}
if (StringUtils.hasText(request.getMemberLevel())) {
user.setMemberLevel(request.getMemberLevel());
}
this.updateById(user);
return convertToResponse(user);
}
@Override
public UserResponse updateCurrentUserProfileWithResponse(UserProfileUpdateRequest request) {
// 从UserContextHolder获取当前用户ID
String currentUserId = UserContextHolder.getCurrentUserId();
if (currentUserId == null) {
return null;
}
User user = this.getById(currentUserId);
if (user == null || user.getIsDeleted() == 1) {
return null;
}
// 只更新非空字段
if (StringUtils.hasText(request.getNickname())) {
user.setNickname(request.getNickname());
}
if (StringUtils.hasText(request.getEmail())) {
user.setEmail(request.getEmail());
}
if (StringUtils.hasText(request.getPhone())) {
user.setPhone(request.getPhone());
}
if (StringUtils.hasText(request.getAvatar())) {
user.setAvatar(request.getAvatar());
}
if (request.getBirthDate() != null) {
user.setBirthDate(request.getBirthDate());
}
if (StringUtils.hasText(request.getLocation())) {
user.setLocation(request.getLocation());
}
if (StringUtils.hasText(request.getBio())) {
user.setBio(request.getBio());
}
this.updateById(user);
return convertToResponse(user);
}
@Override
public UserResponse getCurrentUserProfileWithResponse() {
// 从UserContextHolder获取当前用户ID
String currentUserId = UserContextHolder.getCurrentUserId();
if (currentUserId == null) {
return null;
}
User user = this.getById(currentUserId);
if (user == null || user.getIsDeleted() == 1) {
return null;
}
return convertToResponse(user);
}
@Override
public boolean deleteUser(String id) {
User user = this.getById(id);
if (user == null) {
return false;
}
// 使用 MyBatis-Plus 的 removeById 方法,自动处理逻辑删除
return this.removeById(id);
}
@Override
public User getByAccount(String account) {
if (!StringUtils.hasText(account)) {
return null;
}
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(User::getAccount, account)
.eq(User::getIsDeleted, 0);
return this.getOne(wrapper);
}
@Override
public User getByEmail(String email) {
if (!StringUtils.hasText(email)) {
return null;
}
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(User::getEmail, email)
.eq(User::getIsDeleted, 0);
return this.getOne(wrapper);
}
@Override
public User getByPhone(String phone) {
if (!StringUtils.hasText(phone)) {
return null;
}
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(User::getPhone, phone)
.eq(User::getIsDeleted, 0);
return this.getOne(wrapper);
}
@Override
public User getByThirdParty(String thirdPartyType, String thirdPartyId) {
if (!StringUtils.hasText(thirdPartyType) || !StringUtils.hasText(thirdPartyId)) {
return null;
}
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(User::getThirdPartyType, thirdPartyType)
.eq(User::getThirdPartyId, thirdPartyId)
.eq(User::getIsDeleted, 0);
return this.getOne(wrapper);
}
@Override
public User createUser(String account, String username, String password, String email, String phone) {
User user = new User();
user.setAccount(account);
user.setUsername(username);
user.setNickname(username); // 默认昵称与用户名相同
user.setPassword(passwordEncoder.encode(password)); // 加密密码
user.setEmail(email);
user.setPhone(phone);
user.setMemberLevel("free"); // 默认免费会员
user.setStatus(1); // 默认启用
user.setIsVerified(0); // 默认未验证
user.setLastActiveTime(LocalDateTime.now());
this.save(user);
return user;
}
@Override
public void updateLastActiveTime(String userId, LocalDateTime lastActiveTime) {
if (!StringUtils.hasText(userId) || lastActiveTime == null) {
return;
}
User user = this.getById(userId);
if (user != null && user.getIsDeleted() == 0) {
user.setLastActiveTime(lastActiveTime);
this.updateById(user);
}
}
/**
* 转换为响应对象
*/
private UserResponse convertToResponse(User user) {
UserResponse response = new UserResponse();
BeanUtils.copyProperties(user, response);
response.setId(user.getId());
if (user.getCreateTime() != null) {
response.setCreateTime(user.getCreateTime().format(DATE_TIME_FORMATTER));
}
if (user.getUpdateTime() != null) {
response.setUpdateTime(user.getUpdateTime().format(DATE_TIME_FORMATTER));
}
if (user.getLastActiveTime() != null) {
response.setLastActiveTime(user.getLastActiveTime().format(DATE_TIME_FORMATTER));
}
return response;
}
}
@@ -0,0 +1,195 @@
package com.emotion.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.emotion.common.PageResult;
import com.emotion.dto.request.UserStatsCreateRequest;
import com.emotion.dto.request.UserStatsIncrementRequest;
import com.emotion.dto.request.UserStatsPageRequest;
import com.emotion.dto.request.UserStatsUpdateValueRequest;
import com.emotion.dto.response.UserStatsResponse;
import com.emotion.entity.UserStats;
import com.emotion.mapper.UserStatsMapper;
import com.emotion.service.UserStatsService;
import com.emotion.util.SnowflakeIdGenerator;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.stream.Collectors;
/**
* 用户统计服务实现类
*
* @author huazhongmin
* @date 2025-07-24
*/
@Service
public class UserStatsServiceImpl extends ServiceImpl<UserStatsMapper, UserStats> implements UserStatsService {
private final SnowflakeIdGenerator snowflakeIdGenerator;
private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
public UserStatsServiceImpl(SnowflakeIdGenerator snowflakeIdGenerator) {
this.snowflakeIdGenerator = snowflakeIdGenerator;
}
@Override
public PageResult<UserStatsResponse> getPageWithResponse(UserStatsPageRequest request) {
Page<UserStats> page = new Page<>(request.getCurrent(), request.getSize());
LambdaQueryWrapper<UserStats> wrapper = new LambdaQueryWrapper<>();
// 关键词搜索
if (StringUtils.hasText(request.getKeyword())) {
wrapper.like(UserStats::getUserId, request.getKeyword())
.or().like(UserStats::getStatsType, request.getKeyword());
}
// 用户ID查询
if (StringUtils.hasText(request.getUserId())) {
wrapper.eq(UserStats::getUserId, request.getUserId());
}
// 统计类型查询
if (StringUtils.hasText(request.getStatsType())) {
wrapper.eq(UserStats::getStatsType, request.getStatsType());
}
wrapper.eq(UserStats::getIsDeleted, 0);
// 排序
if (StringUtils.hasText(request.getOrderBy())) {
if ("asc".equalsIgnoreCase(request.getOrderDirection())) {
wrapper.orderByAsc(UserStats::getCreateTime);
} else {
wrapper.orderByDesc(UserStats::getCreateTime);
}
} else {
wrapper.orderByDesc(UserStats::getCreateTime);
}
Page<UserStats> resultPage = this.page(page, wrapper);
List<UserStatsResponse> responses = resultPage.getRecords().stream()
.map(this::convertToResponse)
.collect(Collectors.toList());
PageResult<UserStatsResponse> pageResult = new PageResult<>();
pageResult.setCurrent(resultPage.getCurrent());
pageResult.setSize(resultPage.getSize());
pageResult.setTotal(resultPage.getTotal());
pageResult.setPages(resultPage.getPages());
pageResult.setRecords(responses);
return pageResult;
}
@Override
public boolean updateStatsValue(UserStatsUpdateValueRequest request) {
LambdaUpdateWrapper<UserStats> wrapper = new LambdaUpdateWrapper<>();
wrapper.eq(UserStats::getUserId, request.getUserId())
.eq(UserStats::getStatsType, request.getStatsType())
.eq(UserStats::getIsDeleted, 0)
.set(UserStats::getValue, request.getValue())
.set(UserStats::getUpdateTime, LocalDateTime.now());
return this.update(wrapper);
}
@Override
public boolean incrementStatsValue(UserStatsIncrementRequest request) {
LambdaUpdateWrapper<UserStats> wrapper = new LambdaUpdateWrapper<>();
wrapper.eq(UserStats::getUserId, request.getUserId())
.eq(UserStats::getStatsType, request.getStatsType())
.eq(UserStats::getIsDeleted, 0)
.setSql("value = value + " + request.getIncrement())
.set(UserStats::getUpdateTime, LocalDateTime.now());
return this.update(wrapper);
}
@Override
public boolean recalculateUserStats(String userId) {
// 这里应该根据业务逻辑重新计算用户统计数据
// 示例:更新用户的总活跃天数、总发帖数等
return true;
}
@Override
public boolean recalculateAllUserStats() {
// 这里应该重新计算所有用户的统计数据
return true;
}
@Override
public UserStatsResponse createOrUpdateUserStatsWithResponse(UserStatsCreateRequest request) {
UserStats stats = createOrUpdateUserStats(request.getUserId(), request.getStatsType(), request.getValue(),
request.getPeriod());
return convertToResponse(stats);
}
@Override
public boolean deleteExpiredStats(Integer days) {
LocalDateTime expireTime = LocalDateTime.now().minusDays(days);
LambdaUpdateWrapper<UserStats> wrapper = new LambdaUpdateWrapper<>();
wrapper.lt(UserStats::getCreateTime, expireTime)
.set(UserStats::getIsDeleted, 1)
.set(UserStats::getUpdateTime, LocalDateTime.now());
return this.update(wrapper);
}
/**
* 创建或更新用户统计
*/
private UserStats createOrUpdateUserStats(String userId, String statsType, Double value, String period) {
UserStats stats = getByUserIdAndStatsType(userId, statsType);
if (stats == null) {
stats = new UserStats();
stats.setId(snowflakeIdGenerator.nextIdAsString());
stats.setUserId(userId);
stats.setStatsType(statsType);
stats.setValue(value);
stats.setPeriod(period);
stats.setCreateTime(LocalDateTime.now());
stats.setUpdateTime(LocalDateTime.now());
stats.setIsDeleted(0);
this.save(stats);
} else {
stats.setValue(value);
stats.setPeriod(period);
stats.setUpdateTime(LocalDateTime.now());
this.updateById(stats);
}
return stats;
}
/**
* 根据用户ID和统计类型获取统计信息
*/
private UserStats getByUserIdAndStatsType(String userId, String statsType) {
LambdaQueryWrapper<UserStats> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(UserStats::getUserId, userId)
.eq(UserStats::getStatsType, statsType)
.eq(UserStats::getIsDeleted, 0);
return this.getOne(wrapper);
}
/**
* 转换为响应对象
*/
private UserStatsResponse convertToResponse(UserStats stats) {
UserStatsResponse response = new UserStatsResponse();
BeanUtils.copyProperties(stats, response);
response.setId(stats.getId());
if (stats.getCreateTime() != null) {
response.setCreateTime(stats.getCreateTime().format(DATE_TIME_FORMATTER));
}
if (stats.getUpdateTime() != null) {
response.setUpdateTime(stats.getUpdateTime().format(DATE_TIME_FORMATTER));
}
return response;
}
}
@@ -0,0 +1,711 @@
package com.emotion.service.impl;
import com.alibaba.fastjson2.JSONObject;
import com.emotion.dto.request.ai.AiRuntimeRequest;
import com.emotion.dto.request.WebSocketRequest;
import com.emotion.dto.response.ai.AiStreamEvent;
import com.emotion.dto.websocket.ChatRequest;
import com.emotion.dto.websocket.ConnectRequest;
import com.emotion.dto.websocket.WebSocketMessage;
import com.emotion.entity.Message;
import com.emotion.entity.Conversation;
import com.emotion.service.WebSocketService;
import com.emotion.service.AiRuntimeService;
import com.emotion.service.MessageService;
import com.emotion.service.ConversationService;
import com.emotion.util.SnowflakeIdGenerator;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.security.Principal;
import java.time.LocalDateTime;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
/**
* WebSocket服务实现类
*
* @author huazhongmin
* @date 2025-07-25
*/
@Slf4j
@Service
public class WebSocketServiceImpl implements WebSocketService {
@Autowired
private SimpMessagingTemplate messagingTemplate;
@Autowired
private AiRuntimeService aiRuntimeService;
@Autowired
private MessageService messageService;
@Autowired
private ConversationService conversationService;
@Autowired
private SnowflakeIdGenerator snowflakeIdGenerator;
// 在线用户管理
private final ConcurrentHashMap<String, String> onlineUsers = new ConcurrentHashMap<>();
/**
* 处理聊天消息
*/
@Override
public void handleChatMessage(WebSocketRequest webSocketRequest, String sessionId, Principal principal) {
try {
log.info("处理聊天消息: request={}, sessionId={}, principal={}", webSocketRequest, sessionId, principal);
// 验证请求参数
if (webSocketRequest.getContent() == null || webSocketRequest.getContent().trim().isEmpty()) {
sendErrorMessage(getUserId(principal, sessionId), "消息内容不能为空");
return;
}
// 设置默认值
setWebSocketRequestDefaults(webSocketRequest, principal, sessionId);
log.info("确定用户身份: userId={}, senderType={}", webSocketRequest.getSenderId(),
webSocketRequest.getSenderType());
// 转换请求对象
ChatRequest chatRequest = convertToChatRequest(webSocketRequest);
// 构建用户消息
WebSocketMessage userMessage = WebSocketMessage.builder()
.messageId(UUID.randomUUID().toString())
.conversationId(chatRequest.getConversationId())
.type("TEXT")
.content(chatRequest.getContent())
.senderId(chatRequest.getSenderId())
.senderType(getSenderType(chatRequest.getSenderType()))
.status("SENT")
.createTime(LocalDateTime.now())
.build();
// 发送用户消息到会话
if (chatRequest.getConversationId() != null) {
messagingTemplate.convertAndSend("/topic/conversation/" + chatRequest.getConversationId(), userMessage);
}
// 发送给用户私有队列
messagingTemplate.convertAndSendToUser(chatRequest.getSenderId(), "/queue/messages", userMessage);
// 发送AI思考状态
sendAiThinkingMessage(chatRequest.getSenderId(), chatRequest.getConversationId());
// 异步调用AI服务
processAiResponse(chatRequest);
} catch (Exception e) {
log.error("处理聊天消息失败", e);
sendErrorMessage(getUserId(principal, sessionId), "消息处理失败,请稍后重试");
}
}
/**
* 处理用户连接
*/
@Override
public void handleUserConnect(ConnectRequest request, String sessionId, Principal principal) {
try {
// 设置默认值
setConnectRequestDefaults(request, principal, sessionId);
log.info("用户连接WebSocket: userId={}, sessionId={}, authenticated={}",
request.getUserId(), sessionId, isUserAuthenticated(request.getUserId()));
// 记录在线用户
onlineUsers.put(sessionId, request.getUserId());
// 发送连接成功消息
WebSocketMessage connectMessage = WebSocketMessage.builder()
.messageId(UUID.randomUUID().toString())
.type("CONNECTION")
.content("连接成功")
.senderId("system")
.senderType("SYSTEM")
.status("SENT")
.createTime(LocalDateTime.now())
.build();
messagingTemplate.convertAndSendToUser(request.getUserId(), "/queue/messages", connectMessage);
} catch (Exception e) {
log.error("处理用户连接失败", e);
}
}
/**
* 处理用户断开连接
*/
@Override
public void handleUserDisconnect(String sessionId, Principal principal) {
try {
String userId = onlineUsers.remove(sessionId);
log.info("用户断开WebSocket连接: userId={}, sessionId={}", userId, sessionId);
} catch (Exception e) {
log.error("处理用户断开连接失败", e);
}
}
/**
* 处理心跳消息
*/
@Override
public void handleHeartbeat(String sessionId, Principal principal) {
try {
String userId = onlineUsers.get(sessionId);
if (userId == null && principal != null) {
userId = principal.getName();
}
// 发送心跳响应
WebSocketMessage heartbeatMessage = WebSocketMessage.builder()
.messageId(UUID.randomUUID().toString())
.type("HEARTBEAT")
.content("pong")
.senderId("system")
.senderType("SYSTEM")
.status("SENT")
.createTime(LocalDateTime.now())
.build();
if (userId != null) {
messagingTemplate.convertAndSendToUser(userId, "/queue/messages", heartbeatMessage);
}
} catch (Exception e) {
log.error("处理心跳消息失败", e);
}
}
/**
* 发送AI思考状态消息
*/
private void sendAiThinkingMessage(String userId, String conversationId) {
WebSocketMessage thinkingMessage = WebSocketMessage.builder()
.messageId(UUID.randomUUID().toString())
.conversationId(conversationId)
.type("AI_THINKING")
.content("AI正在思考中...")
.senderId("ai")
.senderType("AI")
.status("SENT")
.createTime(LocalDateTime.now())
.build();
messagingTemplate.convertAndSendToUser(userId, "/queue/messages", thinkingMessage);
if (conversationId != null) {
messagingTemplate.convertAndSend("/topic/conversation/" + conversationId, thinkingMessage);
}
}
/**
* 异步处理AI响应
*/
private void processAiResponse(ChatRequest request) {
// 使用线程池异步处理AI响应
new Thread(() -> {
try {
String userId = request.getSenderId();
String conversationId = request.getConversationId();
// 如果没有会话ID,创建新会话
if (conversationId == null || conversationId.trim().isEmpty()) {
conversationId = createNewConversation(userId, request);
request.setConversationId(conversationId);
}
// 确保会话存在并更新活跃时间
ensureConversationExists(conversationId, userId, request);
// 保存用户消息到数据库
Message userMessage = new Message();
userMessage.setConversationId(conversationId);
userMessage.setUserId(userId);
userMessage.setCreateBy(userId); // 设置创建人为当前用户
userMessage
.setUserType("USER".equals(request.getSenderType()) ? "registered" : "guest");
userMessage.setContent(request.getContent());
userMessage.setType("text");
userMessage.setSender("user");
userMessage.setCozeRole("user");
userMessage.setCozeContentType("text");
userMessage = messageService.createMessage(userMessage);
String aiMessageId = snowflakeIdGenerator.nextIdAsString();
String aiReply = streamChatReply(userId, conversationId, userMessage.getId(), aiMessageId, request.getContent());
if (StringUtils.hasText(aiReply)) {
saveAiMessageOnly(userId, conversationId, aiMessageId, aiReply);
}
// 更新会话的最后活跃时间和消息数量
updateConversationActivity(conversationId);
} catch (Exception e) {
log.error("AI响应处理失败", e);
sendErrorMessage(request.getSenderId(), "AI服务暂时不可用,请稍后重试");
}
}).start();
}
/**
* 发送错误消息
*/
private String streamChatReply(String userId, String conversationId, String userMessageId, String aiMessageId, String content) {
StringBuilder output = new StringBuilder();
AiRuntimeRequest runtimeRequest = new AiRuntimeRequest();
runtimeRequest.setSceneCode("chat");
runtimeRequest.setUserId(userId);
runtimeRequest.setRequestId(aiMessageId);
JSONObject inputs = new JSONObject();
inputs.put("input", content);
inputs.put("message", content);
inputs.put("prompt", content);
inputs.put("conversationId", conversationId);
inputs.put("userMessageId", userMessageId);
runtimeRequest.setInputs(inputs);
aiRuntimeService.invokeStream(runtimeRequest, event -> {
if ("delta".equals(event.getType()) && event.getContent() != null) {
output.append(event.getContent());
}
sendAiStreamEvent(userId, conversationId, aiMessageId, event);
});
return output.toString();
}
private void sendAiStreamEvent(String userId, String conversationId, String aiMessageId, AiStreamEvent event) {
String type = switch (event.getType()) {
case "start" -> "AI_STREAM_START";
case "delta" -> "AI_STREAM_DELTA";
case "done" -> "AI_STREAM_DONE";
case "error" -> "AI_STREAM_ERROR";
default -> "AI_STREAM_EVENT";
};
JSONObject data = new JSONObject();
data.put("eventType", event.getType());
data.put("code", event.getCode());
data.put("message", event.getMessage());
data.put("seq", event.getSeq());
data.put("timestamp", event.getTimestamp());
data.put("metadata", event.getMetadata());
String messageContent = event.getContent();
if (!StringUtils.hasText(messageContent)) {
messageContent = StringUtils.hasText(event.getMessage()) ? event.getMessage() : "";
}
WebSocketMessage wsMessage = WebSocketMessage.builder()
.messageId(aiMessageId)
.conversationId(conversationId)
.type(type)
.content(messageContent)
.senderId("ai")
.senderType("AI")
.status("SENT")
.createTime(LocalDateTime.now())
.data(data)
.build();
messagingTemplate.convertAndSendToUser(userId, "/queue/messages", wsMessage);
if (conversationId != null) {
messagingTemplate.convertAndSend("/topic/conversation/" + conversationId, wsMessage);
}
}
private void saveAiMessageOnly(String userId, String conversationId, String messageId, String content) {
Message aiMessage = new Message();
aiMessage.setId(messageId);
aiMessage.setConversationId(conversationId);
aiMessage.setUserId(userId);
aiMessage.setCreateBy("ai");
aiMessage.setContent(content);
aiMessage.setType("text");
aiMessage.setSender("ai");
aiMessage.setCozeRole("assistant");
aiMessage.setCozeContentType("text");
aiMessage.setTimestamp(LocalDateTime.now());
messageService.createMessage(aiMessage);
}
private void sendErrorMessage(String userId, String errorContent) {
WebSocketMessage errorMessage = WebSocketMessage.builder()
.messageId(UUID.randomUUID().toString())
.type("ERROR")
.content(errorContent)
.senderId("system")
.senderType("SYSTEM")
.status("SENT")
.createTime(LocalDateTime.now())
.build();
messagingTemplate.convertAndSendToUser(userId, "/queue/messages", errorMessage);
}
/**
* 获取在线用户数量
*/
@Override
public int getOnlineUserCount() {
return onlineUsers.size();
}
/**
* 创建新会话
*/
private String createNewConversation(String userId, ChatRequest request) {
try {
String conversationId = "conv_" + System.currentTimeMillis() + "_" + UUID.randomUUID().toString().substring(0, 8);
Conversation conversation = Conversation.builder()
.userId(userId)
.userType("USER".equals(request.getSenderType()) ? "registered" : "guest")
.title("新对话")
.type("chat")
.conversationStatus("active")
.startTime(LocalDateTime.now())
.lastActiveTime(LocalDateTime.now())
.messageCount(0)
.build();
// 设置ID
conversation.setId(conversationId);
conversationService.save(conversation);
log.info("创建新会话: conversationId={}, userId={}", conversationId, userId);
return conversationId;
} catch (Exception e) {
log.error("创建新会话失败: userId={}", userId, e);
throw new RuntimeException("创建会话失败", e);
}
}
/**
* 确保会话存在并更新活跃时间
*/
private void ensureConversationExists(String conversationId, String userId, ChatRequest request) {
try {
Conversation conversation = conversationService.getById(conversationId);
if (conversation == null) {
// 如果会话不存在,创建一个
conversation = Conversation.builder()
.userId(userId)
.userType("USER".equals(request.getSenderType()) ? "registered" : "guest")
.title("对话")
.type("chat")
.conversationStatus("active")
.startTime(LocalDateTime.now())
.lastActiveTime(LocalDateTime.now())
.messageCount(0)
.build();
// 设置ID
conversation.setId(conversationId);
conversationService.save(conversation);
log.info("创建会话: conversationId={}, userId={}", conversationId, userId);
} else {
// 更新最后活跃时间
conversation.setLastActiveTime(LocalDateTime.now());
conversationService.updateById(conversation);
}
} catch (Exception e) {
log.error("确保会话存在失败: conversationId={}, userId={}", conversationId, userId, e);
}
}
/**
* 更新会话活跃状态
*/
private void updateConversationActivity(String conversationId) {
try {
Conversation conversation = conversationService.getById(conversationId);
if (conversation != null) {
conversation.setLastActiveTime(LocalDateTime.now());
conversation.setMessageCount((conversation.getMessageCount() != null ? conversation.getMessageCount() : 0) + 1);
conversationService.updateById(conversation);
}
} catch (Exception e) {
log.error("更新会话活跃状态失败: conversationId={}", conversationId, e);
}
}
/**
* 根据换行符分割AI回复并按顺序发送多条消息
*
* @param userId 用户ID
* @param conversationId 会话ID
* @param aiReply AI回复内容
*/
private void sendAiReplyInParts(String userId, String conversationId, String aiReply) {
try {
log.info("开始处理AI回复消息: userId={}, conversationId={}, aiReply长度={}",
userId, conversationId, aiReply != null ? aiReply.length() : 0);
if (aiReply == null || aiReply.trim().isEmpty()) {
log.warn("AI回复内容为空,跳过发送");
return;
}
// 检查是否需要分割
boolean needsSplit = aiReply.contains("\n\n") || aiReply.contains("\n");
if (!needsSplit) {
// 不需要分割,直接发送完整消息
log.info("AI回复无换行符,发送完整消息");
sendSingleAiMessage(userId, conversationId, aiReply.trim());
return;
}
// 需要分割,按换行符分割并发送多条消息
log.info("AI回复包含换行符,开始分割发送");
String[] replyParts = splitAiReply(aiReply);
log.info("AI回复分割完成,共{}个部分", replyParts.length);
// 按顺序发送每个部分
int sentCount = 0;
for (int i = 0; i < replyParts.length; i++) {
String part = replyParts[i].trim();
// 跳过空白部分
if (part.isEmpty()) {
continue;
}
// 发送消息部分
sendSingleAiMessage(userId, conversationId, part);
sentCount++;
log.info("发送AI回复部分 {}/{}: 内容长度={}", sentCount, replyParts.length, part.length());
// 在多个部分之间添加短暂延迟,模拟自然对话节奏
if (i < replyParts.length - 1) {
try {
Thread.sleep(500); // 延迟500毫秒
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
log.warn("发送AI回复时被中断");
break;
}
}
}
log.info("AI回复发送完成: userId={}, conversationId={}, 实际发送{}条消息",
userId, conversationId, sentCount);
} catch (Exception e) {
log.error("分割发送AI回复失败: userId={}, conversationId={}", userId, conversationId, e);
// 发送错误时,尝试发送完整的原始回复
try {
WebSocketMessage fallbackMessage = WebSocketMessage.builder()
.messageId(UUID.randomUUID().toString())
.conversationId(conversationId)
.type("TEXT")
.content(aiReply)
.senderId("ai")
.senderType("AI")
.status("SENT")
.createTime(LocalDateTime.now())
.build();
messagingTemplate.convertAndSendToUser(userId, "/queue/messages", fallbackMessage);
if (conversationId != null) {
messagingTemplate.convertAndSend("/topic/conversation/" + conversationId, fallbackMessage);
}
log.info("已发送完整AI回复作为备用方案");
} catch (Exception fallbackError) {
log.error("发送备用AI回复也失败", fallbackError);
sendErrorMessage(userId, "AI回复发送失败,请稍后重试");
}
}
}
/**
* 发送单条AI消息
*
* @param userId 用户ID
* @param conversationId 会话ID
* @param content 消息内容
*/
private void sendSingleAiMessage(String userId, String conversationId, String content) {
try {
// 保存AI消息到数据库
Message aiMessage = new Message();
aiMessage.setId(snowflakeIdGenerator.nextIdAsString());
aiMessage.setConversationId(conversationId);
aiMessage.setUserId(userId);
aiMessage.setCreateBy("ai");
aiMessage.setContent(content);
aiMessage.setType("text");
aiMessage.setSender("ai");
aiMessage.setCozeRole("assistant");
aiMessage.setCozeContentType("text");
aiMessage.setTimestamp(LocalDateTime.now());
messageService.createMessage(aiMessage);
log.info("AI消息已保存到数据库: messageId={}, conversationId={}, contentLength={}",
aiMessage.getId(), conversationId, content.length());
// 构建WebSocket消息
WebSocketMessage wsMessage = WebSocketMessage.builder()
.messageId(aiMessage.getId())
.conversationId(conversationId)
.type("TEXT")
.content(content)
.senderId("ai")
.senderType("AI")
.status("SENT")
.createTime(LocalDateTime.now())
.build();
// 发送给用户私有队列
messagingTemplate.convertAndSendToUser(userId, "/queue/messages", wsMessage);
// 发送到会话公共频道
if (conversationId != null) {
messagingTemplate.convertAndSend("/topic/conversation/" + conversationId, wsMessage);
}
} catch (Exception e) {
log.error("发送单条AI消息失败: userId={}, conversationId={}", userId, conversationId, e);
sendErrorMessage(userId, "消息发送失败,请稍后重试");
}
}
/**
* 智能分割AI回复内容
*
* @param aiReply AI回复内容
* @return 分割后的内容数组
*/
private String[] splitAiReply(String aiReply) {
if (aiReply == null || aiReply.trim().isEmpty()) {
return new String[0];
}
// 首先尝试按双换行符分割(段落分割)
if (aiReply.contains("\n\n")) {
String[] parts = aiReply.split("\n\n");
log.debug("按双换行符分割,得到{}个部分", parts.length);
return parts;
}
// 如果没有双换行符,按单换行符分割(行分割)
if (aiReply.contains("\n")) {
String[] parts = aiReply.split("\n");
log.debug("按单换行符分割,得到{}个部分", parts.length);
return parts;
}
// 如果没有换行符,返回原始内容(这种情况不应该到达这里)
log.debug("没有换行符,返回原始内容");
return new String[]{aiReply};
}
/**
* 设置WebSocket请求的默认值
*/
private void setWebSocketRequestDefaults(WebSocketRequest request, Principal principal, String sessionId) {
// 如果请求中没有发送者ID,尝试从Principal获取
if (request.getSenderId() == null && principal != null) {
request.setSenderId(principal.getName());
}
// 如果还是没有发送者ID,使用会话ID作为访客ID
if (request.getSenderId() == null) {
request.setSenderId("guest_" + sessionId);
request.setSenderType("GUEST");
}
// 设置时间戳
if (request.getTimestamp() == null) {
request.setTimestamp(System.currentTimeMillis());
}
}
/**
* 设置连接请求的默认值
*/
private void setConnectRequestDefaults(ConnectRequest request, Principal principal, String sessionId) {
// 优先从Principal获取认证用户信息
if (principal != null) {
request.setUserId(principal.getName());
}
// 如果还没有userId,生成访客ID
if (request.getUserId() == null) {
request.setUserId("guest_" + sessionId);
}
// 设置连接时间戳
if (request.getTimestamp() == null) {
request.setTimestamp(System.currentTimeMillis());
}
}
/**
* 获取用户ID
*/
private String getUserId(Principal principal, String sessionId) {
if (principal != null) {
return principal.getName();
}
return "guest_" + sessionId;
}
/**
* 判断用户是否已认证
*/
private boolean isUserAuthenticated(String userId) {
return userId != null && !userId.startsWith("guest_");
}
/**
* 获取发送者类型
*/
private String getSenderType(String senderType) {
if ("USER".equals(senderType)) {
return "USER";
} else if ("GUEST".equals(senderType)) {
return "GUEST";
} else if ("AI".equals(senderType)) {
return "AI";
} else if ("SYSTEM".equals(senderType)) {
return "SYSTEM";
}
return "USER";
}
/**
* 转换WebSocketRequest到ChatRequest
*
* @param webSocketRequest WebSocket请求对象
* @return ChatRequest对象
*/
private ChatRequest convertToChatRequest(WebSocketRequest webSocketRequest) {
return ChatRequest.builder()
.content(webSocketRequest.getContent())
.senderId(webSocketRequest.getSenderId())
.senderType(webSocketRequest.getSenderType())
.messageType(webSocketRequest.getMessageType())
.conversationId(webSocketRequest.getConversationId())
.timestamp(webSocketRequest.getTimestamp())
.build();
}
}
@@ -0,0 +1,110 @@
package com.emotion.service.impl;
import com.alibaba.fastjson2.JSONObject;
import com.emotion.config.WechatMiniProgramProperties;
import com.emotion.dto.wechat.WechatCodeSessionResponse;
import com.emotion.exception.BusinessException;
import com.emotion.exception.WechatApiException;
import com.emotion.service.WechatMiniProgramService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import org.springframework.web.client.ResourceAccessException;
import org.springframework.web.client.RestClientException;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder;
@Slf4j
@Service
@RequiredArgsConstructor
public class WechatMiniProgramServiceImpl implements WechatMiniProgramService {
private final RestTemplate restTemplate;
private final WechatMiniProgramProperties properties;
@Override
public WechatCodeSessionResponse code2Session(String code) {
validateConfigured();
String url = UriComponentsBuilder.fromHttpUrl(properties.getBaseUrl() + "/sns/jscode2session")
.queryParam("appid", properties.getAppId())
.queryParam("secret", properties.getAppSecret())
.queryParam("js_code", code)
.queryParam("grant_type", "authorization_code")
.toUriString();
ResponseEntity<String> responseEntity;
try {
responseEntity = restTemplate.getForEntity(url, String.class);
} catch (ResourceAccessException e) {
log.error("[WeChatAPI] network error, jsCode={}", code, e);
throw new WechatApiException(502, "Wechat API network error: " + e.getMessage(), null);
} catch (RestClientException e) {
log.error("[WeChatAPI] client error, jsCode={}", code, e);
throw new WechatApiException(502, "Wechat API client error: " + e.getMessage(), null);
}
String responseBody = responseEntity.getBody();
if (!responseEntity.getStatusCode().is2xxSuccessful()) {
log.error("[WeChatAPI] non-2xx response, status={}, body={}",
responseEntity.getStatusCode(), responseBody);
throw new WechatApiException(502,
"Wechat API returned non-2xx status: " + responseEntity.getStatusCode(), responseBody);
}
JSONObject result = parseWechatJsonBody(responseEntity);
int errcode = result.getIntValue("errcode");
if (errcode != 0) {
String errmsg = result.getString("errmsg");
log.error("[WeChatAPI] business error, errcode={}, errmsg={}", errcode, errmsg);
throw new WechatApiException(400,
"Wechat API business error: errcode=" + errcode + ", errmsg=" + errmsg, responseBody);
}
String openid = result.getString("openid");
String sessionKey = result.getString("session_key");
if (!StringUtils.hasText(openid) || !StringUtils.hasText(sessionKey)) {
log.error("[WeChatAPI] response missing openid or session_key, body={}", responseBody);
throw new WechatApiException(502,
"Wechat API response missing openid or session_key", responseBody);
}
WechatCodeSessionResponse response = new WechatCodeSessionResponse();
response.setOpenid(openid);
response.setSessionKey(sessionKey);
response.setUnionid(result.getString("unionid"));
return response;
}
private JSONObject parseWechatJsonBody(ResponseEntity<String> responseEntity) {
String responseBody = responseEntity.getBody();
MediaType contentType = responseEntity.getHeaders().getContentType();
boolean jsonContentType = contentType != null && MediaType.APPLICATION_JSON.isCompatibleWith(contentType);
boolean jsonLikeBody = StringUtils.hasText(responseBody)
&& (responseBody.trim().startsWith("{") || responseBody.trim().startsWith("["));
if (!jsonContentType && !jsonLikeBody) {
log.error("[WeChatAPI] non-JSON response, contentType={}, body={}", contentType, responseBody);
throw new WechatApiException(502,
"Wechat API returned a non-JSON response: " + contentType, responseBody);
}
try {
return JSONObject.parseObject(responseBody);
} catch (Exception e) {
log.error("[WeChatAPI] JSON parse failed, contentType={}, body={}", contentType, responseBody, e);
throw new WechatApiException(jsonContentType ? 500 : 502,
"Wechat API response JSON parse failed: " + e.getMessage(), responseBody);
}
}
private void validateConfigured() {
if (!StringUtils.hasText(properties.getAppId()) || !StringUtils.hasText(properties.getAppSecret())) {
throw new BusinessException("微信小程序登录未配置");
}
}
}