添加字典功能及初始化数据

This commit is contained in:
2025-12-22 21:56:10 +08:00
parent 180fe20347
commit 7d53a059d7
31 changed files with 1894 additions and 79 deletions
+2
View File
@@ -7,7 +7,9 @@
<jdbc-driver>com.mysql.cj.jdbc.Driver</jdbc-driver>
<jdbc-url>jdbc:mysql://localhost:3306</jdbc-url>
<jdbc-additional-properties>
<property name="com.intellij.clouds.kubernetes.db.host.port" />
<property name="com.intellij.clouds.kubernetes.db.enabled" value="false" />
<property name="com.intellij.clouds.kubernetes.db.container.port" />
</jdbc-additional-properties>
<working-dir>$ProjectFileDir$</working-dir>
</data-source>
+1
View File
@@ -2,5 +2,6 @@
<project version="4">
<component name="SqlDialectMappings">
<file url="file://$PROJECT_DIR$/sql/emotion_museum.sql" dialect="MySQL" />
<file url="file://$PROJECT_DIR$/sql/emotion_museum_init.sql" dialect="MySQL" />
</component>
</project>
+413
View File
@@ -0,0 +1,413 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
情绪博物馆后端服务部署脚本
支持本地部署和远程部署到服务器
使用系统自带的ssh/scp命令,无需额外依赖
"""
import os
import sys
import subprocess
import time
from pathlib import Path
# 配置变量
APP_NAME = "emotion-museum-single"
JAR_NAME = "backend-single-1.0.0.jar"
REMOTE_JAR_NAME = "emotion-single-1.0.0.jar"
SPRING_PROFILE = "test"
# 本地路径
SCRIPT_DIR = Path(__file__).parent.absolute()
JAR_PATH = SCRIPT_DIR / "target" / JAR_NAME
LOG_DIR = SCRIPT_DIR / "logs"
PID_FILE = Path(f"/tmp/{APP_NAME}.pid")
# Java配置
JAVA_OPTS = "-Xms512m -Xmx1024m -XX:+UseG1GC -XX:+HeapDumpOnOutOfMemoryError"
# 远程服务器配置
REMOTE_HOST = "101.200.208.45"
REMOTE_USER = "root"
REMOTE_DIR = "/data/programs/emotion-museum"
REMOTE_LOG_DIR = "/data/logs/emotion-museum"
class Colors:
"""终端颜色"""
GREEN = '\033[32m'
RED = '\033[31m'
YELLOW = '\033[33m'
RESET = '\033[0m'
def log_info(msg):
"""打印信息日志"""
print(f"{Colors.GREEN}[INFO]{Colors.RESET} {msg}")
def log_error(msg):
"""打印错误日志"""
print(f"{Colors.RED}[ERROR]{Colors.RESET} {msg}")
def log_warn(msg):
"""打印警告日志"""
print(f"{Colors.YELLOW}[WARN]{Colors.RESET} {msg}")
def run_command(cmd, cwd=None, shell=True, capture=True):
"""执行本地命令"""
try:
if capture:
result = subprocess.run(
cmd,
cwd=cwd,
shell=shell,
capture_output=True,
text=True
)
return result.returncode == 0, result.stdout, result.stderr
else:
result = subprocess.run(cmd, cwd=cwd, shell=shell)
return result.returncode == 0, "", ""
except Exception as e:
return False, "", str(e)
def exec_ssh_cmd(cmd):
"""通过SSH执行远程命令"""
ssh_cmd = f'ssh {REMOTE_USER}@{REMOTE_HOST} "{cmd}"'
return run_command(ssh_cmd)
def scp_upload(local_path, remote_path):
"""通过SCP上传文件"""
scp_cmd = f'scp "{local_path}" {REMOTE_USER}@{REMOTE_HOST}:{remote_path}'
success, stdout, stderr = run_command(scp_cmd)
if not success:
log_error(f"SCP上传失败: {stderr}")
return success
def build_project():
"""构建项目"""
log_info("开始构建项目...")
# 检查Maven
success, _, _ = run_command("mvn --version")
if not success:
log_error("未找到Maven命令,请确保已安装Maven")
sys.exit(1)
# 执行Maven构建
log_info("执行: mvn clean package -DskipTests")
os.chdir(SCRIPT_DIR)
# 不捕获输出,直接显示构建过程
success, _, _ = run_command("mvn clean package -DskipTests", capture=False)
if not success:
log_error("项目构建失败")
sys.exit(1)
# 检查JAR文件
if not JAR_PATH.exists():
log_error(f"项目构建失败,未找到JAR文件: {JAR_PATH}")
sys.exit(1)
file_size = JAR_PATH.stat().st_size / (1024 * 1024)
log_info(f"✅ 项目构建成功: {JAR_PATH}")
log_info(f"文件大小: {file_size:.2f} MB")
def check_jar():
"""检查JAR文件是否存在"""
if not JAR_PATH.exists():
log_error(f"JAR文件不存在: {JAR_PATH}")
log_info("请先执行打包命令: mvn clean package")
sys.exit(1)
log_info(f"JAR文件检查通过: {JAR_PATH}")
def deploy_to_remote(upload_script=None):
"""
远程部署到服务器
Args:
upload_script: 可选,指定要上传的额外文件路径(如 deploy-server.sh
"""
log_info(f"开始远程部署到 {REMOTE_HOST}...")
# 构建项目
build_project()
# 检查JAR文件
check_jar()
# 1. 创建远程目录
log_info("创建远程目录...")
exec_ssh_cmd(f"mkdir -p {REMOTE_DIR}")
exec_ssh_cmd(f"mkdir -p {REMOTE_LOG_DIR}")
# 2. 上传JAR文件
log_info("上传JAR文件到远程服务器...")
log_info(f"本地文件: {JAR_PATH}")
log_info(f"远程路径: {REMOTE_USER}@{REMOTE_HOST}:{REMOTE_DIR}/{REMOTE_JAR_NAME}")
if not scp_upload(JAR_PATH, f"{REMOTE_DIR}/{REMOTE_JAR_NAME}"):
log_error("上传JAR文件失败")
sys.exit(1)
log_info("✅ JAR文件上传成功")
# 3. 验证远程文件
log_info("验证远程文件...")
success, output, _ = exec_ssh_cmd(f"ls -lh {REMOTE_DIR}/{REMOTE_JAR_NAME}")
if not success:
log_error("远程文件验证失败")
sys.exit(1)
log_info(output)
# 4. 如果指定了额外文件,则上传
if upload_script:
script_path = SCRIPT_DIR / upload_script
if script_path.exists():
log_info(f"上传指定文件到远程服务器: {upload_script}")
if not scp_upload(script_path, f"{REMOTE_DIR}/{upload_script}"):
log_error(f"上传文件失败: {upload_script}")
sys.exit(1)
log_info(f"✅ 文件上传成功: {upload_script}")
# 如果是脚本文件,设置执行权限
if upload_script.endswith('.sh'):
exec_ssh_cmd(f"chmod +x {REMOTE_DIR}/{upload_script}")
else:
log_error(f"指定的文件不存在: {script_path}")
sys.exit(1)
else:
log_info("跳过部署脚本上传(服务器已存在)")
# 5. 在远程服务器上执行部署
log_info("在远程服务器上执行部署...")
success, output, error = exec_ssh_cmd(f"cd {REMOTE_DIR} && ./deploy-server.sh {SPRING_PROFILE}")
if output:
print(output)
if error:
print(error)
if not success:
log_error("远程部署脚本执行失败")
sys.exit(1)
log_info("✅ 远程部署完成!")
show_remote_status()
def show_remote_status():
"""显示远程服务状态"""
log_info("=== 远程服务信息 ===")
log_info(f"服务器地址: {REMOTE_HOST}")
log_info(f"部署目录: {REMOTE_DIR}")
log_info(f"日志目录: {REMOTE_LOG_DIR}")
log_info(f"Spring Profile: {SPRING_PROFILE}")
log_info("检查远程服务状态...")
success, output, _ = exec_ssh_cmd(f"ps aux | grep {REMOTE_JAR_NAME} | grep -v grep")
if output:
log_info(f"远程服务运行中:\n{output}")
else:
log_info("远程服务未运行")
def local_deploy():
"""本地部署"""
log_info(f"开始本地部署 {APP_NAME} 服务...")
# 构建项目
build_project()
# 检查JAR文件
check_jar()
# 创建日志目录
LOG_DIR.mkdir(parents=True, exist_ok=True)
# 停止旧服务
stop_local_service()
# 启动新服务
start_local_service()
# 等待启动
if wait_for_startup():
log_info("✅ 本地部署成功!")
show_local_status()
else:
log_error("本地部署失败!")
sys.exit(1)
def stop_local_service():
"""停止本地服务"""
if PID_FILE.exists():
pid = PID_FILE.read_text().strip()
success, _, _ = run_command(f"ps -p {pid}")
if success:
log_info(f"停止旧服务 (PID: {pid})")
run_command(f"kill {pid}")
# 等待服务停止
for _ in range(30):
success, _, _ = run_command(f"ps -p {pid}")
if not success:
log_info("服务已停止")
break
time.sleep(1)
else:
log_warn(f"强制停止服务 (PID: {pid})")
run_command(f"kill -9 {pid}")
else:
log_warn("PID文件存在但进程不存在,清理PID文件")
PID_FILE.unlink()
else:
log_info("没有找到PID文件,服务可能未运行")
def start_local_service():
"""启动本地服务"""
log_info("启动本地服务...")
startup_log = LOG_DIR / "startup.log"
app_log = LOG_DIR / "application.log"
cmd = (
f"nohup java {JAVA_OPTS} "
f"-Dspring.profiles.active={SPRING_PROFILE} "
f"-Dlogging.file.path={LOG_DIR} "
f"-Dlogging.file.name={app_log} "
f"-jar {JAR_PATH} "
f"> {startup_log} 2>&1 &"
)
# 使用subprocess启动后台进程
subprocess.Popen(
cmd,
shell=True,
cwd=SCRIPT_DIR,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL
)
# 获取实际的Java进程PID
time.sleep(2)
success, output, _ = run_command(f"pgrep -f {JAR_NAME}")
if success and output:
pid = output.strip().split('\n')[0]
PID_FILE.write_text(pid)
log_info(f"服务启动中,PID: {pid}")
else:
log_warn("无法获取服务PID")
log_info(f"启动日志: {startup_log}")
log_info(f"应用日志: {app_log}")
def wait_for_startup():
"""等待服务启动"""
log_info("等待服务启动...")
for _ in range(60):
# 检查端口是否监听
success, output, _ = run_command("netstat -tlnp 2>/dev/null | grep ':19089.*LISTEN'")
if success and output:
log_info("服务启动成功!")
return True
time.sleep(2)
log_error(f"服务启动超时,请检查日志: {LOG_DIR}/startup.log")
return False
def show_local_status():
"""显示本地服务状态"""
log_info("=== 本地服务信息 ===")
log_info(f"应用名称: {APP_NAME}")
log_info(f"JAR文件: {JAR_PATH}")
log_info(f"日志目录: {LOG_DIR}")
log_info(f"PID文件: {PID_FILE}")
log_info(f"Java参数: {JAVA_OPTS}")
log_info(f"Spring Profile: {SPRING_PROFILE}")
if PID_FILE.exists():
pid = PID_FILE.read_text().strip()
success, _, _ = run_command(f"ps -p {pid}")
if success:
log_info(f"服务状态: 运行中 (PID: {pid})")
else:
log_info("服务状态: 未运行")
else:
log_info("服务状态: 未运行")
def print_usage():
"""打印使用说明"""
print("""
用法: python deploy.py [命令] [参数]
命令:
deploy - 本地部署服务(默认)
remote - 远程部署到服务器(仅上传JAR)
remote [文件名] - 远程部署并上传指定文件(如 deploy-server.sh
build - 构建项目
start - 启动本地服务
stop - 停止本地服务
restart - 重启本地服务
status - 查看本地服务状态
remote-status - 查看远程服务状态
示例:
python deploy.py remote # 仅上传JAR并部署
python deploy.py remote deploy-server.sh # 同时上传部署脚本
""")
def main():
"""主函数"""
command = sys.argv[1] if len(sys.argv) > 1 else "deploy"
if command == "deploy":
local_deploy()
elif command == "remote":
# 检查是否有额外的文件参数
upload_script = sys.argv[2] if len(sys.argv) > 2 else None
deploy_to_remote(upload_script)
elif command == "build":
build_project()
elif command == "start":
build_project()
check_jar()
LOG_DIR.mkdir(parents=True, exist_ok=True)
start_local_service()
wait_for_startup()
elif command == "stop":
stop_local_service()
elif command == "restart":
stop_local_service()
time.sleep(2)
build_project()
check_jar()
LOG_DIR.mkdir(parents=True, exist_ok=True)
start_local_service()
wait_for_startup()
elif command == "status":
show_local_status()
elif command == "remote-status":
show_remote_status()
else:
print_usage()
sys.exit(1)
if __name__ == "__main__":
main()
+24 -12
View File
@@ -138,7 +138,10 @@ start_local_service() {
}
# 远程部署 - 上传文件到服务器
# 参数: $1 - 可选,指定要上传的额外文件(如 deploy-server.sh
deploy_to_remote() {
UPLOAD_SCRIPT="$2"
log_info "开始远程部署到 $REMOTE_HOST..."
# 检查并构建项目
@@ -180,19 +183,23 @@ deploy_to_remote() {
exit 1
fi
# 上传部署脚本
log_info "上传部署脚本到远程服务器..."
if ! scp "./deploy-server.sh" $REMOTE_USER@$REMOTE_HOST:$REMOTE_DIR/; then
log_error "上传部署脚本失败"
# 如果指定了额外文件,则上传
if [ -n "$UPLOAD_SCRIPT" ] && [ -f "$UPLOAD_SCRIPT" ]; then
log_info "上传指定文件到远程服务器: $UPLOAD_SCRIPT"
if ! scp "$UPLOAD_SCRIPT" $REMOTE_USER@$REMOTE_HOST:$REMOTE_DIR/; then
log_error "上传文件失败: $UPLOAD_SCRIPT"
exit 1
fi
log_info "✅ 部署脚本上传成功"
log_info "✅ 文件上传成功: $UPLOAD_SCRIPT"
# 设置权限
# 如果是脚本文件,设置执行权限
REMOTE_FILENAME=$(basename "$UPLOAD_SCRIPT")
if [[ "$REMOTE_FILENAME" == *.sh ]]; then
log_info "设置远程脚本权限..."
if ! ssh $REMOTE_USER@$REMOTE_HOST "chmod +x $REMOTE_DIR/deploy-server.sh"; then
log_error "设置脚本权限失败"
exit 1
ssh $REMOTE_USER@$REMOTE_HOST "chmod +x $REMOTE_DIR/$REMOTE_FILENAME"
fi
else
log_info "跳过部署脚本上传(服务器已存在)"
fi
# 在远程服务器上执行部署
@@ -313,7 +320,7 @@ case "${1:-deploy}" in
local_deploy
;;
"remote")
deploy_to_remote
deploy_to_remote "$@"
;;
"build")
build_project
@@ -353,9 +360,10 @@ case "${1:-deploy}" in
fi
;;
*)
echo "用法: $0 {deploy|remote|build|start|stop|restart|status|remote-status|logs}"
echo "用法: $0 {deploy|remote [文件名]|build|start|stop|restart|status|remote-status|logs}"
echo " deploy - 本地部署服务(默认)"
echo " remote - 远程部署到服务器"
echo " remote - 远程部署到服务器(仅上传JAR"
echo " remote [文件名] - 远程部署并上传指定文件(如 deploy-server.sh"
echo " build - 构建项目"
echo " start - 启动本地服务"
echo " stop - 停止本地服务"
@@ -363,6 +371,10 @@ case "${1:-deploy}" in
echo " status - 查看本地服务状态"
echo " remote-status - 查看远程服务状态"
echo " logs - 查看本地实时日志"
echo ""
echo "示例:"
echo " $0 remote # 仅上传JAR并部署"
echo " $0 remote deploy-server.sh # 同时上传部署脚本"
exit 1
;;
esac
@@ -0,0 +1,105 @@
package com.emotion.controller;
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.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 字典Controller
*
* @author huazhongmin
* @date 2025-12-22
*/
@RestController
@RequestMapping("/dictionary")
public class DictionaryController {
@Autowired
private DictionaryService dictionaryService;
/**
* 创建字典
*
* @param request 创建请求
* @return 创建结果
*/
@PostMapping
public Result<DictionaryResponse> createDictionary(@Validated @RequestBody DictionaryCreateRequest request) {
return dictionaryService.createDictionary(request);
}
/**
* 更新字典
*
* @param request 更新请求
* @return 更新结果
*/
@PutMapping
public Result<DictionaryResponse> updateDictionary(@Validated @RequestBody DictionaryUpdateRequest request) {
return dictionaryService.updateDictionary(request);
}
/**
* 删除字典
*
* @param id 字典ID
* @return 删除结果
*/
@DeleteMapping("/delete")
public Result<Void> deleteDictionary(@RequestParam String id) {
return dictionaryService.deleteDictionary(id);
}
/**
* 获取字典详情
*
* @param id 字典ID
* @return 字典详情
*/
@GetMapping("/detail")
public Result<DictionaryResponse> getDictionary(@RequestParam String id) {
return dictionaryService.getDictionary(id);
}
/**
* 分页查询字典
*
* @param request 分页请求
* @return 分页结果
*/
@GetMapping("/list")
public Result<PageResult<DictionaryResponse>> listDictionaries(DictionaryPageRequest request) {
return dictionaryService.listDictionaries(request);
}
/**
* 根据字典类型查询字典集合
*
* @param dictType 字典类型
* @return 字典集合
*/
@GetMapping("/byType")
public Result<List<DictionaryResponse>> getDictionariesByType(@RequestParam String dictType) {
return dictionaryService.getDictionariesByType(dictType);
}
/**
* 根据字典类型查询启用的字典集合
*
* @param dictType 字典类型
* @return 启用的字典集合
*/
@GetMapping("/enabledByType")
public Result<List<DictionaryResponse>> getEnabledDictionariesByType(@RequestParam String dictType) {
return dictionaryService.getEnabledDictionariesByType(dictType);
}
}
@@ -0,0 +1,54 @@
package com.emotion.dto.request.dictionary;
import lombok.Data;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
/**
* 创建字典请求
*
* @author huazhongmin
* @date 2025-12-22
*/
@Data
public class DictionaryCreateRequest {
/**
* 字典类型
*/
@NotBlank(message = "字典类型不能为空")
private String dictType;
/**
* 字典编码
*/
@NotBlank(message = "字典编码不能为空")
private String dictCode;
/**
* 字典名称
*/
@NotBlank(message = "字典名称不能为空")
private String dictName;
/**
* 字典值
*/
private String dictValue;
/**
* 排序顺序
*/
private Integer sortOrder;
/**
* 状态: 0-禁用, 1-启用
*/
@NotNull(message = "状态不能为空")
private Integer status;
/**
* 备注
*/
private String remarks;
}
@@ -0,0 +1,36 @@
package com.emotion.dto.request.dictionary;
import com.emotion.common.BasePageRequest;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 字典分页请求
*
* @author huazhongmin
* @date 2025-12-22
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class DictionaryPageRequest extends BasePageRequest {
/**
* 字典类型
*/
private String dictType;
/**
* 字典编码
*/
private String dictCode;
/**
* 字典名称
*/
private String dictName;
/**
* 状态: 0-禁用, 1-启用
*/
private Integer status;
}
@@ -0,0 +1,60 @@
package com.emotion.dto.request.dictionary;
import lombok.Data;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
/**
* 更新字典请求
*
* @author huazhongmin
* @date 2025-12-22
*/
@Data
public class DictionaryUpdateRequest {
/**
* 字典ID
*/
@NotBlank(message = "字典ID不能为空")
private String id;
/**
* 字典类型
*/
@NotBlank(message = "字典类型不能为空")
private String dictType;
/**
* 字典编码
*/
@NotBlank(message = "字典编码不能为空")
private String dictCode;
/**
* 字典名称
*/
@NotBlank(message = "字典名称不能为空")
private String dictName;
/**
* 字典值
*/
private String dictValue;
/**
* 排序顺序
*/
private Integer sortOrder;
/**
* 状态: 0-禁用, 1-启用
*/
@NotNull(message = "状态不能为空")
private Integer status;
/**
* 备注
*/
private String remarks;
}
@@ -29,6 +29,11 @@ public class UserProfileCreateRequest {
*/
private String zodiac;
/**
* 职业
*/
private String profession;
/**
* MBTI人格类型
*/
@@ -34,6 +34,11 @@ public class UserProfileUpdateRequest {
*/
private String zodiac;
/**
* 职业
*/
private String profession;
/**
* MBTI人格类型
*/
@@ -0,0 +1,61 @@
package com.emotion.dto.response.dictionary;
import lombok.Data;
import com.emotion.dto.response.BaseResponse;
import lombok.EqualsAndHashCode;
/**
* 字典响应
*
* @author huazhongmin
* @date 2025-12-22
*/
@Data
@EqualsAndHashCode(callSuper = false)
public class DictionaryResponse extends BaseResponse {
/**
* 字典类型
*/
private String dictType;
/**
* 字典编码
*/
private String dictCode;
/**
* 字典名称
*/
private String dictName;
/**
* 字典值
*/
private String dictValue;
/**
* 排序顺序
*/
private Integer sortOrder;
/**
* 状态: 0-禁用, 1-启用
*/
private Integer status;
/**
* 创建人ID
*/
private String createBy;
/**
* 更新人ID
*/
private String updateBy;
/**
* 备注
*/
private String remarks;
}
@@ -46,6 +46,11 @@ public class UserProfileResponse {
*/
private String zodiac;
/**
* 职业
*/
private String profession;
/**
* MBTI人格类型
*/
@@ -0,0 +1,61 @@
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;
/**
* 字典实体类
*
* @author huazhongmin
* @date 2025-12-22
*/
@Data
@EqualsAndHashCode(callSuper = true)
@SuperBuilder
@NoArgsConstructor
@AllArgsConstructor
@TableName("t_dictionary")
public class Dictionary extends BaseEntity {
/**
* 字典类型 (如: city, constellation, mbti)
*/
@TableField("dict_type")
private String dictType;
/**
* 字典编码
*/
@TableField("dict_code")
private String dictCode;
/**
* 字典名称
*/
@TableField("dict_name")
private String dictName;
/**
* 字典值
*/
@TableField("dict_value")
private String dictValue;
/**
* 排序顺序
*/
@TableField("sort_order")
private Integer sortOrder;
/**
* 状态: 0-禁用, 1-启用
*/
@TableField("status")
private Integer status;
}
@@ -49,6 +49,12 @@ public class UserProfile extends BaseEntity {
@TableField("zodiac")
private String zodiac;
/**
* 职业
*/
@TableField("profession")
private String profession;
/**
* MBTI人格类型
*/
@@ -0,0 +1,32 @@
package com.emotion.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.emotion.entity.Dictionary;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* 字典Mapper接口
*
* @author huazhongmin
* @date 2025-12-22
*/
public interface DictionaryMapper extends BaseMapper<Dictionary> {
/**
* 根据字典类型查询字典集合
*
* @param dictType 字典类型
* @return 字典集合
*/
List<Dictionary> selectByDictType(@Param("dictType") String dictType);
/**
* 根据字典类型和状态查询字典集合
*
* @param dictType 字典类型
* @param status 状态
* @return 字典集合
*/
List<Dictionary> selectByDictTypeAndStatus(@Param("dictType") String dictType, @Param("status") Integer status);
}
@@ -0,0 +1,77 @@
package com.emotion.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.emotion.entity.Dictionary;
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 java.util.List;
/**
* 字典Service接口
*
* @author huazhongmin
* @date 2025-12-22
*/
public interface DictionaryService extends IService<Dictionary> {
/**
* 创建字典
*
* @param request 创建请求
* @return 创建结果
*/
Result<DictionaryResponse> createDictionary(DictionaryCreateRequest request);
/**
* 更新字典
*
* @param request 更新请求
* @return 更新结果
*/
Result<DictionaryResponse> updateDictionary(DictionaryUpdateRequest request);
/**
* 删除字典
*
* @param id 字典ID
* @return 删除结果
*/
Result<Void> deleteDictionary(String id);
/**
* 获取字典详情
*
* @param id 字典ID
* @return 字典详情
*/
Result<DictionaryResponse> getDictionary(String id);
/**
* 分页查询字典
*
* @param request 分页请求
* @return 分页结果
*/
Result<PageResult<DictionaryResponse>> listDictionaries(DictionaryPageRequest request);
/**
* 根据字典类型查询字典集合
*
* @param dictType 字典类型
* @return 字典集合
*/
Result<List<DictionaryResponse>> getDictionariesByType(String dictType);
/**
* 根据字典类型查询启用的字典集合
*
* @param dictType 字典类型
* @return 启用的字典集合
*/
Result<List<DictionaryResponse>> getEnabledDictionariesByType(String dictType);
}
@@ -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;
}
}
@@ -71,21 +71,37 @@ public class UserProfileServiceImpl extends ServiceImpl<UserProfileMapper, UserP
public UserProfileResponse updateProfile(UserProfileUpdateRequest request) {
log.info("Updating user profile: {}", request);
UserProfile userProfile = getById(request.getId());
if (userProfile == null) {
throw new RuntimeException("档案不存在");
}
// 权限校验:只能修改自己的档案
// 获取当前登录用户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)) {
// 管理员可以修改任意档案 (此处假设没有管理员逻辑,严格按需求: 只能修改自己的)
// 如果需要管理员权限,需配合 SecurityUtils 判断角色
// throw new RuntimeException("无权修改他人档案");
// 暂时允许用户修改自己的档案
throw new RuntimeException("无权修改他人档案");
}
// 使用Hutool的BeanUtil进行部分更新,忽略null值
+1 -1
View File
@@ -1,2 +1,2 @@
# 开发环境配置
VITE_API_BASE_URL=http://localhost:8080
VITE_API_BASE_URL=http://localhost:19089/api
+231
View File
@@ -0,0 +1,231 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
life-script 部署脚本
功能:项目构建、文件传输、原子切换、历史版本管理、回滚支持
使用系统自带的ssh/scp命令,无需额外依赖
"""
import os
import sys
import subprocess
import shutil
from datetime import datetime
from pathlib import Path
# 服务器配置
SERVER_IP = "101.200.208.45"
USERNAME = "root"
# 部署配置
APP_NAME = "life-script"
DEPLOY_BASE = "/data/www/course-web-deploy"
RELEASES_DIR = f"{DEPLOY_BASE}/releases"
LINK_PATH = "/data/www/course-of-life"
MAX_RELEASES = 5
# 本地路径
SCRIPT_DIR = Path(__file__).parent.absolute()
DIST_DIR = SCRIPT_DIR / "dist"
class Colors:
"""终端颜色"""
GREEN = '\033[32m'
RED = '\033[31m'
YELLOW = '\033[33m'
RESET = '\033[0m'
def log_info(msg):
"""打印信息日志"""
print(f"{Colors.GREEN}[INFO]{Colors.RESET} {msg}")
def log_error(msg):
"""打印错误日志"""
print(f"{Colors.RED}[ERROR]{Colors.RESET} {msg}")
def log_warn(msg):
"""打印警告日志"""
print(f"{Colors.YELLOW}[WARN]{Colors.RESET} {msg}")
def run_command(cmd, cwd=None, shell=True, capture=True):
"""执行本地命令"""
try:
if capture:
result = subprocess.run(
cmd,
cwd=cwd,
shell=shell,
capture_output=True,
text=True
)
return result.returncode == 0, result.stdout, result.stderr
else:
result = subprocess.run(cmd, cwd=cwd, shell=shell)
return result.returncode == 0, "", ""
except Exception as e:
return False, "", str(e)
def exec_ssh_cmd(cmd):
"""通过SSH执行远程命令"""
ssh_cmd = f'ssh {USERNAME}@{SERVER_IP} "{cmd}"'
return run_command(ssh_cmd)
def scp_upload(local_path, remote_path, recursive=False):
"""通过SCP上传文件或目录"""
r_flag = "-r" if recursive else ""
scp_cmd = f'scp {r_flag} "{local_path}" {USERNAME}@{SERVER_IP}:{remote_path}'
success, stdout, stderr = run_command(scp_cmd)
if not success:
log_error(f"SCP上传失败: {stderr}")
return success
def check_env():
"""检查本地环境"""
log_info("检查本地环境...")
# 检查npm
success, _, _ = run_command("npm --version")
if not success:
log_error("未找到npm命令,请先安装Node.js")
sys.exit(1)
log_info("环境检查通过")
def build_project():
"""构建项目"""
log_info("开始构建项目...")
# 切换到项目目录
os.chdir(SCRIPT_DIR)
# 清理旧构建
if DIST_DIR.exists():
shutil.rmtree(DIST_DIR)
log_info("已清理旧的dist目录")
# 执行构建(不捕获输出,直接显示)
log_info("执行: npm run build")
success, _, _ = run_command("npm run build", capture=False)
if not success:
log_error("项目构建失败")
sys.exit(1)
log_info("项目构建成功")
# 检查dist目录
if not DIST_DIR.exists():
log_error("dist目录不存在")
sys.exit(1)
def deploy():
"""部署到服务器"""
timestamp = datetime.now().strftime("%Y%m%d%H%M%S")
release_path = f"{RELEASES_DIR}/{timestamp}"
log_info(f"准备部署版本: {timestamp}")
# 1. 创建远程目录
log_info("创建远程目录...")
exec_ssh_cmd(f"mkdir -p {release_path}")
# 2. 上传文件
log_info("上传文件到服务器...")
for item in DIST_DIR.iterdir():
if item.is_file():
if not scp_upload(item, f"{release_path}/"):
log_error("文件上传失败")
sys.exit(1)
else:
if not scp_upload(item, f"{release_path}/", recursive=True):
log_error("目录上传失败")
sys.exit(1)
log_info("文件上传成功")
# 3. 设置权限
log_info("设置文件权限...")
exec_ssh_cmd(f"chmod -R 755 {release_path}")
# 4. 原子切换软链接
log_info("切换服务版本...")
# 检查目标路径是否为普通目录
exec_ssh_cmd(f"if [ -d '{LINK_PATH}' ] && [ ! -L '{LINK_PATH}' ]; then mv '{LINK_PATH}' '{LINK_PATH}_backup_$(date +%s)'; fi")
# 创建软链接
exec_ssh_cmd(f"ln -snf '{release_path}' '{LINK_PATH}'")
log_info(f"部署完成!当前版本指向: {release_path}")
# 5. 清理旧版本
clean_old_releases()
def clean_old_releases():
"""清理旧版本,只保留最近的N个"""
log_info(f"清理旧版本(保留最近{MAX_RELEASES}个)...")
clean_cmd = f"cd {RELEASES_DIR} && ls -t | tail -n +{MAX_RELEASES + 1} | xargs -I {{}} rm -rf {{}}"
exec_ssh_cmd(clean_cmd)
def rollback():
"""回滚到上一个版本"""
log_info("开始回滚操作...")
# 获取当前指向的版本
success, current_link, _ = exec_ssh_cmd(f"readlink {LINK_PATH}")
log_info(f"当前版本: {current_link}")
# 获取上一个版本目录
success, prev_version, _ = exec_ssh_cmd(
f"ls -dt {RELEASES_DIR}/* | grep -v '{current_link}' | head -n 1"
)
if not prev_version:
log_error("没有找到可回滚的历史版本")
sys.exit(1)
log_info(f"回滚目标版本: {prev_version}")
exec_ssh_cmd(f"ln -snf {prev_version} {LINK_PATH}")
log_info("回滚成功!")
def print_usage():
"""打印使用说明"""
print("""
用法: python deploy.py [命令]
命令:
deploy - 构建并部署到服务器(默认)
rollback - 回滚到上一个版本
示例:
python deploy.py # 部署
python deploy.py rollback # 回滚
""")
def main():
"""主函数"""
command = sys.argv[1] if len(sys.argv) > 1 else "deploy"
if command == "rollback":
rollback()
elif command == "deploy":
check_env()
build_project()
deploy()
else:
print_usage()
sys.exit(1)
if __name__ == "__main__":
main()
+133
View File
@@ -0,0 +1,133 @@
#!/bin/bash
# course-web 部署脚本
# 功能:项目构建、文件传输、原子切换、历史版本管理、回滚支持
SERVER_IP="101.200.208.45"
USERNAME="root"
APP_NAME="course-web"
DEPLOY_BASE="/data/www/course-web-deploy"
RELEASES_DIR="${DEPLOY_BASE}/releases"
LINK_PATH="/data/www/course-of-life"
MAX_RELEASES=5
# 打印带颜色的信息
function log_info() {
echo -e "\033[32m[INFO] $1\033[0m"
}
function log_error() {
echo -e "\033[31m[ERROR] $1\033[0m"
}
# 检查环境
function check_env() {
log_info "检查本地环境..."
if ! command -v npm &> /dev/null; then
log_error "未找到npm命令,请先安装Node.js"
exit 1
fi
if ! command -v scp &> /dev/null; then
log_error "未找到scp命令"
exit 1
fi
}
# 构建项目
function build_project() {
log_info "开始构建项目..."
# 清理旧构建
rm -rf dist
# 安装依赖并构建
# npm install # 视情况开启,为了速度暂时注释,假设依赖已安装
if npm run build; then
log_info "项目构建成功"
else
log_error "项目构建失败"
exit 1
fi
if [ ! -d "dist" ]; then
log_error "dist目录不存在"
exit 1
fi
}
# 部署到服务器
function deploy() {
TIMESTAMP=$(date +%Y%m%d%H%M%S)
RELEASE_PATH="${RELEASES_DIR}/${TIMESTAMP}"
log_info "准备部署版本: ${TIMESTAMP}"
# 1. 创建远程目录结构
ssh "${USERNAME}@${SERVER_IP}" "mkdir -p ${RELEASE_PATH}"
# 2. 上传文件
log_info "上传文件到服务器..."
if scp -r dist/* "${USERNAME}@${SERVER_IP}:${RELEASE_PATH}/"; then
log_info "文件上传成功"
else
log_error "文件上传失败"
exit 1
fi
# 3. 设置权限
ssh "${USERNAME}@${SERVER_IP}" "chmod -R 755 ${RELEASE_PATH}"
# 4. 原子切换软链接
log_info "切换服务版本..."
# 检查目标路径是否为普通目录(非软链接),如果是则备份并移除,防止ln失败
ssh "${USERNAME}@${SERVER_IP}" "
if [ -d '${LINK_PATH}' ] && [ ! -L '${LINK_PATH}' ]; then
echo '检测到目标路径为普通目录,进行备份...'
mv '${LINK_PATH}' '${LINK_PATH}_backup_$(date +%s)'
fi
ln -snf '${RELEASE_PATH}' '${LINK_PATH}'
"
log_info "部署完成!当前版本指向: ${RELEASE_PATH}"
# 5. 清理旧版本
clean_old_releases
}
# 清理旧版本,只保留最近的N个
function clean_old_releases() {
log_info "清理旧版本(保留最近${MAX_RELEASES}个)..."
ssh "${USERNAME}@${SERVER_IP}" "
cd ${RELEASES_DIR} && ls -t | tail -n +$((${MAX_RELEASES} + 1)) | xargs -I {} rm -rf {}
"
}
# 回滚到上一个版本
function rollback() {
log_info "开始回滚操作..."
# 获取当前指向的版本
CURRENT_LINK=$(ssh "${USERNAME}@${SERVER_IP}" "readlink ${LINK_PATH}")
log_info "当前版本: ${CURRENT_LINK}"
# 获取上一个版本目录
PREV_VERSION=$(ssh "${USERNAME}@${SERVER_IP}" "ls -dt ${RELEASES_DIR}/* | grep -v '${CURRENT_LINK}' | head -n 1")
if [ -z "$PREV_VERSION" ]; then
log_error "没有找到可回滚的历史版本"
exit 1
fi
log_info "回滚目标版本: ${PREV_VERSION}"
ssh "${USERNAME}@${SERVER_IP}" "ln -snf ${PREV_VERSION} ${LINK_PATH}"
log_info "回滚成功!"
}
# 主流程
case "$1" in
"rollback")
rollback
;;
*)
check_env
build_project
deploy
;;
esac
+4 -1
View File
@@ -124,8 +124,11 @@ const AnimatedRoutes = () => {
* App 主组件
*/
function App() {
// 生产环境使用 /course-of-life 作为基础路径
const basename = import.meta.env.PROD ? '/course-of-life' : '';
return (
<BrowserRouter>
<BrowserRouter basename={basename}>
{/* 动态背景 */}
<Background />
+42 -8
View File
@@ -1,10 +1,11 @@
import { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { ArrowRight, Check } from 'lucide-react';
import { GlassCard, GlassInput, GlassTextarea, GlassButton } from '../components/ui';
import { GlassCard, GlassInput, GlassTextarea, GlassButton, GlassSelect } from '../components/ui';
import { PromptTagGroup } from '../components/PromptTag';
import useStore from '../store/useStore';
import { inspirationClusters } from '../utils/constants';
import * as dictionaryService from '../services/dictionary';
/**
* OnboardingPage 组件
@@ -25,10 +26,38 @@ const OnboardingPage = () => {
const [formData, setFormData] = useState(registrationData);
const [isSaving, setIsSaving] = useState(false);
// 字典数据状态
const [zodiacOptions, setZodiacOptions] = useState([]);
const [mbtiOptions, setMbtiOptions] = useState([]);
const [genderOptions, setGenderOptions] = useState([]);
useEffect(() => {
setFormData(registrationData);
}, [registrationData]);
// 加载字典数据
useEffect(() => {
const loadDictionaries = async () => {
try {
const [zodiacList, mbtiList, genderList] = await Promise.all([
dictionaryService.getZodiacList(),
dictionaryService.getMbtiList(),
dictionaryService.getGenderList()
]);
setZodiacOptions([{ value: '', label: '请选择星座' }, ...dictionaryService.transformToOptions(zodiacList)]);
setMbtiOptions([{ value: '', label: '请选择MBTI' }, ...dictionaryService.transformToOptions(mbtiList)]);
setGenderOptions([{ value: '', label: '请选择性别' }, ...dictionaryService.transformToOptions(genderList)]);
} catch (error) {
console.error('加载字典数据失败:', error);
// 使用默认选项
setZodiacOptions([{ value: '', label: '请选择星座' }]);
setMbtiOptions([{ value: '', label: '请选择MBTI' }]);
setGenderOptions([{ value: '', label: '请选择性别' }]);
}
};
loadDictionaries();
}, []);
const updateField = (field, value) => {
setFormData(prev => ({ ...prev, [field]: value }));
};
@@ -46,7 +75,12 @@ const OnboardingPage = () => {
};
const saveStepData = () => {
updateRegistration(formData);
// 保存时将兴趣爱好字符串转换为数组
const dataToSave = { ...formData };
if (typeof dataToSave.hobbies === 'string') {
dataToSave.hobbies = dataToSave.hobbies.split(/[,]/).map(s => s.trim()).filter(s => s);
}
updateRegistration(dataToSave);
};
const handleNext = async () => {
@@ -82,15 +116,15 @@ const OnboardingPage = () => {
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-x-6 gap-y-2">
<GlassInput label="称呼" placeholder="例如:林中鹿" value={formData.nickname} onChange={(v) => updateField('nickname', v)} />
<GlassInput label="性别" placeholder="自由填写" value={formData.gender} onChange={(v) => updateField('gender', v)} />
<GlassInput label="MBTI" placeholder="如:INFJ" value={formData.mbti} onChange={(v) => updateField('mbti', v)} />
<GlassInput label="星座" placeholder="星辰指引" value={formData.zodiac} onChange={(v) => updateField('zodiac', v)} />
<GlassSelect label="性别" options={genderOptions} value={formData.gender} onChange={(v) => updateField('gender', v)} />
<GlassSelect label="MBTI" options={mbtiOptions} value={formData.mbti} onChange={(v) => updateField('mbti', v)} />
<GlassSelect label="星座" options={zodiacOptions} value={formData.zodiac} onChange={(v) => updateField('zodiac', v)} />
</div>
<GlassInput
label="兴趣爱好"
placeholder="用逗号分隔你的热爱"
value={Array.isArray(formData.hobbies) ? formData.hobbies.join(',') : formData.hobbies}
onChange={(v) => updateField('hobbies', v.split(',').map(s => s.trim()).filter(s => s))}
placeholder="用逗号分隔你的热爱,如:阅读,旅行,摄影"
value={Array.isArray(formData.hobbies) ? formData.hobbies.join('') : (formData.hobbies || '')}
onChange={(v) => updateField('hobbies', v)}
/>
</div>
);
+63
View File
@@ -0,0 +1,63 @@
import api from './api';
/**
* 字典服务
* 处理字典数据的获取
*/
/**
* 根据字典类型获取启用的字典列表
* @param {string} dictType - 字典类型
* @returns {Promise<Array>} 字典列表
*/
export const getEnabledDictionariesByType = async (dictType) => {
const response = await api.get('/dictionary/enabledByType', {
params: { dictType }
});
return response.data || [];
};
/**
* 获取星座字典
* @returns {Promise<Array>} 星座列表
*/
export const getZodiacList = async () => {
return getEnabledDictionariesByType('constellation');
};
/**
* 获取MBTI字典
* @returns {Promise<Array>} MBTI列表
*/
export const getMbtiList = async () => {
return getEnabledDictionariesByType('mbti');
};
/**
* 获取性别字典
* @returns {Promise<Array>} 性别列表
*/
export const getGenderList = async () => {
return getEnabledDictionariesByType('gender');
};
/**
* 将字典数据转换为下拉选项格式
* @param {Array} dictList - 字典列表
* @returns {Array<{value: string, label: string}>} 选项列表
*/
export const transformToOptions = (dictList) => {
if (!Array.isArray(dictList)) return [];
return dictList.map(item => ({
value: item.dictValue || item.dictCode || '',
label: item.dictName || item.dictValue || ''
}));
};
export default {
getEnabledDictionariesByType,
getZodiacList,
getMbtiList,
getGenderList,
transformToOptions
};
+4
View File
@@ -72,6 +72,7 @@ const transformToBackendFormat = (frontendData) => {
nickname,
gender,
zodiac,
profession,
mbti,
hobbies,
childhood,
@@ -85,6 +86,7 @@ const transformToBackendFormat = (frontendData) => {
nickname,
gender,
zodiac,
profession,
mbti,
// 兴趣爱好转为 JSON 字符串
hobbies: Array.isArray(hobbies) ? JSON.stringify(hobbies) : hobbies,
@@ -118,6 +120,7 @@ export const transformToFrontendFormat = (backendData) => {
nickname,
gender,
zodiac,
profession,
mbti,
hobbies,
childhoodDate,
@@ -136,6 +139,7 @@ export const transformToFrontendFormat = (backendData) => {
nickname: nickname || '',
gender: gender || '',
zodiac: zodiac || '',
profession: profession || '',
mbti: mbti || '',
// 兴趣爱好从 JSON 字符串解析
hobbies: hobbies ? (typeof hobbies === 'string' ? JSON.parse(hobbies) : hobbies) : [],
+50 -10
View File
@@ -1,8 +1,9 @@
import { useState, useEffect } from 'react';
import { Settings, Settings2 } from 'lucide-react';
import Modal from '../components/Modal';
import { GlassButton, GlassInput } from '../components/ui';
import { GlassButton, GlassInput, GlassSelect } from '../components/ui';
import useStore from '../store/useStore';
import * as dictionaryService from '../services/dictionary';
/**
* ProfileModal 组件
@@ -18,13 +19,19 @@ const ProfileModal = ({ isOpen, onClose }) => {
const [isEditing, setIsEditing] = useState(false);
const [isSaving, setIsSaving] = useState(false);
// 字典数据状态
const [zodiacOptions, setZodiacOptions] = useState([]);
const [mbtiOptions, setMbtiOptions] = useState([]);
const [genderOptions, setGenderOptions] = useState([]);
// 编辑表单状态
const [editForm, setEditForm] = useState({
nickname: registrationData.nickname,
profession: registrationData.profession || '',
gender: registrationData.gender || '',
mbti: registrationData.mbti,
zodiac: registrationData.zodiac,
hobbies: registrationData.hobbies?.join(', ') || ''
hobbies: registrationData.hobbies?.join('') || ''
});
// 同步 registrationData 到 editForm
@@ -32,12 +39,37 @@ const ProfileModal = ({ isOpen, onClose }) => {
setEditForm({
nickname: registrationData.nickname,
profession: registrationData.profession || '',
gender: registrationData.gender || '',
mbti: registrationData.mbti,
zodiac: registrationData.zodiac,
hobbies: registrationData.hobbies?.join(', ') || ''
hobbies: registrationData.hobbies?.join('') || ''
});
}, [registrationData]);
// 加载字典数据
useEffect(() => {
const loadDictionaries = async () => {
try {
const [zodiacList, mbtiList, genderList] = await Promise.all([
dictionaryService.getZodiacList(),
dictionaryService.getMbtiList(),
dictionaryService.getGenderList()
]);
setZodiacOptions([{ value: '', label: '请选择星座' }, ...dictionaryService.transformToOptions(zodiacList)]);
setMbtiOptions([{ value: '', label: '请选择MBTI' }, ...dictionaryService.transformToOptions(mbtiList)]);
setGenderOptions([{ value: '', label: '请选择性别' }, ...dictionaryService.transformToOptions(genderList)]);
} catch (error) {
console.error('加载字典数据失败:', error);
setZodiacOptions([{ value: '', label: '请选择星座' }]);
setMbtiOptions([{ value: '', label: '请选择MBTI' }]);
setGenderOptions([{ value: '', label: '请选择性别' }]);
}
};
if (isOpen) {
loadDictionaries();
}
}, [isOpen]);
/**
* 处理保存
*/
@@ -47,9 +79,10 @@ const ProfileModal = ({ isOpen, onClose }) => {
updateRegistration({
nickname: editForm.nickname,
profession: editForm.profession,
gender: editForm.gender,
mbti: editForm.mbti,
zodiac: editForm.zodiac,
hobbies: editForm.hobbies.split(',').map(s => s.trim()).filter(s => s)
hobbies: editForm.hobbies.split(/[,]/).map(s => s.trim()).filter(s => s)
});
await saveUserProfile();
setIsEditing(false);
@@ -69,9 +102,10 @@ const ProfileModal = ({ isOpen, onClose }) => {
setEditForm({
nickname: registrationData.nickname,
profession: registrationData.profession || '',
gender: registrationData.gender || '',
mbti: registrationData.mbti,
zodiac: registrationData.zodiac,
hobbies: registrationData.hobbies?.join(', ') || ''
hobbies: registrationData.hobbies?.join('') || ''
});
setIsEditing(false);
};
@@ -165,22 +199,28 @@ const ProfileModal = ({ isOpen, onClose }) => {
value={editForm.profession}
onChange={(v) => setEditForm(prev => ({ ...prev, profession: v }))}
/>
<GlassInput
<GlassSelect
label="性别"
options={genderOptions}
value={editForm.gender}
onChange={(v) => setEditForm(prev => ({ ...prev, gender: v }))}
/>
<GlassSelect
label="MBTI"
placeholder="性格色彩"
options={mbtiOptions}
value={editForm.mbti}
onChange={(v) => setEditForm(prev => ({ ...prev, mbti: v }))}
/>
<GlassInput
<GlassSelect
label="星座"
placeholder="星辰指引"
options={zodiacOptions}
value={editForm.zodiac}
onChange={(v) => setEditForm(prev => ({ ...prev, zodiac: v }))}
/>
</div>
<GlassInput
label="兴趣爱好"
placeholder="让灵魂起舞的事物"
placeholder="让灵魂起舞的事物,用逗号分隔"
value={editForm.hobbies}
onChange={(v) => setEditForm(prev => ({ ...prev, hobbies: v }))}
/>
+2
View File
@@ -5,6 +5,8 @@ import tailwindcss from '@tailwindcss/vite'
// https://vite.dev/config/
export default defineConfig({
plugins: [react(), tailwindcss()],
// 生产环境使用 /course-of-life/ 路径
base: '/course-of-life/',
server: {
port: 3000
}
+32 -1
View File
@@ -1189,6 +1189,7 @@ CREATE TABLE t_user_profile (
user_id VARCHAR(64) COMMENT '用户ID (关联t_user.id)',
nickname VARCHAR(50) NOT NULL COMMENT '昵称 (必填)',
gender VARCHAR(20) DEFAULT 'secret' COMMENT '性别',
profession VARCHAR(100) COMMENT '职业',
zodiac VARCHAR(20) COMMENT '星座',
mbti VARCHAR(20) NOT NULL COMMENT 'MBTI人格类型 (必填)',
hobbies JSON COMMENT '兴趣爱好列表',
@@ -1384,6 +1385,36 @@ CREATE INDEX idx_life_path_step_order ON t_life_path_step (path_id, step_order);
CREATE INDEX idx_life_path_step_status ON t_life_path_step (status);
CREATE INDEX idx_life_path_step_is_deleted ON t_life_path_step (is_deleted);
-- ============================================================================
-- 24. 字典表 (t_dictionary)
-- 用途:存储基础字段数据,如城市、星座、MBTI人格类型等
-- ============================================================================
DROP TABLE IF EXISTS t_dictionary;
CREATE TABLE t_dictionary (
id VARCHAR(64) PRIMARY KEY COMMENT 'UUID主键', -- UUID主键
dict_type VARCHAR(50) NOT NULL COMMENT '字典类型 (如: city, constellation, mbti)', -- 字典类型
dict_code VARCHAR(100) NOT NULL COMMENT '字典编码', -- 字典编码
dict_name VARCHAR(100) NOT NULL COMMENT '字典名称', -- 字典名称
dict_value VARCHAR(200) COMMENT '字典值', -- 字典值
sort_order INT DEFAULT 0 COMMENT '排序顺序', -- 排序顺序
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_dictionary)';
-- 提交事务
-- t_dictionary表索引
CREATE INDEX idx_dictionary_dict_type ON t_dictionary (dict_type);
CREATE INDEX idx_dictionary_dict_code ON t_dictionary (dict_code);
CREATE INDEX idx_dictionary_dict_type_status ON t_dictionary (dict_type, status);
CREATE INDEX idx_dictionary_dict_type_sort_order ON t_dictionary (dict_type, sort_order);
CREATE INDEX idx_dictionary_create_time ON t_dictionary (create_time);
CREATE INDEX idx_dictionary_is_deleted ON t_dictionary (is_deleted);
-- 鎻愪氦浜嬪姟
COMMIT;
+8
View File
@@ -0,0 +1,8 @@
-- 理想生活状态
alter table emotion_museum.t_user_profile
add ideal_life varchar(100) null comment '理想生活状态';
-- 职业
alter table emotion_museum.t_user_profile
add profession varchar(100) null comment '职业';
+73
View File
@@ -0,0 +1,73 @@
-- ============================================================================
-- 初始化字典数据
-- 包括:星座数据、MBTI人格类型数据
-- ============================================================================
-- 使用emotion_museum数据库
USE emotion_museum;
-- 设置时间格式
SET time_zone = '+00:00';
-- ============================================================================
-- 1. 星座数据初始化
-- ============================================================================
INSERT INTO t_dictionary (
id, dict_type, dict_code, dict_name, dict_value, sort_order, status,
create_by, create_time, update_by, update_time, is_deleted, remarks
) VALUES
(REPLACE(UUID(), '-', ''), 'constellation', 'aries', '白羊座', '白羊座', 1, 1, 'system', NOW(), 'system', NOW(), 0, '白羊座 (3月21日-4月19日)'),
(REPLACE(UUID(), '-', ''), 'constellation', 'taurus', '金牛座', '金牛座', 2, 1, 'system', NOW(), 'system', NOW(), 0, '金牛座 (4月20日-5月20日)'),
(REPLACE(UUID(), '-', ''), 'constellation', 'gemini', '双子座', '双子座', 3, 1, 'system', NOW(), 'system', NOW(), 0, '双子座 (5月21日-6月21日)'),
(REPLACE(UUID(), '-', ''), 'constellation', 'cancer', '巨蟹座', '巨蟹座', 4, 1, 'system', NOW(), 'system', NOW(), 0, '巨蟹座 (6月22日-7月22日)'),
(REPLACE(UUID(), '-', ''), 'constellation', 'leo', '狮子座', '狮子座', 5, 1, 'system', NOW(), 'system', NOW(), 0, '狮子座 (7月23日-8月22日)'),
(REPLACE(UUID(), '-', ''), 'constellation', 'virgo', '处女座', '处女座', 6, 1, 'system', NOW(), 'system', NOW(), 0, '处女座 (8月23日-9月22日)'),
(REPLACE(UUID(), '-', ''), 'constellation', 'libra', '天秤座', '天秤座', 7, 1, 'system', NOW(), 'system', NOW(), 0, '天秤座 (9月23日-10月23日)'),
(REPLACE(UUID(), '-', ''), 'constellation', 'scorpio', '天蝎座', '天蝎座', 8, 1, 'system', NOW(), 'system', NOW(), 0, '天蝎座 (10月24日-11月22日)'),
(REPLACE(UUID(), '-', ''), 'constellation', 'sagittarius', '射手座', '射手座', 9, 1, 'system', NOW(), 'system', NOW(), 0, '射手座 (11月23日-12月21日)'),
(REPLACE(UUID(), '-', ''), 'constellation', 'capricorn', '摩羯座', '摩羯座', 10, 1, 'system', NOW(), 'system', NOW(), 0, '摩羯座 (12月22日-1月19日)'),
(REPLACE(UUID(), '-', ''), 'constellation', 'aquarius', '水瓶座', '水瓶座', 11, 1, 'system', NOW(), 'system', NOW(), 0, '水瓶座 (1月20日-2月18日)'),
(REPLACE(UUID(), '-', ''), 'constellation', 'pisces', '双鱼座', '双鱼座', 12, 1, 'system', NOW(), 'system', NOW(), 0, '双鱼座 (2月19日-3月20日)');
-- ============================================================================
-- 2. MBTI人格类型数据初始化
-- ============================================================================
INSERT INTO t_dictionary (
id, dict_type, dict_code, dict_name, dict_value, sort_order, status,
create_by, create_time, update_by, update_time, is_deleted, remarks
) VALUES
(REPLACE(UUID(), '-', ''), 'mbti', 'istj', 'ISTJ', 'ISTJ-检查员型', 1, 1, 'system', NOW(), 'system', NOW(), 0, '内倾感觉思维判断 - 一丝不苟的检查者'),
(REPLACE(UUID(), '-', ''), 'mbti', 'isfj', 'ISFJ', 'ISFJ-照顾者型', 2, 1, 'system', NOW(), 'system', NOW(), 0, '内倾感觉情感判断 - 忠诚的照顾者'),
(REPLACE(UUID(), '-', ''), 'mbti', 'infj', 'INFJ', 'INFJ-博爱型', 3, 1, 'system', NOW(), 'system', NOW(), 0, '内倾直觉情感判断 - 富有洞察力的博爱者'),
(REPLACE(UUID(), '-', ''), 'mbti', 'intj', 'INTJ', 'INTJ-专家型', 4, 1, 'system', NOW(), 'system', NOW(), 0, '内倾直觉思维判断 - 独立的战略家'),
(REPLACE(UUID(), '-', ''), 'mbti', 'istp', 'ISTP', 'ISTP-冒险家型', 5, 1, 'system', NOW(), 'system', NOW(), 0, '内倾感觉思维知觉 - 灵活的问题解决者'),
(REPLACE(UUID(), '-', ''), 'mbti', 'isfp', 'ISFP', 'ISFP-艺术家型', 6, 1, 'system', NOW(), 'system', NOW(), 0, '内倾感觉情感知觉 - 敏感的艺术家'),
(REPLACE(UUID(), '-', ''), 'mbti', 'infp', 'INFP', 'INFP-哲学家型', 7, 1, 'system', NOW(), 'system', NOW(), 0, '内倾直觉情感知觉 - 理想主义的哲学家'),
(REPLACE(UUID(), '-', ''), 'mbti', 'intp', 'INTP', 'INTP-学者型', 8, 1, 'system', NOW(), 'system', NOW(), 0, '内倾直觉思维知觉 - 好奇的分析师'),
(REPLACE(UUID(), '-', ''), 'mbti', 'estp', 'ESTP', 'ESTP-挑战者型', 9, 1, 'system', NOW(), 'system', NOW(), 0, '外倾感觉思维知觉 - 大胆的冒险者'),
(REPLACE(UUID(), '-', ''), 'mbti', 'esfp', 'ESFP', 'ESFP-表演者型', 10, 1, 'system', NOW(), 'system', NOW(), 0, '外倾感觉情感知觉 - 热情的表演者'),
(REPLACE(UUID(), '-', ''), 'mbti', 'enfp', 'ENFP', 'ENFP-公关型', 11, 1, 'system', NOW(), 'system', NOW(), 0, '外倾直觉情感知觉 - 充满热情的社交家'),
(REPLACE(UUID(), '-', ''), 'mbti', 'entp', 'ENTP', 'ENTP-智多星型', 12, 1, 'system', NOW(), 'system', NOW(), 0, '外倾直觉思维知觉 - 机智的辩论家'),
(REPLACE(UUID(), '-', ''), 'mbti', 'estj', 'ESTJ', 'ESTJ-管家型', 13, 1, 'system', NOW(), 'system', NOW(), 0, '外倾感觉思维判断 - 高效的组织者'),
(REPLACE(UUID(), '-', ''), 'mbti', 'esfj', 'ESFJ', 'ESFJ-主人型', 14, 1, 'system', NOW(), 'system', NOW(), 0, '外倾感觉情感判断 - 热情的主人'),
(REPLACE(UUID(), '-', ''), 'mbti', 'enfj', 'ENFJ', 'ENFJ-教导型', 15, 1, 'system', NOW(), 'system', NOW(), 0, '外倾直觉情感判断 - 鼓舞人心的领导者'),
(REPLACE(UUID(), '-', ''), 'mbti', 'entj', 'ENTJ', 'ENTJ-统帅型', 16, 1, 'system', NOW(), 'system', NOW(), 0, '外倾直觉思维判断 - 果断的指挥官');
-- ============================================================================
-- 3. 性别数据初始化
-- ============================================================================
INSERT INTO t_dictionary (
id, dict_type, dict_code, dict_name, dict_value, sort_order, status,
create_by, create_time, update_by, update_time, is_deleted, remarks
) VALUES
(REPLACE(UUID(), '-', ''), 'gender', 'male', '', '', 1, 1, 'system', NOW(), 'system', NOW(), 0, '性别:男'),
(REPLACE(UUID(), '-', ''), 'gender', 'female', '', '', 2, 1, 'system', NOW(), 'system', NOW(), 0, '性别:女'),
(REPLACE(UUID(), '-', ''), 'gender', 'secret', '保密', '保密', 3, 1, 'system', NOW(), 'system', NOW(), 0, '性别:保密'),
(REPLACE(UUID(), '-', ''), 'gender', 'other', '其他', '其他', 4, 1, 'system', NOW(), 'system', NOW(), 0, '性别:其他');
-- ============================================================================
-- 初始化完成
-- ============================================================================
COMMIT;
SELECT '字典数据初始化完成' AS result;
@@ -1,24 +0,0 @@
-- ============================================================================
-- 迁移脚本: 添加 ideal_life 字段到 t_user_profile 表
-- 日期: 2025-12-22
-- 描述: 为用户档案表添加理想生活状态字段
-- ============================================================================
-- 检查字段是否存在,不存在则添加
SET @dbname = DATABASE();
SET @tablename = 't_user_profile';
SET @columnname = 'ideal_life';
SET @preparedStatement = (SELECT IF(
(
SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = @dbname
AND TABLE_NAME = @tablename
AND COLUMN_NAME = @columnname
) > 0,
'SELECT 1',
CONCAT('ALTER TABLE ', @tablename, ' ADD COLUMN ', @columnname, ' TEXT COMMENT ''理想生活状态'' AFTER future_vision')
));
PREPARE alterIfNotExists FROM @preparedStatement;
EXECUTE alterIfNotExists;
DEALLOCATE PREPARE alterIfNotExists;