diff --git a/.idea/dataSources.xml b/.idea/dataSources.xml index 0d79ec5..c634ce5 100644 --- a/.idea/dataSources.xml +++ b/.idea/dataSources.xml @@ -7,7 +7,9 @@ com.mysql.cj.jdbc.Driver jdbc:mysql://localhost:3306 + + $ProjectFileDir$ diff --git a/.idea/sqldialects.xml b/.idea/sqldialects.xml index 75adf3d..5f46374 100644 --- a/.idea/sqldialects.xml +++ b/.idea/sqldialects.xml @@ -2,5 +2,6 @@ + \ No newline at end of file diff --git a/backend-single/deploy.py b/backend-single/deploy.py new file mode 100644 index 0000000..28a29c9 --- /dev/null +++ b/backend-single/deploy.py @@ -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() diff --git a/backend-single/deploy.sh b/backend-single/deploy.sh index f2079d4..7396374 100755 --- a/backend-single/deploy.sh +++ b/backend-single/deploy.sh @@ -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 "上传部署脚本失败" - exit 1 - fi - log_info "✅ 部署脚本上传成功" - - # 设置权限 - log_info "设置远程脚本权限..." - if ! ssh $REMOTE_USER@$REMOTE_HOST "chmod +x $REMOTE_DIR/deploy-server.sh"; then - log_error "设置脚本权限失败" - exit 1 + # 如果指定了额外文件,则上传 + 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 "✅ 文件上传成功: $UPLOAD_SCRIPT" + + # 如果是脚本文件,设置执行权限 + REMOTE_FILENAME=$(basename "$UPLOAD_SCRIPT") + if [[ "$REMOTE_FILENAME" == *.sh ]]; then + log_info "设置远程脚本权限..." + 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,16 +360,21 @@ case "${1:-deploy}" in fi ;; *) - echo "用法: $0 {deploy|remote|build|start|stop|restart|status|remote-status|logs}" - echo " deploy - 本地部署服务(默认)" - echo " remote - 远程部署到服务器" - echo " build - 构建项目" - echo " start - 启动本地服务" - echo " stop - 停止本地服务" - echo " restart - 重启本地服务" - echo " status - 查看本地服务状态" - echo " remote-status - 查看远程服务状态" - echo " logs - 查看本地实时日志" + echo "用法: $0 {deploy|remote [文件名]|build|start|stop|restart|status|remote-status|logs}" + echo " deploy - 本地部署服务(默认)" + echo " remote - 远程部署到服务器(仅上传JAR)" + echo " remote [文件名] - 远程部署并上传指定文件(如 deploy-server.sh)" + echo " build - 构建项目" + echo " start - 启动本地服务" + echo " stop - 停止本地服务" + echo " restart - 重启本地服务" + echo " status - 查看本地服务状态" + echo " remote-status - 查看远程服务状态" + echo " logs - 查看本地实时日志" + echo "" + echo "示例:" + echo " $0 remote # 仅上传JAR并部署" + echo " $0 remote deploy-server.sh # 同时上传部署脚本" exit 1 ;; esac \ No newline at end of file diff --git a/backend-single/src/main/java/com/emotion/controller/DictionaryController.java b/backend-single/src/main/java/com/emotion/controller/DictionaryController.java new file mode 100644 index 0000000..5c5c83f --- /dev/null +++ b/backend-single/src/main/java/com/emotion/controller/DictionaryController.java @@ -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 createDictionary(@Validated @RequestBody DictionaryCreateRequest request) { + return dictionaryService.createDictionary(request); + } + + /** + * 更新字典 + * + * @param request 更新请求 + * @return 更新结果 + */ + @PutMapping + public Result updateDictionary(@Validated @RequestBody DictionaryUpdateRequest request) { + return dictionaryService.updateDictionary(request); + } + + /** + * 删除字典 + * + * @param id 字典ID + * @return 删除结果 + */ + @DeleteMapping("/delete") + public Result deleteDictionary(@RequestParam String id) { + return dictionaryService.deleteDictionary(id); + } + + /** + * 获取字典详情 + * + * @param id 字典ID + * @return 字典详情 + */ + @GetMapping("/detail") + public Result getDictionary(@RequestParam String id) { + return dictionaryService.getDictionary(id); + } + + /** + * 分页查询字典 + * + * @param request 分页请求 + * @return 分页结果 + */ + @GetMapping("/list") + public Result> listDictionaries(DictionaryPageRequest request) { + return dictionaryService.listDictionaries(request); + } + + /** + * 根据字典类型查询字典集合 + * + * @param dictType 字典类型 + * @return 字典集合 + */ + @GetMapping("/byType") + public Result> getDictionariesByType(@RequestParam String dictType) { + return dictionaryService.getDictionariesByType(dictType); + } + + /** + * 根据字典类型查询启用的字典集合 + * + * @param dictType 字典类型 + * @return 启用的字典集合 + */ + @GetMapping("/enabledByType") + public Result> getEnabledDictionariesByType(@RequestParam String dictType) { + return dictionaryService.getEnabledDictionariesByType(dictType); + } +} \ No newline at end of file diff --git a/backend-single/src/main/java/com/emotion/dto/request/dictionary/DictionaryCreateRequest.java b/backend-single/src/main/java/com/emotion/dto/request/dictionary/DictionaryCreateRequest.java new file mode 100644 index 0000000..7dcdf49 --- /dev/null +++ b/backend-single/src/main/java/com/emotion/dto/request/dictionary/DictionaryCreateRequest.java @@ -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; +} \ No newline at end of file diff --git a/backend-single/src/main/java/com/emotion/dto/request/dictionary/DictionaryPageRequest.java b/backend-single/src/main/java/com/emotion/dto/request/dictionary/DictionaryPageRequest.java new file mode 100644 index 0000000..6cdc9b7 --- /dev/null +++ b/backend-single/src/main/java/com/emotion/dto/request/dictionary/DictionaryPageRequest.java @@ -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; +} \ No newline at end of file diff --git a/backend-single/src/main/java/com/emotion/dto/request/dictionary/DictionaryUpdateRequest.java b/backend-single/src/main/java/com/emotion/dto/request/dictionary/DictionaryUpdateRequest.java new file mode 100644 index 0000000..565fc3d --- /dev/null +++ b/backend-single/src/main/java/com/emotion/dto/request/dictionary/DictionaryUpdateRequest.java @@ -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; +} \ No newline at end of file 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 index ddf8f4b..60cc254 100644 --- 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 @@ -29,6 +29,11 @@ public class UserProfileCreateRequest { */ private String zodiac; + /** + * 职业 + */ + private String profession; + /** * MBTI人格类型 */ 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 index 5f4cc34..b5b68ac 100644 --- 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 @@ -34,6 +34,11 @@ public class UserProfileUpdateRequest { */ private String zodiac; + /** + * 职业 + */ + private String profession; + /** * MBTI人格类型 */ diff --git a/backend-single/src/main/java/com/emotion/dto/response/dictionary/DictionaryResponse.java b/backend-single/src/main/java/com/emotion/dto/response/dictionary/DictionaryResponse.java new file mode 100644 index 0000000..92cf692 --- /dev/null +++ b/backend-single/src/main/java/com/emotion/dto/response/dictionary/DictionaryResponse.java @@ -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; +} \ No newline at end of file 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 index 3087409..54cf2d5 100644 --- 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 @@ -46,6 +46,11 @@ public class UserProfileResponse { */ private String zodiac; + /** + * 职业 + */ + private String profession; + /** * MBTI人格类型 */ diff --git a/backend-single/src/main/java/com/emotion/entity/Dictionary.java b/backend-single/src/main/java/com/emotion/entity/Dictionary.java new file mode 100644 index 0000000..a204fcf --- /dev/null +++ b/backend-single/src/main/java/com/emotion/entity/Dictionary.java @@ -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; +} \ No newline at end of file diff --git a/backend-single/src/main/java/com/emotion/entity/UserProfile.java b/backend-single/src/main/java/com/emotion/entity/UserProfile.java index f250387..df1cd70 100644 --- a/backend-single/src/main/java/com/emotion/entity/UserProfile.java +++ b/backend-single/src/main/java/com/emotion/entity/UserProfile.java @@ -49,6 +49,12 @@ public class UserProfile extends BaseEntity { @TableField("zodiac") private String zodiac; + /** + * 职业 + */ + @TableField("profession") + private String profession; + /** * MBTI人格类型 */ diff --git a/backend-single/src/main/java/com/emotion/mapper/DictionaryMapper.java b/backend-single/src/main/java/com/emotion/mapper/DictionaryMapper.java new file mode 100644 index 0000000..d9a85a1 --- /dev/null +++ b/backend-single/src/main/java/com/emotion/mapper/DictionaryMapper.java @@ -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 { + + /** + * 根据字典类型查询字典集合 + * + * @param dictType 字典类型 + * @return 字典集合 + */ + List selectByDictType(@Param("dictType") String dictType); + + /** + * 根据字典类型和状态查询字典集合 + * + * @param dictType 字典类型 + * @param status 状态 + * @return 字典集合 + */ + List selectByDictTypeAndStatus(@Param("dictType") String dictType, @Param("status") Integer status); +} \ No newline at end of file diff --git a/backend-single/src/main/java/com/emotion/service/DictionaryService.java b/backend-single/src/main/java/com/emotion/service/DictionaryService.java new file mode 100644 index 0000000..fe6ae19 --- /dev/null +++ b/backend-single/src/main/java/com/emotion/service/DictionaryService.java @@ -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 { + + /** + * 创建字典 + * + * @param request 创建请求 + * @return 创建结果 + */ + Result createDictionary(DictionaryCreateRequest request); + + /** + * 更新字典 + * + * @param request 更新请求 + * @return 更新结果 + */ + Result updateDictionary(DictionaryUpdateRequest request); + + /** + * 删除字典 + * + * @param id 字典ID + * @return 删除结果 + */ + Result deleteDictionary(String id); + + /** + * 获取字典详情 + * + * @param id 字典ID + * @return 字典详情 + */ + Result getDictionary(String id); + + /** + * 分页查询字典 + * + * @param request 分页请求 + * @return 分页结果 + */ + Result> listDictionaries(DictionaryPageRequest request); + + /** + * 根据字典类型查询字典集合 + * + * @param dictType 字典类型 + * @return 字典集合 + */ + Result> getDictionariesByType(String dictType); + + /** + * 根据字典类型查询启用的字典集合 + * + * @param dictType 字典类型 + * @return 启用的字典集合 + */ + Result> getEnabledDictionariesByType(String dictType); +} \ No newline at end of file diff --git a/backend-single/src/main/java/com/emotion/service/impl/DictionaryServiceImpl.java b/backend-single/src/main/java/com/emotion/service/impl/DictionaryServiceImpl.java new file mode 100644 index 0000000..675ac47 --- /dev/null +++ b/backend-single/src/main/java/com/emotion/service/impl/DictionaryServiceImpl.java @@ -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 implements DictionaryService { + + /** + * 创建字典 + * + * @param request 创建请求 + * @return 创建结果 + */ + @Override + public Result createDictionary(DictionaryCreateRequest request) { + // 检查字典类型+字典编码是否已存在 + LambdaQueryWrapper 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 updateDictionary(DictionaryUpdateRequest request) { + // 检查字典是否存在 + Dictionary dictionary = this.getById(request.getId()); + if (dictionary == null || dictionary.getIsDeleted() == 1) { + return Result.error(400, "字典不存在"); + } + + // 检查字典类型+字典编码是否已存在(排除当前字典) + LambdaQueryWrapper 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 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 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> listDictionaries(DictionaryPageRequest request) { + // 构建查询条件 + LambdaQueryWrapper 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 page = this.page(new Page<>(request.getCurrent(), request.getSize()), queryWrapper); + + // 转换为响应对象 + List responseList = page.getRecords().stream() + .map(this::convertToResponse) + .collect(Collectors.toList()); + + // 构建分页结果 + PageResult 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> getDictionariesByType(String dictType) { + // 构建查询条件 + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(Dictionary::getDictType, dictType) + .eq(Dictionary::getIsDeleted, 0) + .orderByAsc(Dictionary::getSortOrder) + .orderByAsc(Dictionary::getCreateTime); + + // 查询字典集合 + List dictionaryList = this.list(queryWrapper); + + // 转换为响应对象 + List responseList = dictionaryList.stream() + .map(this::convertToResponse) + .collect(Collectors.toList()); + + return Result.success(responseList); + } + + /** + * 根据字典类型查询启用的字典集合 + * + * @param dictType 字典类型 + * @return 启用的字典集合 + */ + @Override + public Result> getEnabledDictionariesByType(String dictType) { + // 构建查询条件 + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(Dictionary::getDictType, dictType) + .eq(Dictionary::getStatus, 1) + .eq(Dictionary::getIsDeleted, 0) + .orderByAsc(Dictionary::getSortOrder) + .orderByAsc(Dictionary::getCreateTime); + + // 查询字典集合 + List dictionaryList = this.list(queryWrapper); + + // 转换为响应对象 + List 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; + } +} \ No newline at end of file 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 index b2a4232..ae9f972 100644 --- a/backend-single/src/main/java/com/emotion/service/impl/UserProfileServiceImpl.java +++ b/backend-single/src/main/java/com/emotion/service/impl/UserProfileServiceImpl.java @@ -71,21 +71,37 @@ public class UserProfileServiceImpl extends ServiceImpl 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值 diff --git a/life-script/.env.development b/life-script/.env.development index 8f22d36..88dd8d5 100644 --- a/life-script/.env.development +++ b/life-script/.env.development @@ -1,2 +1,2 @@ # 开发环境配置 -VITE_API_BASE_URL=http://localhost:8080 +VITE_API_BASE_URL=http://localhost:19089/api diff --git a/life-script/deploy.py b/life-script/deploy.py new file mode 100644 index 0000000..2eeed79 --- /dev/null +++ b/life-script/deploy.py @@ -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() diff --git a/life-script/deploy.sh b/life-script/deploy.sh new file mode 100644 index 0000000..a103507 --- /dev/null +++ b/life-script/deploy.sh @@ -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 diff --git a/life-script/src/App.jsx b/life-script/src/App.jsx index 98ec994..3da062f 100644 --- a/life-script/src/App.jsx +++ b/life-script/src/App.jsx @@ -124,8 +124,11 @@ const AnimatedRoutes = () => { * App 主组件 */ function App() { + // 生产环境使用 /course-of-life 作为基础路径 + const basename = import.meta.env.PROD ? '/course-of-life' : ''; + return ( - + {/* 动态背景 */} diff --git a/life-script/src/pages/OnboardingPage.jsx b/life-script/src/pages/OnboardingPage.jsx index 949db21..70e74e8 100644 --- a/life-script/src/pages/OnboardingPage.jsx +++ b/life-script/src/pages/OnboardingPage.jsx @@ -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 组件 @@ -24,11 +25,39 @@ 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 = () => {
updateField('nickname', v)} /> - updateField('gender', v)} /> - updateField('mbti', v)} /> - updateField('zodiac', v)} /> + updateField('gender', v)} /> + updateField('mbti', v)} /> + updateField('zodiac', 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)} /> ); diff --git a/life-script/src/services/dictionary.js b/life-script/src/services/dictionary.js new file mode 100644 index 0000000..c6f8fb8 --- /dev/null +++ b/life-script/src/services/dictionary.js @@ -0,0 +1,63 @@ +import api from './api'; + +/** + * 字典服务 + * 处理字典数据的获取 + */ + +/** + * 根据字典类型获取启用的字典列表 + * @param {string} dictType - 字典类型 + * @returns {Promise} 字典列表 + */ +export const getEnabledDictionariesByType = async (dictType) => { + const response = await api.get('/dictionary/enabledByType', { + params: { dictType } + }); + return response.data || []; +}; + +/** + * 获取星座字典 + * @returns {Promise} 星座列表 + */ +export const getZodiacList = async () => { + return getEnabledDictionariesByType('constellation'); +}; + +/** + * 获取MBTI字典 + * @returns {Promise} MBTI列表 + */ +export const getMbtiList = async () => { + return getEnabledDictionariesByType('mbti'); +}; + +/** + * 获取性别字典 + * @returns {Promise} 性别列表 + */ +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 +}; diff --git a/life-script/src/services/userProfile.js b/life-script/src/services/userProfile.js index 40f1aa8..32c796c 100644 --- a/life-script/src/services/userProfile.js +++ b/life-script/src/services/userProfile.js @@ -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) : [], diff --git a/life-script/src/views/ProfileModal.jsx b/life-script/src/views/ProfileModal.jsx index 832d24d..a3bf7b7 100644 --- a/life-script/src/views/ProfileModal.jsx +++ b/life-script/src/views/ProfileModal.jsx @@ -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 }))} /> - setEditForm(prev => ({ ...prev, gender: v }))} + /> + setEditForm(prev => ({ ...prev, mbti: v }))} /> - setEditForm(prev => ({ ...prev, zodiac: v }))} /> setEditForm(prev => ({ ...prev, hobbies: v }))} /> diff --git a/life-script/vite.config.js b/life-script/vite.config.js index eba555f..79441b1 100644 --- a/life-script/vite.config.js +++ b/life-script/vite.config.js @@ -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 } diff --git a/sql/emotion_museum.sql b/sql/emotion_museum.sql index a3777fd..8b7c5cf 100644 --- a/sql/emotion_museum.sql +++ b/sql/emotion_museum.sql @@ -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; diff --git a/sql/emotion_museum_ddl.sql b/sql/emotion_museum_ddl.sql new file mode 100644 index 0000000..4fc40e2 --- /dev/null +++ b/sql/emotion_museum_ddl.sql @@ -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 '职业'; + diff --git a/sql/emotion_museum_init.sql b/sql/emotion_museum_init.sql new file mode 100644 index 0000000..e84b1ea --- /dev/null +++ b/sql/emotion_museum_init.sql @@ -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; \ No newline at end of file diff --git a/sql/migrations/V20251222_add_ideal_life_column.sql b/sql/migrations/V20251222_add_ideal_life_column.sql deleted file mode 100644 index de07c2c..0000000 --- a/sql/migrations/V20251222_add_ideal_life_column.sql +++ /dev/null @@ -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;