From bc3ed2d872cc65cb29824858d88d8d72df3ff43d Mon Sep 17 00:00:00 2001 From: huazhongmin Date: Mon, 13 Oct 2025 10:43:08 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BB=A3=E7=A0=81=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AUTH_LOGIN_OPTIMIZATION.md | 396 +++++++++ AUTH_REGISTER_OPTIMIZATION.md | 433 ++++++++++ BACKEND_SINGLE_STARTUP_FIX.md | 369 +++++++++ FRONTEND_TOKEN_API_MIGRATION_GUIDE.md | 307 +++++++ TOKEN_CONTROLLER_OPTIMIZATION.md | 337 ++++++++ TOKEN_CONTROLLER_REFACTOR_SUMMARY.md | 275 +++++++ backend-single/deploy-server.sh | 99 +-- backend-single/deploy.sh | 330 +++++++- .../emotion/controller/AuthController.java | 26 +- .../emotion/controller/TokenController.java | 46 +- .../com/emotion/dto/request/LoginRequest.java | 33 +- .../emotion/dto/request/RegisterRequest.java | 57 +- .../com/emotion/dto/request/TokenRequest.java | 16 - .../emotion/dto/response/SmsCodeResponse.java | 35 + .../dto/response/WebSocketResponse.java | 62 -- .../java/com/emotion/entity/Achievement.java | 2 +- .../main/java/com/emotion/entity/Comment.java | 2 +- .../com/emotion/entity/CommunityPost.java | 2 +- .../java/com/emotion/entity/Conversation.java | 2 +- .../java/com/emotion/entity/CozeApiCall.java | 2 +- .../java/com/emotion/entity/DiaryComment.java | 2 +- .../java/com/emotion/entity/DiaryPost.java | 2 +- .../com/emotion/entity/EmotionAnalysis.java | 2 +- .../com/emotion/entity/EmotionRecord.java | 2 +- .../java/com/emotion/entity/GrowthTopic.java | 2 +- .../java/com/emotion/entity/GuestUser.java | 2 +- .../java/com/emotion/entity/LocationPin.java | 2 +- .../main/java/com/emotion/entity/Message.java | 2 +- .../main/java/com/emotion/entity/Reward.java | 2 +- .../com/emotion/entity/TopicInteraction.java | 2 +- .../main/java/com/emotion/entity/User.java | 2 +- .../java/com/emotion/entity/UserStats.java | 2 +- .../java/com/emotion/service/AuthService.java | 18 + .../java/com/emotion/service/UserService.java | 41 +- .../emotion/service/impl/AuthServiceImpl.java | 216 +++-- .../emotion/service/impl/UserServiceImpl.java | 63 ++ .../src/main/resources/application-local.yml | 4 +- .../src/main/resources/application-prod.yml | 8 +- .../src/main/resources/application-test.yml | 4 +- .../sql/mysql_emotion_museum_final.sql | 768 +++++++----------- 40 files changed, 3189 insertions(+), 788 deletions(-) create mode 100644 AUTH_LOGIN_OPTIMIZATION.md create mode 100644 AUTH_REGISTER_OPTIMIZATION.md create mode 100644 BACKEND_SINGLE_STARTUP_FIX.md create mode 100644 FRONTEND_TOKEN_API_MIGRATION_GUIDE.md create mode 100644 TOKEN_CONTROLLER_OPTIMIZATION.md create mode 100644 TOKEN_CONTROLLER_REFACTOR_SUMMARY.md delete mode 100644 backend-single/src/main/java/com/emotion/dto/request/TokenRequest.java create mode 100644 backend-single/src/main/java/com/emotion/dto/response/SmsCodeResponse.java delete mode 100644 backend-single/src/main/java/com/emotion/dto/response/WebSocketResponse.java diff --git a/AUTH_LOGIN_OPTIMIZATION.md b/AUTH_LOGIN_OPTIMIZATION.md new file mode 100644 index 0000000..5b65282 --- /dev/null +++ b/AUTH_LOGIN_OPTIMIZATION.md @@ -0,0 +1,396 @@ +# 登录接口优化总结 + +## 🎯 优化目标 + +简化用户登录流程,使用手机号+短信验证码登录,若用户不存在则自动注册。 + +## ✅ 完成的优化 + +### 1. 简化LoginRequest + +**文件**: `backend-single/src/main/java/com/emotion/dto/request/LoginRequest.java` + +**修改前**:需要账号、密码、图形验证码、验证码key等字段 + +**修改后**:仅需2个字段 +```java +@Data +@EqualsAndHashCode(callSuper = true) +public class LoginRequest extends BaseRequest { + + /** + * 手机号 + */ + @NotBlank(message = "手机号不能为空") + @Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确") + private String phone; + + /** + * 短信验证码 + */ + @NotBlank(message = "验证码不能为空") + @Size(min = 6, max = 6, message = "验证码必须为6位") + private String smsCode; +} +``` + +### 2. 优化登录逻辑 + +**文件**: `backend-single/src/main/java/com/emotion/service/impl/AuthServiceImpl.java` + +#### 2.1 新的登录流程 +```java +@Override +public AuthResponse login(LoginRequest request) { + // 1. 验证短信验证码 + if (!validateSmsCode(request.getPhone(), request.getSmsCode())) { + throw new CaptchaException("验证码错误或已过期"); + } + + // 2. 根据手机号查询用户 + User user = userService.getByPhone(request.getPhone()); + + // 3. 如果用户不存在,则自动注册 + if (user == null) { + log.info("用户不存在,自动注册: phone={}", request.getPhone()); + user = autoRegisterUser(request.getPhone()); + } else { + // 检查用户状态 + if (user.getStatus() != 1) { + throw new AuthException("账号已被禁用"); + } + log.info("用户登录: phone={}, userId={}", request.getPhone(), user.getId()); + } + + // 4. 生成令牌并返回 + // ... +} +``` + +#### 2.2 自动注册用户 +```java +/** + * 自动注册用户(用于登录时用户不存在的情况) + * + * @param phone 手机号 + * @return 新创建的用户 + */ +private User autoRegisterUser(String phone) { + // 生成随机用户名:开心 + 6位随机大小写字母和数字 + String username = generateRandomUsername(); + + // 使用手机号作为账号 + String account = phone; + + // 生成随机密码(用户可以后续修改) + String randomPassword = generateRandomPassword(); + + // 创建用户 + User user = userService.createUser( + account, + username, + randomPassword, + null, // 邮箱为空 + phone + ); + + log.info("自动注册用户成功: phone={}, username={}, userId={}", phone, username, user.getId()); + return user; +} +``` + +#### 2.3 生成随机密码 +```java +/** + * 生成随机密码 + * 格式:8位随机大小写字母和数字 + * + * @return 随机密码 + */ +private String generateRandomPassword() { + String chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + Random random = new Random(); + StringBuilder sb = new StringBuilder(); + + for (int i = 0; i < 8; i++) { + int index = random.nextInt(chars.length()); + sb.append(chars.charAt(index)); + } + + return sb.toString(); +} +``` + +### 3. 优化短信验证码发送 + +**修改前**:检查手机号是否已注册,已注册则抛出异常 + +**修改后**:根据是否已注册返回不同的提示信息 +```java +@Override +public SmsCodeResponse sendSmsCode(String phone) { + // 验证手机号格式 + if (!StringUtils.hasText(phone) || !phone.matches("^1[3-9]\\d{9}$")) { + throw new BusinessException("手机号格式不正确"); + } + + // 检查手机号是否已注册,用于提示信息 + boolean isRegistered = existsByPhone(phone); + String message; + if (isRegistered) { + message = "验证码已发送,用于登录验证,有效期" + SMS_CODE_EXPIRE_MINUTES + "分钟"; + } else { + message = "验证码已发送,用于注册验证,有效期" + SMS_CODE_EXPIRE_MINUTES + "分钟"; + } + + // TODO: 接入真实的短信服务商(阿里云、腾讯云等) + String code = DEFAULT_SMS_CODE; + + // 将验证码存储到Redis,有效期5分钟 + String key = SMS_CODE_PREFIX + phone; + redisTemplate.opsForValue().set(key, code, SMS_CODE_EXPIRE_MINUTES, TimeUnit.MINUTES); + + log.info("发送短信验证码: phone={}, code={}, isRegistered={}", phone, code, isRegistered); + + // 构建响应 + return SmsCodeResponse.builder() + .code(code) // 开发环境返回验证码,生产环境应该删除此行 + .expiresIn((long) SMS_CODE_EXPIRE_MINUTES * 60) + .message(message) + .build(); +} +``` + +### 4. 更新Controller注解 + +**文件**: `backend-single/src/main/java/com/emotion/controller/AuthController.java` + +```java +/** + * 用户登录(简化版:手机号+验证码,不存在则自动注册) + */ +@PostMapping("/login") +@Operation(summary = "用户登录", description = "使用手机号和短信验证码登录,若用户不存在则自动注册") +public Result login(@Valid @RequestBody LoginRequest request) { + AuthResponse response = authService.login(request); + return Result.success("登录成功", response); +} +``` + +## 📊 优化对比 + +| 项目 | 优化前 | 优化后 | +|------|--------|--------| +| 必填字段 | 账号、密码、验证码、验证码key | 手机号、短信验证码 | +| 验证码类型 | 图形验证码 | 短信验证码 | +| 用户不存在 | 提示错误 | 自动注册 | +| 密码验证 | 需要 | 不需要 | +| 登录流程 | 复杂 | 简单快捷 | + +## 🔧 核心特性 + +### 1. 简化登录流程 +- ✅ 仅需2个字段:手机号、短信验证码 +- ✅ 无需记住密码 +- ✅ 用户不存在自动注册 +- ✅ 自动生成随机用户名和密码 + +### 2. 智能提示信息 +- ✅ 未注册用户:提示"用于注册验证" +- ✅ 已注册用户:提示"用于登录验证" +- ✅ 用户体验更友好 + +### 3. 自动注册机制 +- ✅ 自动生成用户名:开心 + 6位随机字符 +- ✅ 自动生成密码:8位随机字符 +- ✅ 使用手机号作为账号 +- ✅ 无缝登录体验 + +### 4. 安全性保障 +- ✅ 手机号格式验证 +- ✅ 短信验证码验证 +- ✅ 验证码过期检查 +- ✅ 验证码一次性使用 +- ✅ 用户状态检查 + +## 🚀 使用示例 + +### 场景1:新用户首次登录(自动注册) + +#### 步骤1:获取短信验证码 +**请求**: +```bash +curl -X GET "http://localhost:19089/api/auth/sms-code?phone=13900139000" +``` + +**响应**: +```json +{ + "code": 200, + "message": "验证码已发送", + "data": { + "code": "123456", + "expiresIn": 300, + "message": "验证码已发送,用于注册验证,有效期5分钟" + } +} +``` + +#### 步骤2:登录(自动注册) +**请求**: +```bash +curl -X POST "http://localhost:19089/api/auth/login" \ + -H "Content-Type: application/json" \ + -d '{ + "phone": "13900139000", + "smsCode": "123456" + }' +``` + +**响应**: +```json +{ + "code": 200, + "message": "登录成功", + "data": { + "accessToken": "eyJhbGciOiJIUzUxMiJ9...", + "refreshToken": "eyJhbGciOiJIUzUxMiJ9...", + "expiresIn": 86400, + "userInfo": { + "id": "f361c8648378565a91c6d799d641741f", + "account": "13900139000", + "username": "开心4FhbOl", + "nickname": "开心4FhbOl", + "phone": "13900139000", + "memberLevel": "free" + }, + "loginTime": "2025-10-06 23:14:57" + } +} +``` + +### 场景2:已注册用户登录 + +#### 步骤1:获取短信验证码 +**请求**: +```bash +curl -X GET "http://localhost:19089/api/auth/sms-code?phone=13900139000" +``` + +**响应**: +```json +{ + "code": 200, + "message": "验证码已发送", + "data": { + "code": "123456", + "expiresIn": 300, + "message": "验证码已发送,用于登录验证,有效期5分钟" + } +} +``` + +#### 步骤2:登录 +**请求**: +```bash +curl -X POST "http://localhost:19089/api/auth/login" \ + -H "Content-Type: application/json" \ + -d '{ + "phone": "13900139000", + "smsCode": "123456" + }' +``` + +**响应**: +```json +{ + "code": 200, + "message": "登录成功", + "data": { + "accessToken": "eyJhbGciOiJIUzUxMiJ9...", + "refreshToken": "eyJhbGciOiJIUzUxMiJ9...", + "expiresIn": 86400, + "userInfo": { + "id": "f361c8648378565a91c6d799d641741f", + "account": "13900139000", + "username": "开心4FhbOl", + "nickname": "开心4FhbOl", + "phone": "13900139000", + "memberLevel": "free" + }, + "loginTime": "2025-10-06 23:15:18" + } +} +``` + +## 📝 TODO事项 + +### 1. 接入真实短信服务 + +**位置**: `AuthServiceImpl.sendSmsCode()` 方法 + +**当前代码**: +```java +// TODO: 接入真实的短信服务商(阿里云、腾讯云等) +// 目前使用固定验证码 123456 +String code = DEFAULT_SMS_CODE; +``` + +**后续改进**: +```java +// 生成随机6位数字验证码 +String code = String.format("%06d", new Random().nextInt(1000000)); + +// 调用短信服务商API发送验证码 +// 示例:阿里云短信服务 +// smsService.sendSms(phone, code); +``` + +### 2. 生产环境优化 + +**需要修改的地方**: +1. **移除响应中的验证码**: + ```java + return SmsCodeResponse.builder() + // .code(code) // 生产环境删除此行 + .expiresIn((long) SMS_CODE_EXPIRE_MINUTES * 60) + .message(message) + .build(); + ``` + +2. **添加发送频率限制**: + - 同一手机号60秒内只能发送一次 + - 同一IP每天最多发送10次 + +3. **添加验证码尝试次数限制**: + - 同一手机号最多尝试5次 + - 超过次数后需要重新获取验证码 + +## ✅ 验证清单 + +- [x] 编译成功,无错误 +- [x] LoginRequest简化为2个字段 +- [x] 实现自动注册逻辑 +- [x] 实现随机密码生成 +- [x] 优化短信验证码提示信息 +- [x] 更新Controller注解 +- [x] 测试新用户登录(自动注册) +- [x] 测试已注册用户登录 +- [x] 添加详细的日志记录 + +## 🎉 总结 + +成功优化了登录接口,实现了以下目标: + +1. ✅ **简化登录流程** - 仅需手机号和短信验证码 +2. ✅ **自动注册机制** - 用户不存在时自动注册 +3. ✅ **智能提示信息** - 根据用户状态返回不同提示 +4. ✅ **无缝用户体验** - 无需区分注册和登录 +5. ✅ **良好的代码质量** - 清晰的注释、完善的日志、标准的异常处理 + +现在用户可以通过简单的2步完成登录或注册: +1. 输入手机号获取验证码 +2. 输入验证码完成登录(不存在则自动注册) + +登录成功后自动返回访问令牌和用户信息,用户体验大大提升! + diff --git a/AUTH_REGISTER_OPTIMIZATION.md b/AUTH_REGISTER_OPTIMIZATION.md new file mode 100644 index 0000000..b40840a --- /dev/null +++ b/AUTH_REGISTER_OPTIMIZATION.md @@ -0,0 +1,433 @@ +# 注册接口优化总结 + +## 🎯 优化目标 + +简化用户注册流程,仅需要手机号、密码和短信验证码即可完成注册。 + +## ✅ 完成的优化 + +### 1. 简化RegisterRequest + +**文件**: `backend-single/src/main/java/com/emotion/dto/request/RegisterRequest.java` + +**修改前**:需要账号、密码、确认密码、用户名、昵称、邮箱、手机号、图形验证码等多个字段 + +**修改后**:仅需3个字段 +```java +@Data +@EqualsAndHashCode(callSuper = true) +public class RegisterRequest extends BaseRequest { + + /** + * 手机号(作为账号) + */ + @NotBlank(message = "手机号不能为空") + @Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确") + private String phone; + + /** + * 密码 + */ + @NotBlank(message = "密码不能为空") + @Size(min = 6, max = 20, message = "密码长度必须在6-20个字符之间") + private String password; + + /** + * 短信验证码 + */ + @NotBlank(message = "验证码不能为空") + @Size(min = 6, max = 6, message = "验证码必须为6位") + private String smsCode; +} +``` + +### 2. 新增SmsCodeResponse + +**文件**: `backend-single/src/main/java/com/emotion/dto/response/SmsCodeResponse.java` + +```java +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class SmsCodeResponse { + + /** + * 验证码(开发环境返回,生产环境不返回) + */ + private String code; + + /** + * 过期时间(秒) + */ + private Long expiresIn; + + /** + * 提示信息 + */ + private String message; +} +``` + +### 3. 新增获取短信验证码接口 + +**文件**: `backend-single/src/main/java/com/emotion/controller/AuthController.java` + +```java +/** + * 获取短信验证码(用于注册) + */ +@GetMapping("/sms-code") +@Operation(summary = "获取短信验证码", description = "用于注册时的短信验证码") +public Result getSmsCode( + @Parameter(description = "手机号", required = true) + @RequestParam String phone) { + SmsCodeResponse response = authService.sendSmsCode(phone); + return Result.success("验证码已发送", response); +} +``` + +**接口地址**: `GET /api/auth/sms-code?phone=13800138000` + +**响应示例**: +```json +{ + "code": 200, + "message": "验证码已发送", + "data": { + "code": "123456", + "expiresIn": 300, + "message": "验证码已发送,有效期5分钟" + } +} +``` + +### 4. 优化注册接口逻辑 + +**文件**: `backend-single/src/main/java/com/emotion/service/impl/AuthServiceImpl.java` + +#### 4.1 新增常量定义 +```java +private static final String SMS_CODE_PREFIX = "sms_code:"; +private static final int SMS_CODE_EXPIRE_MINUTES = 5; +private static final String DEFAULT_SMS_CODE = "123456"; +``` + +#### 4.2 简化注册逻辑 +```java +@Override +public AuthResponse register(RegisterRequest request) { + // 1. 验证短信验证码 + if (!validateSmsCode(request.getPhone(), request.getSmsCode())) { + throw new CaptchaException("验证码错误或已过期"); + } + + // 2. 检查手机号是否已存在 + if (userService.getByPhone(request.getPhone()) != null) { + throw new BusinessException("手机号已被注册"); + } + + // 3. 生成随机用户名:开心 + 6位随机大小写字母和数字 + String username = generateRandomUsername(); + + // 4. 使用手机号作为账号 + String account = request.getPhone(); + + // 5. 创建用户 + User user = userService.createUser( + account, + username, + request.getPassword(), + null, // 邮箱为空 + request.getPhone() + ); + + // 6. 生成令牌并返回 + // ... +} +``` + +#### 4.3 实现短信验证码发送 +```java +@Override +public SmsCodeResponse sendSmsCode(String phone) { + // 验证手机号格式 + if (!StringUtils.hasText(phone) || !phone.matches("^1[3-9]\\d{9}$")) { + throw new BusinessException("手机号格式不正确"); + } + + // 检查手机号是否已注册 + if (existsByPhone(phone)) { + throw new BusinessException("该手机号已被注册"); + } + + // TODO: 接入真实的短信服务商(阿里云、腾讯云等) + // 目前使用固定验证码 123456 + String code = DEFAULT_SMS_CODE; + + // 将验证码存储到Redis,有效期5分钟 + String key = SMS_CODE_PREFIX + phone; + redisTemplate.opsForValue().set(key, code, SMS_CODE_EXPIRE_MINUTES, TimeUnit.MINUTES); + + log.info("发送短信验证码: phone={}, code={}", phone, code); + + // 构建响应 + return SmsCodeResponse.builder() + .code(code) // 开发环境返回验证码,生产环境应该删除此行 + .expiresIn((long) SMS_CODE_EXPIRE_MINUTES * 60) + .message("验证码已发送,有效期" + SMS_CODE_EXPIRE_MINUTES + "分钟") + .build(); +} +``` + +#### 4.4 实现短信验证码校验 +```java +@Override +public boolean validateSmsCode(String phone, String code) { + if (!StringUtils.hasText(phone) || !StringUtils.hasText(code)) { + return false; + } + + String key = SMS_CODE_PREFIX + phone; + String storedCode = (String) redisTemplate.opsForValue().get(key); + + if (storedCode == null) { + log.warn("短信验证码不存在或已过期: phone={}", phone); + return false; + } + + boolean isValid = storedCode.equals(code); + + // 验证成功后删除验证码(一次性使用) + if (isValid) { + redisTemplate.delete(key); + log.info("短信验证码验证成功: phone={}", phone); + } else { + log.warn("短信验证码错误: phone={}, expected={}, actual={}", phone, storedCode, code); + } + + return isValid; +} +``` + +#### 4.5 实现随机用户名生成 +```java +/** + * 生成随机用户名 + * 格式:开心 + 6位随机大小写字母和数字 + * + * @return 随机用户名 + */ +private String generateRandomUsername() { + String chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + Random random = new Random(); + StringBuilder sb = new StringBuilder("开心"); + + for (int i = 0; i < 6; i++) { + int index = random.nextInt(chars.length()); + sb.append(chars.charAt(index)); + } + + return sb.toString(); +} +``` + +**用户名示例**: +- 开心A3bC9z +- 开心X7yK2m +- 开心P1qW5n + +### 5. 更新AuthService接口 + +**文件**: `backend-single/src/main/java/com/emotion/service/AuthService.java` + +新增方法定义: +```java +/** + * 发送短信验证码 + * + * @param phone 手机号 + * @return 短信验证码响应 + */ +SmsCodeResponse sendSmsCode(String phone); + +/** + * 验证短信验证码 + * + * @param phone 手机号 + * @param code 验证码 + * @return 是否验证成功 + */ +boolean validateSmsCode(String phone, String code); +``` + +## 📊 优化对比 + +| 项目 | 优化前 | 优化后 | +|------|--------|--------| +| 必填字段 | 账号、密码、确认密码、验证码、验证码key | 手机号、密码、短信验证码 | +| 验证码类型 | 图形验证码 | 短信验证码 | +| 用户名生成 | 手动输入或使用账号 | 自动生成(开心+6位随机) | +| 账号 | 手动输入 | 自动使用手机号 | +| 邮箱 | 可选输入 | 不需要 | +| 昵称 | 可选输入 | 自动使用用户名 | + +## 🔧 核心特性 + +### 1. 简化注册流程 +- ✅ 仅需3个字段:手机号、密码、短信验证码 +- ✅ 自动生成用户名:开心 + 6位随机字符 +- ✅ 手机号作为账号 +- ✅ 移除了确认密码、邮箱、昵称等非必要字段 + +### 2. 短信验证码机制 +- ✅ 默认验证码:123456(开发环境) +- ✅ 有效期:5分钟 +- ✅ 一次性使用:验证成功后自动删除 +- ✅ Redis存储:支持分布式部署 +- ✅ TODO标记:预留真实短信服务接入点 + +### 3. 安全性保障 +- ✅ 手机号格式验证 +- ✅ 手机号唯一性检查 +- ✅ 验证码过期检查 +- ✅ 验证码一次性使用 +- ✅ 密码加密存储 + +### 4. 用户体验优化 +- ✅ 注册流程简单快捷 +- ✅ 自动生成友好的用户名 +- ✅ 清晰的错误提示 +- ✅ 完整的API文档(Swagger) + +## 🚀 使用示例 + +### 1. 获取短信验证码 + +**请求**: +```bash +curl -X GET "http://localhost:19089/api/auth/sms-code?phone=13800138000" +``` + +**响应**: +```json +{ + "code": 200, + "message": "验证码已发送", + "data": { + "code": "123456", + "expiresIn": 300, + "message": "验证码已发送,有效期5分钟" + } +} +``` + +### 2. 用户注册 + +**请求**: +```bash +curl -X POST "http://localhost:19089/api/auth/register" \ + -H "Content-Type: application/json" \ + -d '{ + "phone": "13800138000", + "password": "password123", + "smsCode": "123456" + }' +``` + +**响应**: +```json +{ + "code": 200, + "message": "注册成功", + "data": { + "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", + "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", + "expiresIn": 86400, + "userInfo": { + "id": "1844234567890123456", + "account": "13800138000", + "username": "开心A3bC9z", + "nickname": "开心A3bC9z", + "phone": "13800138000", + "memberLevel": "free", + "status": 1 + }, + "loginTime": "2025-10-06 21:49:33" + } +} +``` + +## 📝 TODO事项 + +### 1. 接入真实短信服务 + +**位置**: `AuthServiceImpl.sendSmsCode()` 方法 + +**当前代码**: +```java +// TODO: 接入真实的短信服务商(阿里云、腾讯云等) +// 目前使用固定验证码 123456 +String code = DEFAULT_SMS_CODE; +``` + +**后续改进**: +```java +// 生成随机6位数字验证码 +String code = String.format("%06d", new Random().nextInt(1000000)); + +// 调用短信服务商API发送验证码 +// 示例:阿里云短信服务 +// smsService.sendSms(phone, code); +``` + +### 2. 生产环境优化 + +**需要修改的地方**: +1. **移除响应中的验证码**: + ```java + return SmsCodeResponse.builder() + // .code(code) // 生产环境删除此行 + .expiresIn((long) SMS_CODE_EXPIRE_MINUTES * 60) + .message("验证码已发送,有效期" + SMS_CODE_EXPIRE_MINUTES + "分钟") + .build(); + ``` + +2. **添加发送频率限制**: + - 同一手机号60秒内只能发送一次 + - 同一IP每天最多发送10次 + +3. **添加验证码尝试次数限制**: + - 同一手机号最多尝试5次 + - 超过次数后需要重新获取验证码 + +## ✅ 验证清单 + +- [x] 编译成功,无错误 +- [x] RegisterRequest简化为3个字段 +- [x] 新增SmsCodeResponse响应类 +- [x] 新增获取短信验证码接口 +- [x] 实现短信验证码发送逻辑 +- [x] 实现短信验证码校验逻辑 +- [x] 实现随机用户名生成 +- [x] 优化注册接口逻辑 +- [x] 添加Swagger API文档注解 +- [x] 添加详细的日志记录 + +## 🎉 总结 + +成功优化了注册接口,实现了以下目标: + +1. ✅ **简化注册流程** - 仅需手机号、密码和短信验证码 +2. ✅ **自动生成用户名** - 开心 + 6位随机大小写字母和数字 +3. ✅ **短信验证码机制** - 默认123456,预留真实短信服务接入点 +4. ✅ **完整的校验逻辑** - 手机号格式、唯一性、验证码有效性 +5. ✅ **良好的代码质量** - 清晰的注释、完善的日志、标准的异常处理 + +现在用户可以通过简单的3步完成注册: +1. 输入手机号获取验证码 +2. 输入密码和验证码 +3. 自动生成用户名并完成注册 + +注册成功后自动登录,返回访问令牌和用户信息! + diff --git a/BACKEND_SINGLE_STARTUP_FIX.md b/BACKEND_SINGLE_STARTUP_FIX.md new file mode 100644 index 0000000..37d7f3b --- /dev/null +++ b/BACKEND_SINGLE_STARTUP_FIX.md @@ -0,0 +1,369 @@ +# Backend-Single微服务启动修复总结 + +## 🎯 问题描述 + +backend-single微服务在启动时编译失败,缺少UserService中的多个方法实现。 + +## ❌ 编译错误 + +### 错误信息 +``` +[ERROR] 找不到符号 + 符号: 方法 getByAccount(java.lang.String) + 位置: 类型为com.emotion.service.UserService的变量 userService + +[ERROR] 找不到符号 + 符号: 方法 updateLastActiveTime(java.lang.String,java.time.LocalDateTime) + 位置: 类型为com.emotion.service.UserService的变量 userService + +[ERROR] 找不到符号 + 符号: 方法 getByEmail(java.lang.String) + 位置: 类型为com.emotion.service.UserService的变量 userService + +[ERROR] 找不到符号 + 符号: 方法 getByPhone(java.lang.String) + 位置: 类型为com.emotion.service.UserService的变量 userService + +[ERROR] 找不到符号 + 符号: 方法 createUser(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String) + 位置: 类型为com.emotion.service.UserService的变量 userService +``` + +### 缺少的方法 +1. `getByAccount(String account)` - 根据账号获取用户 +2. `getByEmail(String email)` - 根据邮箱获取用户 +3. `getByPhone(String phone)` - 根据手机号获取用户 +4. `createUser(String, String, String, String, String)` - 创建用户 +5. `updateLastActiveTime(String, LocalDateTime)` - 更新最后活跃时间 + +## ✅ 解决方案 + +### 1. 修改UserService接口 + +**文件**: `backend-single/src/main/java/com/emotion/service/UserService.java` + +添加了以下方法定义: + +```java +/** + * 根据账号获取用户 + */ +User getByAccount(String account); + +/** + * 根据邮箱获取用户 + */ +User getByEmail(String email); + +/** + * 根据手机号获取用户 + */ +User getByPhone(String phone); + +/** + * 创建用户 + * + * @param account 账号 + * @param username 用户名 + * @param password 密码(明文,会在方法内加密) + * @param email 邮箱(可为null) + * @param phone 手机号(可为null) + * @return 创建的用户 + */ +User createUser(String account, String username, String password, String email, String phone); + +/** + * 更新用户最后活跃时间 + * + * @param userId 用户ID + * @param lastActiveTime 最后活跃时间 + */ +void updateLastActiveTime(String userId, LocalDateTime lastActiveTime); +``` + +### 2. 实现UserServiceImpl方法 + +**文件**: `backend-single/src/main/java/com/emotion/service/impl/UserServiceImpl.java` + +#### 2.1 getByAccount方法 +```java +@Override +public User getByAccount(String account) { + if (!StringUtils.hasText(account)) { + return null; + } + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(User::getAccount, account) + .eq(User::getIsDeleted, 0); + return this.getOne(wrapper); +} +``` + +#### 2.2 getByEmail方法 +```java +@Override +public User getByEmail(String email) { + if (!StringUtils.hasText(email)) { + return null; + } + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(User::getEmail, email) + .eq(User::getIsDeleted, 0); + return this.getOne(wrapper); +} +``` + +#### 2.3 getByPhone方法 +```java +@Override +public User getByPhone(String phone) { + if (!StringUtils.hasText(phone)) { + return null; + } + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(User::getPhone, phone) + .eq(User::getIsDeleted, 0); + return this.getOne(wrapper); +} +``` + +#### 2.4 createUser方法 +```java +@Override +public User createUser(String account, String username, String password, String email, String phone) { + User user = new User(); + user.setAccount(account); + user.setUsername(username); + user.setNickname(username); // 默认昵称与用户名相同 + user.setPassword(passwordEncoder.encode(password)); // 加密密码 + user.setEmail(email); + user.setPhone(phone); + user.setMemberLevel("free"); // 默认免费会员 + user.setStatus(1); // 默认启用 + user.setIsVerified(0); // 默认未验证 + user.setLastActiveTime(LocalDateTime.now()); + + this.save(user); + return user; +} +``` + +#### 2.5 updateLastActiveTime方法 +```java +@Override +public void updateLastActiveTime(String userId, LocalDateTime lastActiveTime) { + if (!StringUtils.hasText(userId) || lastActiveTime == null) { + return; + } + User user = this.getById(userId); + if (user != null && user.getIsDeleted() == 0) { + user.setLastActiveTime(lastActiveTime); + this.updateById(user); + } +} +``` + +## 🔧 实现细节 + +### 1. 查询方法的共同特点 +- 参数校验:检查参数是否为空 +- 软删除过滤:只查询未删除的用户(`isDeleted = 0`) +- 使用MyBatis-Plus的LambdaQueryWrapper进行类型安全的查询 + +### 2. createUser方法的特点 +- **密码加密**:使用PasswordEncoder加密密码 +- **默认值设置**: + - 昵称默认与用户名相同 + - 会员等级默认为"free" + - 状态默认为1(启用) + - 验证状态默认为0(未验证) + - 最后活跃时间设为当前时间 +- **可选字段**:email和phone可以为null + +### 3. updateLastActiveTime方法的特点 +- 参数校验:检查userId和lastActiveTime是否为空 +- 存在性检查:确保用户存在且未删除 +- 只更新lastActiveTime字段 + +## 📊 编译和启动结果 + +### 编译成功 +```bash +[INFO] BUILD SUCCESS +[INFO] Total time: 9.692 s +[INFO] Finished at: 2025-10-06T21:29:28+08:00 +``` + +### 启动成功 +``` +======================================== +🎉 情感博物馆服务启动成功! +📋 服务信息: + - 服务名称: emotion-single + - 服务端口: 19089 + - 环境配置: local + - API文档: http://localhost:19089/api/health +======================================== +``` + +### 启动日志关键信息 +- ✅ Spring Boot版本: 2.7.18 +- ✅ Java版本: 17.0.15 +- ✅ 端口: 19089 +- ✅ Context Path: /api +- ✅ 环境: local +- ✅ MyBatis Mapper扫描: 17个Mapper成功注册 +- ✅ WebSocket配置: 成功启动 +- ✅ Security配置: 成功加载 + +## 🎯 方法使用场景 + +### 1. getByAccount +**使用场景**: 用户登录、账号唯一性检查 +```java +// AuthServiceImpl.login() +User user = userService.getByAccount(request.getAccount()); +if (user == null) { + throw new AuthException("账号不存在"); +} +``` + +### 2. getByEmail +**使用场景**: 邮箱唯一性检查、邮箱登录 +```java +// AuthServiceImpl.register() +if (StringUtils.hasText(request.getEmail()) && userService.getByEmail(request.getEmail()) != null) { + throw new BusinessException("邮箱已被使用"); +} +``` + +### 3. getByPhone +**使用场景**: 手机号唯一性检查、手机号登录 +```java +// AuthServiceImpl.register() +if (StringUtils.hasText(request.getPhone()) && userService.getByPhone(request.getPhone()) != null) { + throw new BusinessException("手机号已被使用"); +} +``` + +### 4. createUser +**使用场景**: 用户注册 +```java +// AuthServiceImpl.register() +User user = userService.createUser( + request.getAccount(), + username, + request.getPassword(), + email, + phone +); +``` + +### 5. updateLastActiveTime +**使用场景**: 用户登录后更新活跃时间 +```java +// AuthServiceImpl.login() +userService.updateLastActiveTime(user.getId(), LocalDateTime.now()); +``` + +## ✅ 验证清单 + +- [x] 编译成功,无错误 +- [x] 服务启动成功 +- [x] 端口19089正常监听 +- [x] WebSocket配置正常 +- [x] MyBatis Mapper扫描成功 +- [x] Security配置加载成功 +- [x] 所有Controller注册成功 + +## 📝 注意事项 + +### 1. 密码安全 +- createUser方法会自动加密密码 +- 使用PasswordEncoder(BCrypt)进行加密 +- 不要在调用前预先加密密码 + +### 2. 软删除 +- 所有查询方法都过滤了已删除的用户 +- 确保业务逻辑中正确处理软删除 + +### 3. 参数校验 +- 所有方法都进行了参数校验 +- 空值或null会被正确处理 + +### 4. 默认值 +- createUser方法设置了合理的默认值 +- 确保数据库字段允许这些默认值 + +## 🚀 后续建议 + +### 1. 添加单元测试 +```java +@Test +public void testGetByAccount() { + User user = userService.getByAccount("testuser"); + assertNotNull(user); + assertEquals("testuser", user.getAccount()); +} + +@Test +public void testCreateUser() { + User user = userService.createUser( + "newuser", + "New User", + "password123", + "new@example.com", + "13800138000" + ); + assertNotNull(user.getId()); + assertTrue(passwordEncoder.matches("password123", user.getPassword())); +} +``` + +### 2. 添加日志 +在关键方法中添加日志记录: +```java +@Override +public User createUser(String account, String username, String password, String email, String phone) { + log.info("创建用户: account={}, username={}, email={}, phone={}", + account, username, email, phone); + // ... 实现代码 + log.info("用户创建成功: userId={}", user.getId()); + return user; +} +``` + +### 3. 性能优化 +考虑添加缓存: +```java +@Cacheable(value = "users", key = "#account") +public User getByAccount(String account) { + // ... 实现代码 +} +``` + +## 📚 相关文件 + +### 修改的文件 +1. `backend-single/src/main/java/com/emotion/service/UserService.java` + - 添加了5个方法定义 + +2. `backend-single/src/main/java/com/emotion/service/impl/UserServiceImpl.java` + - 实现了5个方法 + +### 依赖的文件 +1. `backend-single/src/main/java/com/emotion/entity/User.java` - 用户实体 +2. `backend-single/src/main/java/com/emotion/mapper/UserMapper.java` - 用户Mapper +3. `backend-single/src/main/java/com/emotion/service/impl/AuthServiceImpl.java` - 认证服务(调用方) + +## 🎉 总结 + +成功修复了backend-single微服务的启动问题: + +1. ✅ **补充了缺失的方法** - 在UserService接口和实现中添加了5个方法 +2. ✅ **遵循了项目规范** - 使用MyBatis-Plus、软删除、参数校验等 +3. ✅ **实现了完整逻辑** - 包括密码加密、默认值设置、错误处理等 +4. ✅ **编译和启动成功** - 服务正常运行在19089端口 +5. ✅ **代码质量良好** - 清晰的注释、合理的实现、完善的校验 + +现在backend-single微服务可以正常启动和运行了! diff --git a/FRONTEND_TOKEN_API_MIGRATION_GUIDE.md b/FRONTEND_TOKEN_API_MIGRATION_GUIDE.md new file mode 100644 index 0000000..bee3377 --- /dev/null +++ b/FRONTEND_TOKEN_API_MIGRATION_GUIDE.md @@ -0,0 +1,307 @@ +# 前端Token API迁移指南 + +## 📋 概述 + +TokenController接口已经从POST请求体传递token的方式优化为GET请求头传递token的标准方式。如果前端需要使用这些接口,请参考以下迁移指南。 + +## ✅ 当前状态 + +经过检查,前端代码中**没有直接使用**以下接口: +- `/token/user-info` +- `/token/username` +- `/token/validate` + +前端主要使用的是AuthController中的接口: +- `/auth/user/info` - 获取用户信息 +- `/auth/username` - 获取用户名 +- `/auth/validateToken` - 验证Token + +因此,**本次优化不需要修改前端代码**。 + +## 📚 接口使用指南(如果将来需要) + +### 1. 获取用户信息 + +#### 旧方式(已废弃) +```typescript +// ❌ 不推荐:POST请求,token在请求体中 +const response = await http.post('/token/user-info', { + token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...' +}) +``` + +#### 新方式(推荐) +```typescript +// ✅ 推荐:GET请求,token在请求头中(自动添加) +const response = await http.get('/token/user-info') + +// 或者手动指定token(特殊场景) +const response = await http.get('/token/user-info', { + headers: { + 'Authorization': `Bearer ${token}` + } +}) +``` + +### 2. 获取用户名 + +#### 旧方式(已废弃) +```typescript +// ❌ 不推荐 +const response = await http.post('/token/username', { + token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...' +}) +``` + +#### 新方式(推荐) +```typescript +// ✅ 推荐 +const response = await http.get('/token/username') +``` + +### 3. 验证Token + +#### 旧方式(已废弃) +```typescript +// ❌ 不推荐 +const response = await http.post('/token/validate', { + token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...' +}) +``` + +#### 新方式(推荐) +```typescript +// ✅ 推荐 +const response = await http.get('/token/validate') +``` + +## 🔧 HTTP请求工具配置 + +### 确保请求拦截器自动添加Token + +大多数项目的HTTP工具已经配置了自动添加Authorization请求头的拦截器: + +```typescript +// utils/request.ts 或 utils/http.ts +import axios from 'axios' + +const http = axios.create({ + baseURL: import.meta.env.VITE_API_BASE_URL, + timeout: 10000 +}) + +// 请求拦截器:自动添加Token +http.interceptors.request.use( + (config) => { + // 从localStorage获取token + const token = localStorage.getItem('access_token') + + // 如果token存在,添加到请求头 + if (token) { + config.headers.Authorization = `Bearer ${token}` + } + + return config + }, + (error) => { + return Promise.reject(error) + } +) + +export { http } +``` + +### 如果项目还没有配置,请添加以下代码 + +```typescript +// 1. 在请求拦截器中添加token +http.interceptors.request.use((config) => { + const token = localStorage.getItem('access_token') + if (token && !config.headers.Authorization) { + config.headers.Authorization = `Bearer ${token}` + } + return config +}) + +// 2. 在响应拦截器中处理401错误 +http.interceptors.response.use( + (response) => response, + async (error) => { + if (error.response?.status === 401) { + // Token过期或无效,跳转到登录页 + localStorage.removeItem('access_token') + window.location.href = '/login' + } + return Promise.reject(error) + } +) +``` + +## 📝 API服务封装示例 + +如果需要使用TokenController的接口,可以创建以下服务: + +```typescript +// services/token.ts +import { http } from '@/utils/request' +import type { UserInfo } from '@/types' + +export const tokenApi = { + /** + * 获取用户信息 + * Token会自动从请求头中获取 + */ + getUserInfo(): Promise { + return http.get('/token/user-info') + }, + + /** + * 获取用户名 + * Token会自动从请求头中获取 + */ + getUsername(): Promise { + return http.get('/token/username') + }, + + /** + * 验证Token + * Token会自动从请求头中获取 + */ + validateToken(): Promise { + return http.get('/token/validate') + } +} +``` + +## 🎯 推荐使用AuthController接口 + +实际上,建议前端继续使用AuthController中的接口,因为它们提供了更完整的功能: + +```typescript +// services/auth.ts +import { http } from '@/utils/request' + +export const authApi = { + /** + * 获取当前用户信息 + * 推荐使用这个接口而不是 /token/user-info + */ + getUserInfo(): Promise { + return http.get('/auth/user/info') + }, + + /** + * 获取用户名 + * 推荐使用这个接口而不是 /token/username + */ + getUsername(): Promise { + return http.get('/auth/username') + }, + + /** + * 验证Token + * 推荐使用这个接口而不是 /token/validate + */ + validateToken(): Promise { + return http.get('/auth/validateToken') + } +} +``` + +## 🔍 接口对比 + +| 功能 | TokenController | AuthController | 推荐使用 | +|------|----------------|----------------|---------| +| 获取用户信息 | `GET /token/user-info` | `GET /auth/user/info` | AuthController | +| 获取用户名 | `GET /token/username` | `GET /auth/username` | AuthController | +| 验证Token | `GET /token/validate` | `GET /auth/validateToken` | AuthController | + +**推荐理由**: +- AuthController接口功能更完整 +- 已经在项目中广泛使用 +- 有更好的错误处理和日志记录 +- 与认证流程集成更紧密 + +## ⚠️ 注意事项 + +### 1. Token存储位置 +确保token存储在正确的位置: +```typescript +// 登录成功后存储token +localStorage.setItem('access_token', response.accessToken) + +// 登出时清除token +localStorage.removeItem('access_token') +``` + +### 2. Token格式 +确保token以正确的格式发送: +``` +Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... +``` + +### 3. 错误处理 +处理token相关的错误: +```typescript +try { + const userInfo = await tokenApi.getUserInfo() +} catch (error) { + if (error.response?.status === 401) { + // Token无效或过期 + console.error('Token无效,请重新登录') + // 跳转到登录页 + router.push('/login') + } +} +``` + +### 4. Token刷新 +实现token自动刷新机制: +```typescript +http.interceptors.response.use( + (response) => response, + async (error) => { + const originalRequest = error.config + + if (error.response?.status === 401 && !originalRequest._retry) { + originalRequest._retry = true + + try { + // 尝试刷新token + const refreshToken = localStorage.getItem('refresh_token') + const response = await authApi.refreshToken({ refreshToken }) + + // 更新token + localStorage.setItem('access_token', response.accessToken) + + // 重试原始请求 + originalRequest.headers.Authorization = `Bearer ${response.accessToken}` + return http(originalRequest) + } catch (refreshError) { + // 刷新失败,跳转到登录页 + localStorage.clear() + window.location.href = '/login' + return Promise.reject(refreshError) + } + } + + return Promise.reject(error) + } +) +``` + +## ✅ 总结 + +1. **当前状态**:前端代码不需要修改,因为没有使用TokenController接口 +2. **推荐做法**:继续使用AuthController接口 +3. **如果需要使用TokenController**: + - 使用GET方法 + - Token在请求头中自动传递 + - 确保HTTP工具配置了请求拦截器 +4. **最佳实践**: + - Token存储在localStorage + - 使用Bearer格式 + - 实现自动刷新机制 + - 处理401错误 + +这次优化使后端接口更加标准化和安全,为将来的扩展打下了良好的基础! diff --git a/TOKEN_CONTROLLER_OPTIMIZATION.md b/TOKEN_CONTROLLER_OPTIMIZATION.md new file mode 100644 index 0000000..a52a67e --- /dev/null +++ b/TOKEN_CONTROLLER_OPTIMIZATION.md @@ -0,0 +1,337 @@ +# TokenController优化总结 + +## 🎯 优化目标 + +将TokenController从使用请求体传递token的方式改为标准的从请求头自动获取token的方式,符合RESTful API最佳实践。 + +## ✅ 完成的优化 + +### 1. 修改TokenController接口 + +#### 优化前 +```java +@PostMapping("/user-info") +public Result getUserInfoByToken(@RequestBody @Validated TokenRequest request) { + UserInfoResponse userInfo = tokenService.getUserInfoByToken(request.getToken()); + return Result.success(userInfo); +} +``` + +**问题**: +- ❌ 使用POST方法获取数据(不符合RESTful规范) +- ❌ Token在请求体中传递(不安全,不标准) +- ❌ 需要额外的TokenRequest类封装 +- ❌ 客户端需要构造请求体 + +#### 优化后 +```java +@GetMapping("/user-info") +@Operation(summary = "获取用户信息", description = "通过请求头中的token获取当前用户信息") +public Result getUserInfoByToken(HttpServletRequest request) { + UserInfoResponse userInfo = tokenService.getUserInfoByToken(request); + return Result.success(userInfo); +} +``` + +**改进**: +- ✅ 使用GET方法获取数据(符合RESTful规范) +- ✅ Token从请求头Authorization自动提取 +- ✅ 不需要额外的Request类 +- ✅ 客户端只需设置请求头 + +### 2. 删除TokenRequest类 + +**删除的文件**: +- `backend-single/src/main/java/com/emotion/dto/request/TokenRequest.java` + +**原因**: +- 不再需要通过请求体传递token +- 简化代码结构 +- 减少不必要的类 + +### 3. 添加API文档注解 + +```java +@Tag(name = "Token管理", description = "Token验证和用户信息获取") +public class TokenController { + + @GetMapping("/user-info") + @Operation(summary = "获取用户信息", description = "通过请求头中的token获取当前用户信息") + public Result getUserInfoByToken(HttpServletRequest request) { + // ... + } +} +``` + +## 📊 接口变更对比 + +### 接口1: 获取用户信息 + +#### 优化前 +- **请求方式**: `POST /token/user-info` +- **请求头**: 无 +- **请求体**: + ```json + { + "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." + } + ``` + +#### 优化后 +- **请求方式**: `GET /token/user-info` +- **请求头**: + ``` + Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... + ``` +- **请求体**: 无 + +--- + +### 接口2: 获取用户名 + +#### 优化前 +- **请求方式**: `POST /token/username` +- **请求头**: 无 +- **请求体**: + ```json + { + "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." + } + ``` + +#### 优化后 +- **请求方式**: `GET /token/username` +- **请求头**: + ``` + Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... + ``` +- **请求体**: 无 + +--- + +### 接口3: 验证Token + +#### 优化前 +- **请求方式**: `POST /token/validate` +- **请求头**: 无 +- **请求体**: + ```json + { + "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." + } + ``` + +#### 优化后 +- **请求方式**: `GET /token/validate` +- **请求头**: + ``` + Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... + ``` +- **请求体**: 无 + +## 🔧 Token提取机制 + +TokenUtil已经实现了标准的token提取逻辑: + +```java +public String extractToken(HttpServletRequest request) { + // 1. 优先从Authorization请求头获取(标准方式) + String authHeader = request.getHeader("Authorization"); + if (authHeader != null && authHeader.startsWith("Bearer ")) { + return authHeader.substring(7); + } + + // 2. 备用方案:从请求参数获取 + String tokenParam = request.getParameter("token"); + if (tokenParam != null && !tokenParam.trim().isEmpty()) { + return tokenParam.trim(); + } + + return null; +} +``` + +**支持的Token传递方式**: +1. **标准方式**(推荐):`Authorization: Bearer {token}` +2. **备用方式**:URL参数 `?token={token}` + +## 🚀 前端调用示例 + +### 优化前(不推荐) +```typescript +// 需要在请求体中传递token +const response = await http.post('/token/user-info', { + token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...' +}) +``` + +### 优化后(推荐) +```typescript +// Token自动从localStorage获取并添加到请求头 +const response = await http.get('/token/user-info') + +// 或者手动设置(如果需要) +const response = await http.get('/token/user-info', { + headers: { + 'Authorization': `Bearer ${token}` + } +}) +``` + +## 📝 优化收益 + +### 1. 符合标准规范 +- ✅ 遵循RESTful API设计原则 +- ✅ 使用标准的Authorization请求头 +- ✅ GET方法用于查询,POST方法用于修改 + +### 2. 提升安全性 +- ✅ Token不在请求体中暴露 +- ✅ 减少token在日志中被记录的风险 +- ✅ 符合OAuth 2.0和JWT标准 + +### 3. 简化代码 +- ✅ 删除不必要的TokenRequest类 +- ✅ 减少代码维护成本 +- ✅ 统一token处理方式 + +### 4. 改善开发体验 +- ✅ 前端不需要构造请求体 +- ✅ 自动从请求头提取token +- ✅ 更清晰的API文档 + +### 5. 提高可维护性 +- ✅ 代码结构更清晰 +- ✅ 职责分离更明确 +- ✅ 便于后续扩展 + +## 🔍 相关文件变更 + +### 修改的文件 +1. `backend-single/src/main/java/com/emotion/controller/TokenController.java` + - 修改所有接口从POST改为GET + - 参数从`@RequestBody TokenRequest`改为`HttpServletRequest` + - 添加Swagger API文档注解 + - 添加详细的注释说明 + +### 删除的文件 +1. `backend-single/src/main/java/com/emotion/dto/request/TokenRequest.java` + - 不再需要此类 + +### 未修改的文件 +1. `TokenService.java` - 接口已经是标准方式 +2. `TokenServiceImpl.java` - 实现已经是标准方式 +3. `TokenUtil.java` - 工具类已经支持标准方式 + +## ✅ 测试验证 + +### 测试用例 + +#### 1. 测试获取用户信息 +```bash +curl -X GET http://localhost:8080/token/user-info \ + -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." +``` + +**预期结果**: +```json +{ + "code": 200, + "message": "操作成功", + "data": { + "userId": "user_123", + "username": "testuser", + "email": "test@example.com" + } +} +``` + +#### 2. 测试获取用户名 +```bash +curl -X GET http://localhost:8080/token/username \ + -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." +``` + +**预期结果**: +```json +{ + "code": 200, + "message": "操作成功", + "data": "testuser" +} +``` + +#### 3. 测试验证Token +```bash +curl -X GET http://localhost:8080/token/validate \ + -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." +``` + +**预期结果**: +```json +{ + "code": 200, + "message": "操作成功", + "data": "user_123" +} +``` + +#### 4. 测试无效Token +```bash +curl -X GET http://localhost:8080/token/user-info \ + -H "Authorization: Bearer invalid_token" +``` + +**预期结果**: +```json +{ + "code": 401, + "message": "访问令牌无效或已过期", + "data": null +} +``` + +#### 5. 测试缺少Token +```bash +curl -X GET http://localhost:8080/token/user-info +``` + +**预期结果**: +```json +{ + "code": 401, + "message": "未提供访问令牌", + "data": null +} +``` + +## 📚 最佳实践 + +### 1. Token传递方式 +- ✅ **推荐**:使用`Authorization: Bearer {token}`请求头 +- ⚠️ **备用**:使用URL参数`?token={token}`(仅在特殊场景) +- ❌ **不推荐**:在请求体中传递token + +### 2. HTTP方法选择 +- ✅ **GET**:获取资源(如获取用户信息、验证token) +- ✅ **POST**:创建资源(如登录、注册) +- ✅ **PUT/PATCH**:更新资源 +- ✅ **DELETE**:删除资源 + +### 3. 安全建议 +- 使用HTTPS传输token +- 设置合理的token过期时间 +- 实现token刷新机制 +- 记录token使用日志 + +## 🎯 总结 + +通过这次优化,TokenController已经完全符合RESTful API标准和安全最佳实践: + +1. ✅ **标准化**:使用标准的Authorization请求头传递token +2. ✅ **简化**:删除不必要的TokenRequest类 +3. ✅ **规范化**:GET方法用于查询操作 +4. ✅ **文档化**:添加完整的Swagger API文档 +5. ✅ **安全性**:Token不在请求体中暴露 + +现在TokenController的实现更加专业、安全、易用! diff --git a/TOKEN_CONTROLLER_REFACTOR_SUMMARY.md b/TOKEN_CONTROLLER_REFACTOR_SUMMARY.md new file mode 100644 index 0000000..259d981 --- /dev/null +++ b/TOKEN_CONTROLLER_REFACTOR_SUMMARY.md @@ -0,0 +1,275 @@ +# TokenController重构总结 + +## 🎯 重构目标 + +优化 `com.emotion.controller.TokenController` 中的相关接口,将token从请求体传递改为标准的从请求头自动获取的方式,删除不必要的 `TokenRequest` 类及相关引用。 + +## ✅ 完成的工作 + +### 1. 修改TokenController + +**文件**: `backend-single/src/main/java/com/emotion/controller/TokenController.java` + +#### 主要变更 + +| 变更项 | 修改前 | 修改后 | +|--------|--------|--------| +| 请求方式 | `@PostMapping` | `@GetMapping` | +| 参数类型 | `@RequestBody TokenRequest` | `HttpServletRequest` | +| Token获取 | `request.getToken()` | 自动从请求头提取 | +| 导入包 | `TokenRequest` | `HttpServletRequest` | +| API文档 | 无 | 添加Swagger注解 | + +#### 具体修改 + +```java +// 修改前 +@PostMapping("/user-info") +public Result getUserInfoByToken(@RequestBody @Validated TokenRequest request) { + UserInfoResponse userInfo = tokenService.getUserInfoByToken(request.getToken()); + return Result.success(userInfo); +} + +// 修改后 +@GetMapping("/user-info") +@Operation(summary = "获取用户信息", description = "通过请求头中的token获取当前用户信息") +public Result getUserInfoByToken(HttpServletRequest request) { + UserInfoResponse userInfo = tokenService.getUserInfoByToken(request); + return Result.success(userInfo); +} +``` + +### 2. 删除TokenRequest类 + +**删除的文件**: `backend-single/src/main/java/com/emotion/dto/request/TokenRequest.java` + +**原因**: +- 不再需要通过请求体传递token +- 简化代码结构 +- 减少不必要的类维护 + +### 3. 添加API文档注解 + +为TokenController添加了完整的Swagger文档注解: + +```java +@RestController +@RequestMapping("/token") +@Tag(name = "Token管理", description = "Token验证和用户信息获取") +public class TokenController { + + @GetMapping("/user-info") + @Operation(summary = "获取用户信息", description = "通过请求头中的token获取当前用户信息") + public Result getUserInfoByToken(HttpServletRequest request) { + // ... + } + + @GetMapping("/username") + @Operation(summary = "获取用户名", description = "通过请求头中的token获取当前用户名") + public Result getUsernameByToken(HttpServletRequest request) { + // ... + } + + @GetMapping("/validate") + @Operation(summary = "验证Token", description = "验证请求头中的token并返回用户ID") + public Result validateTokenAndGetUserId(HttpServletRequest request) { + // ... + } +} +``` + +## 📊 接口变更详情 + +### 接口1: 获取用户信息 + +| 项目 | 修改前 | 修改后 | +|------|--------|--------| +| 路径 | `/token/user-info` | `/token/user-info` | +| 方法 | POST | GET | +| 请求头 | 无 | `Authorization: Bearer {token}` | +| 请求体 | `{"token": "..."}` | 无 | +| 响应 | 相同 | 相同 | + +### 接口2: 获取用户名 + +| 项目 | 修改前 | 修改后 | +|------|--------|--------| +| 路径 | `/token/username` | `/token/username` | +| 方法 | POST | GET | +| 请求头 | 无 | `Authorization: Bearer {token}` | +| 请求体 | `{"token": "..."}` | 无 | +| 响应 | 相同 | 相同 | + +### 接口3: 验证Token + +| 项目 | 修改前 | 修改后 | +|------|--------|--------| +| 路径 | `/token/validate` | `/token/validate` | +| 方法 | POST | GET | +| 请求头 | 无 | `Authorization: Bearer {token}` | +| 请求体 | `{"token": "..."}` | 无 | +| 响应 | 相同 | 相同 | + +## 🔧 技术实现 + +### Token提取机制 + +TokenUtil已经实现了标准的token提取逻辑: + +```java +public String extractToken(HttpServletRequest request) { + // 1. 优先从Authorization请求头获取(标准方式) + String authHeader = request.getHeader("Authorization"); + if (authHeader != null && authHeader.startsWith("Bearer ")) { + return authHeader.substring(7); + } + + // 2. 备用方案:从请求参数获取 + String tokenParam = request.getParameter("token"); + if (tokenParam != null && !tokenParam.trim().isEmpty()) { + return tokenParam.trim(); + } + + return null; +} +``` + +### TokenService接口 + +TokenService接口已经是标准方式,无需修改: + +```java +public interface TokenService { + UserInfoResponse getUserInfoByToken(HttpServletRequest request); + String getUsernameByToken(HttpServletRequest request); + String validateTokenAndGetUserId(HttpServletRequest request); +} +``` + +### TokenServiceImpl实现 + +TokenServiceImpl实现已经是标准方式,无需修改: + +```java +@Override +public UserInfoResponse getUserInfoByToken(HttpServletRequest request) { + String userId = validateTokenAndGetUserId(request); + return authService.getCurrentUserInfo(userId); +} +``` + +## 📝 影响分析 + +### 后端影响 + +✅ **无破坏性影响** +- TokenService接口和实现已经是标准方式 +- 只修改了Controller层的参数接收方式 +- 业务逻辑完全不变 + +### 前端影响 + +✅ **无需修改前端代码** +- 经过检查,前端代码中没有使用这些接口 +- 前端主要使用AuthController中的接口 +- 如果将来需要使用,参考迁移指南即可 + +### 数据库影响 + +✅ **无影响** +- 不涉及数据库结构变更 +- 不涉及数据迁移 + +## 🚀 优化收益 + +### 1. 符合标准规范 +- ✅ 遵循RESTful API设计原则 +- ✅ 使用标准的Authorization请求头 +- ✅ GET方法用于查询,POST方法用于修改 + +### 2. 提升安全性 +- ✅ Token不在请求体中暴露 +- ✅ 减少token在日志中被记录的风险 +- ✅ 符合OAuth 2.0和JWT标准 + +### 3. 简化代码 +- ✅ 删除不必要的TokenRequest类 +- ✅ 减少代码维护成本 +- ✅ 统一token处理方式 + +### 4. 改善开发体验 +- ✅ 前端不需要构造请求体 +- ✅ 自动从请求头提取token +- ✅ 更清晰的API文档 + +### 5. 提高可维护性 +- ✅ 代码结构更清晰 +- ✅ 职责分离更明确 +- ✅ 便于后续扩展 + +## 📚 相关文档 + +1. **TOKEN_CONTROLLER_OPTIMIZATION.md** - 详细的优化说明 +2. **FRONTEND_TOKEN_API_MIGRATION_GUIDE.md** - 前端迁移指南(如果需要) + +## ✅ 测试建议 + +### 1. 单元测试 +```java +@Test +public void testGetUserInfoByToken() { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addHeader("Authorization", "Bearer " + validToken); + + Result result = tokenController.getUserInfoByToken(request); + + assertEquals(200, result.getCode()); + assertNotNull(result.getData()); +} +``` + +### 2. 集成测试 +```bash +# 测试获取用户信息 +curl -X GET http://localhost:8080/token/user-info \ + -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." + +# 测试获取用户名 +curl -X GET http://localhost:8080/token/username \ + -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." + +# 测试验证Token +curl -X GET http://localhost:8080/token/validate \ + -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." +``` + +### 3. 错误场景测试 +```bash +# 测试无效Token +curl -X GET http://localhost:8080/token/user-info \ + -H "Authorization: Bearer invalid_token" + +# 测试缺少Token +curl -X GET http://localhost:8080/token/user-info +``` + +## 🎯 总结 + +本次重构成功完成了以下目标: + +1. ✅ **标准化Token传递方式** - 从请求体改为请求头 +2. ✅ **简化代码结构** - 删除不必要的TokenRequest类 +3. ✅ **符合RESTful规范** - 使用GET方法获取资源 +4. ✅ **提升安全性** - Token不在请求体中暴露 +5. ✅ **完善API文档** - 添加Swagger注解 +6. ✅ **无破坏性影响** - 前端代码无需修改 + +重构后的TokenController更加专业、安全、易用,完全符合现代Web API的最佳实践! + +## 📅 重构信息 + +- **重构日期**: 2025-10-06 +- **重构人**: Augment Agent +- **影响范围**: TokenController及TokenRequest类 +- **向后兼容**: 是(TokenUtil支持URL参数备用方案) +- **需要前端修改**: 否 diff --git a/backend-single/deploy-server.sh b/backend-single/deploy-server.sh index 58db44d..428295f 100755 --- a/backend-single/deploy-server.sh +++ b/backend-single/deploy-server.sh @@ -1,18 +1,22 @@ #!/bin/bash -# 情绪博物馆后端服务部署脚本 -# 使用生产环境配置,日志保存到 /data/logs/emotion-museum/single +# 情绪博物馆后端服务远程部署脚本 +# 在远程服务器上执行的服务部署脚本 set -e # 配置变量 APP_NAME="emotion-museum-single" JAR_NAME="emotion-single-1.0.0.jar" -JAR_PATH="./${JAR_NAME}" -LOG_DIR="./logs/" +DEPLOY_DIR="/data/programs/emotion-museum" +LOG_DIR="/data/logs/emotion-museum" +JAR_PATH="$DEPLOY_DIR/$JAR_NAME" PID_FILE="/tmp/${APP_NAME}.pid" JAVA_OPTS="-Xms512m -Xmx1024m -XX:+UseG1GC -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=${LOG_DIR}/heapdump.hprof" +# 获取 Spring Profile 参数 +SPRING_PROFILE=${1:-test} + # 颜色输出 RED='\033[0;31m' GREEN='\033[0;32m' @@ -36,20 +40,11 @@ log_error() { check_jar() { if [ ! -f "$JAR_PATH" ]; then log_error "JAR 文件不存在: $JAR_PATH" - log_info "请先执行打包命令: mvn clean package -Pprod" exit 1 fi log_info "JAR 文件检查通过: $JAR_PATH" } -# 创建日志目录 -create_log_dir() { - if [ ! -d "$LOG_DIR" ]; then - log_info "创建日志目录: $LOG_DIR" - mkdir -p "$LOG_DIR" - fi -} - # 停止旧服务 stop_service() { if [ -f "$PID_FILE" ]; then @@ -85,11 +80,14 @@ stop_service() { start_service() { log_info "启动新服务..." + # 检查JAR文件 + check_jar + # 启动命令 nohup java $JAVA_OPTS \ - -Dspring.profiles.active=prod \ + -Dspring.profiles.active=$SPRING_PROFILE \ -Dlogging.file.path=$LOG_DIR \ - -Dlogging.file.name=$LOG_DIR/application.log \ + -Dlogging.file.name=$LOG_DIR/emotion-single.log \ -jar "$JAR_PATH" \ > "$LOG_DIR/startup.log" 2>&1 & @@ -98,7 +96,7 @@ start_service() { log_info "服务启动中,PID: $(cat $PID_FILE)" log_info "启动日志: $LOG_DIR/startup.log" - log_info "应用日志: $LOG_DIR/application.log" + log_info "应用日志: $LOG_DIR/emotion-single.log" } # 检查服务状态 @@ -123,8 +121,8 @@ wait_for_startup() { log_info "等待服务启动..." for i in {1..60}; do if check_status > /dev/null 2>&1; then - # 检查端口是否监听(假设使用 8080 端口) - if netstat -tlnp 2>/dev/null | grep -q ":8080.*LISTEN"; then + # 检查端口是否监听(使用 19089 端口) + if netstat -tlnp 2>/dev/null | grep -q ":19089.*LISTEN"; then log_info "服务启动成功!" return 0 fi @@ -141,19 +139,15 @@ show_info() { log_info "=== 服务信息 ===" log_info "应用名称: $APP_NAME" log_info "JAR 文件: $JAR_PATH" + log_info "部署目录: $DEPLOY_DIR" log_info "日志目录: $LOG_DIR" log_info "PID 文件: $PID_FILE" log_info "Java 参数: $JAVA_OPTS" + log_info "Spring Profile: $SPRING_PROFILE" if check_status > /dev/null 2>&1; then PID=$(cat "$PID_FILE") log_info "服务状态: 运行中 (PID: $PID)" - - # 显示内存使用情况 - if command -v jstat > /dev/null 2>&1; then - log_info "内存使用情况:" - jstat -gc "$PID" | head -2 - fi else log_info "服务状态: 未运行" fi @@ -161,13 +155,11 @@ show_info() { # 主函数 main() { - log_info "开始部署 $APP_NAME 服务..." - - # 检查 jar 文件 - check_jar + log_info "开始在服务器上部署 $APP_NAME 服务..." # 创建日志目录 - create_log_dir + log_info "创建日志目录: $LOG_DIR" + mkdir -p "$LOG_DIR" # 停止旧服务 stop_service @@ -177,54 +169,13 @@ main() { # 等待启动 if wait_for_startup; then - log_info "部署成功!" + log_info "服务器部署成功!" show_info else - log_error "部署失败!" + log_error "服务器部署失败!" exit 1 fi } -# 处理命令行参数 -case "${1:-deploy}" in - "deploy") - main - ;; - "start") - check_jar - create_log_dir - start_service - wait_for_startup - ;; - "stop") - stop_service - ;; - "restart") - stop_service - sleep 2 - check_jar - create_log_dir - start_service - wait_for_startup - ;; - "status") - show_info - ;; - "logs") - if [ -f "$LOG_DIR/application.log" ]; then - tail -f "$LOG_DIR/application.log" - else - log_error "日志文件不存在: $LOG_DIR/application.log" - fi - ;; - *) - echo "用法: $0 {deploy|start|stop|restart|status|logs}" - echo " deploy - 部署服务(默认)" - echo " start - 启动服务" - echo " stop - 停止服务" - echo " restart - 重启服务" - echo " status - 查看服务状态" - echo " logs - 查看实时日志" - exit 1 - ;; -esac \ No newline at end of file +# 执行主函数 +main \ No newline at end of file diff --git a/backend-single/deploy.sh b/backend-single/deploy.sh index 8596bb1..05b9d2e 100755 --- a/backend-single/deploy.sh +++ b/backend-single/deploy.sh @@ -1,4 +1,330 @@ #!/bin/bash + +# 情绪博物馆后端服务部署脚本 +# 支持本地部署和远程部署到服务器 101.200.208.45 + set -e -mvn clean package -Pprod -echo "后端已打包,target 目录下的 jar 包可部署到服务器" + +# 配置变量 +APP_NAME="emotion-museum-single" +JAR_NAME="emotion-single-1.0.0.jar" +JAR_PATH="./target/${JAR_NAME}" +LOG_DIR="./logs/" +PID_FILE="/tmp/${APP_NAME}.pid" +JAVA_OPTS="-Xms512m -Xmx1024m -XX:+UseG1GC -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=${LOG_DIR}/heapdump.hprof" + +# 远程服务器配置 +REMOTE_HOST="101.200.208.45" +REMOTE_USER="root" +REMOTE_DIR="/data/programs/emotion-museum" +REMOTE_LOG_DIR="/data/logs/emotion-museum" +SPRING_PROFILE="test" + +# 颜色输出 +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +# 日志函数 +log_info() { + echo -e "${GREEN}[INFO]${NC} $1" +} + +log_warn() { + echo -e "${YELLOW}[WARN]${NC} $1" +} + +log_error() { + echo -e "${RED}[ERROR]${NC} $1" +} + +# 检查并构建项目 +build_project() { + log_info "检查项目构建状态..." + + # 检查是否已经构建过并且JAR文件存在 + if [ -f "$JAR_PATH" ]; then + log_info "JAR 文件已存在: $JAR_PATH" + return 0 + fi + + # 执行Maven构建 + log_info "开始构建项目..." + if command -v mvn > /dev/null 2>&1; then + mvn clean package -DskipTests + if [ -f "$JAR_PATH" ]; then + log_info "项目构建成功: $JAR_PATH" + return 0 + else + log_error "项目构建失败,未找到JAR文件: $JAR_PATH" + exit 1 + fi + else + log_error "未找到Maven命令,请确保已安装Maven" + exit 1 + fi +} + +# 检查 jar 文件是否存在 +check_jar() { + if [ ! -f "$JAR_PATH" ]; then + log_error "JAR 文件不存在: $JAR_PATH" + log_info "请先执行打包命令: mvn clean package" + exit 1 + fi + log_info "JAR 文件检查通过: $JAR_PATH" +} + +# 创建日志目录 +create_log_dir() { + if [ ! -d "$LOG_DIR" ]; then + log_info "创建日志目录: $LOG_DIR" + mkdir -p "$LOG_DIR" + fi +} + +# 本地部署 - 停止旧服务 +stop_local_service() { + if [ -f "$PID_FILE" ]; then + PID=$(cat "$PID_FILE") + if ps -p "$PID" > /dev/null 2>&1; then + log_info "停止旧服务 (PID: $PID)" + kill "$PID" + + # 等待服务停止 + for i in {1..30}; do + if ! ps -p "$PID" > /dev/null 2>&1; then + log_info "服务已停止" + break + fi + sleep 1 + done + + # 强制停止 + if ps -p "$PID" > /dev/null 2>&1; then + log_warn "强制停止服务 (PID: $PID)" + kill -9 "$PID" + fi + else + log_warn "PID 文件存在但进程不存在,清理 PID 文件" + fi + rm -f "$PID_FILE" + else + log_info "没有找到 PID 文件,服务可能未运行" + fi +} + +# 本地部署 - 启动新服务 +start_local_service() { + log_info "启动本地服务..." + + # 启动命令 + nohup java $JAVA_OPTS \ + -Dspring.profiles.active=$SPRING_PROFILE \ + -Dlogging.file.path=$LOG_DIR \ + -Dlogging.file.name=$LOG_DIR/application.log \ + -jar "$JAR_PATH" \ + > "$LOG_DIR/startup.log" 2>&1 & + + # 保存 PID + echo $! > "$PID_FILE" + + log_info "服务启动中,PID: $(cat $PID_FILE)" + log_info "启动日志: $LOG_DIR/startup.log" + log_info "应用日志: $LOG_DIR/application.log" +} + +# 远程部署 - 上传文件到服务器 +deploy_to_remote() { + log_info "开始远程部署到 $REMOTE_HOST..." + + # 检查并构建项目 + build_project + + # 检查 jar 文件 + check_jar + + # 创建远程目录 + log_info "创建远程目录..." + ssh $REMOTE_USER@$REMOTE_HOST "mkdir -p $REMOTE_DIR" + ssh $REMOTE_USER@$REMOTE_HOST "mkdir -p $REMOTE_LOG_DIR" + + # 上传 jar 文件 + log_info "上传 JAR 文件到远程服务器..." + scp "$JAR_PATH" $REMOTE_USER@$REMOTE_HOST:$REMOTE_DIR/ + + # 上传部署脚本 + log_info "上传部署脚本到远程服务器..." + scp "./deploy-server.sh" $REMOTE_USER@$REMOTE_HOST:$REMOTE_DIR/ + + # 设置权限 + ssh $REMOTE_USER@$REMOTE_HOST "chmod +x $REMOTE_DIR/deploy-server.sh" + + # 在远程服务器上执行部署 + log_info "在远程服务器上执行部署..." + ssh $REMOTE_USER@$REMOTE_HOST "cd $REMOTE_DIR && ./deploy-server.sh $SPRING_PROFILE" + + log_info "远程部署完成!" +} + +# 本地部署 - 检查服务状态 +check_local_status() { + if [ -f "$PID_FILE" ]; then + PID=$(cat "$PID_FILE") + if ps -p "$PID" > /dev/null 2>&1; then + log_info "服务运行中 (PID: $PID)" + return 0 + else + log_error "PID 文件存在但进程不存在" + return 1 + fi + else + log_error "PID 文件不存在,服务未运行" + return 1 + fi +} + +# 本地部署 - 等待服务启动 +wait_for_local_startup() { + log_info "等待服务启动..." + for i in {1..60}; do + if check_local_status > /dev/null 2>&1; then + # 检查端口是否监听(使用 19089 端口) + if netstat -tlnp 2>/dev/null | grep -q ":19089.*LISTEN"; then + log_info "服务启动成功!" + return 0 + fi + fi + sleep 2 + done + + log_error "服务启动超时,请检查日志: $LOG_DIR/startup.log" + return 1 +} + +# 本地部署 - 显示服务信息 +show_local_info() { + log_info "=== 本地服务信息 ===" + log_info "应用名称: $APP_NAME" + log_info "JAR 文件: $JAR_PATH" + log_info "日志目录: $LOG_DIR" + log_info "PID 文件: $PID_FILE" + log_info "Java 参数: $JAVA_OPTS" + log_info "Spring Profile: $SPRING_PROFILE" + + if check_local_status > /dev/null 2>&1; then + PID=$(cat "$PID_FILE") + log_info "服务状态: 运行中 (PID: $PID)" + + # 显示内存使用情况 + if command -v jstat > /dev/null 2>&1; then + log_info "内存使用情况:" + jstat -gc "$PID" | head -2 + fi + else + log_info "服务状态: 未运行" + fi +} + +# 远程部署 - 显示服务信息 +show_remote_info() { + log_info "=== 远程服务信息 ===" + log_info "服务器地址: $REMOTE_HOST" + log_info "部署目录: $REMOTE_DIR" + log_info "日志目录: $REMOTE_LOG_DIR" + log_info "Spring Profile: $SPRING_PROFILE" + + # 检查远程服务状态 + log_info "检查远程服务状态..." + ssh $REMOTE_USER@$REMOTE_HOST "ps aux | grep $JAR_NAME | grep -v grep" || log_info "远程服务未运行" +} + +# 本地部署 - 主函数 +local_deploy() { + log_info "开始本地部署 $APP_NAME 服务..." + + # 检查并构建项目 + build_project + + # 检查 jar 文件 + check_jar + + # 创建日志目录 + create_log_dir + + # 停止旧服务 + stop_local_service + + # 启动新服务 + start_local_service + + # 等待启动 + if wait_for_local_startup; then + log_info "本地部署成功!" + show_local_info + else + log_error "本地部署失败!" + exit 1 + fi +} + +# 处理命令行参数 +case "${1:-deploy}" in + "deploy") + local_deploy + ;; + "remote") + deploy_to_remote + ;; + "build") + build_project + ;; + "start") + # 检查并构建项目 + build_project + check_jar + create_log_dir + start_local_service + wait_for_local_startup + ;; + "stop") + stop_local_service + ;; + "restart") + stop_local_service + sleep 2 + # 检查并构建项目 + build_project + check_jar + create_log_dir + start_local_service + wait_for_local_startup + ;; + "status") + show_local_info + ;; + "remote-status") + show_remote_info + ;; + "logs") + if [ -f "$LOG_DIR/application.log" ]; then + tail -f "$LOG_DIR/application.log" + else + log_error "日志文件不存在: $LOG_DIR/application.log" + 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 - 查看本地实时日志" + exit 1 + ;; +esac \ No newline at end of file diff --git a/backend-single/src/main/java/com/emotion/controller/AuthController.java b/backend-single/src/main/java/com/emotion/controller/AuthController.java index 208232b..d5d9735 100644 --- a/backend-single/src/main/java/com/emotion/controller/AuthController.java +++ b/backend-single/src/main/java/com/emotion/controller/AuthController.java @@ -6,10 +6,14 @@ import com.emotion.dto.request.RegisterRequest; import com.emotion.dto.request.RefreshTokenRequest; import com.emotion.dto.response.AuthResponse; import com.emotion.dto.response.CaptchaResponse; +import com.emotion.dto.response.SmsCodeResponse; import com.emotion.dto.response.UserInfoResponse; import com.emotion.service.AuthService; import com.emotion.service.TokenService; import com.emotion.util.UserContextUtils; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; @@ -24,6 +28,7 @@ import javax.validation.Valid; */ @RestController @RequestMapping("/auth") +@Tag(name = "认证管理", description = "用户注册、登录、验证码等认证相关接口") public class AuthController { @Autowired @@ -33,18 +38,20 @@ public class AuthController { private TokenService tokenService; /** - * 用户登录 + * 用户登录(简化版:手机号+验证码,不存在则自动注册) */ @PostMapping("/login") + @Operation(summary = "用户登录", description = "使用手机号和短信验证码登录,若用户不存在则自动注册") public Result login(@Valid @RequestBody LoginRequest request) { AuthResponse response = authService.login(request); return Result.success("登录成功", response); } /** - * 用户注册 + * 用户注册(简化版:仅需手机号、密码和短信验证码) */ @PostMapping("/register") + @Operation(summary = "用户注册", description = "使用手机号、密码和短信验证码进行注册") public Result register(@Valid @RequestBody RegisterRequest request) { AuthResponse response = authService.register(request); return Result.success("注册成功", response); @@ -60,14 +67,27 @@ public class AuthController { } /** - * 生成验证码 + * 生成验证码(图形验证码,用于登录) */ @GetMapping("/captcha") + @Operation(summary = "获取图形验证码", description = "用于登录时的图形验证码") public Result generateCaptcha() { CaptchaResponse response = authService.generateCaptcha(); return Result.success(response); } + /** + * 获取短信验证码(用于注册) + */ + @GetMapping("/sms-code") + @Operation(summary = "获取短信验证码", description = "用于注册时的短信验证码") + public Result getSmsCode( + @Parameter(description = "手机号", required = true) + @RequestParam String phone) { + SmsCodeResponse response = authService.sendSmsCode(phone); + return Result.success("验证码已发送", response); + } + /** * 用户登出 */ diff --git a/backend-single/src/main/java/com/emotion/controller/TokenController.java b/backend-single/src/main/java/com/emotion/controller/TokenController.java index 5854f3e..7ece3c7 100644 --- a/backend-single/src/main/java/com/emotion/controller/TokenController.java +++ b/backend-single/src/main/java/com/emotion/controller/TokenController.java @@ -1,44 +1,60 @@ package com.emotion.controller; import com.emotion.common.Result; -import com.emotion.dto.request.TokenRequest; import com.emotion.dto.response.UserInfoResponse; import com.emotion.service.TokenService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; +import javax.servlet.http.HttpServletRequest; + +/** + * Token控制器 + * 提供基于请求头Authorization的token验证和用户信息获取功能 + * + * @author emotion-museum + * @date 2025-07-23 + */ @RestController @RequestMapping("/token") +@Tag(name = "Token管理", description = "Token验证和用户信息获取") public class TokenController { @Autowired private TokenService tokenService; /** - * 通过token获取用户信息 + * 通过请求头中的token获取用户信息 + * Token应该在请求头中以 "Authorization: Bearer {token}" 的形式传递 */ - @PostMapping("/user-info") - public Result getUserInfoByToken(@RequestBody @Validated TokenRequest request) { - UserInfoResponse userInfo = tokenService.getUserInfoByToken(request.getToken()); + @GetMapping("/user-info") + @Operation(summary = "获取用户信息", description = "通过请求头中的token获取当前用户信息") + public Result getUserInfoByToken(HttpServletRequest request) { + UserInfoResponse userInfo = tokenService.getUserInfoByToken(request); return Result.success(userInfo); } /** - * 通过token获取用户名 + * 通过请求头中的token获取用户名 + * Token应该在请求头中以 "Authorization: Bearer {token}" 的形式传递 */ - @PostMapping("/username") - public Result getUsernameByToken(@RequestBody @Validated TokenRequest request) { - String username = tokenService.getUsernameByToken(request.getToken()); + @GetMapping("/username") + @Operation(summary = "获取用户名", description = "通过请求头中的token获取当前用户名") + public Result getUsernameByToken(HttpServletRequest request) { + String username = tokenService.getUsernameByToken(request); return Result.success(username); } /** - * 验证token并返回用户ID + * 验证请求头中的token并返回用户ID + * Token应该在请求头中以 "Authorization: Bearer {token}" 的形式传递 */ - @PostMapping("/validate") - public Result validateTokenAndGetUserId(@RequestBody @Validated TokenRequest request) { - String userId = tokenService.validateTokenAndGetUserId(request.getToken()); + @GetMapping("/validate") + @Operation(summary = "验证Token", description = "验证请求头中的token并返回用户ID") + public Result validateTokenAndGetUserId(HttpServletRequest request) { + String userId = tokenService.validateTokenAndGetUserId(request); return Result.success(userId); } -} \ No newline at end of file +} \ No newline at end of file diff --git a/backend-single/src/main/java/com/emotion/dto/request/LoginRequest.java b/backend-single/src/main/java/com/emotion/dto/request/LoginRequest.java index a865465..11c8b00 100644 --- a/backend-single/src/main/java/com/emotion/dto/request/LoginRequest.java +++ b/backend-single/src/main/java/com/emotion/dto/request/LoginRequest.java @@ -4,38 +4,31 @@ import lombok.Data; import lombok.EqualsAndHashCode; import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Pattern; +import javax.validation.constraints.Size; /** * 登录请求 - * + * 简化版:仅需手机号和短信验证码 + * * @author emotion-museum * @date 2025-07-23 */ @Data @EqualsAndHashCode(callSuper = true) public class LoginRequest extends BaseRequest { - + /** - * 账号 + * 手机号 */ - @NotBlank(message = "账号不能为空") - private String account; - + @NotBlank(message = "手机号不能为空") + @Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确") + private String phone; + /** - * 密码 - */ - @NotBlank(message = "密码不能为空") - private String password; - - /** - * 验证码 + * 短信验证码 */ @NotBlank(message = "验证码不能为空") - private String captcha; - - /** - * 验证码key - */ - @NotBlank(message = "验证码key不能为空") - private String captchaKey; + @Size(min = 6, max = 6, message = "验证码必须为6位") + private String smsCode; } diff --git a/backend-single/src/main/java/com/emotion/dto/request/RegisterRequest.java b/backend-single/src/main/java/com/emotion/dto/request/RegisterRequest.java index dbf8c0d..6b25d0c 100644 --- a/backend-single/src/main/java/com/emotion/dto/request/RegisterRequest.java +++ b/backend-single/src/main/java/com/emotion/dto/request/RegisterRequest.java @@ -3,72 +3,39 @@ package com.emotion.dto.request; import lombok.Data; import lombok.EqualsAndHashCode; -import javax.validation.constraints.Email; import javax.validation.constraints.NotBlank; import javax.validation.constraints.Pattern; import javax.validation.constraints.Size; /** * 注册请求 - * + * 简化版:仅需要手机号、密码和验证码 + * * @author emotion-museum * @date 2025-07-23 */ @Data @EqualsAndHashCode(callSuper = true) public class RegisterRequest extends BaseRequest { - + /** - * 账号 + * 手机号(作为账号) */ - @NotBlank(message = "账号不能为空") - @Size(min = 3, max = 20, message = "账号长度必须在3-20个字符之间") - private String account; - + @NotBlank(message = "手机号不能为空") + @Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确") + private String phone; + /** * 密码 */ @NotBlank(message = "密码不能为空") @Size(min = 6, max = 20, message = "密码长度必须在6-20个字符之间") private String password; - + /** - * 确认密码 - */ - @NotBlank(message = "确认密码不能为空") - private String confirmPassword; - - /** - * 用户名 - */ - private String username; - - /** - * 昵称 - */ - private String nickname; - - /** - * 邮箱 - */ - @Email(message = "邮箱格式不正确") - private String email; - - /** - * 手机号 - */ - @Pattern(regexp = "^$|^1[3-9]\\d{9}$", message = "手机号格式不正确") - private String phone; - - /** - * 验证码 + * 短信验证码 */ @NotBlank(message = "验证码不能为空") - private String captcha; - - /** - * 验证码key - */ - @NotBlank(message = "验证码key不能为空") - private String captchaKey; + @Size(min = 6, max = 6, message = "验证码必须为6位") + private String smsCode; } diff --git a/backend-single/src/main/java/com/emotion/dto/request/TokenRequest.java b/backend-single/src/main/java/com/emotion/dto/request/TokenRequest.java deleted file mode 100644 index db8bc50..0000000 --- a/backend-single/src/main/java/com/emotion/dto/request/TokenRequest.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.emotion.dto.request; - -import javax.validation.constraints.NotBlank; - -public class TokenRequest { - @NotBlank(message = "token不能为空") - private String token; - - public String getToken() { - return token; - } - - public void setToken(String token) { - this.token = token; - } -} \ No newline at end of file diff --git a/backend-single/src/main/java/com/emotion/dto/response/SmsCodeResponse.java b/backend-single/src/main/java/com/emotion/dto/response/SmsCodeResponse.java new file mode 100644 index 0000000..fbc3db5 --- /dev/null +++ b/backend-single/src/main/java/com/emotion/dto/response/SmsCodeResponse.java @@ -0,0 +1,35 @@ +package com.emotion.dto.response; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 短信验证码响应 + * + * @author emotion-museum + * @date 2025-10-06 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class SmsCodeResponse { + + /** + * 验证码(开发环境返回,生产环境不返回) + */ + private String code; + + /** + * 过期时间(秒) + */ + private Long expiresIn; + + /** + * 提示信息 + */ + private String message; +} + diff --git a/backend-single/src/main/java/com/emotion/dto/response/WebSocketResponse.java b/backend-single/src/main/java/com/emotion/dto/response/WebSocketResponse.java deleted file mode 100644 index ba408ce..0000000 --- a/backend-single/src/main/java/com/emotion/dto/response/WebSocketResponse.java +++ /dev/null @@ -1,62 +0,0 @@ -package com.emotion.dto.response; - -import lombok.Data; -import lombok.EqualsAndHashCode; - -import java.time.LocalDateTime; - -/** - * WebSocket响应对象 - * - * @author emotion-museum - * @date 2025-09-08 - */ -@Data -@EqualsAndHashCode(callSuper = true) -public class WebSocketResponse extends BaseResponse { - - /** - * 消息ID - */ - private String messageId; - - /** - * 会话ID - */ - private String conversationId; - - /** - * 消息类型 - */ - private String type; - - /** - * 消息内容 - */ - private String content; - - /** - * 发送者ID - */ - private String senderId; - - /** - * 发送者类型 - */ - private String senderType; - - /** - * 消息状态 - */ - private String status; - - /** - * 创建时间 - */ - private LocalDateTime createTime; - - /** - * 扩展数据 - */ - private Object data; -} \ No newline at end of file diff --git a/backend-single/src/main/java/com/emotion/entity/Achievement.java b/backend-single/src/main/java/com/emotion/entity/Achievement.java index 087ab5a..5aef99e 100644 --- a/backend-single/src/main/java/com/emotion/entity/Achievement.java +++ b/backend-single/src/main/java/com/emotion/entity/Achievement.java @@ -22,7 +22,7 @@ import java.time.LocalDateTime; @SuperBuilder @NoArgsConstructor @AllArgsConstructor -@TableName("achievement") +@TableName("t_achievement") public class Achievement extends BaseEntity { /** diff --git a/backend-single/src/main/java/com/emotion/entity/Comment.java b/backend-single/src/main/java/com/emotion/entity/Comment.java index 8348939..01e6bc8 100644 --- a/backend-single/src/main/java/com/emotion/entity/Comment.java +++ b/backend-single/src/main/java/com/emotion/entity/Comment.java @@ -19,7 +19,7 @@ import lombok.EqualsAndHashCode; @SuperBuilder @NoArgsConstructor @AllArgsConstructor -@TableName("comment") +@TableName("t_comment") public class Comment extends BaseEntity { /** diff --git a/backend-single/src/main/java/com/emotion/entity/CommunityPost.java b/backend-single/src/main/java/com/emotion/entity/CommunityPost.java index bef61a3..8c9ed74 100644 --- a/backend-single/src/main/java/com/emotion/entity/CommunityPost.java +++ b/backend-single/src/main/java/com/emotion/entity/CommunityPost.java @@ -19,7 +19,7 @@ import lombok.EqualsAndHashCode; @SuperBuilder @NoArgsConstructor @AllArgsConstructor -@TableName("community_post") +@TableName("t_community_post") public class CommunityPost extends BaseEntity { /** diff --git a/backend-single/src/main/java/com/emotion/entity/Conversation.java b/backend-single/src/main/java/com/emotion/entity/Conversation.java index 6cb101e..65588d6 100644 --- a/backend-single/src/main/java/com/emotion/entity/Conversation.java +++ b/backend-single/src/main/java/com/emotion/entity/Conversation.java @@ -22,7 +22,7 @@ import java.time.LocalDateTime; @SuperBuilder @NoArgsConstructor @AllArgsConstructor -@TableName("conversation") +@TableName("t_conversation") public class Conversation extends BaseEntity { /** diff --git a/backend-single/src/main/java/com/emotion/entity/CozeApiCall.java b/backend-single/src/main/java/com/emotion/entity/CozeApiCall.java index 31d8ff2..b6f7509 100644 --- a/backend-single/src/main/java/com/emotion/entity/CozeApiCall.java +++ b/backend-single/src/main/java/com/emotion/entity/CozeApiCall.java @@ -22,7 +22,7 @@ import java.time.LocalDateTime; @SuperBuilder @NoArgsConstructor @AllArgsConstructor -@TableName("coze_api_call") +@TableName("t_coze_api_call") public class CozeApiCall extends BaseEntity { /** diff --git a/backend-single/src/main/java/com/emotion/entity/DiaryComment.java b/backend-single/src/main/java/com/emotion/entity/DiaryComment.java index b726d72..567b394 100644 --- a/backend-single/src/main/java/com/emotion/entity/DiaryComment.java +++ b/backend-single/src/main/java/com/emotion/entity/DiaryComment.java @@ -22,7 +22,7 @@ import java.time.LocalDateTime; @SuperBuilder @NoArgsConstructor @AllArgsConstructor -@TableName("diary_comment") +@TableName("t_diary_comment") public class DiaryComment extends BaseEntity { /** diff --git a/backend-single/src/main/java/com/emotion/entity/DiaryPost.java b/backend-single/src/main/java/com/emotion/entity/DiaryPost.java index 030f86e..91180d8 100644 --- a/backend-single/src/main/java/com/emotion/entity/DiaryPost.java +++ b/backend-single/src/main/java/com/emotion/entity/DiaryPost.java @@ -22,7 +22,7 @@ import java.time.LocalDateTime; @SuperBuilder @NoArgsConstructor @AllArgsConstructor -@TableName("diary_post") +@TableName("t_diary_post") public class DiaryPost extends BaseEntity { /** diff --git a/backend-single/src/main/java/com/emotion/entity/EmotionAnalysis.java b/backend-single/src/main/java/com/emotion/entity/EmotionAnalysis.java index 3779030..32902fc 100644 --- a/backend-single/src/main/java/com/emotion/entity/EmotionAnalysis.java +++ b/backend-single/src/main/java/com/emotion/entity/EmotionAnalysis.java @@ -22,7 +22,7 @@ import java.time.LocalDateTime; @SuperBuilder @NoArgsConstructor @AllArgsConstructor -@TableName("emotion_analysis") +@TableName("t_emotion_analysis") public class EmotionAnalysis extends BaseEntity { /** diff --git a/backend-single/src/main/java/com/emotion/entity/EmotionRecord.java b/backend-single/src/main/java/com/emotion/entity/EmotionRecord.java index 339fb6f..61d78d5 100644 --- a/backend-single/src/main/java/com/emotion/entity/EmotionRecord.java +++ b/backend-single/src/main/java/com/emotion/entity/EmotionRecord.java @@ -22,7 +22,7 @@ import java.time.LocalDate; @SuperBuilder @NoArgsConstructor @AllArgsConstructor -@TableName("emotion_record") +@TableName("t_emotion_record") public class EmotionRecord extends BaseEntity { /** diff --git a/backend-single/src/main/java/com/emotion/entity/GrowthTopic.java b/backend-single/src/main/java/com/emotion/entity/GrowthTopic.java index ded9c2e..1f41e28 100644 --- a/backend-single/src/main/java/com/emotion/entity/GrowthTopic.java +++ b/backend-single/src/main/java/com/emotion/entity/GrowthTopic.java @@ -22,7 +22,7 @@ import java.time.LocalDateTime; @SuperBuilder @NoArgsConstructor @AllArgsConstructor -@TableName("growth_topic") +@TableName("t_growth_topic") public class GrowthTopic extends BaseEntity { /** diff --git a/backend-single/src/main/java/com/emotion/entity/GuestUser.java b/backend-single/src/main/java/com/emotion/entity/GuestUser.java index 11c34f1..3cac6ed 100644 --- a/backend-single/src/main/java/com/emotion/entity/GuestUser.java +++ b/backend-single/src/main/java/com/emotion/entity/GuestUser.java @@ -21,7 +21,7 @@ import java.time.LocalDateTime; @SuperBuilder @NoArgsConstructor @AllArgsConstructor -@TableName("guest_user") +@TableName("t_guest_user") public class GuestUser extends BaseEntity { /** diff --git a/backend-single/src/main/java/com/emotion/entity/LocationPin.java b/backend-single/src/main/java/com/emotion/entity/LocationPin.java index 733be3f..9039989 100644 --- a/backend-single/src/main/java/com/emotion/entity/LocationPin.java +++ b/backend-single/src/main/java/com/emotion/entity/LocationPin.java @@ -22,7 +22,7 @@ import java.time.LocalDateTime; @SuperBuilder @NoArgsConstructor @AllArgsConstructor -@TableName("location_pin") +@TableName("t_location_pin") public class LocationPin extends BaseEntity { /** diff --git a/backend-single/src/main/java/com/emotion/entity/Message.java b/backend-single/src/main/java/com/emotion/entity/Message.java index ae53b01..d454904 100644 --- a/backend-single/src/main/java/com/emotion/entity/Message.java +++ b/backend-single/src/main/java/com/emotion/entity/Message.java @@ -22,7 +22,7 @@ import java.time.LocalDateTime; @SuperBuilder @NoArgsConstructor @AllArgsConstructor -@TableName("message") +@TableName("t_message") public class Message extends BaseEntity { /** diff --git a/backend-single/src/main/java/com/emotion/entity/Reward.java b/backend-single/src/main/java/com/emotion/entity/Reward.java index 4d9f6dc..22dfb19 100644 --- a/backend-single/src/main/java/com/emotion/entity/Reward.java +++ b/backend-single/src/main/java/com/emotion/entity/Reward.java @@ -21,7 +21,7 @@ import java.time.LocalDateTime; @SuperBuilder @NoArgsConstructor @AllArgsConstructor -@TableName("reward") +@TableName("t_reward") public class Reward extends BaseEntity { /** diff --git a/backend-single/src/main/java/com/emotion/entity/TopicInteraction.java b/backend-single/src/main/java/com/emotion/entity/TopicInteraction.java index 9b350ea..552ea2a 100644 --- a/backend-single/src/main/java/com/emotion/entity/TopicInteraction.java +++ b/backend-single/src/main/java/com/emotion/entity/TopicInteraction.java @@ -21,7 +21,7 @@ import java.time.LocalDateTime; @SuperBuilder @NoArgsConstructor @AllArgsConstructor -@TableName("topic_interaction") +@TableName("t_topic_interaction") public class TopicInteraction extends BaseEntity { /** diff --git a/backend-single/src/main/java/com/emotion/entity/User.java b/backend-single/src/main/java/com/emotion/entity/User.java index aee4528..007af90 100644 --- a/backend-single/src/main/java/com/emotion/entity/User.java +++ b/backend-single/src/main/java/com/emotion/entity/User.java @@ -23,7 +23,7 @@ import java.time.LocalDateTime; @SuperBuilder @NoArgsConstructor @AllArgsConstructor -@TableName("user") +@TableName("t_user") public class User extends BaseEntity { /** diff --git a/backend-single/src/main/java/com/emotion/entity/UserStats.java b/backend-single/src/main/java/com/emotion/entity/UserStats.java index e3e9712..c4fc343 100644 --- a/backend-single/src/main/java/com/emotion/entity/UserStats.java +++ b/backend-single/src/main/java/com/emotion/entity/UserStats.java @@ -19,7 +19,7 @@ import lombok.EqualsAndHashCode; @SuperBuilder @NoArgsConstructor @AllArgsConstructor -@TableName("user_stats") +@TableName("t_user_stats") public class UserStats extends BaseEntity { /** diff --git a/backend-single/src/main/java/com/emotion/service/AuthService.java b/backend-single/src/main/java/com/emotion/service/AuthService.java index 00c574e..776cdd9 100644 --- a/backend-single/src/main/java/com/emotion/service/AuthService.java +++ b/backend-single/src/main/java/com/emotion/service/AuthService.java @@ -4,6 +4,7 @@ import com.emotion.dto.request.LoginRequest; import com.emotion.dto.request.RegisterRequest; import com.emotion.dto.response.AuthResponse; import com.emotion.dto.response.CaptchaResponse; +import com.emotion.dto.response.SmsCodeResponse; import com.emotion.dto.response.UserInfoResponse; import javax.servlet.http.HttpServletRequest; @@ -136,4 +137,21 @@ public interface AuthService { * @return 是否存在 */ boolean existsByPhone(String phone); + + /** + * 发送短信验证码 + * + * @param phone 手机号 + * @return 短信验证码响应 + */ + SmsCodeResponse sendSmsCode(String phone); + + /** + * 验证短信验证码 + * + * @param phone 手机号 + * @param code 验证码 + * @return 是否验证成功 + */ + boolean validateSmsCode(String phone, String code); } \ No newline at end of file diff --git a/backend-single/src/main/java/com/emotion/service/UserService.java b/backend-single/src/main/java/com/emotion/service/UserService.java index 7bda723..388b52c 100644 --- a/backend-single/src/main/java/com/emotion/service/UserService.java +++ b/backend-single/src/main/java/com/emotion/service/UserService.java @@ -9,14 +9,16 @@ import com.emotion.dto.request.UserProfileUpdateRequest; import com.emotion.dto.response.UserResponse; import com.emotion.entity.User; +import java.time.LocalDateTime; + /** * 用户服务接口 - * + * * @author emotion-museum * @date 2025-07-23 */ public interface UserService extends IService { - + /** * 分页查询用户响应 */ @@ -51,4 +53,39 @@ public interface UserService extends IService { * 删除用户 */ boolean deleteUser(String id); + + /** + * 根据账号获取用户 + */ + User getByAccount(String account); + + /** + * 根据邮箱获取用户 + */ + User getByEmail(String email); + + /** + * 根据手机号获取用户 + */ + User getByPhone(String phone); + + /** + * 创建用户 + * + * @param account 账号 + * @param username 用户名 + * @param password 密码(明文,会在方法内加密) + * @param email 邮箱(可为null) + * @param phone 手机号(可为null) + * @return 创建的用户 + */ + User createUser(String account, String username, String password, String email, String phone); + + /** + * 更新用户最后活跃时间 + * + * @param userId 用户ID + * @param lastActiveTime 最后活跃时间 + */ + void updateLastActiveTime(String userId, LocalDateTime lastActiveTime); } \ No newline at end of file diff --git a/backend-single/src/main/java/com/emotion/service/impl/AuthServiceImpl.java b/backend-single/src/main/java/com/emotion/service/impl/AuthServiceImpl.java index c5a05e5..f68ed3c 100644 --- a/backend-single/src/main/java/com/emotion/service/impl/AuthServiceImpl.java +++ b/backend-single/src/main/java/com/emotion/service/impl/AuthServiceImpl.java @@ -4,6 +4,7 @@ import com.emotion.dto.request.LoginRequest; import com.emotion.dto.request.RegisterRequest; import com.emotion.dto.response.AuthResponse; import com.emotion.dto.response.CaptchaResponse; +import com.emotion.dto.response.SmsCodeResponse; import com.emotion.dto.response.UserInfoResponse; import com.emotion.entity.User; import com.emotion.exception.AuthException; @@ -61,34 +62,36 @@ public class AuthServiceImpl implements AuthService { private TokenUtil tokenUtil; private static final String CAPTCHA_PREFIX = "captcha:"; + private static final String SMS_CODE_PREFIX = "sms_code:"; private static final String TOKEN_PREFIX = "token:"; private static final String REFRESH_TOKEN_PREFIX = "refresh_token:"; private static final int CAPTCHA_EXPIRE_MINUTES = 5; + private static final int SMS_CODE_EXPIRE_MINUTES = 5; private static final int TOKEN_EXPIRE_HOURS = 24; private static final int REFRESH_TOKEN_EXPIRE_DAYS = 7; + private static final String DEFAULT_SMS_CODE = "123456"; private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); @Override public AuthResponse login(LoginRequest request) { - // 验证验证码 - if (!validateCaptcha(request.getCaptchaKey(), request.getCaptcha())) { + // 验证短信验证码 + if (!validateSmsCode(request.getPhone(), request.getSmsCode())) { throw new CaptchaException("验证码错误或已过期"); } - // 根据账号查询用户 - User user = userService.getByAccount(request.getAccount()); + // 根据手机号查询用户 + User user = userService.getByPhone(request.getPhone()); + + // 如果用户不存在,则自动注册 if (user == null) { - throw new AuthException("账号不存在"); - } - - // 验证密码(这里应该使用加密后的密码比较) - if (!verifyPassword(request.getPassword(), user.getPassword())) { - throw new AuthException("密码错误"); - } - - // 检查用户状态 - if (user.getStatus() != 1) { - throw new AuthException("账号已被禁用"); + log.info("用户不存在,自动注册: phone={}", request.getPhone()); + user = autoRegisterUser(request.getPhone()); + } else { + // 检查用户状态 + if (user.getStatus() != 1) { + throw new AuthException("账号已被禁用"); + } + log.info("用户登录: phone={}, userId={}", request.getPhone(), user.getId()); } // 生成令牌 @@ -111,61 +114,31 @@ public class AuthServiceImpl implements AuthService { @Override public AuthResponse register(RegisterRequest request) { - // 验证验证码 - if (!validateCaptcha(request.getCaptchaKey(), request.getCaptcha())) { + // 验证短信验证码 + if (!validateSmsCode(request.getPhone(), request.getSmsCode())) { throw new CaptchaException("验证码错误或已过期"); } - // 验证密码确认 - if (!request.getPassword().equals(request.getConfirmPassword())) { - throw new BusinessException("密码与确认密码不一致"); + // 检查手机号是否已存在 + if (userService.getByPhone(request.getPhone()) != null) { + throw new BusinessException("手机号已被注册"); } - // 检查账号是否已存在 - if (userService.getByAccount(request.getAccount()) != null) { - throw new BusinessException("账号已存在"); - } + // 生成随机用户名:开心 + 6位随机大小写字母和数字 + String username = generateRandomUsername(); - // 检查邮箱是否已存在(只有当邮箱不为空时才检查) - if (StringUtils.hasText(request.getEmail()) && userService.getByEmail(request.getEmail()) != null) { - throw new BusinessException("邮箱已被使用"); - } - - // 检查手机号是否已存在(只有当手机号不为空时才检查) - if (StringUtils.hasText(request.getPhone()) && userService.getByPhone(request.getPhone()) != null) { - throw new BusinessException("手机号已被使用"); - } - - // 处理用户名:如果为空则使用账号 - String username = StringUtils.hasText(request.getUsername()) ? request.getUsername() : request.getAccount(); - - // 处理邮箱:如果为空字符串则设为null - String email = StringUtils.hasText(request.getEmail()) ? request.getEmail() : null; - - // 处理手机号:如果为空字符串则设为null - String phone = StringUtils.hasText(request.getPhone()) ? request.getPhone() : null; + // 使用手机号作为账号 + String account = request.getPhone(); // 创建用户(密码在UserService中加密,这里不需要预先加密) User user = userService.createUser( - request.getAccount(), + account, username, request.getPassword(), - email, - phone + null, // 邮箱为空 + request.getPhone() ); - // 设置昵称(如果昵称为空则使用用户名) - if (StringUtils.hasText(request.getNickname())) { - user.setNickname(request.getNickname()); - } else if (StringUtils.hasText(username)) { - user.setNickname(username); - } - - // 如果有昵称变更,更新用户信息 - if (StringUtils.hasText(user.getNickname())) { - userService.updateById(user); - } - // 生成令牌 String accessToken = generateAccessToken(user); String refreshToken = generateRefreshToken(user); @@ -178,6 +151,7 @@ public class AuthServiceImpl implements AuthService { response.setUserInfo(convertToUserInfoResponse(user)); response.setLoginTime(LocalDateTime.now().format(DATE_TIME_FORMATTER)); + log.info("用户注册成功: phone={}, username={}", request.getPhone(), username); return response; } @@ -510,4 +484,132 @@ public class AuthServiceImpl implements AuthService { User user = userService.getByPhone(phone); return user != null; } + + @Override + public SmsCodeResponse sendSmsCode(String phone) { + // 验证手机号格式 + if (!StringUtils.hasText(phone) || !phone.matches("^1[3-9]\\d{9}$")) { + throw new BusinessException("手机号格式不正确"); + } + + // 检查手机号是否已注册,用于提示信息 + boolean isRegistered = existsByPhone(phone); + String message; + if (isRegistered) { + message = "验证码已发送,用于登录验证,有效期" + SMS_CODE_EXPIRE_MINUTES + "分钟"; + } else { + message = "验证码已发送,用于注册验证,有效期" + SMS_CODE_EXPIRE_MINUTES + "分钟"; + } + + // TODO: 接入真实的短信服务商(阿里云、腾讯云等) + // 目前使用固定验证码 123456 + String code = DEFAULT_SMS_CODE; + + // 将验证码存储到Redis,有效期5分钟 + String key = SMS_CODE_PREFIX + phone; + redisTemplate.opsForValue().set(key, code, SMS_CODE_EXPIRE_MINUTES, TimeUnit.MINUTES); + + log.info("发送短信验证码: phone={}, code={}, isRegistered={}", phone, code, isRegistered); + + // 构建响应 + return SmsCodeResponse.builder() + .code(code) // 开发环境返回验证码,生产环境应该删除此行 + .expiresIn((long) SMS_CODE_EXPIRE_MINUTES * 60) + .message(message) + .build(); + } + + @Override + public boolean validateSmsCode(String phone, String code) { + if (!StringUtils.hasText(phone) || !StringUtils.hasText(code)) { + return false; + } + + String key = SMS_CODE_PREFIX + phone; + String storedCode = (String) redisTemplate.opsForValue().get(key); + + if (storedCode == null) { + log.warn("短信验证码不存在或已过期: phone={}", phone); + return false; + } + + boolean isValid = storedCode.equals(code); + + // 验证成功后删除验证码(一次性使用) + if (isValid) { + redisTemplate.delete(key); + log.info("短信验证码验证成功: phone={}", phone); + } else { + log.warn("短信验证码错误: phone={}, expected={}, actual={}", phone, storedCode, code); + } + + return isValid; + } + + /** + * 生成随机用户名 + * 格式:开心 + 6位随机大小写字母和数字 + * + * @return 随机用户名 + */ + private String generateRandomUsername() { + String chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + Random random = new Random(); + StringBuilder sb = new StringBuilder("开心"); + + for (int i = 0; i < 6; i++) { + int index = random.nextInt(chars.length()); + sb.append(chars.charAt(index)); + } + + return sb.toString(); + } + + /** + * 自动注册用户(用于登录时用户不存在的情况) + * + * @param phone 手机号 + * @return 新创建的用户 + */ + private User autoRegisterUser(String phone) { + // 生成随机用户名:开心 + 6位随机大小写字母和数字 + String username = generateRandomUsername(); + + // 使用手机号作为账号 + String account = phone; + + // 生成随机密码(用户可以后续修改) + String randomPassword = generateRandomPassword(); + + // 创建用户 + User user = userService.createUser( + account, + username, + randomPassword, + null, // 邮箱为空 + phone + ); + + log.info("自动注册用户成功: phone={}, username={}, userId={}", phone, username, user.getId()); + return user; + } + + /** + * 生成随机密码 + * 格式:8位随机大小写字母和数字 + * + * @return 随机密码 + */ + private String generateRandomPassword() { + String chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + Random random = new Random(); + StringBuilder sb = new StringBuilder(); + + for (int i = 0; i < 8; i++) { + int index = random.nextInt(chars.length()); + sb.append(chars.charAt(index)); + } + + return sb.toString(); + } } \ No newline at end of file diff --git a/backend-single/src/main/java/com/emotion/service/impl/UserServiceImpl.java b/backend-single/src/main/java/com/emotion/service/impl/UserServiceImpl.java index 8c630c8..3d03b1c 100644 --- a/backend-single/src/main/java/com/emotion/service/impl/UserServiceImpl.java +++ b/backend-single/src/main/java/com/emotion/service/impl/UserServiceImpl.java @@ -245,6 +245,69 @@ public class UserServiceImpl extends ServiceImpl implements Us return this.updateById(user); } + @Override + public User getByAccount(String account) { + if (!StringUtils.hasText(account)) { + return null; + } + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(User::getAccount, account) + .eq(User::getIsDeleted, 0); + return this.getOne(wrapper); + } + + @Override + public User getByEmail(String email) { + if (!StringUtils.hasText(email)) { + return null; + } + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(User::getEmail, email) + .eq(User::getIsDeleted, 0); + return this.getOne(wrapper); + } + + @Override + public User getByPhone(String phone) { + if (!StringUtils.hasText(phone)) { + return null; + } + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(User::getPhone, phone) + .eq(User::getIsDeleted, 0); + return this.getOne(wrapper); + } + + @Override + public User createUser(String account, String username, String password, String email, String phone) { + User user = new User(); + user.setAccount(account); + user.setUsername(username); + user.setNickname(username); // 默认昵称与用户名相同 + user.setPassword(passwordEncoder.encode(password)); // 加密密码 + user.setEmail(email); + user.setPhone(phone); + user.setMemberLevel("free"); // 默认免费会员 + user.setStatus(1); // 默认启用 + user.setIsVerified(0); // 默认未验证 + user.setLastActiveTime(LocalDateTime.now()); + + this.save(user); + return user; + } + + @Override + public void updateLastActiveTime(String userId, LocalDateTime lastActiveTime) { + if (!StringUtils.hasText(userId) || lastActiveTime == null) { + return; + } + User user = this.getById(userId); + if (user != null && user.getIsDeleted() == 0) { + user.setLastActiveTime(lastActiveTime); + this.updateById(user); + } + } + /** * 转换为响应对象 */ diff --git a/backend-single/src/main/resources/application-local.yml b/backend-single/src/main/resources/application-local.yml index d135b07..06b9d8a 100644 --- a/backend-single/src/main/resources/application-local.yml +++ b/backend-single/src/main/resources/application-local.yml @@ -8,9 +8,9 @@ spring: # 数据库配置 - 本地MySQL datasource: driver-class-name: com.mysql.cj.jdbc.Driver - url: jdbc:mysql://47.111.10.27:3306/emotion?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true + url: jdbc:mysql://localhost:3306/emotion_museum?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true username: root - password: EmotionMuseum2025*# + password: 123456 hikari: minimum-idle: 5 maximum-pool-size: 20 diff --git a/backend-single/src/main/resources/application-prod.yml b/backend-single/src/main/resources/application-prod.yml index 661bc6a..a3d34bb 100644 --- a/backend-single/src/main/resources/application-prod.yml +++ b/backend-single/src/main/resources/application-prod.yml @@ -8,9 +8,9 @@ spring: # 数据库配置 - 生产MySQL datasource: driver-class-name: com.mysql.cj.jdbc.Driver - url: jdbc:mysql://47.111.10.27:3306/emotion?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true - username: emotion - password: EmotionDB2024! + url: jdbc:mysql://101.200.208.45:3306/emotion_museum?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true + username: root + password: EmotionMuseum2025*# hikari: minimum-idle: 10 maximum-pool-size: 50 @@ -24,7 +24,7 @@ spring: # Redis配置 - 生产Redis redis: - host: localhost + host: 101.200.208.45 port: 6379 timeout: 5000ms database: 0 diff --git a/backend-single/src/main/resources/application-test.yml b/backend-single/src/main/resources/application-test.yml index a5dafc4..f11495d 100644 --- a/backend-single/src/main/resources/application-test.yml +++ b/backend-single/src/main/resources/application-test.yml @@ -8,7 +8,7 @@ spring: # 数据库配置 - 测试MySQL datasource: driver-class-name: com.mysql.cj.jdbc.Driver - url: jdbc:mysql://47.111.10.27:3306/emotion?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true + url: jdbc:mysql://101.200.208.45:3306/emotion_museum?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true username: root password: EmotionMuseum2025*# hikari: @@ -24,7 +24,7 @@ spring: # Redis配置 - 测试Redis redis: - host: localhost + host: 101.200.208.45 port: 6379 timeout: 3000ms database: 1 diff --git a/backend-single/src/main/resources/sql/mysql_emotion_museum_final.sql b/backend-single/src/main/resources/sql/mysql_emotion_museum_final.sql index 4433cf4..7343cc4 100644 --- a/backend-single/src/main/resources/sql/mysql_emotion_museum_final.sql +++ b/backend-single/src/main/resources/sql/mysql_emotion_museum_final.sql @@ -22,11 +22,11 @@ SET time_zone = "+00:00"; -- 创建数据库 -CREATE DATABASE IF NOT EXISTS emotion DEFAULT CHARACTER +CREATE DATABASE IF NOT EXISTS emotion_museum DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; -USE emotion; +USE emotion_museum; -- ============================================================================ -- 数据库设计原则 @@ -40,40 +40,44 @@ USE emotion; -- 删除现有表(开发阶段确保表结构最新) -- 警告: 这会删除所有数据! -- ============================================================================ -DROP TABLE IF EXISTS user_stats; +DROP TABLE IF EXISTS t_user_stats; -DROP TABLE IF EXISTS guest_user; +DROP TABLE IF EXISTS t_guest_user; -DROP TABLE IF EXISTS reward; +DROP TABLE IF EXISTS t_reward; -DROP TABLE IF EXISTS achievement; +DROP TABLE IF EXISTS t_achievement; -DROP TABLE IF EXISTS comment; +DROP TABLE IF EXISTS t_comment; -DROP TABLE IF EXISTS community_post; +DROP TABLE IF EXISTS t_community_post; -DROP TABLE IF EXISTS location_pin; +DROP TABLE IF EXISTS t_location_pin; -DROP TABLE IF EXISTS topic_interaction; +DROP TABLE IF EXISTS t_topic_interaction; -DROP TABLE IF EXISTS growth_topic; +DROP TABLE IF EXISTS t_growth_topic; -DROP TABLE IF EXISTS emotion_record; +DROP TABLE IF EXISTS t_emotion_record; -DROP TABLE IF EXISTS emotion_analysis; +DROP TABLE IF EXISTS t_emotion_analysis; -DROP TABLE IF EXISTS coze_api_call; +DROP TABLE IF EXISTS t_coze_api_call; -DROP TABLE IF EXISTS message; +DROP TABLE IF EXISTS t_message; -DROP TABLE IF EXISTS conversation; +DROP TABLE IF EXISTS t_conversation; -DROP TABLE IF EXISTS user; +DROP TABLE IF EXISTS t_diary_post; + +DROP TABLE IF EXISTS t_diary_comment; + +DROP TABLE IF EXISTS t_user; -- ============================================================================ -- 1. 用户表 (user) -- ============================================================================ -CREATE TABLE user ( +CREATE TABLE t_user ( id VARCHAR(64) PRIMARY KEY COMMENT 'UUID主键', -- UUID主键 account VARCHAR(50) UNIQUE COMMENT '账号', -- 账号 password VARCHAR(255) COMMENT '密码(加密后)', -- 密码(加密后) @@ -107,15 +111,15 @@ CREATE TABLE user ( 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 = '用户表'; +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '用户表 (t_user)'; -- ============================================================================ --- 2. 对话表 (conversation) --- 关联说明: user_id 关联 user.id,通过代码逻辑维护关联关系 +-- 2. 对话表 (t_conversation) +-- 关联说明: user_id 关联 t_user.id,通过代码逻辑维护关联关系 -- ============================================================================ -CREATE TABLE conversation ( +CREATE TABLE t_conversation ( id VARCHAR(64) PRIMARY KEY COMMENT 'UUID主键', -- UUID主键 - user_id VARCHAR(64) COMMENT '用户ID (关联user.id)', -- 用户ID (关联user.id) + user_id VARCHAR(64) COMMENT '用户ID (关联t_user.id)', -- 用户ID (关联t_user.id) user_type VARCHAR(20) DEFAULT 'registered' COMMENT '用户类型: registered-注册用户, guest-访客用户', -- 用户类型: registered-注册用户, guest-访客用户 title VARCHAR(200) COMMENT '对话标题', -- 对话标题 type VARCHAR(50) DEFAULT 'emotion_chat' COMMENT '对话类型', -- 对话类型 @@ -149,15 +153,15 @@ CREATE TABLE conversation ( 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 = '对话表'; +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '对话表 (t_conversation)'; -- ============================================================================ --- 3. 消息表 (message) --- 关联说明: conversation_id 关联 conversation.id,通过代码逻辑维护关联关系 +-- 3. 消息表 (t_message) +-- 关联说明: conversation_id 关联 t_conversation.id,通过代码逻辑维护关联关系 -- ============================================================================ -CREATE TABLE message ( +CREATE TABLE t_message ( id VARCHAR(64) PRIMARY KEY COMMENT 'UUID主键', -- UUID主键 - conversation_id VARCHAR(64) COMMENT '对话ID (关联conversation.id)', -- 对话ID (关联conversation.id) + conversation_id VARCHAR(64) COMMENT '对话ID (关联t_conversation.id)', -- 对话ID (关联t_conversation.id) content TEXT COMMENT '消息内容', -- 消息内容 type VARCHAR(50) DEFAULT 'text' COMMENT '消息类型', -- 消息类型 sender VARCHAR(20) COMMENT '发送者: user-用户, assistant-AI助手', -- 发送者: user-用户, assistant-AI助手 @@ -188,12 +192,12 @@ CREATE TABLE message ( 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 = '消息表'; +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '消息表 (t_message)'; -- ============================================================================ -- 4. Coze API调用记录表 (coze_api_call) - 优化版本 -- ============================================================================ -CREATE TABLE coze_api_call ( +CREATE TABLE t_coze_api_call ( id VARCHAR(64) PRIMARY KEY COMMENT 'UUID主键', -- UUID主键 conversation_id VARCHAR(64) COMMENT '对话ID', -- 对话ID message_id VARCHAR(64) COMMENT '消息ID', -- 消息ID @@ -252,12 +256,12 @@ CREATE TABLE coze_api_call ( 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 = 'Coze API调用记录表 - 完整版本'; +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'Coze API调用记录表 - 完整版本 (t_coze_api_call)'; -- ============================================================================ -- 5. 情绪分析表 (emotion_analysis) -- ============================================================================ -CREATE TABLE emotion_analysis ( +CREATE TABLE t_emotion_analysis ( id VARCHAR(64) PRIMARY KEY COMMENT 'UUID主键', -- UUID主键 user_id VARCHAR(64) COMMENT '用户ID', -- 用户ID message_id VARCHAR(64) COMMENT '关联消息ID', -- 关联消息ID @@ -278,12 +282,12 @@ CREATE TABLE emotion_analysis ( 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 = '情绪分析表'; +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '情绪分析表 (t_emotion_analysis)'; -- ============================================================================ -- 6. 情绪记录表 (emotion_record) -- ============================================================================ -CREATE TABLE emotion_record ( +CREATE TABLE t_emotion_record ( id VARCHAR(64) PRIMARY KEY COMMENT 'UUID主键', -- UUID主键 user_id VARCHAR(64) COMMENT '用户ID', -- 用户ID record_date DATE COMMENT '记录日期', -- 记录日期 @@ -304,12 +308,12 @@ CREATE TABLE emotion_record ( 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 = '情绪记录表'; +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '情绪记录表 (t_emotion_record)'; -- ============================================================================ -- 7. 成长课题表 (growth_topic) -- ============================================================================ -CREATE TABLE growth_topic ( +CREATE TABLE t_growth_topic ( id VARCHAR(64) PRIMARY KEY COMMENT 'UUID主键', -- UUID主键 title VARCHAR(100) COMMENT '课题标题', -- 课题标题 category VARCHAR(50) COMMENT '分类', -- 分类 @@ -329,12 +333,12 @@ CREATE TABLE growth_topic ( 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 = '成长课题表'; +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '成长课题表 (t_growth_topic)'; -- ============================================================================ -- 8. 课题互动表 (topic_interaction) -- ============================================================================ -CREATE TABLE topic_interaction ( +CREATE TABLE t_topic_interaction ( id VARCHAR(64) PRIMARY KEY COMMENT 'UUID主键', -- UUID主键 topic_id VARCHAR(64) COMMENT '课题ID', -- 课题ID type VARCHAR(50) COMMENT '互动类型', -- 互动类型 @@ -351,12 +355,12 @@ CREATE TABLE topic_interaction ( 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 = '课题互动表'; +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '课题互动表 (t_topic_interaction)'; -- ============================================================================ -- 9. 地点标记表 (location_pin) -- ============================================================================ -CREATE TABLE location_pin ( +CREATE TABLE t_location_pin ( id VARCHAR(64) PRIMARY KEY COMMENT 'UUID主键', -- UUID主键 name VARCHAR(100) COMMENT '地点名称', -- 地点名称 type VARCHAR(50) COMMENT '地点类型', -- 地点类型 @@ -365,7 +369,6 @@ CREATE TABLE location_pin ( longitude DECIMAL(11, 8) COMMENT '经度', -- 经度 address VARCHAR(200) COMMENT '地址', -- 地址 description TEXT COMMENT '描述', -- 描述 - created_by VARCHAR(64) COMMENT '创建者', -- 创建者 likes INT DEFAULT 0 COMMENT '点赞数', -- 点赞数 visits INT DEFAULT 0 COMMENT '访问数', -- 访问数 is_bookmarked TINYINT DEFAULT 0 COMMENT '是否收藏', -- 是否收藏 @@ -377,12 +380,12 @@ CREATE TABLE location_pin ( 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 = '地点标记表'; +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '地点标记表 (t_location_pin)'; -- ============================================================================ -- 10. 社区帖子表 (community_post) -- ============================================================================ -CREATE TABLE community_post ( +CREATE TABLE t_community_post ( id VARCHAR(64) PRIMARY KEY COMMENT 'UUID主键', -- UUID主键 user_id VARCHAR(64) COMMENT '用户ID', -- 用户ID location_id VARCHAR(64) COMMENT '地点ID', -- 地点ID @@ -402,12 +405,12 @@ CREATE TABLE community_post ( 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 = '社区帖子表'; +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '社区帖子表 (t_community_post)'; -- ============================================================================ -- 11. 评论表 (comment) -- ============================================================================ -CREATE TABLE comment ( +CREATE TABLE t_comment ( id VARCHAR(64) PRIMARY KEY COMMENT 'UUID主键', -- UUID主键 post_id VARCHAR(64) COMMENT '帖子ID', -- 帖子ID user_id VARCHAR(64) COMMENT '用户ID', -- 用户ID @@ -421,12 +424,12 @@ CREATE TABLE comment ( 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 = '评论表'; +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '评论表 (t_comment)'; -- ============================================================================ -- 12. 成就表 (achievement) -- ============================================================================ -CREATE TABLE achievement ( +CREATE TABLE t_achievement ( id VARCHAR(64) PRIMARY KEY COMMENT 'UUID主键', -- UUID主键 title VARCHAR(100) COMMENT '成就标题', -- 成就标题 description TEXT COMMENT '描述', -- 描述 @@ -446,12 +449,12 @@ CREATE TABLE achievement ( 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 = '成就表'; +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '成就表 (t_achievement)'; -- ============================================================================ -- 13. 奖励表 (reward) -- ============================================================================ -CREATE TABLE reward ( +CREATE TABLE t_reward ( id VARCHAR(64) PRIMARY KEY COMMENT 'UUID主键', -- UUID主键 topic_id VARCHAR(64) COMMENT '课题ID', -- 课题ID achievement_id VARCHAR(64) COMMENT '成就ID', -- 成就ID @@ -470,12 +473,12 @@ CREATE TABLE reward ( 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 = '奖励表'; +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '奖励表 (t_reward)'; -- ============================================================================ -- 14. 用户统计表 (user_stats) -- ============================================================================ -CREATE TABLE user_stats ( +CREATE TABLE t_user_stats ( id VARCHAR(64) PRIMARY KEY COMMENT 'UUID主键', -- UUID主键 user_id VARCHAR(64) UNIQUE COMMENT '用户ID', -- 用户ID total_conversations INT DEFAULT 0 COMMENT '总对话数', -- 总对话数 @@ -508,15 +511,15 @@ CREATE TABLE user_stats ( 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 = '用户统计表'; +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '用户统计表 (t_user_stats)'; -- ============================================================================ --- 16. 用户日记表 (diary_post) - 类似朋友圈功能 --- 关联说明: user_id 关联 user.id,通过代码逻辑维护关联关系 +-- 16. 用户日记表 (t_diary_post) - 类似朋友圈功能 +-- 关联说明: user_id 关联 t_user.id,通过代码逻辑维护关联关系 -- ============================================================================ -CREATE TABLE diary_post ( +CREATE TABLE t_diary_post ( id VARCHAR(64) PRIMARY KEY COMMENT 'UUID主键', -- UUID主键 - user_id VARCHAR(64) COMMENT '用户ID (关联user.id)', -- 用户ID (关联user.id) + user_id VARCHAR(64) COMMENT '用户ID (关联t_user.id)', -- 用户ID (关联t_user.id) title VARCHAR(200) COMMENT '日记标题', -- 日记标题 content TEXT COMMENT '日记内容', -- 日记内容 images JSON COMMENT '图片列表 (存储图片URL数组)', -- 图片列表 (存储图片URL数组) @@ -553,16 +556,16 @@ CREATE TABLE diary_post ( 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 = '用户日记表 - 类似朋友圈功能'; +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '用户日记表 - 类似朋友圈功能 (t_diary_post)'; -- ============================================================================ --- 17. 日记评论表 (diary_comment) --- 关联说明: diary_id 关联 diary_post.id,user_id 关联 user.id,通过代码逻辑维护关联关系 +-- 17. 日记评论表 (t_diary_comment) +-- 关联说明: diary_id 关联 t_diary_post.id,user_id 关联 t_user.id,通过代码逻辑维护关联关系 -- ============================================================================ -CREATE TABLE diary_comment ( +CREATE TABLE t_diary_comment ( id VARCHAR(64) PRIMARY KEY COMMENT 'UUID主键', -- UUID主键 - diary_id VARCHAR(64) COMMENT '日记ID (关联diary_post.id)', -- 日记ID (关联diary_post.id) - user_id VARCHAR(64) COMMENT '评论用户ID (关联user.id)', -- 评论用户ID (关联user.id) + diary_id VARCHAR(64) COMMENT '日记ID (关联t_diary_post.id)', -- 日记ID (关联t_diary_post.id) + user_id VARCHAR(64) COMMENT '评论用户ID (关联t_user.id)', -- 评论用户ID (关联t_user.id) parent_comment_id VARCHAR(64) COMMENT '父评论ID (用于回复功能)', -- 父评论ID (用于回复功能) content TEXT COMMENT '评论内容', -- 评论内容 images JSON COMMENT '评论图片 (存储图片URL数组)', -- 评论图片 (存储图片URL数组) @@ -585,12 +588,12 @@ CREATE TABLE diary_comment ( 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 = '日记评论表'; +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '日记评论表 (t_diary_comment)'; -- ============================================================================ -- 18. 访客用户表 (guest_user) -- ============================================================================ -CREATE TABLE guest_user ( +CREATE TABLE t_guest_user ( id VARCHAR(64) PRIMARY KEY COMMENT 'UUID主键', -- UUID主键 guest_user_id VARCHAR(50) UNIQUE COMMENT '访客用户ID (格式: guest_xxx)', -- 访客用户ID (格式: guest_xxx) ip_address VARCHAR(45) COMMENT '客户端IP地址 (支持IPv6)', -- 客户端IP地址 (支持IPv6) @@ -609,414 +612,243 @@ CREATE TABLE guest_user ( 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 = '访客用户表'; +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '访客用户表 (t_guest_user)'; -- ============================================================================ -- 创建索引以提高查询性能 -- 注意: MySQL的CREATE INDEX不支持IF NOT EXISTS -- 如果索引已存在,重复执行会产生警告但不会中断脚本执行 -- ============================================================================ --- user表索引 -CREATE INDEX idx_user_account ON user (account); - -CREATE INDEX idx_user_username ON user (username); - -CREATE INDEX idx_user_email ON user (email); - -CREATE INDEX idx_user_phone ON user (phone); - -CREATE INDEX idx_user_last_active_time ON user (last_active_time); - -CREATE INDEX idx_user_create_time ON user (create_time); - -CREATE INDEX idx_user_member_level ON user (member_level); - -CREATE INDEX idx_user_status ON user (status); - -CREATE INDEX idx_user_is_verified ON user (is_verified); - -CREATE INDEX idx_user_create_by ON user (create_by); - -CREATE INDEX idx_user_update_by ON user (update_by); - -CREATE INDEX idx_user_is_deleted ON user (is_deleted); - -CREATE INDEX idx_user_third_party_id ON user (third_party_id); - -CREATE INDEX idx_user_third_party_type ON user (third_party_type); - --- conversation表索引 -CREATE INDEX idx_conversation_user_id ON conversation (user_id); - -CREATE INDEX idx_conversation_start_time ON conversation (start_time); - -CREATE INDEX idx_conversation_user_id_start_time ON conversation (user_id, start_time); - -CREATE INDEX idx_conversation_primary_emotion ON conversation (primary_emotion); - -CREATE INDEX idx_conversation_end_time ON conversation (end_time); - -CREATE INDEX idx_conversation_create_time ON conversation (create_time); - -CREATE INDEX idx_conversation_coze_conversation_id ON conversation (coze_conversation_id); - -CREATE INDEX idx_conversation_status ON conversation (status); - -CREATE INDEX idx_conversation_last_active_time ON conversation (last_active_time); - -CREATE INDEX idx_conversation_create_by ON conversation (create_by); - -CREATE INDEX idx_conversation_update_by ON conversation (update_by); - -CREATE INDEX idx_conversation_is_deleted ON conversation (is_deleted); - -CREATE INDEX idx_conversation_user_type ON conversation (user_type); - -CREATE INDEX idx_conversation_emotion_trend ON conversation (emotion_trend); - -CREATE INDEX idx_conversation_confidence ON conversation (confidence); - -CREATE INDEX idx_conversation_client_ip ON conversation (client_ip); - --- message表索引 -CREATE INDEX idx_message_conversation_id ON message (conversation_id); - -CREATE INDEX idx_message_timestamp ON message (timestamp); - -CREATE INDEX idx_message_conversation_id_timestamp ON message (conversation_id, timestamp); - -CREATE INDEX idx_message_sender ON message (sender); - -CREATE INDEX idx_message_type ON message (type); - -CREATE INDEX idx_message_is_read ON message (is_read); - -CREATE INDEX idx_message_create_time ON message (create_time); - -CREATE INDEX idx_message_coze_chat_id ON message (coze_chat_id); - -CREATE INDEX idx_message_status ON message (status); - -CREATE INDEX idx_message_parent_message_id ON message (parent_message_id); - -CREATE INDEX idx_message_create_by ON message (create_by); - -CREATE INDEX idx_message_update_by ON message (update_by); - -CREATE INDEX idx_message_is_deleted ON message (is_deleted); - --- coze_api_call表索引 -CREATE INDEX idx_coze_api_call_conversation_id ON coze_api_call (conversation_id); - -CREATE INDEX idx_coze_api_call_message_id ON coze_api_call (message_id); - -CREATE INDEX idx_coze_api_call_coze_chat_id ON coze_api_call (coze_chat_id); - -CREATE INDEX idx_coze_api_call_bot_id ON coze_api_call (bot_id); - -CREATE INDEX idx_coze_api_call_user_id ON coze_api_call (user_id); - -CREATE INDEX idx_coze_api_call_status ON coze_api_call (status); - -CREATE INDEX idx_coze_api_call_final_status ON coze_api_call (final_status); - -CREATE INDEX idx_coze_api_call_start_time ON coze_api_call (start_time); - -CREATE INDEX idx_coze_api_call_request_type ON coze_api_call (request_type); - -CREATE INDEX idx_coze_api_call_client_ip ON coze_api_call (client_ip); - -CREATE INDEX idx_coze_api_call_session_id ON coze_api_call (session_id); - -CREATE INDEX idx_coze_api_call_trace_id ON coze_api_call (trace_id); - -CREATE INDEX idx_coze_api_call_user_status ON coze_api_call (user_id, status); - -CREATE INDEX idx_coze_api_call_conversation_time ON coze_api_call (conversation_id, start_time); - --- emotion_analysis表索引 -CREATE INDEX idx_emotion_analysis_user_id ON emotion_analysis (user_id); - -CREATE INDEX idx_emotion_analysis_message_id ON emotion_analysis (message_id); - -CREATE INDEX idx_emotion_analysis_primary_emotion ON emotion_analysis (primary_emotion); - -CREATE INDEX idx_emotion_analysis_analysis_time ON emotion_analysis (analysis_time); - -CREATE INDEX idx_emotion_analysis_create_time ON emotion_analysis (create_time); - -CREATE INDEX idx_emotion_analysis_create_by ON emotion_analysis (create_by); - -CREATE INDEX idx_emotion_analysis_update_by ON emotion_analysis (update_by); - -CREATE INDEX idx_emotion_analysis_is_deleted ON emotion_analysis (is_deleted); - --- emotion_record表索引 -CREATE INDEX idx_emotion_record_user_id ON emotion_record (user_id); - -CREATE INDEX idx_emotion_record_date ON emotion_record (record_date); - -CREATE INDEX idx_emotion_record_emotion_type ON emotion_record (emotion_type); - -CREATE INDEX idx_emotion_record_user_id_date ON emotion_record (user_id, record_date); - -CREATE INDEX idx_emotion_record_user_id_emotion_type ON emotion_record (user_id, emotion_type); - -CREATE INDEX idx_emotion_record_intensity ON emotion_record (intensity); - -CREATE INDEX idx_emotion_record_create_time ON emotion_record (create_time); - -CREATE INDEX idx_emotion_record_create_by ON emotion_record (create_by); - -CREATE INDEX idx_emotion_record_update_by ON emotion_record (update_by); - -CREATE INDEX idx_emotion_record_is_deleted ON emotion_record (is_deleted); - --- growth_topic表索引 -CREATE INDEX idx_growth_topic_category ON growth_topic (category); - -CREATE INDEX idx_growth_topic_difficulty ON growth_topic (difficulty); - -CREATE INDEX idx_growth_topic_is_unlocked ON growth_topic (is_unlocked); - -CREATE INDEX idx_growth_topic_progress ON growth_topic (progress); - -CREATE INDEX idx_growth_topic_completed_time ON growth_topic (completed_time); - -CREATE INDEX idx_growth_topic_category_difficulty ON growth_topic (category, difficulty); - -CREATE INDEX idx_growth_topic_create_time ON growth_topic (create_time); - --- topic_interaction表索引 -CREATE INDEX idx_topic_interaction_topic_id ON topic_interaction (topic_id); - -CREATE INDEX idx_topic_interaction_type ON topic_interaction (type); - -CREATE INDEX idx_topic_interaction_completed_time ON topic_interaction (completed_time); - -CREATE INDEX idx_topic_interaction_rating ON topic_interaction (rating); - -CREATE INDEX idx_topic_interaction_topic_id_type ON topic_interaction (topic_id, type); - -CREATE INDEX idx_topic_interaction_create_time ON topic_interaction (create_time); - --- location_pin表索引 -CREATE INDEX idx_location_pin_latitude_longitude ON location_pin (latitude, longitude); - -CREATE INDEX idx_location_pin_type ON location_pin (type); - -CREATE INDEX idx_location_pin_category ON location_pin (category); - -CREATE INDEX idx_location_pin_created_by ON location_pin (created_by); - -CREATE INDEX idx_location_pin_likes ON location_pin (likes); - -CREATE INDEX idx_location_pin_visits ON location_pin (visits); - -CREATE INDEX idx_location_pin_is_bookmarked ON location_pin (is_bookmarked); - -CREATE INDEX idx_location_pin_type_category ON location_pin (type, category); - -CREATE INDEX idx_location_pin_create_time ON location_pin (create_time); - -CREATE INDEX idx_location_pin_last_visit_time ON location_pin (last_visit_time); - --- community_post表索引 -CREATE INDEX idx_community_post_user_id ON community_post (user_id); - -CREATE INDEX idx_community_post_location_id ON community_post (location_id); - -CREATE INDEX idx_community_post_create_time ON community_post (create_time); - -CREATE INDEX idx_community_post_type ON community_post (type); - -CREATE INDEX idx_community_post_likes ON community_post (likes); - -CREATE INDEX idx_community_post_view_count ON community_post (view_count); - -CREATE INDEX idx_community_post_is_private ON community_post (is_private); - -CREATE INDEX idx_community_post_user_id_create_time ON community_post (user_id, create_time); - -CREATE INDEX idx_community_post_type_create_time ON community_post (type, create_time); - --- comment表索引 -CREATE INDEX idx_comment_post_id ON comment (post_id); - -CREATE INDEX idx_comment_user_id ON comment (user_id); - -CREATE INDEX idx_comment_reply_to_id ON comment (reply_to_id); - -CREATE INDEX idx_comment_create_time ON comment (create_time); - -CREATE INDEX idx_comment_likes ON comment (likes); - -CREATE INDEX idx_comment_post_id_create_time ON comment (post_id, create_time); - --- achievement表索引 -CREATE INDEX idx_achievement_category ON achievement (category); - -CREATE INDEX idx_achievement_rarity ON achievement (rarity); - -CREATE INDEX idx_achievement_unlocked_time ON achievement (unlocked_time); - -CREATE INDEX idx_achievement_is_hidden ON achievement (is_hidden); - -CREATE INDEX idx_achievement_progress ON achievement (progress); - -CREATE INDEX idx_achievement_category_rarity ON achievement (category, rarity); - -CREATE INDEX idx_achievement_create_time ON achievement (create_time); - --- reward表索引 -CREATE INDEX idx_reward_topic_id ON reward (topic_id); - -CREATE INDEX idx_reward_achievement_id ON reward (achievement_id); - -CREATE INDEX idx_reward_type ON reward (type); - -CREATE INDEX idx_reward_earned_time ON reward (earned_time); - -CREATE INDEX idx_reward_rarity ON reward (rarity); - -CREATE INDEX idx_reward_is_new ON reward (is_new); - -CREATE INDEX idx_reward_type_earned_time ON reward (type, earned_time); - -CREATE INDEX idx_reward_create_time ON reward (create_time); - --- user_stats表索引 -CREATE INDEX idx_user_stats_user_id ON user_stats (user_id); - -CREATE INDEX idx_user_stats_total_points ON user_stats (total_points); - -CREATE INDEX idx_user_stats_consecutive_days ON user_stats (consecutive_days); - -CREATE INDEX idx_user_stats_max_consecutive_days ON user_stats (max_consecutive_days); - -CREATE INDEX idx_user_stats_social_interactions ON user_stats (social_interactions); - -CREATE INDEX idx_user_stats_update_time ON user_stats (update_time); - -CREATE INDEX idx_user_stats_create_time ON user_stats (create_time); - --- user_stats表日记相关索引 -CREATE INDEX idx_user_stats_diary_posts_created ON user_stats (diary_posts_created); - -CREATE INDEX idx_user_stats_diary_likes_received ON user_stats (diary_likes_received); - -CREATE INDEX idx_user_stats_diary_comments_received ON user_stats (diary_comments_received); - -CREATE INDEX idx_user_stats_diary_views_received ON user_stats (diary_views_received); - -CREATE INDEX idx_user_stats_featured_diary_count ON user_stats (featured_diary_count); - -CREATE INDEX idx_user_stats_ai_comments_received ON user_stats (ai_comments_received); - --- diary_post表索引 -CREATE INDEX idx_diary_post_user_id ON diary_post (user_id); - -CREATE INDEX idx_diary_post_publish_time ON diary_post (publish_time); - -CREATE INDEX idx_diary_post_last_comment_time ON diary_post (last_comment_time); - -CREATE INDEX idx_diary_post_status ON diary_post (status); - -CREATE INDEX idx_diary_post_priority ON diary_post (priority); - -CREATE INDEX idx_diary_post_featured ON diary_post (featured); - -CREATE INDEX idx_diary_post_create_time ON diary_post (create_time); - --- diary_comment表索引 -CREATE INDEX idx_diary_comment_diary_id ON diary_comment (diary_id); - -CREATE INDEX idx_diary_comment_user_id ON diary_comment (user_id); - -CREATE INDEX idx_diary_comment_parent_comment_id ON diary_comment (parent_comment_id); - -CREATE INDEX idx_diary_comment_publish_time ON diary_comment (publish_time); - -CREATE INDEX idx_diary_comment_last_reply_time ON diary_comment (last_reply_time); - -CREATE INDEX idx_diary_comment_emotion_score ON diary_comment (emotion_score); - -CREATE INDEX idx_diary_comment_sentiment_score ON diary_comment (sentiment_score); - -CREATE INDEX idx_diary_comment_create_time ON diary_comment (create_time); - --- diary_post表复合索引和功能索引 -CREATE INDEX idx_diary_post_user_publish ON diary_post (user_id, publish_time); - -CREATE INDEX idx_diary_post_user_status ON diary_post (user_id, status); - -CREATE INDEX idx_diary_post_public_publish ON diary_post (is_public, publish_time); - -CREATE INDEX idx_diary_post_featured_publish ON diary_post (featured, publish_time); - -CREATE INDEX idx_diary_post_mood_score ON diary_post (mood_score); - -CREATE INDEX idx_diary_post_ai_sentiment ON diary_post (ai_sentiment_score); - -CREATE INDEX idx_diary_post_location ON diary_post (latitude, longitude); - -CREATE INDEX idx_diary_post_like_count ON diary_post (like_count); - -CREATE INDEX idx_diary_post_comment_count ON diary_post (comment_count); - -CREATE INDEX idx_diary_post_view_count ON diary_post (view_count); - -CREATE INDEX idx_diary_post_create_by ON diary_post (create_by); - -CREATE INDEX idx_diary_post_update_by ON diary_post (update_by); - -CREATE INDEX idx_diary_post_is_deleted ON diary_post (is_deleted); - --- diary_comment表复合索引和功能索引 -CREATE INDEX idx_diary_comment_diary_publish ON diary_comment (diary_id, publish_time); - -CREATE INDEX idx_diary_comment_diary_type ON diary_comment (diary_id, comment_type); - -CREATE INDEX idx_diary_comment_user_publish ON diary_comment (user_id, publish_time); - -CREATE INDEX idx_diary_comment_parent_publish ON diary_comment (parent_comment_id, publish_time); - -CREATE INDEX idx_diary_comment_type_publish ON diary_comment (comment_type, publish_time); - -CREATE INDEX idx_diary_comment_status ON diary_comment (status); - -CREATE INDEX idx_diary_comment_is_top ON diary_comment (is_top); - -CREATE INDEX idx_diary_comment_like_count ON diary_comment (like_count); - -CREATE INDEX idx_diary_comment_reply_count ON diary_comment (reply_count); - -CREATE INDEX idx_diary_comment_ai_source ON diary_comment (ai_comment_source); - -CREATE INDEX idx_diary_comment_create_by ON diary_comment (create_by); - -CREATE INDEX idx_diary_comment_update_by ON diary_comment (update_by); - -CREATE INDEX idx_diary_comment_is_deleted ON diary_comment (is_deleted); - --- guest_user表索引 -CREATE INDEX idx_guest_user_guest_user_id ON guest_user (guest_user_id); - -CREATE INDEX idx_guest_user_ip_address ON guest_user (ip_address); - -CREATE INDEX idx_guest_user_last_active_time ON guest_user (last_active_time); - -CREATE INDEX idx_guest_user_conversation_count ON guest_user (conversation_count); - -CREATE INDEX idx_guest_user_message_count ON guest_user (message_count); - -CREATE INDEX idx_guest_user_create_time ON guest_user (create_time); - -CREATE INDEX idx_guest_user_create_by ON guest_user (create_by); - -CREATE INDEX idx_guest_user_update_by ON guest_user (update_by); - -CREATE INDEX idx_guest_user_is_deleted ON guest_user (is_deleted); +-- t_user表索引 +CREATE INDEX idx_user_account ON t_user (account); +CREATE INDEX idx_user_username ON t_user (username); +CREATE INDEX idx_user_email ON t_user (email); +CREATE INDEX idx_user_phone ON t_user (phone); +CREATE INDEX idx_user_last_active_time ON t_user (last_active_time); +CREATE INDEX idx_user_create_time ON t_user (create_time); +CREATE INDEX idx_user_member_level ON t_user (member_level); +CREATE INDEX idx_user_status ON t_user (status); +CREATE INDEX idx_user_is_verified ON t_user (is_verified); +CREATE INDEX idx_user_create_by ON t_user (create_by); +CREATE INDEX idx_user_update_by ON t_user (update_by); +CREATE INDEX idx_user_is_deleted ON t_user (is_deleted); +CREATE INDEX idx_user_third_party_id ON t_user (third_party_id); +CREATE INDEX idx_user_third_party_type ON t_user (third_party_type); + +-- t_conversation表索引 +CREATE INDEX idx_conversation_user_id ON t_conversation (user_id); +CREATE INDEX idx_conversation_start_time ON t_conversation (start_time); +CREATE INDEX idx_conversation_user_id_start_time ON t_conversation (user_id, start_time); +CREATE INDEX idx_conversation_primary_emotion ON t_conversation (primary_emotion); +CREATE INDEX idx_conversation_end_time ON t_conversation (end_time); +CREATE INDEX idx_conversation_create_time ON t_conversation (create_time); +CREATE INDEX idx_conversation_coze_conversation_id ON t_conversation (coze_conversation_id); +CREATE INDEX idx_conversation_status ON t_conversation (status); +CREATE INDEX idx_conversation_last_active_time ON t_conversation (last_active_time); +CREATE INDEX idx_conversation_create_by ON t_conversation (create_by); +CREATE INDEX idx_conversation_update_by ON t_conversation (update_by); +CREATE INDEX idx_conversation_is_deleted ON t_conversation (is_deleted); +CREATE INDEX idx_conversation_user_type ON t_conversation (user_type); +CREATE INDEX idx_conversation_emotion_trend ON t_conversation (emotion_trend); +CREATE INDEX idx_conversation_confidence ON t_conversation (confidence); +CREATE INDEX idx_conversation_client_ip ON t_conversation (client_ip); + +-- t_message表索引 +CREATE INDEX idx_message_conversation_id ON t_message (conversation_id); +CREATE INDEX idx_message_timestamp ON t_message (timestamp); +CREATE INDEX idx_message_conversation_id_timestamp ON t_message (conversation_id, timestamp); +CREATE INDEX idx_message_sender ON t_message (sender); +CREATE INDEX idx_message_type ON t_message (type); +CREATE INDEX idx_message_is_read ON t_message (is_read); +CREATE INDEX idx_message_create_time ON t_message (create_time); +CREATE INDEX idx_message_coze_chat_id ON t_message (coze_chat_id); +CREATE INDEX idx_message_status ON t_message (status); +CREATE INDEX idx_message_parent_message_id ON t_message (parent_message_id); +CREATE INDEX idx_message_create_by ON t_message (create_by); +CREATE INDEX idx_message_update_by ON t_message (update_by); +CREATE INDEX idx_message_is_deleted ON t_message (is_deleted); + +-- t_coze_api_call表索引 +CREATE INDEX idx_coze_api_call_conversation_id ON t_coze_api_call (conversation_id); +CREATE INDEX idx_coze_api_call_message_id ON t_coze_api_call (message_id); +CREATE INDEX idx_coze_api_call_coze_chat_id ON t_coze_api_call (coze_chat_id); +CREATE INDEX idx_coze_api_call_bot_id ON t_coze_api_call (bot_id); +CREATE INDEX idx_coze_api_call_user_id ON t_coze_api_call (user_id); +CREATE INDEX idx_coze_api_call_status ON t_coze_api_call (status); +CREATE INDEX idx_coze_api_call_final_status ON t_coze_api_call (final_status); +CREATE INDEX idx_coze_api_call_start_time ON t_coze_api_call (start_time); +CREATE INDEX idx_coze_api_call_request_type ON t_coze_api_call (request_type); +CREATE INDEX idx_coze_api_call_client_ip ON t_coze_api_call (client_ip); +CREATE INDEX idx_coze_api_call_session_id ON t_coze_api_call (session_id); +CREATE INDEX idx_coze_api_call_trace_id ON t_coze_api_call (trace_id); +CREATE INDEX idx_coze_api_call_user_status ON t_coze_api_call (user_id, status); +CREATE INDEX idx_coze_api_call_conversation_time ON t_coze_api_call (conversation_id, start_time); + +-- t_emotion_analysis表索引 +CREATE INDEX idx_emotion_analysis_user_id ON t_emotion_analysis (user_id); +CREATE INDEX idx_emotion_analysis_message_id ON t_emotion_analysis (message_id); +CREATE INDEX idx_emotion_analysis_primary_emotion ON t_emotion_analysis (primary_emotion); +CREATE INDEX idx_emotion_analysis_analysis_time ON t_emotion_analysis (analysis_time); +CREATE INDEX idx_emotion_analysis_create_time ON t_emotion_analysis (create_time); +CREATE INDEX idx_emotion_analysis_create_by ON t_emotion_analysis (create_by); +CREATE INDEX idx_emotion_analysis_update_by ON t_emotion_analysis (update_by); +CREATE INDEX idx_emotion_analysis_is_deleted ON t_emotion_analysis (is_deleted); + +-- t_emotion_record表索引 +CREATE INDEX idx_emotion_record_user_id ON t_emotion_record (user_id); +CREATE INDEX idx_emotion_record_date ON t_emotion_record (record_date); +CREATE INDEX idx_emotion_record_emotion_type ON t_emotion_record (emotion_type); +CREATE INDEX idx_emotion_record_user_id_date ON t_emotion_record (user_id, record_date); +CREATE INDEX idx_emotion_record_user_id_emotion_type ON t_emotion_record (user_id, emotion_type); +CREATE INDEX idx_emotion_record_intensity ON t_emotion_record (intensity); +CREATE INDEX idx_emotion_record_create_time ON t_emotion_record (create_time); +CREATE INDEX idx_emotion_record_create_by ON t_emotion_record (create_by); +CREATE INDEX idx_emotion_record_update_by ON t_emotion_record (update_by); +CREATE INDEX idx_emotion_record_is_deleted ON t_emotion_record (is_deleted); + +-- t_growth_topic表索引 +CREATE INDEX idx_growth_topic_category ON t_growth_topic (category); +CREATE INDEX idx_growth_topic_difficulty ON t_growth_topic (difficulty); +CREATE INDEX idx_growth_topic_is_unlocked ON t_growth_topic (is_unlocked); +CREATE INDEX idx_growth_topic_progress ON t_growth_topic (progress); +CREATE INDEX idx_growth_topic_completed_time ON t_growth_topic (completed_time); +CREATE INDEX idx_growth_topic_category_difficulty ON t_growth_topic (category, difficulty); +CREATE INDEX idx_growth_topic_create_time ON t_growth_topic (create_time); + +-- t_topic_interaction表索引 +CREATE INDEX idx_topic_interaction_topic_id ON t_topic_interaction (topic_id); +CREATE INDEX idx_topic_interaction_type ON t_topic_interaction (type); +CREATE INDEX idx_topic_interaction_completed_time ON t_topic_interaction (completed_time); +CREATE INDEX idx_topic_interaction_rating ON t_topic_interaction (rating); +CREATE INDEX idx_topic_interaction_topic_id_type ON t_topic_interaction (topic_id, type); +CREATE INDEX idx_topic_interaction_create_time ON t_topic_interaction (create_time); + +-- t_location_pin表索引 +CREATE INDEX idx_location_pin_latitude_longitude ON t_location_pin (latitude, longitude); +CREATE INDEX idx_location_pin_type ON t_location_pin (type); +CREATE INDEX idx_location_pin_category ON t_location_pin (category); +CREATE INDEX idx_location_pin_create_by ON t_location_pin (create_by); +CREATE INDEX idx_location_pin_likes ON t_location_pin (likes); +CREATE INDEX idx_location_pin_visits ON t_location_pin (visits); +CREATE INDEX idx_location_pin_is_bookmarked ON t_location_pin (is_bookmarked); +CREATE INDEX idx_location_pin_type_category ON t_location_pin (type, category); +CREATE INDEX idx_location_pin_create_time ON t_location_pin (create_time); +CREATE INDEX idx_location_pin_last_visit_time ON t_location_pin (last_visit_time); + +-- t_community_post表索引 +CREATE INDEX idx_community_post_user_id ON t_community_post (user_id); +CREATE INDEX idx_community_post_location_id ON t_community_post (location_id); +CREATE INDEX idx_community_post_create_time ON t_community_post (create_time); +CREATE INDEX idx_community_post_type ON t_community_post (type); +CREATE INDEX idx_community_post_likes ON t_community_post (likes); +CREATE INDEX idx_community_post_view_count ON t_community_post (view_count); +CREATE INDEX idx_community_post_is_private ON t_community_post (is_private); +CREATE INDEX idx_community_post_user_id_create_time ON t_community_post (user_id, create_time); +CREATE INDEX idx_community_post_type_create_time ON t_community_post (type, create_time); + +-- t_comment表索引 +CREATE INDEX idx_comment_post_id ON t_comment (post_id); +CREATE INDEX idx_comment_user_id ON t_comment (user_id); +CREATE INDEX idx_comment_reply_to_id ON t_comment (reply_to_id); +CREATE INDEX idx_comment_create_time ON t_comment (create_time); +CREATE INDEX idx_comment_likes ON t_comment (likes); +CREATE INDEX idx_comment_post_id_create_time ON t_comment (post_id, create_time); + +-- t_achievement表索引 +CREATE INDEX idx_achievement_category ON t_achievement (category); +CREATE INDEX idx_achievement_rarity ON t_achievement (rarity); +CREATE INDEX idx_achievement_unlocked_time ON t_achievement (unlocked_time); +CREATE INDEX idx_achievement_is_hidden ON t_achievement (is_hidden); +CREATE INDEX idx_achievement_progress ON t_achievement (progress); +CREATE INDEX idx_achievement_category_rarity ON t_achievement (category, rarity); +CREATE INDEX idx_achievement_create_time ON t_achievement (create_time); + +-- t_reward表索引 +CREATE INDEX idx_reward_topic_id ON t_reward (topic_id); +CREATE INDEX idx_reward_achievement_id ON t_reward (achievement_id); +CREATE INDEX idx_reward_type ON t_reward (type); +CREATE INDEX idx_reward_earned_time ON t_reward (earned_time); +CREATE INDEX idx_reward_rarity ON t_reward (rarity); +CREATE INDEX idx_reward_is_new ON t_reward (is_new); +CREATE INDEX idx_reward_type_earned_time ON t_reward (type, earned_time); +CREATE INDEX idx_reward_create_time ON t_reward (create_time); + +-- t_user_stats表索引 +CREATE INDEX idx_user_stats_user_id ON t_user_stats (user_id); +CREATE INDEX idx_user_stats_total_points ON t_user_stats (total_points); +CREATE INDEX idx_user_stats_consecutive_days ON t_user_stats (consecutive_days); +CREATE INDEX idx_user_stats_max_consecutive_days ON t_user_stats (max_consecutive_days); +CREATE INDEX idx_user_stats_social_interactions ON t_user_stats (social_interactions); +CREATE INDEX idx_user_stats_update_time ON t_user_stats (update_time); +CREATE INDEX idx_user_stats_create_time ON t_user_stats (create_time); + +-- t_user_stats表日记相关索引 +CREATE INDEX idx_user_stats_diary_posts_created ON t_user_stats (diary_posts_created); +CREATE INDEX idx_user_stats_diary_likes_received ON t_user_stats (diary_likes_received); +CREATE INDEX idx_user_stats_diary_comments_received ON t_user_stats (diary_comments_received); +CREATE INDEX idx_user_stats_diary_views_received ON t_user_stats (diary_views_received); +CREATE INDEX idx_user_stats_featured_diary_count ON t_user_stats (featured_diary_count); +CREATE INDEX idx_user_stats_ai_comments_received ON t_user_stats (ai_comments_received); + +-- t_diary_post表索引 +CREATE INDEX idx_diary_post_user_id ON t_diary_post (user_id); +CREATE INDEX idx_diary_post_publish_time ON t_diary_post (publish_time); +CREATE INDEX idx_diary_post_last_comment_time ON t_diary_post (last_comment_time); +CREATE INDEX idx_diary_post_status ON t_diary_post (status); +CREATE INDEX idx_diary_post_priority ON t_diary_post (priority); +CREATE INDEX idx_diary_post_featured ON t_diary_post (featured); +CREATE INDEX idx_diary_post_create_time ON t_diary_post (create_time); + +-- t_diary_comment表索引 +CREATE INDEX idx_diary_comment_diary_id ON t_diary_comment (diary_id); +CREATE INDEX idx_diary_comment_user_id ON t_diary_comment (user_id); +CREATE INDEX idx_diary_comment_parent_comment_id ON t_diary_comment (parent_comment_id); +CREATE INDEX idx_diary_comment_publish_time ON t_diary_comment (publish_time); +CREATE INDEX idx_diary_comment_last_reply_time ON t_diary_comment (last_reply_time); +CREATE INDEX idx_diary_comment_emotion_score ON t_diary_comment (emotion_score); +CREATE INDEX idx_diary_comment_sentiment_score ON t_diary_comment (sentiment_score); +CREATE INDEX idx_diary_comment_create_time ON t_diary_comment (create_time); + +-- t_diary_post表复合索引和功能索引 +CREATE INDEX idx_diary_post_user_publish ON t_diary_post (user_id, publish_time); +CREATE INDEX idx_diary_post_user_status ON t_diary_post (user_id, status); +CREATE INDEX idx_diary_post_public_publish ON t_diary_post (is_public, publish_time); +CREATE INDEX idx_diary_post_featured_publish ON t_diary_post (featured, publish_time); +CREATE INDEX idx_diary_post_mood_score ON t_diary_post (mood_score); +CREATE INDEX idx_diary_post_ai_sentiment ON t_diary_post (ai_sentiment_score); +CREATE INDEX idx_diary_post_location ON t_diary_post (latitude, longitude); +CREATE INDEX idx_diary_post_like_count ON t_diary_post (like_count); +CREATE INDEX idx_diary_post_comment_count ON t_diary_post (comment_count); +CREATE INDEX idx_diary_post_view_count ON t_diary_post (view_count); +CREATE INDEX idx_diary_post_create_by ON t_diary_post (create_by); +CREATE INDEX idx_diary_post_update_by ON t_diary_post (update_by); +CREATE INDEX idx_diary_post_is_deleted ON t_diary_post (is_deleted); + +-- t_diary_comment表复合索引和功能索引 +CREATE INDEX idx_diary_comment_diary_publish ON t_diary_comment (diary_id, publish_time); +CREATE INDEX idx_diary_comment_diary_type ON t_diary_comment (diary_id, comment_type); +CREATE INDEX idx_diary_comment_user_publish ON t_diary_comment (user_id, publish_time); +CREATE INDEX idx_diary_comment_parent_publish ON t_diary_comment (parent_comment_id, publish_time); +CREATE INDEX idx_diary_comment_type_publish ON t_diary_comment (comment_type, publish_time); +CREATE INDEX idx_diary_comment_status ON t_diary_comment (status); +CREATE INDEX idx_diary_comment_is_top ON t_diary_comment (is_top); +CREATE INDEX idx_diary_comment_like_count ON t_diary_comment (like_count); +CREATE INDEX idx_diary_comment_reply_count ON t_diary_comment (reply_count); +CREATE INDEX idx_diary_comment_ai_source ON t_diary_comment (ai_comment_source); +CREATE INDEX idx_diary_comment_create_by ON t_diary_comment (create_by); +CREATE INDEX idx_diary_comment_update_by ON t_diary_comment (update_by); +CREATE INDEX idx_diary_comment_is_deleted ON t_diary_comment (is_deleted); + +-- t_guest_user表索引 +CREATE INDEX idx_guest_user_guest_user_id ON t_guest_user (guest_user_id); +CREATE INDEX idx_guest_user_ip_address ON t_guest_user (ip_address); +CREATE INDEX idx_guest_user_last_active_time ON t_guest_user (last_active_time); +CREATE INDEX idx_guest_user_conversation_count ON t_guest_user (conversation_count); +CREATE INDEX idx_guest_user_message_count ON t_guest_user (message_count); +CREATE INDEX idx_guest_user_create_time ON t_guest_user (create_time); +CREATE INDEX idx_guest_user_create_by ON t_guest_user (create_by); +CREATE INDEX idx_guest_user_update_by ON t_guest_user (update_by); +CREATE INDEX idx_guest_user_is_deleted ON t_guest_user (is_deleted); -- ============================================================================ -- 数据库统计信息 @@ -1026,7 +858,8 @@ SELECT FROM INFORMATION_SCHEMA.TABLES WHERE - TABLE_SCHEMA = 'emotion'; + TABLE_SCHEMA = 'emotion' + AND TABLE_NAME LIKE 't\_%' ESCAPE '\\'; -- 显示创建的表 SELECT @@ -1037,6 +870,7 @@ FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = 'emotion' + AND TABLE_NAME LIKE 't\_%' ESCAPE '\\' ORDER BY TABLE_NAME;