diff --git a/backend-single/src/main/java/com/emotion/controller/UserProfileController.java b/backend-single/src/main/java/com/emotion/controller/UserProfileController.java new file mode 100644 index 0000000..4d50644 --- /dev/null +++ b/backend-single/src/main/java/com/emotion/controller/UserProfileController.java @@ -0,0 +1,100 @@ +package com.emotion.controller; + +import com.emotion.common.PageResult; +import com.emotion.common.Result; +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.service.UserProfileService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.validation.Valid; +import java.util.List; + +/** + * 用户档案控制器 + * + * @author huazhongmin + * @date 2025-12-21 + */ +@RestController +@RequestMapping("/user-profile") +public class UserProfileController { + + @Autowired + private UserProfileService userProfileService; + + /** + * 新增档案 + */ + @PostMapping("/create") + public Result create(@Valid @RequestBody UserProfileCreateRequest request) { + UserProfileResponse response = userProfileService.createProfile(request); + return Result.success(response); + } + + /** + * 删除档案 + */ + @DeleteMapping("/delete") + public Result delete(@RequestParam String id) { + boolean success = userProfileService.deleteProfile(id); + if (success) { + return Result.success(); + } else { + return Result.error("删除失败"); + } + } + + /** + * 修改档案 + */ + @PutMapping("/update") + public Result update(@Valid @RequestBody UserProfileUpdateRequest request) { + UserProfileResponse response = userProfileService.updateProfile(request); + return Result.success(response); + } + + /** + * 根据ID查询详情 + */ + @GetMapping("/detail") + public Result getById(@RequestParam String id) { + UserProfileResponse response = userProfileService.getProfileById(id); + if (response == null) { + return Result.notFound("档案不存在"); + } + return Result.success(response); + } + + /** + * 获取当前登录用户的档案 + */ + @GetMapping("/me") + public Result getCurrentProfile() { + UserProfileResponse response = userProfileService.getCurrentUserProfile(); + // 如果不存在,返回null data,不报错 + return Result.success(response); + } + + /** + * 分页查询 + */ + @GetMapping("/page") + public Result> getPage(@Validated UserProfilePageRequest request) { + PageResult pageResult = userProfileService.getProfilePage(request); + return Result.success(pageResult); + } + + /** + * 列表查询 + */ + @GetMapping("/list") + public Result> getList(@Validated UserProfilePageRequest request) { + List list = userProfileService.getProfileList(request); + return Result.success(list); + } +} diff --git a/backend-single/src/main/java/com/emotion/dto/request/userprofile/UserProfileCreateRequest.java b/backend-single/src/main/java/com/emotion/dto/request/userprofile/UserProfileCreateRequest.java new file mode 100644 index 0000000..37c3f51 --- /dev/null +++ b/backend-single/src/main/java/com/emotion/dto/request/userprofile/UserProfileCreateRequest.java @@ -0,0 +1,87 @@ +package com.emotion.dto.request.userprofile; + +import lombok.Data; +import javax.validation.constraints.NotBlank; +import java.time.LocalDate; + +/** + * 用户档案创建请求对象 + * + * @author huazhongmin + * @date 2025-12-21 + */ +@Data +public class UserProfileCreateRequest { + + /** + * 昵称 + */ + @NotBlank(message = "昵称不能为空") + private String nickname; + + /** + * 性别 + */ + private String gender; + + /** + * 星座 + */ + private String zodiac; + + /** + * MBTI人格类型 + */ + @NotBlank(message = "MBTI人格类型不能为空") + private String mbti; + + /** + * 兴趣爱好 (JSON字符串) + */ + private String hobbies; + + /** + * 童年经历日期 + */ + private LocalDate childhoodDate; + + /** + * 童年经历描述 + */ + private String childhoodContent; + + /** + * 高光时刻日期 + */ + private LocalDate peakDate; + + /** + * 高光时刻描述 + */ + private String peakContent; + + /** + * 低谷时期日期 + */ + private LocalDate valleyDate; + + /** + * 低谷时期描述 + */ + private String valleyContent; + + /** + * 未来期许 + */ + private String futureVision; + + /** + * 生成的剧本列表 (JSON字符串) + */ + private String scripts; + + /** + * 选择的路径列表 (JSON字符串) + */ + private String paths; +} diff --git a/backend-single/src/main/java/com/emotion/dto/request/userprofile/UserProfilePageRequest.java b/backend-single/src/main/java/com/emotion/dto/request/userprofile/UserProfilePageRequest.java new file mode 100644 index 0000000..9d28b4e --- /dev/null +++ b/backend-single/src/main/java/com/emotion/dto/request/userprofile/UserProfilePageRequest.java @@ -0,0 +1,36 @@ +package com.emotion.dto.request.userprofile; + +import com.emotion.dto.request.PageRequest; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 用户档案分页查询请求对象 + * + * @author huazhongmin + * @date 2025-12-21 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class UserProfilePageRequest extends PageRequest { + + /** + * 用户ID + */ + private String userId; + + /** + * 昵称 + */ + private String nickname; + + /** + * MBTI人格类型 + */ + private String mbti; + + /** + * 状态 + */ + private Integer status; +} diff --git a/backend-single/src/main/java/com/emotion/dto/request/userprofile/UserProfileUpdateRequest.java b/backend-single/src/main/java/com/emotion/dto/request/userprofile/UserProfileUpdateRequest.java new file mode 100644 index 0000000..75dff95 --- /dev/null +++ b/backend-single/src/main/java/com/emotion/dto/request/userprofile/UserProfileUpdateRequest.java @@ -0,0 +1,96 @@ +package com.emotion.dto.request.userprofile; + +import lombok.Data; +import javax.validation.constraints.NotBlank; +import java.time.LocalDate; + +/** + * 用户档案更新请求对象 + * + * @author huazhongmin + * @date 2025-12-21 + */ +@Data +public class UserProfileUpdateRequest { + + /** + * ID + */ + @NotBlank(message = "ID不能为空") + private String id; + + /** + * 昵称 + */ + private String nickname; + + /** + * 性别 + */ + private String gender; + + /** + * 星座 + */ + private String zodiac; + + /** + * MBTI人格类型 + */ + private String mbti; + + /** + * 兴趣爱好 (JSON字符串) + */ + private String hobbies; + + /** + * 童年经历日期 + */ + private LocalDate childhoodDate; + + /** + * 童年经历描述 + */ + private String childhoodContent; + + /** + * 高光时刻日期 + */ + private LocalDate peakDate; + + /** + * 高光时刻描述 + */ + private String peakContent; + + /** + * 低谷时期日期 + */ + private LocalDate valleyDate; + + /** + * 低谷时期描述 + */ + private String valleyContent; + + /** + * 未来期许 + */ + private String futureVision; + + /** + * 生成的剧本列表 (JSON字符串) + */ + private String scripts; + + /** + * 选择的路径列表 (JSON字符串) + */ + private String paths; + + /** + * 状态: 0-禁用, 1-正常 + */ + private Integer status; +} diff --git a/backend-single/src/main/java/com/emotion/dto/response/userprofile/UserProfileResponse.java b/backend-single/src/main/java/com/emotion/dto/response/userprofile/UserProfileResponse.java new file mode 100644 index 0000000..66e9909 --- /dev/null +++ b/backend-single/src/main/java/com/emotion/dto/response/userprofile/UserProfileResponse.java @@ -0,0 +1,123 @@ +package com.emotion.dto.response.userprofile; + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDate; +import java.time.LocalDateTime; + +/** + * 用户档案响应对象 + * + * @author huazhongmin + * @date 2025-12-21 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class UserProfileResponse { + + /** + * ID + */ + private String id; + + /** + * 用户ID + */ + private String userId; + + /** + * 昵称 + */ + private String nickname; + + /** + * 性别 + */ + private String gender; + + /** + * 星座 + */ + private String zodiac; + + /** + * MBTI人格类型 + */ + private String mbti; + + /** + * 兴趣爱好 (JSON字符串) + */ + private String hobbies; + + /** + * 童年经历日期 + */ + @JsonFormat(pattern = "yyyy-MM-dd") + private LocalDate childhoodDate; + + /** + * 童年经历描述 + */ + private String childhoodContent; + + /** + * 高光时刻日期 + */ + @JsonFormat(pattern = "yyyy-MM-dd") + private LocalDate peakDate; + + /** + * 高光时刻描述 + */ + private String peakContent; + + /** + * 低谷时期日期 + */ + @JsonFormat(pattern = "yyyy-MM-dd") + private LocalDate valleyDate; + + /** + * 低谷时期描述 + */ + private String valleyContent; + + /** + * 未来期许 + */ + private String futureVision; + + /** + * 生成的剧本列表 (JSON字符串) + */ + private String scripts; + + /** + * 选择的路径列表 (JSON字符串) + */ + private String paths; + + /** + * 状态: 0-禁用, 1-正常 + */ + private Integer status; + + /** + * 创建时间 + */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime createTime; + + /** + * 更新时间 + */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime updateTime; +} diff --git a/backend-single/src/main/java/com/emotion/entity/UserProfile.java b/backend-single/src/main/java/com/emotion/entity/UserProfile.java new file mode 100644 index 0000000..6e38ef1 --- /dev/null +++ b/backend-single/src/main/java/com/emotion/entity/UserProfile.java @@ -0,0 +1,123 @@ +package com.emotion.entity; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableName; +import com.emotion.common.BaseEntity; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +import java.time.LocalDate; + +/** + * 用户档案实体类 + * + * @author huazhongmin + * @date 2025-12-21 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@SuperBuilder +@NoArgsConstructor +@AllArgsConstructor +@TableName("t_user_profile") +public class UserProfile extends BaseEntity { + + /** + * 用户ID (关联t_user.id) + */ + @TableField("user_id") + private String userId; + + /** + * 昵称 + */ + @TableField("nickname") + private String nickname; + + /** + * 性别: male, female, secret + */ + @TableField("gender") + private String gender; + + /** + * 星座 + */ + @TableField("zodiac") + private String zodiac; + + /** + * MBTI人格类型 + */ + @TableField("mbti") + private String mbti; + + /** + * 兴趣爱好 (JSON字符串) + */ + @TableField("hobbies") + private String hobbies; + + /** + * 童年经历日期 + */ + @TableField("childhood_date") + private LocalDate childhoodDate; + + /** + * 童年经历描述 + */ + @TableField("childhood_content") + private String childhoodContent; + + /** + * 高光时刻日期 + */ + @TableField("peak_date") + private LocalDate peakDate; + + /** + * 高光时刻描述 + */ + @TableField("peak_content") + private String peakContent; + + /** + * 低谷时期日期 + */ + @TableField("valley_date") + private LocalDate valleyDate; + + /** + * 低谷时期描述 + */ + @TableField("valley_content") + private String valleyContent; + + /** + * 未来期许 + */ + @TableField("future_vision") + private String futureVision; + + /** + * 生成的剧本列表 (JSON字符串) + */ + @TableField("scripts") + private String scripts; + + /** + * 选择的路径列表 (JSON字符串) + */ + @TableField("paths") + private String paths; + + /** + * 状态: 0-禁用, 1-正常 + */ + @TableField("status") + private Integer status; +} diff --git a/backend-single/src/main/java/com/emotion/mapper/UserProfileMapper.java b/backend-single/src/main/java/com/emotion/mapper/UserProfileMapper.java new file mode 100644 index 0000000..9598688 --- /dev/null +++ b/backend-single/src/main/java/com/emotion/mapper/UserProfileMapper.java @@ -0,0 +1,15 @@ +package com.emotion.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.emotion.entity.UserProfile; +import org.apache.ibatis.annotations.Mapper; + +/** + * 用户档案 Mapper 接口 + * + * @author huazhongmin + * @date 2025-12-21 + */ +@Mapper +public interface UserProfileMapper extends BaseMapper { +} diff --git a/backend-single/src/main/java/com/emotion/service/UserProfileService.java b/backend-single/src/main/java/com/emotion/service/UserProfileService.java new file mode 100644 index 0000000..17ae0d6 --- /dev/null +++ b/backend-single/src/main/java/com/emotion/service/UserProfileService.java @@ -0,0 +1,75 @@ +package com.emotion.service; + +import com.baomidou.mybatisplus.extension.service.IService; +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.UserProfile; + +import java.util.List; + +/** + * 用户档案服务接口 + * + * @author huazhongmin + * @date 2025-12-21 + */ +public interface UserProfileService extends IService { + + /** + * 创建用户档案 + * + * @param request 创建请求 + * @return 用户档案响应对象 + */ + UserProfileResponse createProfile(UserProfileCreateRequest request); + + /** + * 更新用户档案 + * + * @param request 更新请求 + * @return 用户档案响应对象 + */ + UserProfileResponse updateProfile(UserProfileUpdateRequest request); + + /** + * 获取用户档案详情 + * + * @param id 档案ID + * @return 用户档案响应对象 + */ + UserProfileResponse getProfileById(String id); + + /** + * 获取当前用户档案详情 + * + * @return 用户档案响应对象 + */ + UserProfileResponse getCurrentUserProfile(); + + /** + * 分页查询用户档案 + * + * @param request 分页请求 + * @return 分页结果 + */ + PageResult getProfilePage(UserProfilePageRequest request); + + /** + * 列表查询用户档案 + * + * @param request 查询请求 + * @return 列表结果 + */ + List getProfileList(UserProfilePageRequest request); + + /** + * 删除用户档案 + * + * @param id 档案ID + * @return 是否成功 + */ + boolean deleteProfile(String id); +} diff --git a/backend-single/src/main/java/com/emotion/service/impl/UserProfileServiceImpl.java b/backend-single/src/main/java/com/emotion/service/impl/UserProfileServiceImpl.java new file mode 100644 index 0000000..b2a4232 --- /dev/null +++ b/backend-single/src/main/java/com/emotion/service/impl/UserProfileServiceImpl.java @@ -0,0 +1,202 @@ +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.User; +import com.emotion.entity.UserProfile; +import com.emotion.mapper.UserProfileMapper; +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.stereotype.Service; +import org.springframework.util.StringUtils; + +import java.util.List; +import java.util.stream.Collectors; + +/** + * 用户档案服务实现类 + * + * @author huazhongmin + * @date 2025-12-21 + */ +@Slf4j +@Service +public class UserProfileServiceImpl extends ServiceImpl implements UserProfileService { + + @Autowired + private UserService userService; + + @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 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); + + UserProfile userProfile = getById(request.getId()); + if (userProfile == null) { + throw new RuntimeException("档案不存在"); + } + + // 权限校验:只能修改自己的档案 + String currentUserId = UserContextHolder.getCurrentUserId(); + if (!StringUtils.hasText(currentUserId)) { + throw new RuntimeException("用户未登录"); + } + if (!userProfile.getUserId().equals(currentUserId)) { + // 管理员可以修改任意档案 (此处假设没有管理员逻辑,严格按需求: 只能修改自己的) + // 如果需要管理员权限,需配合 SecurityUtils 判断角色 + // 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 queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(UserProfile::getUserId, currentUserId); + // 获取最新的一条(理论上应该只有一条) + List 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 getProfilePage(UserProfilePageRequest request) { + Page page = new Page<>(request.getCurrent(), request.getSize()); + LambdaQueryWrapper 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 userProfilePage = page(page, queryWrapper); + + List list = userProfilePage.getRecords().stream() + .map(this::convertToResponse) + .collect(Collectors.toList()); + + PageResult 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 getProfileList(UserProfilePageRequest request) { + LambdaQueryWrapper 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 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); + } + + private UserProfileResponse convertToResponse(UserProfile userProfile) { + if (userProfile == null) { + return null; + } + UserProfileResponse response = new UserProfileResponse(); + BeanUtils.copyProperties(userProfile, response); + return response; + } +} diff --git a/course-web/src/api/user.js b/course-web/src/api/user.js new file mode 100644 index 0000000..25634be --- /dev/null +++ b/course-web/src/api/user.js @@ -0,0 +1,57 @@ +import request from '../utils/request'; + +export const userApi = { + /** + * 获取当前登录用户的档案 + */ + getCurrentUser() { + return request({ + url: '/user-profile/me', + method: 'get' + }); + }, + + /** + * 新增档案 + */ + createUserProfile(data) { + return request({ + url: '/user-profile/create', + method: 'post', + data + }); + }, + + /** + * 修改档案 + */ + updateUserProfile(data) { + return request({ + url: '/user-profile/update', + method: 'put', + data + }); + }, + + /** + * 根据ID查询详情 + */ + getProfileById(id) { + return request({ + url: '/user-profile/detail', + method: 'get', + params: { id } + }); + }, + + /** + * 删除档案 + */ + deleteUserProfile(id) { + return request({ + url: '/user-profile/delete', + method: 'delete', + params: { id } + }); + } +}; diff --git a/course-web/src/components/views/PathView.jsx b/course-web/src/components/views/PathView.jsx index de74c85..fefde68 100644 --- a/course-web/src/components/views/PathView.jsx +++ b/course-web/src/components/views/PathView.jsx @@ -68,16 +68,46 @@ export function PathView({ onSwitchToScript }) { } }; - const handleDelete = () => { + const handleDelete = async () => { if (window.confirm('确定要删除这个路径规划吗?')) { Store.deletePath(currentPath.id); + + // Sync to backend + try { + const allPaths = Store.get().paths; + const profileRes = await userApi.getCurrentUser(); + if (profileRes.data) { + await userApi.updateUserProfile({ + id: profileRes.data.id, + paths: JSON.stringify(allPaths) + }); + } + } catch (err) { + console.error("Failed to sync path deletion to backend", err); + } + setIsEditing(false); } }; - const handleSaveEdit = () => { + const handleSaveEdit = async () => { if (editedPath) { Store.updatePath(editedPath.id, { steps: editedPath.steps }); + + // Sync to backend + try { + const allPaths = Store.get().paths; + const profileRes = await userApi.getCurrentUser(); + if (profileRes.data) { + await userApi.updateUserProfile({ + id: profileRes.data.id, + paths: JSON.stringify(allPaths) + }); + } + } catch (err) { + console.error("Failed to sync path update to backend", err); + } + setIsEditing(false); } }; diff --git a/course-web/src/components/views/ScriptView.jsx b/course-web/src/components/views/ScriptView.jsx index ffb5155..64a17de 100644 --- a/course-web/src/components/views/ScriptView.jsx +++ b/course-web/src/components/views/ScriptView.jsx @@ -1,5 +1,6 @@ import { useState, useEffect } from 'react'; import { Store } from '../../utils/store'; +import { userApi } from '../../api/user'; import { AI } from '../../utils/aiLogic'; import { useStoreData } from '../../hooks/useStoreData'; import { Fingerprint, Film, Sparkles, History, Trash2, Stars, Zap, Loader, X, ArrowRight, BookOpen } from 'lucide-react'; @@ -29,6 +30,21 @@ export function ScriptView({ onSwitchToPath }) { const requirements = form; const script = await AI.generateScript(data.userProfile, data.lifeTimeline, requirements); Store.addScript(script); + + // Sync to backend + try { + const allScripts = Store.get().generatedScripts; + const profileRes = await userApi.getCurrentUser(); + if (profileRes.data) { + await userApi.updateUserProfile({ + id: profileRes.data.id, + scripts: JSON.stringify(allScripts) + }); + } + } catch (err) { + console.error("Failed to sync script to backend", err); + } + setSelectedScriptId(script.id); setForm({ ...form, theme: '' }); } catch (e) { @@ -39,10 +55,25 @@ export function ScriptView({ onSwitchToPath }) { } }; - const handleDelete = (id, e) => { + const handleDelete = async (id, e) => { e.stopPropagation(); if (confirm('确定删除这个剧本吗?')) { Store.deleteScript(id); + + // Sync to backend + try { + const allScripts = Store.get().generatedScripts; + const profileRes = await userApi.getCurrentUser(); + if (profileRes.data) { + await userApi.updateUserProfile({ + id: profileRes.data.id, + scripts: JSON.stringify(allScripts) + }); + } + } catch (err) { + console.error("Failed to sync script deletion to backend", err); + } + if (selectedScriptId === id) setSelectedScriptId(null); } }; diff --git a/course-web/src/pages/OnboardingPage.jsx b/course-web/src/pages/OnboardingPage.jsx index 27639e6..4712dab 100644 --- a/course-web/src/pages/OnboardingPage.jsx +++ b/course-web/src/pages/OnboardingPage.jsx @@ -1,6 +1,7 @@ -import React, { useState } from 'react'; +import React, { useState, useEffect } from 'react'; import { Store } from '../utils/store'; -import { ArrowLeft, ArrowRight, Check, X, Sparkles, Star, AlertCircle, CheckCircle } from 'lucide-react'; +import { userApi } from '../api/user'; +import { ArrowLeft, ArrowRight, Check, X, Sparkles, Star, AlertCircle, CheckCircle, Loader } from 'lucide-react'; import { Button } from '../components/ui/Button'; import { Input, Select, Textarea } from '../components/ui/Input'; import clsx from 'clsx'; @@ -17,6 +18,55 @@ export function OnboardingPage({ onFinish }) { const [step, setStep] = useState(0); const [formData, setFormData] = useState(Store.get().userProfile); const [toast, setToast] = useState(null); // { msg, type } + const [submitting, setSubmitting] = useState(false); + + // Load existing profile from backend on mount + useEffect(() => { + const loadProfile = async () => { + try { + const res = await userApi.getCurrentUser(); + if (res.data) { + // Merge backend data with local state, handling JSON strings if necessary + // Backend returns scripts/paths as strings, but profile fields are direct + // Note: formData structure matches userProfile in store.js + // We need to map backend fields (camelCase) to frontend structure if they differ + // Backend: childhoodDate, childhoodContent etc. + // Frontend: history: { childhood: { date, content } } + // We need a mapper if structures differ. + // Backend UserProfileResponse has: nickname, gender, zodiac, mbti, hobbies (String), childhoodDate... + + const backendData = res.data; + const mappedData = { + ...formData, + nickname: backendData.nickname || formData.nickname, + gender: backendData.gender || formData.gender, + zodiac: backendData.zodiac || formData.zodiac, + mbti: backendData.mbti || formData.mbti, + hobbies: backendData.hobbies ? JSON.parse(backendData.hobbies) : formData.hobbies, + history: { + childhood: { + date: backendData.childhoodDate || '', + content: backendData.childhoodContent || '' + }, + peak: { + date: backendData.peakDate || '', + content: backendData.peakContent || '' + }, + valley: { + date: backendData.valleyDate || '', + content: backendData.valleyContent || '' + } + }, + futureVision: backendData.futureVision || formData.futureVision + }; + setFormData(mappedData); + } + } catch (e) { + console.warn("Failed to load profile", e); + } + }; + loadProfile(); + }, []); const showToast = (msg, type = 'error') => { setToast({ msg, type }); @@ -40,15 +90,57 @@ export function OnboardingPage({ onFinish }) { })); }; - const handleNext = () => { + const handleNext = async () => { if (step === 0) { if (!formData.nickname?.trim()) { showToast('请填写昵称'); return; } if (!formData.mbti) { showToast('请选择MBTI人格类型'); return; } } else if (step === 4) { if (!formData.futureVision?.trim()) { showToast('写下一句对未来的期许吧'); return; } - Store.updateProfile(formData); - Store.completeOnboarding(); - onFinish(); + + setSubmitting(true); + try { + // 1. Save to local store + Store.updateProfile(formData); + Store.completeOnboarding(); + + // 2. Prepare data for backend + // Flatten history structure to match UserProfileCreateRequest + const requestData = { + nickname: formData.nickname, + gender: formData.gender, + zodiac: formData.zodiac, + mbti: formData.mbti, + hobbies: JSON.stringify(formData.hobbies), + childhoodDate: formData.history?.childhood?.date || null, + childhoodContent: formData.history?.childhood?.content || '', + peakDate: formData.history?.peak?.date || null, + peakContent: formData.history?.peak?.content || '', + valleyDate: formData.history?.valley?.date || null, + valleyContent: formData.history?.valley?.content || '', + futureVision: formData.futureVision, + // Also sync scripts/paths if they exist in store (re-registration case) + scripts: JSON.stringify(Store.get().generatedScripts || []), + paths: JSON.stringify(Store.get().paths || []) + }; + + // 3. Call backend + // Check if profile exists first + const currentProfile = await userApi.getCurrentUser(); + if (currentProfile.data) { + // Update + await userApi.updateUserProfile({ ...requestData, id: currentProfile.data.id }); + } else { + // Create + await userApi.createUserProfile(requestData); + } + + onFinish(); + } catch (e) { + console.error(e); + showToast('保存失败,请重试'); + } finally { + setSubmitting(false); + } return; } setStep(prev => prev + 1); @@ -225,9 +317,9 @@ export function OnboardingPage({ onFinish }) { ) :
} - diff --git a/sql/emotion_museum.sql b/sql/emotion_museum.sql index e2e50c3..c93f5e0 100644 --- a/sql/emotion_museum.sql +++ b/sql/emotion_museum.sql @@ -37,50 +37,10 @@ USE emotion_museum; -- 4. 索引优化: 为查询频繁的字段创建合适的索引 -- 5. 字符集: 统一使用utf8mb4支持emoji和特殊字符 -- ============================================================================ --- 删除现有表(开发阶段确保表结构最新) --- 警告: 这会删除所有数据! --- ============================================================================ -DROP TABLE IF EXISTS t_user_stats; - -DROP TABLE IF EXISTS t_guest_user; - -DROP TABLE IF EXISTS t_reward; - -DROP TABLE IF EXISTS t_achievement; - -DROP TABLE IF EXISTS t_comment; - -DROP TABLE IF EXISTS t_community_post; - -DROP TABLE IF EXISTS t_location_pin; - -DROP TABLE IF EXISTS t_topic_interaction; - -DROP TABLE IF EXISTS t_growth_topic; - -DROP TABLE IF EXISTS t_emotion_record; - -DROP TABLE IF EXISTS t_emotion_analysis; - -DROP TABLE IF EXISTS t_coze_api_call; - -DROP TABLE IF EXISTS t_message; - -DROP TABLE IF EXISTS t_conversation; - -DROP TABLE IF EXISTS t_diary_post; - -DROP TABLE IF EXISTS t_diary_comment; - -DROP TABLE IF EXISTS t_user; - -DROP TABLE IF EXISTS t_admin; - -DROP TABLE IF EXISTS t_ai_config; - -- ============================================================================ -- 1. 用户表 (user) -- ============================================================================ +DROP TABLE IF EXISTS t_user; CREATE TABLE t_user ( id VARCHAR(64) PRIMARY KEY COMMENT 'UUID主键', -- UUID主键 account VARCHAR(50) UNIQUE COMMENT '账号', -- 账号 @@ -121,6 +81,7 @@ CREATE TABLE t_user ( -- 2. 对话表 (t_conversation) -- 关联说明: user_id 关联 t_user.id,通过代码逻辑维护关联关系 -- ============================================================================ +DROP TABLE IF EXISTS t_conversation; CREATE TABLE t_conversation ( id VARCHAR(64) PRIMARY KEY COMMENT 'UUID主键', -- UUID主键 user_id VARCHAR(64) COMMENT '用户ID (关联t_user.id)', -- 用户ID (关联t_user.id) @@ -163,6 +124,7 @@ CREATE TABLE t_conversation ( -- 3. 消息表 (t_message) -- 关联说明: conversation_id 关联 t_conversation.id,通过代码逻辑维护关联关系 -- ============================================================================ +DROP TABLE IF EXISTS t_message; CREATE TABLE t_message ( id VARCHAR(64) PRIMARY KEY COMMENT 'UUID主键', -- UUID主键 conversation_id VARCHAR(64) COMMENT '对话ID (关联t_conversation.id)', -- 对话ID (关联t_conversation.id) @@ -202,6 +164,7 @@ CREATE TABLE t_message ( -- ============================================================================ -- 4. Coze API调用记录表 (coze_api_call) - 优化版本 -- ============================================================================ +DROP TABLE IF EXISTS t_coze_api_call; CREATE TABLE t_coze_api_call ( id VARCHAR(64) PRIMARY KEY COMMENT 'UUID主键', -- UUID主键 conversation_id VARCHAR(64) COMMENT '对话ID', -- 对话ID @@ -266,6 +229,7 @@ CREATE TABLE t_coze_api_call ( -- ============================================================================ -- 5. 情绪分析表 (emotion_analysis) -- ============================================================================ +DROP TABLE IF EXISTS t_emotion_analysis; CREATE TABLE t_emotion_analysis ( id VARCHAR(64) PRIMARY KEY COMMENT 'UUID主键', -- UUID主键 user_id VARCHAR(64) COMMENT '用户ID', -- 用户ID @@ -292,6 +256,7 @@ CREATE TABLE t_emotion_analysis ( -- ============================================================================ -- 6. 情绪记录表 (emotion_record) -- ============================================================================ +DROP TABLE IF EXISTS t_emotion_record; CREATE TABLE t_emotion_record ( id VARCHAR(64) PRIMARY KEY COMMENT 'UUID主键', -- UUID主键 user_id VARCHAR(64) COMMENT '用户ID', -- 用户ID @@ -318,6 +283,7 @@ CREATE TABLE t_emotion_record ( -- ============================================================================ -- 7. 成长课题表 (growth_topic) -- ============================================================================ +DROP TABLE IF EXISTS t_growth_topic; CREATE TABLE t_growth_topic ( id VARCHAR(64) PRIMARY KEY COMMENT 'UUID主键', -- UUID主键 title VARCHAR(100) COMMENT '课题标题', -- 课题标题 @@ -343,6 +309,7 @@ CREATE TABLE t_growth_topic ( -- ============================================================================ -- 8. 课题互动表 (topic_interaction) -- ============================================================================ +DROP TABLE IF EXISTS t_topic_interaction; CREATE TABLE t_topic_interaction ( id VARCHAR(64) PRIMARY KEY COMMENT 'UUID主键', -- UUID主键 topic_id VARCHAR(64) COMMENT '课题ID', -- 课题ID @@ -365,6 +332,7 @@ CREATE TABLE t_topic_interaction ( -- ============================================================================ -- 9. 地点标记表 (location_pin) -- ============================================================================ +DROP TABLE IF EXISTS t_location_pin; CREATE TABLE t_location_pin ( id VARCHAR(64) PRIMARY KEY COMMENT 'UUID主键', -- UUID主键 name VARCHAR(100) COMMENT '地点名称', -- 地点名称 @@ -390,6 +358,7 @@ CREATE TABLE t_location_pin ( -- ============================================================================ -- 10. 社区帖子表 (community_post) -- ============================================================================ +DROP TABLE IF EXISTS t_community_post; CREATE TABLE t_community_post ( id VARCHAR(64) PRIMARY KEY COMMENT 'UUID主键', -- UUID主键 user_id VARCHAR(64) COMMENT '用户ID', -- 用户ID @@ -415,6 +384,7 @@ CREATE TABLE t_community_post ( -- ============================================================================ -- 11. 评论表 (comment) -- ============================================================================ +DROP TABLE IF EXISTS t_comment; CREATE TABLE t_comment ( id VARCHAR(64) PRIMARY KEY COMMENT 'UUID主键', -- UUID主键 post_id VARCHAR(64) COMMENT '帖子ID', -- 帖子ID @@ -434,6 +404,7 @@ CREATE TABLE t_comment ( -- ============================================================================ -- 12. 成就表 (achievement) -- ============================================================================ +DROP TABLE IF EXISTS t_achievement; CREATE TABLE t_achievement ( id VARCHAR(64) PRIMARY KEY COMMENT 'UUID主键', -- UUID主键 title VARCHAR(100) COMMENT '成就标题', -- 成就标题 @@ -459,6 +430,7 @@ CREATE TABLE t_achievement ( -- ============================================================================ -- 13. 奖励表 (reward) -- ============================================================================ +DROP TABLE IF EXISTS t_reward; CREATE TABLE t_reward ( id VARCHAR(64) PRIMARY KEY COMMENT 'UUID主键', -- UUID主键 topic_id VARCHAR(64) COMMENT '课题ID', -- 课题ID @@ -483,6 +455,7 @@ CREATE TABLE t_reward ( -- ============================================================================ -- 14. 用户统计表 (user_stats) -- ============================================================================ +DROP TABLE IF EXISTS t_user_stats; CREATE TABLE t_user_stats ( id VARCHAR(64) PRIMARY KEY COMMENT 'UUID主键', -- UUID主键 user_id VARCHAR(64) UNIQUE COMMENT '用户ID', -- 用户ID @@ -522,6 +495,7 @@ CREATE TABLE t_user_stats ( -- 16. 用户日记表 (t_diary_post) - 类似朋友圈功能 -- 关联说明: user_id 关联 t_user.id,通过代码逻辑维护关联关系 -- ============================================================================ +DROP TABLE IF EXISTS t_diary_post; CREATE TABLE t_diary_post ( id VARCHAR(64) PRIMARY KEY COMMENT 'UUID主键', -- UUID主键 user_id VARCHAR(64) COMMENT '用户ID (关联t_user.id)', -- 用户ID (关联t_user.id) @@ -567,6 +541,7 @@ CREATE TABLE t_diary_post ( -- 17. 日记评论表 (t_diary_comment) -- 关联说明: diary_id 关联 t_diary_post.id,user_id 关联 t_user.id,通过代码逻辑维护关联关系 -- ============================================================================ +DROP TABLE IF EXISTS t_diary_comment; CREATE TABLE t_diary_comment ( id VARCHAR(64) PRIMARY KEY COMMENT 'UUID主键', -- UUID主键 diary_id VARCHAR(64) COMMENT '日记ID (关联t_diary_post.id)', -- 日记ID (关联t_diary_post.id) @@ -598,6 +573,7 @@ CREATE TABLE t_diary_comment ( -- ============================================================================ -- 18. 访客用户表 (guest_user) -- ============================================================================ +DROP TABLE IF EXISTS t_guest_user; CREATE TABLE t_guest_user ( id VARCHAR(64) PRIMARY KEY COMMENT 'UUID主键', -- UUID主键 guest_user_id VARCHAR(50) UNIQUE COMMENT '访客用户ID (格式: guest_xxx)', -- 访客用户ID (格式: guest_xxx) @@ -858,6 +834,7 @@ CREATE INDEX idx_guest_user_is_deleted ON t_guest_user (is_deleted); -- ============================================================================ -- 18. 管理员用户表 (admin) -- ============================================================================ +DROP TABLE IF EXISTS t_admin; CREATE TABLE t_admin ( id VARCHAR(64) PRIMARY KEY COMMENT 'UUID主键', -- UUID主键 account VARCHAR(50) NOT NULL UNIQUE COMMENT '管理员账号', -- 管理员账号 @@ -943,23 +920,24 @@ ORDER BY -- 19. AI接口配置表 (t_ai_config) -- 用于存储各种AI接口的调用配置,支持从配置文件迁移到数据库管理 -- ============================================================================ +DROP TABLE IF EXISTS t_ai_config; CREATE TABLE t_ai_config ( id VARCHAR(64) PRIMARY KEY COMMENT 'UUID主键', config_name VARCHAR(100) NOT NULL COMMENT '配置名称', config_key VARCHAR(100) NOT NULL UNIQUE COMMENT '配置键值 (唯一标识)', config_type VARCHAR(50) NOT NULL DEFAULT 'coze' COMMENT '配置类型: coze-扣子, openai-OpenAI, claude-Claude, gemini-Gemini等', provider VARCHAR(50) NOT NULL COMMENT '服务提供商: coze, openai, anthropic, google等', - + -- API基础配置 api_base_url VARCHAR(500) NOT NULL COMMENT 'API基础URL', api_token VARCHAR(1000) NOT NULL COMMENT 'API访问令牌 (加密存储)', api_version VARCHAR(20) COMMENT 'API版本', - + -- 模型配置 model_name VARCHAR(100) COMMENT '模型名称', bot_id VARCHAR(100) COMMENT 'Bot ID (Coze专用)', workflow_id VARCHAR(100) COMMENT 'Workflow ID (Coze专用)', - + -- 请求配置 timeout_ms INT DEFAULT 30000 COMMENT '超时时间(毫秒)', retry_count INT DEFAULT 3 COMMENT '重试次数', @@ -967,52 +945,52 @@ CREATE TABLE t_ai_config ( max_tokens INT DEFAULT 4000 COMMENT '最大Token数', temperature DECIMAL(3,2) DEFAULT 0.7 COMMENT '温度参数 (0.0-2.0)', top_p DECIMAL(3,2) DEFAULT 1.0 COMMENT 'Top-p参数 (0.0-1.0)', - + -- 功能配置 support_stream TINYINT DEFAULT 1 COMMENT '是否支持流式输出: 0-不支持, 1-支持', support_function_call TINYINT DEFAULT 0 COMMENT '是否支持函数调用: 0-不支持, 1-支持', support_vision TINYINT DEFAULT 0 COMMENT '是否支持视觉理解: 0-不支持, 1-支持', support_file_upload TINYINT DEFAULT 0 COMMENT '是否支持文件上传: 0-不支持, 1-支持', - + -- 使用场景配置 usage_scenario VARCHAR(100) NOT NULL COMMENT '使用场景: chat-聊天, summary-总结, emotion_analysis-情绪分析, content_generation-内容生成等', priority INT DEFAULT 0 COMMENT '优先级 (数值越大优先级越高)', - + -- 费用配置 input_price_per_1k DECIMAL(10,6) DEFAULT 0.000000 COMMENT '输入Token价格(每1K)', output_price_per_1k DECIMAL(10,6) DEFAULT 0.000000 COMMENT '输出Token价格(每1K)', currency VARCHAR(10) DEFAULT 'USD' COMMENT '货币单位', - + -- 限制配置 rate_limit_per_minute INT DEFAULT 60 COMMENT '每分钟请求限制', rate_limit_per_hour INT DEFAULT 3600 COMMENT '每小时请求限制', rate_limit_per_day INT DEFAULT 86400 COMMENT '每日请求限制', - + -- 状态配置 is_enabled TINYINT DEFAULT 1 COMMENT '是否启用: 0-禁用, 1-启用', is_default TINYINT DEFAULT 0 COMMENT '是否为默认配置: 0-否, 1-是', environment VARCHAR(20) DEFAULT 'production' COMMENT '环境: development-开发, testing-测试, production-生产', - + -- 扩展配置 custom_headers JSON COMMENT '自定义请求头', custom_params JSON COMMENT '自定义参数', webhook_url VARCHAR(500) COMMENT 'Webhook回调地址', - + -- 监控配置 health_check_url VARCHAR(500) COMMENT '健康检查URL', health_check_interval_minutes INT DEFAULT 5 COMMENT '健康检查间隔(分钟)', - + -- 描述信息 description TEXT COMMENT '配置描述', usage_notes TEXT COMMENT '使用说明', - + -- 公共字段 - create_by VARCHAR(64) COMMENT '创建人ID', - create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', - update_by VARCHAR(64) COMMENT '更新人ID', - update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', - is_deleted TINYINT DEFAULT 0 COMMENT '是否删除: 0-未删除, 1-已删除', - remarks VARCHAR(500) COMMENT '备注' + create_by VARCHAR(64) COMMENT '创建人ID', -- 创建人ID + create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', -- 创建时间 + update_by VARCHAR(64) COMMENT '更新人ID', -- 更新人ID + update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', -- 更新时间 + is_deleted TINYINT DEFAULT 0 COMMENT '是否删除: 0-未删除, 1-已删除', -- 是否删除: 0-未删除, 1-已删除 + remarks VARCHAR(500) COMMENT '备注' -- 备注 ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'AI接口配置表 (t_ai_config)'; -- AI配置表索引 @@ -1199,5 +1177,44 @@ INSERT INTO t_ai_config ( NOW(), NOW(), 0 ); + + + +-- ============================================================================ +-- 19. 用户档案表 (t_user_profile) +-- ============================================================================ +DROP TABLE IF EXISTS t_user_profile; +CREATE TABLE t_user_profile ( + id VARCHAR(64) PRIMARY KEY COMMENT 'UUID主键', + user_id VARCHAR(64) COMMENT '用户ID (关联t_user.id)', + nickname VARCHAR(50) NOT NULL COMMENT '昵称 (必填)', + gender VARCHAR(20) DEFAULT 'secret' COMMENT '性别', + zodiac VARCHAR(20) COMMENT '星座', + mbti VARCHAR(20) NOT NULL COMMENT 'MBTI人格类型 (必填)', + hobbies JSON COMMENT '兴趣爱好列表', + childhood_date DATE COMMENT '童年记忆日期', + childhood_content TEXT COMMENT '童年记忆内容', + peak_date DATE COMMENT '高光时刻日期', + peak_content TEXT COMMENT '高光时刻内容', + valley_date DATE COMMENT '低谷时刻日期', + valley_content TEXT COMMENT '低谷时刻内容', + future_vision TEXT COMMENT '未来愿景', + scripts JSON COMMENT '生成的剧本列表 (JSON)', + paths JSON COMMENT '选择的路径列表 (JSON)', + status TINYINT DEFAULT 1 COMMENT '状态: 0-禁用, 1-正常', + -- 公共字段 + create_by VARCHAR(64) COMMENT '创建人ID', -- 创建人ID + create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', -- 创建时间 + update_by VARCHAR(64) COMMENT '更新人ID', -- 更新人ID + update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', -- 更新时间 + is_deleted TINYINT DEFAULT 0 COMMENT '是否删除: 0-未删除, 1-已删除', -- 是否删除: 0-未删除, 1-已删除 + remarks VARCHAR(500) COMMENT '备注' -- 备注 +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT '用户个人信息表 (t_user_profile)'; + +-- t_user_profile表索引 +CREATE INDEX idx_user_profile_user_id ON t_user_profile (user_id); +CREATE INDEX idx_user_profile_create_time ON t_user_profile (create_time); + + -- 提交事务 -COMMIT; \ No newline at end of file +COMMIT;