feat: 完成项目整理优化和生产环境配置
🧹 项目结构优化: - 删除重复和过时的文件 - 整理文档到docs目录结构 - 优化配置文件到configs目录 - 创建清晰的PROJECT_STRUCTURE.md 🔧 中间件配置: - 重启MySQL/Redis/Nacos中间件 - 使用现有数据目录,确保数据完整性 - 统一密码配置: MySQL(EmotionMuseum2025*#), Nacos(Peanut2817*#) 🌐 Nginx配置: - 配置前端路径: /emotion-museum - 配置API代理: /api/ -> 19000 - 配置WebSocket代理: /ws/ -> 19007 - 添加健康检查端点: /health 📋 部署脚本优化: - restart-middleware.sh - 中间件重启脚本 - setup-nginx.sh - Nginx配置脚本 - cleanup-project.sh - 项目清理脚本 - one-click-deploy.sh - 一键部署脚本 📖 文档完善: - DEPLOYMENT_FINAL.md - 最终部署指南 - PROJECT_STRUCTURE.md - 项目结构说明 - 完整的运维和故障排查指南 ✅ 生产环境就绪: - 中间件: MySQL/Redis/Nacos 运行正常 - Nginx: 反向代理配置完成 - 访问地址: http://47.111.10.27/emotion-museum - 健康检查: http://47.111.10.27/health 🎯 项目现状: - 10个微服务模块完整 - 前后端分离架构 - 容器化部署 - 统一配置管理 - 完整的部署和运维体系
This commit is contained in:
@@ -1,415 +0,0 @@
|
|||||||
# 情绪博物馆全栈项目 - Claude 开发指南
|
|
||||||
|
|
||||||
## 项目概述
|
|
||||||
|
|
||||||
这是一个情绪博物馆全栈项目,包含iOS客户端、Spring Boot微服务后端和Vue.js Web管理界面。项目采用现代化技术栈,实现情绪记录、AI对话、成长陪伴等核心功能。
|
|
||||||
|
|
||||||
### 项目结构
|
|
||||||
- **EmotionMuseum/**: iOS SwiftUI客户端应用
|
|
||||||
- **backend/**: Spring Cloud Alibaba微服务后端
|
|
||||||
- **web/**: Vue.js + Vite Web管理界面
|
|
||||||
- **mysql_*.sql**: 数据库脚本文件
|
|
||||||
- ***.md**: 项目文档和需求规格书
|
|
||||||
|
|
||||||
## 技术栈
|
|
||||||
|
|
||||||
### iOS客户端 (SwiftUI)
|
|
||||||
- **SwiftUI**: 声明式UI框架
|
|
||||||
- **Core Data**: 本地数据存储
|
|
||||||
- **Combine**: 响应式编程
|
|
||||||
- **MapKit**: 地图功能 (高德地图集成)
|
|
||||||
- **UIKit**: 部分复杂UI组件
|
|
||||||
|
|
||||||
### 后端微服务 (Spring Boot)
|
|
||||||
- **Spring Boot**: 3.0.2 (主框架)
|
|
||||||
- **Spring Cloud**: 2022.0.0 (微服务支持)
|
|
||||||
- **Spring Cloud Alibaba**: 2022.0.0.0 (微服务生态)
|
|
||||||
- **Java**: 17+ (编程语言)
|
|
||||||
- **Maven**: 3.6+ (依赖管理)
|
|
||||||
|
|
||||||
### Web前端 (Vue.js)
|
|
||||||
- **Vue.js**: 3.x (前端框架)
|
|
||||||
- **Vite**: 构建工具
|
|
||||||
- **Pinia**: 状态管理
|
|
||||||
- **Vue Router**: 路由管理
|
|
||||||
- **SCSS**: 样式预处理器
|
|
||||||
|
|
||||||
### 数据存储
|
|
||||||
- **MySQL**: 8.0+ (主数据库)
|
|
||||||
- **Redis**: 7.0+ (缓存)
|
|
||||||
- **MyBatis Plus**: 3.5.3.1 (ORM框架)
|
|
||||||
- **Druid**: 1.2.16 (数据库连接池)
|
|
||||||
|
|
||||||
### 微服务基础设施
|
|
||||||
- **Nacos**: 2.2.0+ (服务注册发现和配置中心)
|
|
||||||
- **Gateway**: Spring Cloud Gateway (API网关)
|
|
||||||
|
|
||||||
### 工具和库
|
|
||||||
- **Lombok**: 代码简化
|
|
||||||
- **Hutool**: 工具类库
|
|
||||||
- **FastJSON2**: JSON处理
|
|
||||||
- **JWT**: 认证授权
|
|
||||||
- **Knife4j**: API文档
|
|
||||||
- **MapStruct**: 对象映射
|
|
||||||
|
|
||||||
## 微服务架构
|
|
||||||
|
|
||||||
### 服务列表
|
|
||||||
| 服务名称 | 端口 | 描述 | 包路径 |
|
|
||||||
|---------|------|------|--------|
|
|
||||||
| emotion-gateway | 9000 | API网关 | com.emotionmuseum.gateway |
|
|
||||||
| emotion-user | 9001 | 用户服务 | com.emotionmuseum.user |
|
|
||||||
| emotion-ai | 9002 | AI对话服务 | com.emotionmuseum.ai |
|
|
||||||
| emotion-record | 9003 | 情绪记录服务 | com.emotionmuseum.record |
|
|
||||||
| emotion-growth | 9004 | 成长课题服务 | com.emotionmuseum.growth |
|
|
||||||
| emotion-explore | 9005 | 地图探索服务 | com.emotionmuseum.explore |
|
|
||||||
| emotion-reward | 9006 | 成就奖励服务 | com.emotionmuseum.reward |
|
|
||||||
| emotion-stats | 9007 | 统计分析服务 | com.emotionmuseum.stats |
|
|
||||||
| emotion-common | - | 公共模块 | com.emotionmuseum.common |
|
|
||||||
|
|
||||||
## 开发规范
|
|
||||||
|
|
||||||
### 代码结构约定
|
|
||||||
|
|
||||||
每个微服务模块遵循标准的MVC分层架构:
|
|
||||||
```
|
|
||||||
emotion-[service]/
|
|
||||||
├── sql # 数据库脚本
|
|
||||||
├── src/main/java/com/emotionmuseum/[service]/
|
|
||||||
│ ├── [Service]Application.java # 启动类
|
|
||||||
│ ├── controller/ # 控制器层
|
|
||||||
│ ├── service/ # 服务层
|
|
||||||
│ │ └── impl/ # 服务实现
|
|
||||||
│ ├── mapper/ # 数据访问层
|
|
||||||
│ ├── constants/ # 常量类
|
|
||||||
│ ├── enums/ # 枚举类
|
|
||||||
│ ├── entity/ # 实体类
|
|
||||||
│ ├── request/ # 请求体
|
|
||||||
│ ├── response/ # 响应体
|
|
||||||
│ ├── dto/ # 数据传输对象
|
|
||||||
│ ├── vo/ # 视图对象
|
|
||||||
│ └── config/ # 配置类
|
|
||||||
├── src/main/resources/
|
|
||||||
│ ├── application.yml # 配置文件
|
|
||||||
│ └── mapper/ # MyBatis XML文件
|
|
||||||
└── pom.xml # 依赖配置
|
|
||||||
```
|
|
||||||
|
|
||||||
### 命名规范
|
|
||||||
|
|
||||||
1. **包命名**: 全小写,使用点分隔
|
|
||||||
2. **类命名**: 驼峰命名法,首字母大写
|
|
||||||
3. **方法命名**: 驼峰命名法,首字母小写
|
|
||||||
4. **常量命名**: 全大写,下划线分隔
|
|
||||||
5. **数据库表名**: 小写,下划线分隔
|
|
||||||
6. **字段命名**: 小写,下划线分隔
|
|
||||||
|
|
||||||
### API设计规范
|
|
||||||
|
|
||||||
1. **RESTful风格**: 使用标准HTTP方法(GET/POST/PUT/DELETE)
|
|
||||||
2. **URL路径**: `/api/{service}/{resource}`
|
|
||||||
3. **统一响应**: 使用`Result<T>`包装所有API响应
|
|
||||||
4. **状态码**: 使用`ResultCode`枚举定义业务状态码
|
|
||||||
5. **分页**: 使用`PageQuery`进行分页查询
|
|
||||||
6. **请求体**: 所有请求封装在request中
|
|
||||||
7. **响应体**: 所有响应封装在response中
|
|
||||||
|
|
||||||
### 数据库规范
|
|
||||||
|
|
||||||
1. **表命名**: 使用模块前缀,如`user_info`、`ai_conversation`
|
|
||||||
2. **字段命名**: 全小写,下划线分隔
|
|
||||||
3. **主键**: 统一使用`id`,使用雪花算法生成,类型为varchar(36) UUID
|
|
||||||
4. **公共字段**: 继承`BaseEntity`,包含创建时间、更新时间、逻辑删除等
|
|
||||||
5. **逻辑删除**: 使用`deleted`字段,0=未删除,1=已删除
|
|
||||||
|
|
||||||
### 异常处理
|
|
||||||
|
|
||||||
1. **全局异常处理**: 在公共模块统一处理
|
|
||||||
2. **业务异常**: 继承`RuntimeException`
|
|
||||||
3. **异常响应**: 统一返回`Result.error()`格式
|
|
||||||
|
|
||||||
## 开发工具指令
|
|
||||||
|
|
||||||
### 自动化开发环境 (推荐)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 启动所有服务并监控文件变化
|
|
||||||
./dev-auto.sh
|
|
||||||
|
|
||||||
# 查看服务状态
|
|
||||||
./dev-auto.sh status
|
|
||||||
|
|
||||||
# 停止所有服务
|
|
||||||
./dev-auto.sh stop
|
|
||||||
|
|
||||||
# 查看实时日志
|
|
||||||
./dev-auto.sh logs
|
|
||||||
```
|
|
||||||
|
|
||||||
**自动化开发环境特性:**
|
|
||||||
- **自动编译**: 检测Java/YAML/XML文件变化时自动重新编译
|
|
||||||
- **热重载**: 使用Spring Boot DevTools实现代码变更后自动重启
|
|
||||||
- **服务监控**: 实时监控所有微服务的健康状态
|
|
||||||
- **统一日志**: 所有服务日志统一存储在`dev-logs/`目录
|
|
||||||
- **一键管理**: 支持启动、停止、状态查看、日志查看等操作
|
|
||||||
|
|
||||||
建议安装`fswatch`以获得更好的文件监控体验:
|
|
||||||
```bash
|
|
||||||
brew install fswatch # macOS
|
|
||||||
apt-get install inotify-tools # Ubuntu
|
|
||||||
```
|
|
||||||
|
|
||||||
### 手动构建和启动
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 清理编译
|
|
||||||
mvn clean compile -DskipTests
|
|
||||||
|
|
||||||
# 编译所有模块
|
|
||||||
mvn clean install -DskipTests
|
|
||||||
|
|
||||||
# 启动单个服务
|
|
||||||
cd emotion-[service] && mvn spring-boot:run
|
|
||||||
```
|
|
||||||
|
|
||||||
### 服务操作
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 健康检查
|
|
||||||
curl http://localhost:808[x]/actuator/health
|
|
||||||
|
|
||||||
# 查看服务日志 (手动启动)
|
|
||||||
tail -f logs/emotion-[service].log
|
|
||||||
|
|
||||||
# 查看服务日志 (自动化环境)
|
|
||||||
tail -f dev-logs/emotion-[service].log
|
|
||||||
```
|
|
||||||
|
|
||||||
### iOS开发工具指令
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 在Xcode中打开项目
|
|
||||||
open EmotionMuseum/EmotionMuseum.xcodeproj
|
|
||||||
|
|
||||||
# 构建iOS应用
|
|
||||||
xcodebuild -project EmotionMuseum/EmotionMuseum.xcodeproj -scheme EmotionMuseum -configuration Debug
|
|
||||||
|
|
||||||
# 运行iOS模拟器
|
|
||||||
xcrun simctl boot "iPhone 15 Pro"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Web前端工具指令
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 进入web目录
|
|
||||||
cd web
|
|
||||||
|
|
||||||
# 安装依赖
|
|
||||||
npm install
|
|
||||||
|
|
||||||
# 启动开发服务器
|
|
||||||
npm run dev
|
|
||||||
|
|
||||||
# 构建生产版本
|
|
||||||
npm run build
|
|
||||||
```
|
|
||||||
|
|
||||||
### 数据库操作
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 导入数据库结构
|
|
||||||
mysql -u root -p emotion_museum < mysql_deploy_database.sql
|
|
||||||
|
|
||||||
# 执行迁移脚本
|
|
||||||
mysql -u root -p emotion_museum < [migration_file].sql
|
|
||||||
```
|
|
||||||
|
|
||||||
## 测试规范
|
|
||||||
|
|
||||||
### 单元测试
|
|
||||||
- 使用JUnit 5和Spring Boot Test
|
|
||||||
- 测试覆盖率要求:控制器层>80%,服务层>90%
|
|
||||||
- Mock外部依赖
|
|
||||||
|
|
||||||
### 集成测试
|
|
||||||
- 使用TestContainers进行数据库测试
|
|
||||||
- 测试完整的API调用链路
|
|
||||||
|
|
||||||
### API测试
|
|
||||||
- 使用脚本文件测试API端点
|
|
||||||
- 参考`test-api.sh`、`test-coze-api.sh`
|
|
||||||
|
|
||||||
## 配置管理
|
|
||||||
|
|
||||||
### Nacos配置
|
|
||||||
- 环境: `emotion-dev` (开发环境)
|
|
||||||
- 配置文件: `common-mysql.yml`、`common-redis.yml`、`coze-config.yml`
|
|
||||||
- 各服务配置: `emotion-[service].yml`
|
|
||||||
|
|
||||||
### 敏感信息
|
|
||||||
- 数据库密码存储在Nacos配置中心
|
|
||||||
- API密钥使用环境变量或配置中心管理
|
|
||||||
- 生产环境配置独立管理
|
|
||||||
|
|
||||||
## 代码提交规范
|
|
||||||
|
|
||||||
### 提交信息格式
|
|
||||||
```
|
|
||||||
type(scope): description
|
|
||||||
|
|
||||||
[optional body]
|
|
||||||
|
|
||||||
[optional footer]
|
|
||||||
```
|
|
||||||
|
|
||||||
### 类型说明
|
|
||||||
- `feat`: 新功能
|
|
||||||
- `fix`: 修复bug
|
|
||||||
- `docs`: 文档修改
|
|
||||||
- `style`: 代码格式
|
|
||||||
- `refactor`: 重构
|
|
||||||
- `test`: 测试相关
|
|
||||||
- `chore`: 构建工具
|
|
||||||
|
|
||||||
### 示例
|
|
||||||
```
|
|
||||||
feat(user): add user registration API
|
|
||||||
|
|
||||||
- Add user registration endpoint
|
|
||||||
- Implement email validation
|
|
||||||
- Add password encryption
|
|
||||||
|
|
||||||
Closes #123
|
|
||||||
```
|
|
||||||
|
|
||||||
## 安全要求
|
|
||||||
|
|
||||||
1. **认证授权**: 使用JWT token进行用户认证
|
|
||||||
2. **数据加密**: 敏感数据加密存储
|
|
||||||
3. **SQL注入**: 使用参数化查询防止SQL注入
|
|
||||||
4. **XSS防护**: 对用户输入进行过滤和转义
|
|
||||||
5. **CORS配置**: 限制跨域访问
|
|
||||||
|
|
||||||
## 性能优化
|
|
||||||
|
|
||||||
1. **数据库优化**: 合理使用索引,避免N+1查询
|
|
||||||
2. **缓存策略**: 热点数据使用Redis缓存
|
|
||||||
3. **连接池**: 合理配置数据库连接池参数
|
|
||||||
4. **异步处理**: 耗时操作使用异步执行
|
|
||||||
|
|
||||||
## 监控和日志
|
|
||||||
|
|
||||||
1. **健康检查**: 所有服务提供`/actuator/health`端点
|
|
||||||
2. **Prometheus指标**: 通过`/actuator/prometheus`暴露指标
|
|
||||||
3. **日志级别**: 开发环境使用DEBUG,生产环境使用INFO
|
|
||||||
4. **日志格式**: 统一使用结构化日志
|
|
||||||
|
|
||||||
## 常见问题处理
|
|
||||||
|
|
||||||
### 服务启动失败
|
|
||||||
1. 检查Nacos服务是否启动
|
|
||||||
2. 检查数据库连接配置
|
|
||||||
3. 检查端口是否被占用
|
|
||||||
4. 查看服务日志定位错误
|
|
||||||
|
|
||||||
### 数据库问题
|
|
||||||
1. 确认数据库服务状态
|
|
||||||
2. 检查连接参数配置
|
|
||||||
3. 验证数据库权限
|
|
||||||
4. 查看慢查询日志
|
|
||||||
|
|
||||||
### 缓存问题
|
|
||||||
1. 检查Redis服务状态
|
|
||||||
2. 验证缓存配置
|
|
||||||
3. 清理过期缓存数据
|
|
||||||
|
|
||||||
## 扩展开发
|
|
||||||
|
|
||||||
### 添加新微服务
|
|
||||||
1. 复制现有服务模块结构
|
|
||||||
2. 修改包名和端口配置
|
|
||||||
3. 添加到父工程pom.xml
|
|
||||||
4. 更新启动脚本
|
|
||||||
5. 配置Nacos注册信息
|
|
||||||
|
|
||||||
### 添加新API
|
|
||||||
1. 在对应服务的controller包下创建控制器
|
|
||||||
2. 定义DTO和VO对象
|
|
||||||
3. 实现服务层逻辑
|
|
||||||
4. 添加数据访问层
|
|
||||||
5. 编写单元测试
|
|
||||||
6. 更新API文档
|
|
||||||
|
|
||||||
## 部署说明
|
|
||||||
|
|
||||||
### 开发环境
|
|
||||||
- 本地启动:使用Maven插件
|
|
||||||
- Docker部署:提供Dockerfile和docker-compose.yml
|
|
||||||
|
|
||||||
### 生产环境
|
|
||||||
- 容器化部署:使用Kubernetes
|
|
||||||
- 配置管理:环境变量和配置中心
|
|
||||||
- 服务监控:Prometheus + Grafana
|
|
||||||
|
|
||||||
## Claude 开发助手指南
|
|
||||||
|
|
||||||
### 任务执行原则
|
|
||||||
|
|
||||||
1. **准确性第一**: 在执行任何任务前,必须先分析项目结构和现有代码,确保理解正确
|
|
||||||
2. **规范性约束**: 严格遵循项目的代码规范、命名约定和架构模式
|
|
||||||
3. **功能完整性**: 确保所有修改不会破坏现有功能,新增功能要完整实现
|
|
||||||
4. **测试验证**: 重要修改后需要运行相关测试,确保代码质量
|
|
||||||
|
|
||||||
### 文件操作规范
|
|
||||||
|
|
||||||
1. **iOS项目 (SwiftUI)**
|
|
||||||
- 修改前先读取 `EmotionMuseum/EmotionMuseum.xcodeproj/project.pbxproj` 了解项目结构
|
|
||||||
- 遵循SwiftUI声明式编程范式
|
|
||||||
- 使用现有的颜色主题和组件样式
|
|
||||||
- 新增文件要正确添加到Xcode项目中
|
|
||||||
|
|
||||||
2. **后端微服务 (Spring Boot)**
|
|
||||||
- 修改前检查 `backend/pom.xml` 了解模块依赖
|
|
||||||
- 遵循MVC分层架构
|
|
||||||
- 使用统一的Result响应格式
|
|
||||||
- 数据库操作使用MyBatis Plus
|
|
||||||
|
|
||||||
3. **Web前端 (Vue.js)**
|
|
||||||
- 修改前检查 `web/package.json` 了解依赖版本
|
|
||||||
- 使用Vue 3 Composition API
|
|
||||||
- 遵循组件化开发模式
|
|
||||||
- 样式使用SCSS预处理器
|
|
||||||
|
|
||||||
### 常用检查清单
|
|
||||||
|
|
||||||
#### 开始任务前
|
|
||||||
- [ ] 读取相关文档 (*.md 文件) 了解需求
|
|
||||||
- [ ] 分析现有代码结构和实现模式
|
|
||||||
- [ ] 确认修改范围和影响面
|
|
||||||
- [ ] 检查是否有相关测试需要更新
|
|
||||||
|
|
||||||
#### 完成任务后
|
|
||||||
- [ ] 代码符合项目规范
|
|
||||||
- [ ] 新增功能完整可用
|
|
||||||
- [ ] 没有破坏现有功能
|
|
||||||
- [ ] 相关文档已更新
|
|
||||||
- [ ] 提交信息规范
|
|
||||||
|
|
||||||
### 错误预防
|
|
||||||
|
|
||||||
1. **避免盲目修改**: 不要在不了解项目结构的情况下直接修改代码
|
|
||||||
2. **保持一致性**: 新增代码要与现有代码风格保持一致
|
|
||||||
3. **完整实现**: 不要留下未完成的功能或空实现
|
|
||||||
4. **谨慎删除**: 删除代码前确认没有其他地方在使用
|
|
||||||
|
|
||||||
### 紧急情况处理
|
|
||||||
|
|
||||||
如果在开发过程中遇到以下情况,需要立即停止并寻求确认:
|
|
||||||
- 发现代码存在安全风险
|
|
||||||
- 修改可能影响核心业务逻辑
|
|
||||||
- 需要修改数据库结构
|
|
||||||
- 涉及第三方API集成
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
以上是情绪博物馆全栈项目的完整开发指南,请在开发过程中严格遵循相关规范,确保代码质量和项目的可维护性。
|
|
||||||
@@ -1,342 +0,0 @@
|
|||||||
# 情绪博物馆自定义目录部署指南
|
|
||||||
|
|
||||||
## 📋 部署架构
|
|
||||||
|
|
||||||
根据您的要求,本部署方案采用以下目录结构:
|
|
||||||
|
|
||||||
```
|
|
||||||
/data/
|
|
||||||
├── www/emotion-museum/ # 前端静态文件目录
|
|
||||||
│ ├── index.html
|
|
||||||
│ ├── assets/
|
|
||||||
│ └── ...
|
|
||||||
├── builds/ # 后端JAR文件目录
|
|
||||||
│ ├── emotion-gateway.jar
|
|
||||||
│ ├── emotion-ai.jar
|
|
||||||
│ └── emotion-user.jar
|
|
||||||
└── logs/emotion-museum/ # 日志目录
|
|
||||||
├── nginx/
|
|
||||||
├── gateway/
|
|
||||||
├── ai/
|
|
||||||
├── user/
|
|
||||||
├── mysql/
|
|
||||||
├── redis/
|
|
||||||
└── nacos/
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🏗️ 服务架构
|
|
||||||
|
|
||||||
```
|
|
||||||
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
|
|
||||||
│ 用户访问 │───▶│ Nginx │───▶│ 静态文件 │
|
|
||||||
└─────────────┘ │ (80/443) │ │ /data/www/ │
|
|
||||||
└─────────────┘ └─────────────┘
|
|
||||||
│
|
|
||||||
▼
|
|
||||||
┌─────────────┐ ┌─────────────┐
|
|
||||||
│ API代理 │───▶│ Gateway │
|
|
||||||
│ /api/* │ │ (9000) │
|
|
||||||
└─────────────┘ └─────────────┘
|
|
||||||
│
|
|
||||||
▼
|
|
||||||
┌─────────────┐ ┌─────────────┐
|
|
||||||
│ 微服务集群 │ │ JAR文件 │
|
|
||||||
│ AI/User/... │ │ /data/builds│
|
|
||||||
└─────────────┘ └─────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🚀 快速部署
|
|
||||||
|
|
||||||
### 1. 准备环境
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 确保Docker和Docker Compose已安装
|
|
||||||
docker --version
|
|
||||||
docker-compose --version
|
|
||||||
|
|
||||||
# 创建必要目录
|
|
||||||
sudo mkdir -p /data/www/emotion-museum
|
|
||||||
sudo mkdir -p /data/builds
|
|
||||||
sudo mkdir -p /data/logs/emotion-museum
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. 配置环境变量
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 编辑环境变量文件
|
|
||||||
vim .env
|
|
||||||
```
|
|
||||||
|
|
||||||
**配置说明**:
|
|
||||||
```bash
|
|
||||||
# Coze API配置(已配置为与开发环境一致)
|
|
||||||
COZE_API_TOKEN=pat_GCR4qKzqpf90wMCvKsldMrB18KG3QsLDci65bZthssKsbLxu8X70BKYumleDcabO
|
|
||||||
|
|
||||||
# 数据库密码(可根据需要修改)
|
|
||||||
MYSQL_ROOT_PASSWORD=123456
|
|
||||||
MYSQL_PASSWORD=emotion123
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. 一键部署
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 给脚本执行权限
|
|
||||||
chmod +x deploy-custom.sh
|
|
||||||
|
|
||||||
# 执行自定义部署
|
|
||||||
./deploy-custom.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
## 📁 文件说明
|
|
||||||
|
|
||||||
### 新增配置文件
|
|
||||||
|
|
||||||
1. **`docker-compose.custom.yml`** - 自定义Docker配置
|
|
||||||
- 前端文件直接从宿主机目录提供
|
|
||||||
- 后端JAR文件挂载到容器
|
|
||||||
- 日志统一保存到指定目录
|
|
||||||
|
|
||||||
2. **`deploy-custom.sh`** - 自定义部署脚本
|
|
||||||
- 自动检查和构建前后端
|
|
||||||
- 部署文件到指定目录
|
|
||||||
- 启动Docker服务
|
|
||||||
|
|
||||||
3. **`manage-custom.sh`** - 自定义管理脚本
|
|
||||||
- 服务管理和监控
|
|
||||||
- 日志查看和健康检查
|
|
||||||
- 数据备份和恢复
|
|
||||||
|
|
||||||
### 修改的配置文件
|
|
||||||
|
|
||||||
1. **`deploy/nginx/conf.d/emotion-museum.conf`** - Nginx配置
|
|
||||||
- 前端文件直接从 `/data/www/emotion-museum` 提供
|
|
||||||
- API请求代理到Docker容器内的服务
|
|
||||||
- 日志保存到 `/data/logs/emotion-museum/nginx/`
|
|
||||||
|
|
||||||
2. **`deploy/nginx/nginx.conf`** - Nginx主配置
|
|
||||||
- 更新上游服务器定义
|
|
||||||
- 支持容器间通信
|
|
||||||
|
|
||||||
## 🔧 端口配置
|
|
||||||
|
|
||||||
| 服务 | 容器端口 | 宿主机端口 | 说明 |
|
|
||||||
|------|----------|------------|------|
|
|
||||||
| Nginx | 80/443 | 80/443 | Web访问 |
|
|
||||||
| Gateway | 9000 | 9000 | API网关 |
|
|
||||||
| AI Service | 9002 | 9002 | AI服务 |
|
|
||||||
| User Service | 9001 | 9001 | 用户服务 |
|
|
||||||
| MySQL | 3306 | 3306 | 数据库 |
|
|
||||||
| Redis | 6379 | 6379 | 缓存 |
|
|
||||||
| Nacos | 8848 | 8848 | 注册中心 |
|
|
||||||
|
|
||||||
## 🛠️ 管理命令
|
|
||||||
|
|
||||||
### 服务管理
|
|
||||||
```bash
|
|
||||||
# 启动所有服务
|
|
||||||
./manage-custom.sh start
|
|
||||||
|
|
||||||
# 停止所有服务
|
|
||||||
./manage-custom.sh stop
|
|
||||||
|
|
||||||
# 重启所有服务
|
|
||||||
./manage-custom.sh restart
|
|
||||||
|
|
||||||
# 重启指定服务
|
|
||||||
./manage-custom.sh restart emotion-ai
|
|
||||||
|
|
||||||
# 查看服务状态
|
|
||||||
./manage-custom.sh status
|
|
||||||
```
|
|
||||||
|
|
||||||
### 日志管理
|
|
||||||
```bash
|
|
||||||
# 查看所有日志
|
|
||||||
./manage-custom.sh logs
|
|
||||||
|
|
||||||
# 跟踪日志输出
|
|
||||||
./manage-custom.sh logs -f
|
|
||||||
|
|
||||||
# 查看指定服务日志
|
|
||||||
./manage-custom.sh logs -s nginx
|
|
||||||
./manage-custom.sh logs -s emotion-gateway
|
|
||||||
```
|
|
||||||
|
|
||||||
### 健康检查
|
|
||||||
```bash
|
|
||||||
# 执行健康检查
|
|
||||||
./manage-custom.sh health
|
|
||||||
|
|
||||||
# 实时监控
|
|
||||||
./manage-custom.sh monitor
|
|
||||||
```
|
|
||||||
|
|
||||||
### 数据管理
|
|
||||||
```bash
|
|
||||||
# 备份数据
|
|
||||||
./manage-custom.sh backup
|
|
||||||
|
|
||||||
# 更新服务
|
|
||||||
./manage-custom.sh update
|
|
||||||
|
|
||||||
# 清理资源
|
|
||||||
./manage-custom.sh clean
|
|
||||||
```
|
|
||||||
|
|
||||||
## 📊 目录详情
|
|
||||||
|
|
||||||
### 前端目录 `/data/www/emotion-museum/`
|
|
||||||
```
|
|
||||||
/data/www/emotion-museum/
|
|
||||||
├── index.html # 主页面
|
|
||||||
├── assets/ # 静态资源
|
|
||||||
│ ├── css/ # 样式文件
|
|
||||||
│ ├── js/ # JavaScript文件
|
|
||||||
│ └── images/ # 图片文件
|
|
||||||
└── favicon.ico # 网站图标
|
|
||||||
```
|
|
||||||
|
|
||||||
### 后端目录 `/data/builds/`
|
|
||||||
```
|
|
||||||
/data/builds/
|
|
||||||
├── emotion-gateway.jar # 网关服务JAR
|
|
||||||
├── emotion-ai.jar # AI服务JAR
|
|
||||||
└── emotion-user.jar # 用户服务JAR
|
|
||||||
```
|
|
||||||
|
|
||||||
### 日志目录 `/data/logs/emotion-museum/`
|
|
||||||
```
|
|
||||||
/data/logs/emotion-museum/
|
|
||||||
├── nginx/ # Nginx日志
|
|
||||||
│ ├── access.log
|
|
||||||
│ └── error.log
|
|
||||||
├── gateway/ # 网关服务日志
|
|
||||||
├── ai/ # AI服务日志
|
|
||||||
├── user/ # 用户服务日志
|
|
||||||
├── mysql/ # MySQL日志
|
|
||||||
├── redis/ # Redis日志
|
|
||||||
└── nacos/ # Nacos日志
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🔄 更新流程
|
|
||||||
|
|
||||||
### 更新前端
|
|
||||||
```bash
|
|
||||||
# 1. 重新构建前端
|
|
||||||
cd web
|
|
||||||
npm run build
|
|
||||||
|
|
||||||
# 2. 部署到目标目录
|
|
||||||
sudo rm -rf /data/www/emotion-museum/*
|
|
||||||
sudo cp -r dist/* /data/www/emotion-museum/
|
|
||||||
sudo chown -R www-data:www-data /data/www/emotion-museum
|
|
||||||
|
|
||||||
# 3. 重启Nginx(可选)
|
|
||||||
./manage-custom.sh restart nginx
|
|
||||||
```
|
|
||||||
|
|
||||||
### 更新后端
|
|
||||||
```bash
|
|
||||||
# 1. 重新构建后端
|
|
||||||
cd backend
|
|
||||||
mvn clean package -DskipTests
|
|
||||||
|
|
||||||
# 2. 部署JAR文件
|
|
||||||
sudo cp emotion-gateway/target/emotion-gateway-1.0.0.jar /data/builds/emotion-gateway.jar
|
|
||||||
sudo cp emotion-ai/target/emotion-ai-1.0.0.jar /data/builds/emotion-ai.jar
|
|
||||||
sudo cp emotion-user/target/emotion-user-1.0.0.jar /data/builds/emotion-user.jar
|
|
||||||
|
|
||||||
# 3. 重启相关服务
|
|
||||||
./manage-custom.sh restart emotion-gateway
|
|
||||||
./manage-custom.sh restart emotion-ai
|
|
||||||
./manage-custom.sh restart emotion-user
|
|
||||||
```
|
|
||||||
|
|
||||||
### 一键更新
|
|
||||||
```bash
|
|
||||||
# 自动构建和部署
|
|
||||||
./manage-custom.sh update
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🚨 故障排除
|
|
||||||
|
|
||||||
### 常见问题
|
|
||||||
|
|
||||||
#### 1. 前端访问404
|
|
||||||
```bash
|
|
||||||
# 检查前端文件是否存在
|
|
||||||
ls -la /data/www/emotion-museum/
|
|
||||||
|
|
||||||
# 检查Nginx配置
|
|
||||||
./manage-custom.sh logs -s nginx
|
|
||||||
|
|
||||||
# 检查文件权限
|
|
||||||
sudo chown -R www-data:www-data /data/www/emotion-museum
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 2. API调用失败
|
|
||||||
```bash
|
|
||||||
# 检查网关服务状态
|
|
||||||
./manage-custom.sh logs -s emotion-gateway
|
|
||||||
|
|
||||||
# 检查服务健康状态
|
|
||||||
curl http://localhost:9000/actuator/health
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 3. 服务启动失败
|
|
||||||
```bash
|
|
||||||
# 检查JAR文件是否存在
|
|
||||||
ls -la /data/builds/
|
|
||||||
|
|
||||||
# 检查服务日志
|
|
||||||
./manage-custom.sh logs -s emotion-ai
|
|
||||||
|
|
||||||
# 检查容器状态
|
|
||||||
docker-compose -f docker-compose.custom.yml ps
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 4. 日志文件过大
|
|
||||||
```bash
|
|
||||||
# 清理日志文件
|
|
||||||
sudo find /data/logs/emotion-museum -name "*.log" -size +100M -delete
|
|
||||||
|
|
||||||
# 设置日志轮转
|
|
||||||
sudo logrotate -f /etc/logrotate.conf
|
|
||||||
```
|
|
||||||
|
|
||||||
## 📞 技术支持
|
|
||||||
|
|
||||||
### 快速诊断
|
|
||||||
```bash
|
|
||||||
# 执行健康检查
|
|
||||||
./manage-custom.sh health
|
|
||||||
|
|
||||||
# 查看服务状态
|
|
||||||
./manage-custom.sh status
|
|
||||||
|
|
||||||
# 查看实时监控
|
|
||||||
./manage-custom.sh monitor
|
|
||||||
```
|
|
||||||
|
|
||||||
### 获取帮助
|
|
||||||
```bash
|
|
||||||
# 查看管理命令帮助
|
|
||||||
./manage-custom.sh --help
|
|
||||||
|
|
||||||
# 查看部署脚本帮助
|
|
||||||
./deploy-custom.sh --help
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ✅ 部署检查清单
|
|
||||||
|
|
||||||
- [ ] **目录创建**: `/data/www/emotion-museum`, `/data/builds`, `/data/logs/emotion-museum`
|
|
||||||
- [ ] **环境配置**: `COZE_API_TOKEN` 已配置为与开发环境一致
|
|
||||||
- [ ] **前端部署**: 静态文件已复制到 `/data/www/emotion-museum/`
|
|
||||||
- [ ] **后端部署**: JAR文件已复制到 `/data/builds/`
|
|
||||||
- [ ] **服务启动**: 所有Docker容器正常运行
|
|
||||||
- [ ] **访问测试**: 前端页面和API接口正常访问
|
|
||||||
- [ ] **日志检查**: 日志文件正常生成到 `/data/logs/emotion-museum/`
|
|
||||||
|
|
||||||
**🎉 恭喜!您的情绪博物馆项目已成功部署到自定义目录结构!**
|
|
||||||
@@ -1,313 +0,0 @@
|
|||||||
# 情绪博物馆容器部署指南
|
|
||||||
|
|
||||||
## 📋 概述
|
|
||||||
|
|
||||||
本文档提供了情绪博物馆项目的完整容器化部署方案,支持开发环境和生产环境的快速部署。
|
|
||||||
|
|
||||||
## 🏗️ 架构说明
|
|
||||||
|
|
||||||
### 服务组件
|
|
||||||
- **前端应用** (Vue3 + Ant Design) - 端口: 80/3000
|
|
||||||
- **API网关** (Spring Cloud Gateway) - 端口: 9000
|
|
||||||
- **AI服务** (Spring Boot + Coze API) - 端口: 9002
|
|
||||||
- **用户服务** (Spring Boot) - 端口: 9001
|
|
||||||
- **MySQL数据库** - 端口: 3306
|
|
||||||
- **Redis缓存** - 端口: 6379
|
|
||||||
- **Nacos注册中心** - 端口: 8848
|
|
||||||
- **Nginx反向代理** - 端口: 80/443
|
|
||||||
|
|
||||||
### 网络架构
|
|
||||||
```
|
|
||||||
Internet → Nginx → Frontend/Gateway → Microservices → Database
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🚀 快速开始
|
|
||||||
|
|
||||||
### 1. 系统要求
|
|
||||||
- **操作系统**: Linux/macOS/Windows
|
|
||||||
- **Docker**: 20.10+
|
|
||||||
- **Docker Compose**: 1.29+
|
|
||||||
- **内存**: 最少4GB,推荐8GB+
|
|
||||||
- **磁盘**: 最少10GB可用空间
|
|
||||||
|
|
||||||
### 2. 一键部署
|
|
||||||
```bash
|
|
||||||
# 克隆项目
|
|
||||||
git clone <repository-url>
|
|
||||||
cd EmotionMuseum
|
|
||||||
|
|
||||||
# 快速部署(自动安装依赖)
|
|
||||||
chmod +x quick-deploy.sh
|
|
||||||
./quick-deploy.sh
|
|
||||||
|
|
||||||
# 或者手动部署
|
|
||||||
chmod +x deploy.sh
|
|
||||||
./deploy.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. 访问应用
|
|
||||||
- **前端应用**: http://localhost
|
|
||||||
- **API文档**: http://localhost:9000/doc.html
|
|
||||||
- **Nacos控制台**: http://localhost:8848/nacos (nacos/nacos)
|
|
||||||
|
|
||||||
## 📁 文件结构
|
|
||||||
|
|
||||||
```
|
|
||||||
EmotionMuseum/
|
|
||||||
├── docker-compose.yml # 开发环境配置
|
|
||||||
├── docker-compose.prod.yml # 生产环境配置
|
|
||||||
├── deploy.sh # 部署脚本
|
|
||||||
├── quick-deploy.sh # 快速部署脚本
|
|
||||||
├── manage.sh # 管理脚本
|
|
||||||
├── .env # 环境变量
|
|
||||||
├── deploy/ # 部署配置
|
|
||||||
│ ├── nginx/ # Nginx配置
|
|
||||||
│ │ ├── nginx.conf
|
|
||||||
│ │ ├── conf.d/
|
|
||||||
│ │ └── ssl/
|
|
||||||
│ ├── mysql/ # MySQL配置
|
|
||||||
│ └── redis/ # Redis配置
|
|
||||||
├── backend/ # 后端服务
|
|
||||||
│ ├── emotion-gateway/
|
|
||||||
│ │ └── Dockerfile
|
|
||||||
│ ├── emotion-ai/
|
|
||||||
│ │ └── Dockerfile
|
|
||||||
│ └── emotion-user/
|
|
||||||
│ └── Dockerfile
|
|
||||||
└── web/ # 前端应用
|
|
||||||
├── Dockerfile
|
|
||||||
└── nginx.conf
|
|
||||||
```
|
|
||||||
|
|
||||||
## ⚙️ 配置说明
|
|
||||||
|
|
||||||
### 环境变量配置
|
|
||||||
编辑 `.env` 文件:
|
|
||||||
```bash
|
|
||||||
# 数据库配置
|
|
||||||
MYSQL_ROOT_PASSWORD=123456
|
|
||||||
MYSQL_DATABASE=emotion_museum
|
|
||||||
MYSQL_USER=emotion
|
|
||||||
MYSQL_PASSWORD=emotion123
|
|
||||||
|
|
||||||
# Coze API配置
|
|
||||||
COZE_API_TOKEN=your-coze-api-token
|
|
||||||
|
|
||||||
# 时区设置
|
|
||||||
TZ=Asia/Shanghai
|
|
||||||
```
|
|
||||||
|
|
||||||
### Nginx配置
|
|
||||||
- **主配置**: `deploy/nginx/nginx.conf`
|
|
||||||
- **站点配置**: `deploy/nginx/conf.d/emotion-museum.conf`
|
|
||||||
- **SSL证书**: `deploy/nginx/ssl/`
|
|
||||||
|
|
||||||
### 数据库配置
|
|
||||||
- **MySQL配置**: `deploy/mysql/conf.d/my.cnf`
|
|
||||||
- **初始化脚本**: `backend/mysql_emotion_museum_final.sql`
|
|
||||||
|
|
||||||
## 🛠️ 管理命令
|
|
||||||
|
|
||||||
### 基础操作
|
|
||||||
```bash
|
|
||||||
# 启动所有服务
|
|
||||||
./manage.sh start
|
|
||||||
|
|
||||||
# 停止所有服务
|
|
||||||
./manage.sh stop
|
|
||||||
|
|
||||||
# 重启所有服务
|
|
||||||
./manage.sh restart
|
|
||||||
|
|
||||||
# 查看服务状态
|
|
||||||
./manage.sh status
|
|
||||||
```
|
|
||||||
|
|
||||||
### 日志管理
|
|
||||||
```bash
|
|
||||||
# 查看所有服务日志
|
|
||||||
./manage.sh logs
|
|
||||||
|
|
||||||
# 跟踪日志输出
|
|
||||||
./manage.sh logs -f
|
|
||||||
|
|
||||||
# 查看特定服务日志
|
|
||||||
./manage.sh logs -s gateway
|
|
||||||
./manage.sh logs -s ai-service
|
|
||||||
```
|
|
||||||
|
|
||||||
### 服务管理
|
|
||||||
```bash
|
|
||||||
# 重启特定服务
|
|
||||||
./manage.sh restart gateway
|
|
||||||
./manage.sh restart ai-service
|
|
||||||
|
|
||||||
# 健康检查
|
|
||||||
./manage.sh health
|
|
||||||
|
|
||||||
# 监控面板
|
|
||||||
./manage.sh monitor
|
|
||||||
```
|
|
||||||
|
|
||||||
### 数据管理
|
|
||||||
```bash
|
|
||||||
# 备份数据
|
|
||||||
./manage.sh backup
|
|
||||||
|
|
||||||
# 恢复数据
|
|
||||||
./manage.sh restore backup_file.tar.gz
|
|
||||||
|
|
||||||
# 更新服务
|
|
||||||
./manage.sh update
|
|
||||||
|
|
||||||
# 清理资源
|
|
||||||
./manage.sh clean
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🔧 生产环境部署
|
|
||||||
|
|
||||||
### 1. 使用生产配置
|
|
||||||
```bash
|
|
||||||
# 使用生产环境配置文件
|
|
||||||
docker-compose -f docker-compose.prod.yml up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. SSL证书配置
|
|
||||||
```bash
|
|
||||||
# 放置SSL证书
|
|
||||||
cp your-domain.crt deploy/nginx/ssl/emotion-museum.crt
|
|
||||||
cp your-domain.key deploy/nginx/ssl/emotion-museum.key
|
|
||||||
|
|
||||||
# 修改Nginx配置启用HTTPS
|
|
||||||
vim deploy/nginx/conf.d/emotion-museum.conf
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. 域名配置
|
|
||||||
修改 `deploy/nginx/conf.d/emotion-museum.conf`:
|
|
||||||
```nginx
|
|
||||||
server_name your-domain.com www.your-domain.com;
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. 防火墙配置
|
|
||||||
```bash
|
|
||||||
# Ubuntu/Debian
|
|
||||||
sudo ufw allow 80/tcp
|
|
||||||
sudo ufw allow 443/tcp
|
|
||||||
|
|
||||||
# CentOS/RHEL
|
|
||||||
sudo firewall-cmd --permanent --add-port=80/tcp
|
|
||||||
sudo firewall-cmd --permanent --add-port=443/tcp
|
|
||||||
sudo firewall-cmd --reload
|
|
||||||
```
|
|
||||||
|
|
||||||
## 📊 监控和维护
|
|
||||||
|
|
||||||
### 服务监控
|
|
||||||
```bash
|
|
||||||
# 实时监控
|
|
||||||
./manage.sh monitor
|
|
||||||
|
|
||||||
# 资源使用情况
|
|
||||||
docker stats
|
|
||||||
|
|
||||||
# 服务状态
|
|
||||||
docker-compose ps
|
|
||||||
```
|
|
||||||
|
|
||||||
### 日志查看
|
|
||||||
```bash
|
|
||||||
# 应用日志
|
|
||||||
./manage.sh logs -f
|
|
||||||
|
|
||||||
# 系统日志
|
|
||||||
tail -f logs/nginx/access.log
|
|
||||||
tail -f logs/mysql/error.log
|
|
||||||
```
|
|
||||||
|
|
||||||
### 性能优化
|
|
||||||
1. **数据库优化**: 调整 `deploy/mysql/conf.d/my.cnf`
|
|
||||||
2. **Redis优化**: 调整 `deploy/redis/redis.conf`
|
|
||||||
3. **Nginx优化**: 调整 `deploy/nginx/nginx.conf`
|
|
||||||
4. **JVM优化**: 修改Dockerfile中的JVM参数
|
|
||||||
|
|
||||||
## 🔒 安全配置
|
|
||||||
|
|
||||||
### 1. 数据库安全
|
|
||||||
- 修改默认密码
|
|
||||||
- 限制访问IP
|
|
||||||
- 启用SSL连接
|
|
||||||
|
|
||||||
### 2. Redis安全
|
|
||||||
- 设置密码认证
|
|
||||||
- 绑定特定IP
|
|
||||||
- 禁用危险命令
|
|
||||||
|
|
||||||
### 3. Nginx安全
|
|
||||||
- 启用HTTPS
|
|
||||||
- 配置安全头
|
|
||||||
- 限制请求频率
|
|
||||||
|
|
||||||
### 4. 应用安全
|
|
||||||
- 配置JWT密钥
|
|
||||||
- 启用CORS限制
|
|
||||||
- 设置API限流
|
|
||||||
|
|
||||||
## 🚨 故障排除
|
|
||||||
|
|
||||||
### 常见问题
|
|
||||||
|
|
||||||
#### 1. 服务启动失败
|
|
||||||
```bash
|
|
||||||
# 查看服务日志
|
|
||||||
./manage.sh logs -s service-name
|
|
||||||
|
|
||||||
# 检查端口占用
|
|
||||||
netstat -tlnp | grep :port
|
|
||||||
|
|
||||||
# 重启服务
|
|
||||||
./manage.sh restart service-name
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 2. 数据库连接失败
|
|
||||||
```bash
|
|
||||||
# 检查MySQL状态
|
|
||||||
docker-compose exec mysql mysqladmin ping -u root -p
|
|
||||||
|
|
||||||
# 查看数据库日志
|
|
||||||
./manage.sh logs -s mysql
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 3. 前端访问异常
|
|
||||||
```bash
|
|
||||||
# 检查Nginx配置
|
|
||||||
nginx -t
|
|
||||||
|
|
||||||
# 查看Nginx日志
|
|
||||||
./manage.sh logs -s nginx
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 4. API调用失败
|
|
||||||
```bash
|
|
||||||
# 检查网关状态
|
|
||||||
curl http://localhost:9000/actuator/health
|
|
||||||
|
|
||||||
# 查看网关日志
|
|
||||||
./manage.sh logs -s gateway
|
|
||||||
```
|
|
||||||
|
|
||||||
### 性能问题
|
|
||||||
1. **内存不足**: 增加服务器内存或调整JVM参数
|
|
||||||
2. **磁盘空间**: 清理日志文件和Docker镜像
|
|
||||||
3. **网络延迟**: 检查服务间网络连接
|
|
||||||
|
|
||||||
## 📞 技术支持
|
|
||||||
|
|
||||||
如遇到问题,请:
|
|
||||||
1. 查看相关服务日志
|
|
||||||
2. 检查配置文件
|
|
||||||
3. 参考故障排除指南
|
|
||||||
4. 联系技术支持团队
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**部署完成后,请及时修改默认密码和配置文件中的敏感信息!**
|
|
||||||
-256
@@ -1,256 +0,0 @@
|
|||||||
# 情绪博物馆项目部署指南
|
|
||||||
|
|
||||||
## 概述
|
|
||||||
|
|
||||||
本文档提供了情绪博物馆项目的完整部署指南,包括一键部署脚本的使用方法和手动部署步骤。
|
|
||||||
|
|
||||||
## 系统要求
|
|
||||||
|
|
||||||
### 本地开发环境
|
|
||||||
- Java 17+
|
|
||||||
- Maven 3.6+
|
|
||||||
- Node.js 18+
|
|
||||||
- SSH客户端
|
|
||||||
|
|
||||||
### 服务器环境
|
|
||||||
- CentOS 7/8 或 RHEL 7/8
|
|
||||||
- 最小 4GB RAM,推荐 8GB+
|
|
||||||
- 最小 50GB 磁盘空间
|
|
||||||
- Docker 支持
|
|
||||||
|
|
||||||
## 快速部署
|
|
||||||
|
|
||||||
### 1. 一键部署(推荐)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 克隆项目
|
|
||||||
git clone <repository-url>
|
|
||||||
cd EmotionMuseum
|
|
||||||
|
|
||||||
# 执行一键部署
|
|
||||||
./deploy-final.sh all
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. 分步部署
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 1. 构建项目
|
|
||||||
./deploy-final.sh build
|
|
||||||
|
|
||||||
# 2. 配置服务器环境
|
|
||||||
./deploy-final.sh env
|
|
||||||
|
|
||||||
# 3. 配置数据库
|
|
||||||
./deploy-final.sh mysql
|
|
||||||
|
|
||||||
# 4. 配置Redis
|
|
||||||
./deploy-final.sh redis
|
|
||||||
|
|
||||||
# 5. 配置Nacos
|
|
||||||
./deploy-final.sh nacos
|
|
||||||
|
|
||||||
# 6. 上传构建产物
|
|
||||||
./deploy-final.sh upload
|
|
||||||
|
|
||||||
# 7. 导入数据库
|
|
||||||
./deploy-final.sh import-db
|
|
||||||
|
|
||||||
# 8. 部署应用服务
|
|
||||||
./deploy-final.sh deploy
|
|
||||||
|
|
||||||
# 9. 配置Nginx
|
|
||||||
./deploy-final.sh nginx
|
|
||||||
|
|
||||||
# 10. 创建密码记录
|
|
||||||
./deploy-final.sh passwords
|
|
||||||
|
|
||||||
# 11. 健康检查
|
|
||||||
./deploy-final.sh health
|
|
||||||
```
|
|
||||||
|
|
||||||
## 服务管理
|
|
||||||
|
|
||||||
### 启动/停止服务
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 查看服务状态
|
|
||||||
./deploy-final.sh status
|
|
||||||
|
|
||||||
# 启动服务
|
|
||||||
./deploy-final.sh start
|
|
||||||
|
|
||||||
# 停止服务
|
|
||||||
./deploy-final.sh stop
|
|
||||||
|
|
||||||
# 重启服务
|
|
||||||
./deploy-final.sh restart
|
|
||||||
```
|
|
||||||
|
|
||||||
### 查看日志
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 查看网关服务日志
|
|
||||||
./deploy-final.sh logs gateway
|
|
||||||
|
|
||||||
# 查看AI服务日志
|
|
||||||
./deploy-final.sh logs ai
|
|
||||||
|
|
||||||
# 查看用户服务日志
|
|
||||||
./deploy-final.sh logs user
|
|
||||||
```
|
|
||||||
|
|
||||||
## 配置说明
|
|
||||||
|
|
||||||
### 环境变量配置
|
|
||||||
|
|
||||||
主要配置文件:
|
|
||||||
- `.env.prod` - 生产环境配置
|
|
||||||
- `web/.env.production` - 前端生产环境配置
|
|
||||||
|
|
||||||
### 服务器配置
|
|
||||||
|
|
||||||
默认配置:
|
|
||||||
- 服务器IP: 47.111.10.27
|
|
||||||
- MySQL端口: 3306
|
|
||||||
- Redis端口: 6379
|
|
||||||
- Nacos端口: 8848
|
|
||||||
- 网关端口: 9000
|
|
||||||
- AI服务端口: 9002
|
|
||||||
- 用户服务端口: 9001
|
|
||||||
|
|
||||||
### 目录结构
|
|
||||||
|
|
||||||
```
|
|
||||||
/data/
|
|
||||||
├── builds/ # 应用JAR文件
|
|
||||||
├── www/ # 前端文件
|
|
||||||
│ └── emotion-museum/
|
|
||||||
│ └── web/
|
|
||||||
├── logs/ # 日志文件
|
|
||||||
│ └── emotion-museum/
|
|
||||||
│ ├── gateway/
|
|
||||||
│ ├── ai/
|
|
||||||
│ └── user/
|
|
||||||
└── programs/ # 其他程序文件
|
|
||||||
```
|
|
||||||
|
|
||||||
## 访问地址
|
|
||||||
|
|
||||||
部署完成后的访问地址:
|
|
||||||
|
|
||||||
- **前端应用**: http://47.111.10.27/emotion-museum/
|
|
||||||
- **API网关**: http://47.111.10.27:9000
|
|
||||||
- **Nacos控制台**: http://47.111.10.27:8848/nacos
|
|
||||||
|
|
||||||
## 故障排除
|
|
||||||
|
|
||||||
### 常见问题
|
|
||||||
|
|
||||||
1. **服务无法启动**
|
|
||||||
```bash
|
|
||||||
# 检查服务状态
|
|
||||||
./deploy-final.sh status
|
|
||||||
|
|
||||||
# 查看日志
|
|
||||||
./deploy-final.sh logs <service>
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **数据库连接失败**
|
|
||||||
```bash
|
|
||||||
# 检查MySQL容器状态
|
|
||||||
ssh root@47.111.10.27 "docker ps | grep mysql"
|
|
||||||
|
|
||||||
# 检查数据库连接
|
|
||||||
ssh root@47.111.10.27 "docker exec emotion-mysql-prod mysql -uemotion -pEmotionDB2024! -e 'SELECT 1;'"
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **前端页面无法访问**
|
|
||||||
```bash
|
|
||||||
# 检查Nginx状态
|
|
||||||
ssh root@47.111.10.27 "systemctl status nginx"
|
|
||||||
|
|
||||||
# 检查前端文件
|
|
||||||
ssh root@47.111.10.27 "ls -la /data/www/emotion-museum/web/"
|
|
||||||
```
|
|
||||||
|
|
||||||
### 日志位置
|
|
||||||
|
|
||||||
- 应用日志: `/data/logs/emotion-museum/*/app.log`
|
|
||||||
- Nginx日志: `/var/log/nginx/`
|
|
||||||
- Docker日志: `docker logs <container-name>`
|
|
||||||
|
|
||||||
## 安全建议
|
|
||||||
|
|
||||||
1. **修改默认密码**
|
|
||||||
- MySQL root密码
|
|
||||||
- 应用数据库密码
|
|
||||||
- 服务器SSH密钥
|
|
||||||
|
|
||||||
2. **配置防火墙**
|
|
||||||
```bash
|
|
||||||
# 只开放必要端口
|
|
||||||
firewall-cmd --permanent --add-port=80/tcp
|
|
||||||
firewall-cmd --permanent --add-port=8848/tcp
|
|
||||||
firewall-cmd --reload
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **定期备份**
|
|
||||||
```bash
|
|
||||||
# 数据库备份
|
|
||||||
docker exec emotion-mysql-prod mysqldump -uemotion -pEmotionDB2024! emotion_museum > backup.sql
|
|
||||||
```
|
|
||||||
|
|
||||||
## 更新部署
|
|
||||||
|
|
||||||
### 应用更新
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 1. 构建新版本
|
|
||||||
./deploy-final.sh build
|
|
||||||
|
|
||||||
# 2. 停止服务
|
|
||||||
./deploy-final.sh stop
|
|
||||||
|
|
||||||
# 3. 上传新文件
|
|
||||||
./deploy-final.sh upload
|
|
||||||
|
|
||||||
# 4. 启动服务
|
|
||||||
./deploy-final.sh start
|
|
||||||
```
|
|
||||||
|
|
||||||
### 配置更新
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 重新配置Nginx
|
|
||||||
./deploy-final.sh nginx
|
|
||||||
|
|
||||||
# 重启服务
|
|
||||||
./deploy-final.sh restart
|
|
||||||
```
|
|
||||||
|
|
||||||
## 监控和维护
|
|
||||||
|
|
||||||
### 健康检查
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 执行完整健康检查
|
|
||||||
./deploy-final.sh health
|
|
||||||
```
|
|
||||||
|
|
||||||
### 性能监控
|
|
||||||
|
|
||||||
建议使用以下工具进行监控:
|
|
||||||
- Prometheus + Grafana
|
|
||||||
- ELK Stack (日志分析)
|
|
||||||
- Docker监控
|
|
||||||
|
|
||||||
## 联系支持
|
|
||||||
|
|
||||||
如遇到部署问题,请提供以下信息:
|
|
||||||
1. 错误日志
|
|
||||||
2. 系统环境信息
|
|
||||||
3. 部署步骤和配置
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**注意**: 请确保在生产环境中修改默认密码和配置,并定期进行安全更新。
|
|
||||||
@@ -0,0 +1,253 @@
|
|||||||
|
# 情感博物馆 - 最终部署指南
|
||||||
|
|
||||||
|
## 🎯 项目概述
|
||||||
|
|
||||||
|
情感博物馆是一个基于Spring Cloud Alibaba微服务架构的情感AI应用,包含10个微服务模块和Vue前端。
|
||||||
|
|
||||||
|
## 🏗️ 系统架构
|
||||||
|
|
||||||
|
### 后端微服务 (Spring Cloud Alibaba)
|
||||||
|
- **emotion-gateway** (19000) - API网关,统一入口
|
||||||
|
- **emotion-user** (19001) - 用户管理服务
|
||||||
|
- **emotion-ai** (19002) - AI聊天服务,集成Coze平台
|
||||||
|
- **emotion-record** (19003) - 记录管理服务
|
||||||
|
- **emotion-growth** (19004) - 成长跟踪服务
|
||||||
|
- **emotion-explore** (19005) - 探索服务
|
||||||
|
- **emotion-reward** (19006) - 奖励服务
|
||||||
|
- **emotion-websocket** (19007) - WebSocket实时通信
|
||||||
|
- **emotion-auth** (19008) - 认证授权服务
|
||||||
|
- **emotion-stats** (19009) - 统计分析服务
|
||||||
|
|
||||||
|
### 前端 (Vue + Ant Design)
|
||||||
|
- 基于Vue 3 + TypeScript + Ant Design
|
||||||
|
- 响应式设计,支持移动端
|
||||||
|
- WebSocket实时通信
|
||||||
|
- 集成AI聊天功能
|
||||||
|
|
||||||
|
### 中间件
|
||||||
|
- **MySQL 8.0** (3306) - 主数据库
|
||||||
|
- **Redis 7** (6379) - 缓存和会话存储
|
||||||
|
- **Nacos 2.2.0** (8848) - 服务注册发现和配置中心
|
||||||
|
|
||||||
|
## 🚀 快速部署
|
||||||
|
|
||||||
|
### 1. 一键部署(推荐)
|
||||||
|
```bash
|
||||||
|
# 完整部署前后端
|
||||||
|
./one-click-deploy.sh
|
||||||
|
|
||||||
|
# 仅部署后端
|
||||||
|
./one-click-deploy.sh backend
|
||||||
|
|
||||||
|
# 仅部署前端
|
||||||
|
./one-click-deploy.sh frontend
|
||||||
|
|
||||||
|
# 健康检查
|
||||||
|
./one-click-deploy.sh check
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 中间件管理
|
||||||
|
```bash
|
||||||
|
# 重启中间件(MySQL, Redis, Nacos)
|
||||||
|
./restart-middleware.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Nginx配置
|
||||||
|
```bash
|
||||||
|
# 配置Nginx反向代理
|
||||||
|
./setup-nginx.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📋 分步部署
|
||||||
|
|
||||||
|
### 步骤1: 准备环境
|
||||||
|
确保本地环境已安装:
|
||||||
|
- Java 17+
|
||||||
|
- Maven 3.6+
|
||||||
|
- Node.js 16+
|
||||||
|
- Docker (远程服务器)
|
||||||
|
|
||||||
|
### 步骤2: 启动中间件
|
||||||
|
```bash
|
||||||
|
./restart-middleware.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### 步骤3: 构建后端
|
||||||
|
```bash
|
||||||
|
cd backend
|
||||||
|
./build-all.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### 步骤4: 部署后端
|
||||||
|
```bash
|
||||||
|
cd backend
|
||||||
|
./deploy-remote.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### 步骤5: 部署前端
|
||||||
|
```bash
|
||||||
|
cd web-flowith
|
||||||
|
./deploy.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### 步骤6: 配置Nginx
|
||||||
|
```bash
|
||||||
|
./setup-nginx.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🌐 访问地址
|
||||||
|
|
||||||
|
### 生产环境
|
||||||
|
- **前端应用**: http://47.111.10.27/emotion-museum
|
||||||
|
- **API网关**: http://47.111.10.27/api/
|
||||||
|
- **WebSocket**: ws://47.111.10.27/ws/
|
||||||
|
- **健康检查**: http://47.111.10.27/health
|
||||||
|
|
||||||
|
### 管理后台
|
||||||
|
- **Nacos控制台**: http://47.111.10.27:8848/nacos
|
||||||
|
- 用户名: nacos
|
||||||
|
- 密码: Peanut2817*#
|
||||||
|
|
||||||
|
### 数据库连接
|
||||||
|
- **MySQL**: 47.111.10.27:3306
|
||||||
|
- 用户名: root
|
||||||
|
- 密码: EmotionMuseum2025*#
|
||||||
|
- 数据库: emotion_museum
|
||||||
|
|
||||||
|
- **Redis**: 47.111.10.27:6379
|
||||||
|
|
||||||
|
## 🔧 运维管理
|
||||||
|
|
||||||
|
### 查看服务状态
|
||||||
|
```bash
|
||||||
|
# 查看所有容器
|
||||||
|
ssh root@47.111.10.27 "docker ps"
|
||||||
|
|
||||||
|
# 查看特定服务日志
|
||||||
|
ssh root@47.111.10.27 "docker logs emotion-gateway --tail 50"
|
||||||
|
|
||||||
|
# 查看服务健康状态
|
||||||
|
curl http://47.111.10.27:19000/actuator/health
|
||||||
|
```
|
||||||
|
|
||||||
|
### 重启服务
|
||||||
|
```bash
|
||||||
|
# 重启单个服务
|
||||||
|
ssh root@47.111.10.27 "docker restart emotion-gateway"
|
||||||
|
|
||||||
|
# 重启所有微服务
|
||||||
|
ssh root@47.111.10.27 "docker restart \$(docker ps -q --filter name=emotion-)"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 更新部署
|
||||||
|
```bash
|
||||||
|
# 更新后端服务
|
||||||
|
cd backend && ./deploy-remote.sh
|
||||||
|
|
||||||
|
# 更新前端
|
||||||
|
cd web-flowith && ./deploy.sh
|
||||||
|
|
||||||
|
# 完整更新
|
||||||
|
./one-click-deploy.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📊 监控和日志
|
||||||
|
|
||||||
|
### 应用日志
|
||||||
|
- 容器日志: `docker logs <service_name>`
|
||||||
|
- 应用日志: `/data/logs/emotion-museum/`
|
||||||
|
|
||||||
|
### Nginx日志
|
||||||
|
- 访问日志: `/var/log/nginx/emotion-museum.access.log`
|
||||||
|
- 错误日志: `/var/log/nginx/emotion-museum.error.log`
|
||||||
|
|
||||||
|
### 健康检查端点
|
||||||
|
- 网关: http://47.111.10.27:19000/actuator/health
|
||||||
|
- 用户服务: http://47.111.10.27:19001/actuator/health
|
||||||
|
- AI服务: http://47.111.10.27:19002/actuator/health
|
||||||
|
|
||||||
|
## 🛠️ 故障排查
|
||||||
|
|
||||||
|
### 常见问题
|
||||||
|
|
||||||
|
#### 1. 服务启动失败
|
||||||
|
```bash
|
||||||
|
# 查看容器日志
|
||||||
|
docker logs <service_name> --tail 50
|
||||||
|
|
||||||
|
# 检查端口占用
|
||||||
|
netstat -tlnp | grep <port>
|
||||||
|
|
||||||
|
# 重启服务
|
||||||
|
docker restart <service_name>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. 数据库连接失败
|
||||||
|
```bash
|
||||||
|
# 检查MySQL状态
|
||||||
|
docker exec emotion-mysql mysqladmin ping
|
||||||
|
|
||||||
|
# 检查数据库连接
|
||||||
|
mysql -h 47.111.10.27 -u root -p
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. Nacos连接失败
|
||||||
|
```bash
|
||||||
|
# 检查Nacos状态
|
||||||
|
curl http://47.111.10.27:8848/nacos/v1/console/health
|
||||||
|
|
||||||
|
# 重启Nacos
|
||||||
|
docker restart emotion-nacos
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 4. 前端访问404
|
||||||
|
```bash
|
||||||
|
# 检查Nginx配置
|
||||||
|
nginx -t
|
||||||
|
|
||||||
|
# 检查前端文件
|
||||||
|
ls -la /data/www/emotion-museum/
|
||||||
|
|
||||||
|
# 重载Nginx
|
||||||
|
systemctl reload nginx
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📁 项目结构
|
||||||
|
|
||||||
|
```
|
||||||
|
emotion-museum/
|
||||||
|
├── 📁 backend/ # 后端微服务
|
||||||
|
├── 📁 web-flowith/ # 前端Vue项目
|
||||||
|
├── 📁 docs/ # 项目文档
|
||||||
|
├── 📁 configs/ # 配置文件
|
||||||
|
├── 🔧 one-click-deploy.sh # 一键部署脚本
|
||||||
|
├── 🔧 restart-middleware.sh # 中间件重启脚本
|
||||||
|
├── 🔧 setup-nginx.sh # Nginx配置脚本
|
||||||
|
└── 📄 DEPLOYMENT_FINAL.md # 部署指南
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔐 安全配置
|
||||||
|
|
||||||
|
### 密码管理
|
||||||
|
- MySQL root密码: EmotionMuseum2025*#
|
||||||
|
- Nacos密码: Peanut2817*#
|
||||||
|
- 所有密码已在配置文件中统一
|
||||||
|
|
||||||
|
### 网络安全
|
||||||
|
- 所有服务运行在Docker网络中
|
||||||
|
- Nginx反向代理保护内部服务
|
||||||
|
- 仅必要端口对外开放
|
||||||
|
|
||||||
|
## 📞 技术支持
|
||||||
|
|
||||||
|
如遇到问题,请:
|
||||||
|
1. 查看相关日志文件
|
||||||
|
2. 检查服务健康状态
|
||||||
|
3. 参考故障排查章节
|
||||||
|
4. 联系开发团队并提供完整日志
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**版本**: v2.0
|
||||||
|
**更新时间**: 2025-07-21
|
||||||
|
**维护团队**: 情感博物馆开发团队
|
||||||
@@ -1,371 +0,0 @@
|
|||||||
# 情绪博物馆完整部署指南
|
|
||||||
|
|
||||||
## 📦 部署包信息
|
|
||||||
|
|
||||||
**包名称**: `emotion-museum-1.0.0-20250713_111829.tar.gz`
|
|
||||||
**包大小**: 680KB
|
|
||||||
**SHA256**: `900d585f575b1619e74296496e2fe22f2c2e71b6ad8901d7cab82634765cc10d`
|
|
||||||
**构建时间**: 2025-07-13 11:18:29
|
|
||||||
|
|
||||||
## 🎯 部署概述
|
|
||||||
|
|
||||||
本部署包包含了情绪博物馆项目的完整容器化部署方案,支持:
|
|
||||||
- ✅ 前端Vue3应用(已构建)
|
|
||||||
- ✅ 后端微服务(Gateway、AI、User)
|
|
||||||
- ✅ 数据库脚本(MySQL)
|
|
||||||
- ✅ 完整的Docker配置
|
|
||||||
- ✅ 自动化部署脚本
|
|
||||||
- ✅ 监控和管理工具
|
|
||||||
|
|
||||||
## 🏗️ 系统架构
|
|
||||||
|
|
||||||
```
|
|
||||||
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
|
|
||||||
│ 用户访问 │───▶│ Nginx │───▶│ 前端应用 │
|
|
||||||
└─────────────┘ │ (80/443) │ │ (3000) │
|
|
||||||
└─────────────┘ └─────────────┘
|
|
||||||
│
|
|
||||||
▼
|
|
||||||
┌─────────────┐ ┌─────────────┐
|
|
||||||
│ API网关 │───▶│ 微服务集群 │
|
|
||||||
│ (9000) │ │ AI/User/... │
|
|
||||||
└─────────────┘ └─────────────┘
|
|
||||||
│
|
|
||||||
▼
|
|
||||||
┌─────────────┐ ┌─────────────┐
|
|
||||||
│ MySQL │ │ Redis │
|
|
||||||
│ (3306) │ │ (6379) │
|
|
||||||
└─────────────┘ └─────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🚀 快速部署(推荐)
|
|
||||||
|
|
||||||
### 1. 下载和解压
|
|
||||||
```bash
|
|
||||||
# 下载部署包到服务器
|
|
||||||
wget https://your-domain.com/emotion-museum-1.0.0-20250713_111829.tar.gz
|
|
||||||
|
|
||||||
# 验证文件完整性
|
|
||||||
echo "900d585f575b1619e74296496e2fe22f2c2e71b6ad8901d7cab82634765cc10d emotion-museum-1.0.0-20250713_111829.tar.gz" | sha256sum -c
|
|
||||||
|
|
||||||
# 解压部署包
|
|
||||||
tar -xzf emotion-museum-1.0.0-20250713_111829.tar.gz
|
|
||||||
cd emotion-museum-1.0.0-20250713_111829
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. 配置环境变量
|
|
||||||
```bash
|
|
||||||
# 复制环境变量模板
|
|
||||||
cp .env .env.local
|
|
||||||
|
|
||||||
# 编辑配置文件
|
|
||||||
vim .env.local
|
|
||||||
```
|
|
||||||
|
|
||||||
**必须配置的项目**:
|
|
||||||
```bash
|
|
||||||
# Coze API配置(必须修改)
|
|
||||||
COZE_API_TOKEN=your-actual-coze-api-token
|
|
||||||
|
|
||||||
# 数据库密码(建议修改)
|
|
||||||
MYSQL_ROOT_PASSWORD=your-secure-password
|
|
||||||
MYSQL_PASSWORD=your-secure-password
|
|
||||||
|
|
||||||
# 时区设置
|
|
||||||
TZ=Asia/Shanghai
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. 一键部署
|
|
||||||
```bash
|
|
||||||
# 给脚本执行权限
|
|
||||||
chmod +x quick-deploy.sh
|
|
||||||
|
|
||||||
# 执行一键部署(自动安装Docker等依赖)
|
|
||||||
./quick-deploy.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. 验证部署
|
|
||||||
```bash
|
|
||||||
# 查看服务状态
|
|
||||||
./manage.sh status
|
|
||||||
|
|
||||||
# 健康检查
|
|
||||||
./manage.sh health
|
|
||||||
|
|
||||||
# 查看日志
|
|
||||||
./manage.sh logs
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🔧 手动部署(高级用户)
|
|
||||||
|
|
||||||
### 1. 环境准备
|
|
||||||
```bash
|
|
||||||
# 安装Docker
|
|
||||||
curl -fsSL https://get.docker.com | sh
|
|
||||||
sudo systemctl start docker
|
|
||||||
sudo systemctl enable docker
|
|
||||||
|
|
||||||
# 安装Docker Compose
|
|
||||||
sudo curl -L "https://github.com/docker/compose/releases/download/1.29.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
|
|
||||||
sudo chmod +x /usr/local/bin/docker-compose
|
|
||||||
|
|
||||||
# 添加用户到docker组
|
|
||||||
sudo usermod -aG docker $USER
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. 配置防火墙
|
|
||||||
```bash
|
|
||||||
# Ubuntu/Debian
|
|
||||||
sudo ufw allow 80/tcp
|
|
||||||
sudo ufw allow 443/tcp
|
|
||||||
sudo ufw allow 8848/tcp # Nacos(可选)
|
|
||||||
|
|
||||||
# CentOS/RHEL
|
|
||||||
sudo firewall-cmd --permanent --add-port=80/tcp
|
|
||||||
sudo firewall-cmd --permanent --add-port=443/tcp
|
|
||||||
sudo firewall-cmd --permanent --add-port=8848/tcp
|
|
||||||
sudo firewall-cmd --reload
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. 部署服务
|
|
||||||
```bash
|
|
||||||
# 开发环境部署
|
|
||||||
./deploy.sh
|
|
||||||
|
|
||||||
# 或生产环境部署
|
|
||||||
docker-compose -f docker-compose.prod.yml up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
## ⚙️ 配置说明
|
|
||||||
|
|
||||||
### 环境变量配置
|
|
||||||
| 变量名 | 说明 | 默认值 | 是否必须 |
|
|
||||||
|--------|------|--------|----------|
|
|
||||||
| `COZE_API_TOKEN` | Coze API令牌 | - | ✅ 必须 |
|
|
||||||
| `MYSQL_ROOT_PASSWORD` | MySQL root密码 | 123456 | 🔶 建议修改 |
|
|
||||||
| `MYSQL_PASSWORD` | MySQL用户密码 | emotion123 | 🔶 建议修改 |
|
|
||||||
| `TZ` | 时区设置 | Asia/Shanghai | ⭕ 可选 |
|
|
||||||
| `DOMAIN_NAME` | 域名(生产环境) | localhost | ⭕ 可选 |
|
|
||||||
|
|
||||||
### 端口配置
|
|
||||||
| 服务 | 端口 | 说明 |
|
|
||||||
|------|------|------|
|
|
||||||
| Nginx | 80, 443 | Web访问端口 |
|
|
||||||
| Gateway | 9000 | API网关 |
|
|
||||||
| AI Service | 9002 | AI服务 |
|
|
||||||
| User Service | 9001 | 用户服务 |
|
|
||||||
| MySQL | 3306 | 数据库 |
|
|
||||||
| Redis | 6379 | 缓存 |
|
|
||||||
| Nacos | 8848 | 注册中心 |
|
|
||||||
|
|
||||||
## 🌐 生产环境配置
|
|
||||||
|
|
||||||
### 1. HTTPS配置
|
|
||||||
```bash
|
|
||||||
# 1. 准备SSL证书
|
|
||||||
mkdir -p deploy/nginx/ssl
|
|
||||||
cp your-domain.crt deploy/nginx/ssl/emotion-museum.crt
|
|
||||||
cp your-domain.key deploy/nginx/ssl/emotion-museum.key
|
|
||||||
|
|
||||||
# 2. 修改Nginx配置
|
|
||||||
vim deploy/nginx/conf.d/emotion-museum.conf
|
|
||||||
# 取消HTTPS相关配置的注释
|
|
||||||
|
|
||||||
# 3. 重启Nginx
|
|
||||||
docker-compose restart nginx
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. 域名配置
|
|
||||||
```bash
|
|
||||||
# 修改Nginx配置中的域名
|
|
||||||
vim deploy/nginx/conf.d/emotion-museum.conf
|
|
||||||
# 将 localhost 替换为您的实际域名
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. 性能优化
|
|
||||||
```bash
|
|
||||||
# 1. 调整MySQL配置
|
|
||||||
vim deploy/mysql/conf.d/my.cnf
|
|
||||||
|
|
||||||
# 2. 调整Redis配置
|
|
||||||
vim deploy/redis/redis.conf
|
|
||||||
|
|
||||||
# 3. 调整JVM参数(在Dockerfile中)
|
|
||||||
# -Xms512m -Xmx1024m
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🛠️ 管理命令
|
|
||||||
|
|
||||||
### 服务管理
|
|
||||||
```bash
|
|
||||||
./manage.sh start # 启动所有服务
|
|
||||||
./manage.sh stop # 停止所有服务
|
|
||||||
./manage.sh restart # 重启所有服务
|
|
||||||
./manage.sh restart gateway # 重启指定服务
|
|
||||||
./manage.sh status # 查看服务状态
|
|
||||||
```
|
|
||||||
|
|
||||||
### 日志管理
|
|
||||||
```bash
|
|
||||||
./manage.sh logs # 查看所有日志
|
|
||||||
./manage.sh logs -f # 跟踪日志输出
|
|
||||||
./manage.sh logs -s gateway # 查看网关日志
|
|
||||||
./manage.sh logs -s ai-service # 查看AI服务日志
|
|
||||||
```
|
|
||||||
|
|
||||||
### 数据管理
|
|
||||||
```bash
|
|
||||||
./manage.sh backup # 备份数据
|
|
||||||
./manage.sh restore backup.tar.gz # 恢复数据
|
|
||||||
./manage.sh update # 更新服务
|
|
||||||
./manage.sh clean # 清理资源
|
|
||||||
```
|
|
||||||
|
|
||||||
### 监控工具
|
|
||||||
```bash
|
|
||||||
./manage.sh monitor # 实时监控面板
|
|
||||||
./manage.sh health # 健康检查
|
|
||||||
```
|
|
||||||
|
|
||||||
## 📊 访问地址
|
|
||||||
|
|
||||||
部署完成后,您可以通过以下地址访问:
|
|
||||||
|
|
||||||
| 服务 | 地址 | 说明 |
|
|
||||||
|------|------|------|
|
|
||||||
| 前端应用 | http://localhost | 主要访问入口 |
|
|
||||||
| API文档 | http://localhost:9000/doc.html | Swagger文档 |
|
|
||||||
| Nacos控制台 | http://localhost:8848/nacos | 服务注册中心 |
|
|
||||||
| 网关健康检查 | http://localhost:9000/actuator/health | 服务状态 |
|
|
||||||
|
|
||||||
**默认账号**:
|
|
||||||
- Nacos: nacos / nacos
|
|
||||||
|
|
||||||
## 🚨 故障排除
|
|
||||||
|
|
||||||
### 常见问题
|
|
||||||
|
|
||||||
#### 1. 端口冲突
|
|
||||||
```bash
|
|
||||||
# 检查端口占用
|
|
||||||
netstat -tlnp | grep :80
|
|
||||||
netstat -tlnp | grep :3306
|
|
||||||
|
|
||||||
# 解决方案:修改docker-compose.yml中的端口映射
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 2. 服务启动失败
|
|
||||||
```bash
|
|
||||||
# 查看具体错误
|
|
||||||
./manage.sh logs -s service-name
|
|
||||||
|
|
||||||
# 常见原因:
|
|
||||||
# - 内存不足
|
|
||||||
# - 端口被占用
|
|
||||||
# - 配置文件错误
|
|
||||||
# - 依赖服务未启动
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 3. 数据库连接失败
|
|
||||||
```bash
|
|
||||||
# 检查MySQL状态
|
|
||||||
docker-compose exec mysql mysqladmin ping -u root -p
|
|
||||||
|
|
||||||
# 检查网络连接
|
|
||||||
docker network ls
|
|
||||||
docker network inspect emotion-network
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 4. 前端访问404
|
|
||||||
```bash
|
|
||||||
# 检查Nginx配置
|
|
||||||
docker-compose exec nginx nginx -t
|
|
||||||
|
|
||||||
# 检查前端容器状态
|
|
||||||
docker-compose ps web
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 5. API调用失败
|
|
||||||
```bash
|
|
||||||
# 检查网关状态
|
|
||||||
curl http://localhost:9000/actuator/health
|
|
||||||
|
|
||||||
# 检查服务注册
|
|
||||||
curl http://localhost:8848/nacos/v1/ns/instance/list?serviceName=emotion-ai
|
|
||||||
```
|
|
||||||
|
|
||||||
### 性能问题
|
|
||||||
|
|
||||||
#### 1. 内存不足
|
|
||||||
```bash
|
|
||||||
# 查看内存使用
|
|
||||||
free -h
|
|
||||||
docker stats
|
|
||||||
|
|
||||||
# 解决方案:
|
|
||||||
# - 增加服务器内存
|
|
||||||
# - 调整JVM参数
|
|
||||||
# - 减少并发连接数
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 2. 磁盘空间不足
|
|
||||||
```bash
|
|
||||||
# 查看磁盘使用
|
|
||||||
df -h
|
|
||||||
|
|
||||||
# 清理Docker资源
|
|
||||||
./manage.sh clean
|
|
||||||
docker system prune -a
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 3. 网络延迟
|
|
||||||
```bash
|
|
||||||
# 检查服务间网络
|
|
||||||
docker-compose exec gateway ping mysql
|
|
||||||
docker-compose exec gateway ping redis
|
|
||||||
|
|
||||||
# 优化网络配置
|
|
||||||
# 使用自定义网络
|
|
||||||
# 调整网络参数
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🔒 安全建议
|
|
||||||
|
|
||||||
### 1. 密码安全
|
|
||||||
- ✅ 修改所有默认密码
|
|
||||||
- ✅ 使用强密码策略
|
|
||||||
- ✅ 定期更换密码
|
|
||||||
|
|
||||||
### 2. 网络安全
|
|
||||||
- ✅ 配置防火墙规则
|
|
||||||
- ✅ 使用HTTPS加密
|
|
||||||
- ✅ 限制不必要的端口访问
|
|
||||||
|
|
||||||
### 3. 数据安全
|
|
||||||
- ✅ 定期备份数据
|
|
||||||
- ✅ 启用数据库SSL
|
|
||||||
- ✅ 配置访问控制
|
|
||||||
|
|
||||||
### 4. 应用安全
|
|
||||||
- ✅ 配置JWT密钥
|
|
||||||
- ✅ 启用API限流
|
|
||||||
- ✅ 监控异常访问
|
|
||||||
|
|
||||||
## 📞 技术支持
|
|
||||||
|
|
||||||
### 获取帮助
|
|
||||||
1. **查看文档**: 包内的 `DEPLOY.md` 和 `QUICK_START.md`
|
|
||||||
2. **查看日志**: `./manage.sh logs -f`
|
|
||||||
3. **健康检查**: `./manage.sh health`
|
|
||||||
4. **查看版本**: `cat VERSION.txt`
|
|
||||||
|
|
||||||
### 联系方式
|
|
||||||
- 📧 技术支持邮箱: support@emotion-museum.com
|
|
||||||
- 📱 技术支持QQ群: 123456789
|
|
||||||
- 🌐 官方网站: https://emotion-museum.com
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**🎉 恭喜!您已成功部署情绪博物馆项目!**
|
|
||||||
|
|
||||||
**⚠️ 重要提醒:部署完成后请及时修改默认密码和敏感配置!**
|
|
||||||
@@ -1,575 +0,0 @@
|
|||||||
// !$*UTF8*$!
|
|
||||||
{
|
|
||||||
archiveVersion = 1;
|
|
||||||
classes = {
|
|
||||||
};
|
|
||||||
objectVersion = 77;
|
|
||||||
objects = {
|
|
||||||
|
|
||||||
/* Begin PBXContainerItemProxy section */
|
|
||||||
2FB3451A2DFBE273001A8A67 /* PBXContainerItemProxy */ = {
|
|
||||||
isa = PBXContainerItemProxy;
|
|
||||||
containerPortal = 2FB344FF2DFBE270001A8A67 /* Project object */;
|
|
||||||
proxyType = 1;
|
|
||||||
remoteGlobalIDString = 2FB345062DFBE270001A8A67;
|
|
||||||
remoteInfo = EmotionMuseum;
|
|
||||||
};
|
|
||||||
2FB345242DFBE273001A8A67 /* PBXContainerItemProxy */ = {
|
|
||||||
isa = PBXContainerItemProxy;
|
|
||||||
containerPortal = 2FB344FF2DFBE270001A8A67 /* Project object */;
|
|
||||||
proxyType = 1;
|
|
||||||
remoteGlobalIDString = 2FB345062DFBE270001A8A67;
|
|
||||||
remoteInfo = EmotionMuseum;
|
|
||||||
};
|
|
||||||
/* End PBXContainerItemProxy section */
|
|
||||||
|
|
||||||
/* Begin PBXFileReference section */
|
|
||||||
2FB345072DFBE270001A8A67 /* EmotionMuseum.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = EmotionMuseum.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
|
||||||
2FB345192DFBE273001A8A67 /* EmotionMuseumTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = EmotionMuseumTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
|
||||||
2FB345232DFBE273001A8A67 /* EmotionMuseumUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = EmotionMuseumUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
|
||||||
/* End PBXFileReference section */
|
|
||||||
|
|
||||||
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
|
||||||
2FB345092DFBE270001A8A67 /* EmotionMuseum */ = {
|
|
||||||
isa = PBXFileSystemSynchronizedRootGroup;
|
|
||||||
path = EmotionMuseum;
|
|
||||||
sourceTree = "<group>";
|
|
||||||
};
|
|
||||||
2FB3451C2DFBE273001A8A67 /* EmotionMuseumTests */ = {
|
|
||||||
isa = PBXFileSystemSynchronizedRootGroup;
|
|
||||||
path = EmotionMuseumTests;
|
|
||||||
sourceTree = "<group>";
|
|
||||||
};
|
|
||||||
2FB345262DFBE273001A8A67 /* EmotionMuseumUITests */ = {
|
|
||||||
isa = PBXFileSystemSynchronizedRootGroup;
|
|
||||||
path = EmotionMuseumUITests;
|
|
||||||
sourceTree = "<group>";
|
|
||||||
};
|
|
||||||
/* End PBXFileSystemSynchronizedRootGroup section */
|
|
||||||
|
|
||||||
/* Begin PBXFrameworksBuildPhase section */
|
|
||||||
2FB345042DFBE270001A8A67 /* Frameworks */ = {
|
|
||||||
isa = PBXFrameworksBuildPhase;
|
|
||||||
buildActionMask = 2147483647;
|
|
||||||
files = (
|
|
||||||
);
|
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
|
||||||
};
|
|
||||||
2FB345162DFBE273001A8A67 /* Frameworks */ = {
|
|
||||||
isa = PBXFrameworksBuildPhase;
|
|
||||||
buildActionMask = 2147483647;
|
|
||||||
files = (
|
|
||||||
);
|
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
|
||||||
};
|
|
||||||
2FB345202DFBE273001A8A67 /* Frameworks */ = {
|
|
||||||
isa = PBXFrameworksBuildPhase;
|
|
||||||
buildActionMask = 2147483647;
|
|
||||||
files = (
|
|
||||||
);
|
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
|
||||||
};
|
|
||||||
/* End PBXFrameworksBuildPhase section */
|
|
||||||
|
|
||||||
/* Begin PBXGroup section */
|
|
||||||
2FB344FE2DFBE270001A8A67 = {
|
|
||||||
isa = PBXGroup;
|
|
||||||
children = (
|
|
||||||
2FB345092DFBE270001A8A67 /* EmotionMuseum */,
|
|
||||||
2FB3451C2DFBE273001A8A67 /* EmotionMuseumTests */,
|
|
||||||
2FB345262DFBE273001A8A67 /* EmotionMuseumUITests */,
|
|
||||||
2FB345082DFBE270001A8A67 /* Products */,
|
|
||||||
);
|
|
||||||
sourceTree = "<group>";
|
|
||||||
};
|
|
||||||
2FB345082DFBE270001A8A67 /* Products */ = {
|
|
||||||
isa = PBXGroup;
|
|
||||||
children = (
|
|
||||||
2FB345072DFBE270001A8A67 /* EmotionMuseum.app */,
|
|
||||||
2FB345192DFBE273001A8A67 /* EmotionMuseumTests.xctest */,
|
|
||||||
2FB345232DFBE273001A8A67 /* EmotionMuseumUITests.xctest */,
|
|
||||||
);
|
|
||||||
name = Products;
|
|
||||||
sourceTree = "<group>";
|
|
||||||
};
|
|
||||||
/* End PBXGroup section */
|
|
||||||
|
|
||||||
/* Begin PBXNativeTarget section */
|
|
||||||
2FB345062DFBE270001A8A67 /* EmotionMuseum */ = {
|
|
||||||
isa = PBXNativeTarget;
|
|
||||||
buildConfigurationList = 2FB3452D2DFBE273001A8A67 /* Build configuration list for PBXNativeTarget "EmotionMuseum" */;
|
|
||||||
buildPhases = (
|
|
||||||
2FB345032DFBE270001A8A67 /* Sources */,
|
|
||||||
2FB345042DFBE270001A8A67 /* Frameworks */,
|
|
||||||
2FB345052DFBE270001A8A67 /* Resources */,
|
|
||||||
);
|
|
||||||
buildRules = (
|
|
||||||
);
|
|
||||||
dependencies = (
|
|
||||||
);
|
|
||||||
fileSystemSynchronizedGroups = (
|
|
||||||
2FB345092DFBE270001A8A67 /* EmotionMuseum */,
|
|
||||||
);
|
|
||||||
name = EmotionMuseum;
|
|
||||||
packageProductDependencies = (
|
|
||||||
);
|
|
||||||
productName = EmotionMuseum;
|
|
||||||
productReference = 2FB345072DFBE270001A8A67 /* EmotionMuseum.app */;
|
|
||||||
productType = "com.apple.product-type.application";
|
|
||||||
};
|
|
||||||
2FB345182DFBE273001A8A67 /* EmotionMuseumTests */ = {
|
|
||||||
isa = PBXNativeTarget;
|
|
||||||
buildConfigurationList = 2FB345302DFBE273001A8A67 /* Build configuration list for PBXNativeTarget "EmotionMuseumTests" */;
|
|
||||||
buildPhases = (
|
|
||||||
2FB345152DFBE273001A8A67 /* Sources */,
|
|
||||||
2FB345162DFBE273001A8A67 /* Frameworks */,
|
|
||||||
2FB345172DFBE273001A8A67 /* Resources */,
|
|
||||||
);
|
|
||||||
buildRules = (
|
|
||||||
);
|
|
||||||
dependencies = (
|
|
||||||
2FB3451B2DFBE273001A8A67 /* PBXTargetDependency */,
|
|
||||||
);
|
|
||||||
fileSystemSynchronizedGroups = (
|
|
||||||
2FB3451C2DFBE273001A8A67 /* EmotionMuseumTests */,
|
|
||||||
);
|
|
||||||
name = EmotionMuseumTests;
|
|
||||||
packageProductDependencies = (
|
|
||||||
);
|
|
||||||
productName = EmotionMuseumTests;
|
|
||||||
productReference = 2FB345192DFBE273001A8A67 /* EmotionMuseumTests.xctest */;
|
|
||||||
productType = "com.apple.product-type.bundle.unit-test";
|
|
||||||
};
|
|
||||||
2FB345222DFBE273001A8A67 /* EmotionMuseumUITests */ = {
|
|
||||||
isa = PBXNativeTarget;
|
|
||||||
buildConfigurationList = 2FB345332DFBE273001A8A67 /* Build configuration list for PBXNativeTarget "EmotionMuseumUITests" */;
|
|
||||||
buildPhases = (
|
|
||||||
2FB3451F2DFBE273001A8A67 /* Sources */,
|
|
||||||
2FB345202DFBE273001A8A67 /* Frameworks */,
|
|
||||||
2FB345212DFBE273001A8A67 /* Resources */,
|
|
||||||
);
|
|
||||||
buildRules = (
|
|
||||||
);
|
|
||||||
dependencies = (
|
|
||||||
2FB345252DFBE273001A8A67 /* PBXTargetDependency */,
|
|
||||||
);
|
|
||||||
fileSystemSynchronizedGroups = (
|
|
||||||
2FB345262DFBE273001A8A67 /* EmotionMuseumUITests */,
|
|
||||||
);
|
|
||||||
name = EmotionMuseumUITests;
|
|
||||||
packageProductDependencies = (
|
|
||||||
);
|
|
||||||
productName = EmotionMuseumUITests;
|
|
||||||
productReference = 2FB345232DFBE273001A8A67 /* EmotionMuseumUITests.xctest */;
|
|
||||||
productType = "com.apple.product-type.bundle.ui-testing";
|
|
||||||
};
|
|
||||||
/* End PBXNativeTarget section */
|
|
||||||
|
|
||||||
/* Begin PBXProject section */
|
|
||||||
2FB344FF2DFBE270001A8A67 /* Project object */ = {
|
|
||||||
isa = PBXProject;
|
|
||||||
attributes = {
|
|
||||||
BuildIndependentTargetsInParallel = 1;
|
|
||||||
LastSwiftUpdateCheck = 1640;
|
|
||||||
LastUpgradeCheck = 1640;
|
|
||||||
TargetAttributes = {
|
|
||||||
2FB345062DFBE270001A8A67 = {
|
|
||||||
CreatedOnToolsVersion = 16.4;
|
|
||||||
};
|
|
||||||
2FB345182DFBE273001A8A67 = {
|
|
||||||
CreatedOnToolsVersion = 16.4;
|
|
||||||
TestTargetID = 2FB345062DFBE270001A8A67;
|
|
||||||
};
|
|
||||||
2FB345222DFBE273001A8A67 = {
|
|
||||||
CreatedOnToolsVersion = 16.4;
|
|
||||||
TestTargetID = 2FB345062DFBE270001A8A67;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
buildConfigurationList = 2FB345022DFBE270001A8A67 /* Build configuration list for PBXProject "EmotionMuseum" */;
|
|
||||||
developmentRegion = en;
|
|
||||||
hasScannedForEncodings = 0;
|
|
||||||
knownRegions = (
|
|
||||||
en,
|
|
||||||
Base,
|
|
||||||
);
|
|
||||||
mainGroup = 2FB344FE2DFBE270001A8A67;
|
|
||||||
minimizedProjectReferenceProxies = 1;
|
|
||||||
preferredProjectObjectVersion = 77;
|
|
||||||
productRefGroup = 2FB345082DFBE270001A8A67 /* Products */;
|
|
||||||
projectDirPath = "";
|
|
||||||
projectRoot = "";
|
|
||||||
targets = (
|
|
||||||
2FB345062DFBE270001A8A67 /* EmotionMuseum */,
|
|
||||||
2FB345182DFBE273001A8A67 /* EmotionMuseumTests */,
|
|
||||||
2FB345222DFBE273001A8A67 /* EmotionMuseumUITests */,
|
|
||||||
);
|
|
||||||
};
|
|
||||||
/* End PBXProject section */
|
|
||||||
|
|
||||||
/* Begin PBXResourcesBuildPhase section */
|
|
||||||
2FB345052DFBE270001A8A67 /* Resources */ = {
|
|
||||||
isa = PBXResourcesBuildPhase;
|
|
||||||
buildActionMask = 2147483647;
|
|
||||||
files = (
|
|
||||||
);
|
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
|
||||||
};
|
|
||||||
2FB345172DFBE273001A8A67 /* Resources */ = {
|
|
||||||
isa = PBXResourcesBuildPhase;
|
|
||||||
buildActionMask = 2147483647;
|
|
||||||
files = (
|
|
||||||
);
|
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
|
||||||
};
|
|
||||||
2FB345212DFBE273001A8A67 /* Resources */ = {
|
|
||||||
isa = PBXResourcesBuildPhase;
|
|
||||||
buildActionMask = 2147483647;
|
|
||||||
files = (
|
|
||||||
);
|
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
|
||||||
};
|
|
||||||
/* End PBXResourcesBuildPhase section */
|
|
||||||
|
|
||||||
/* Begin PBXSourcesBuildPhase section */
|
|
||||||
2FB345032DFBE270001A8A67 /* Sources */ = {
|
|
||||||
isa = PBXSourcesBuildPhase;
|
|
||||||
buildActionMask = 2147483647;
|
|
||||||
files = (
|
|
||||||
);
|
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
|
||||||
};
|
|
||||||
2FB345152DFBE273001A8A67 /* Sources */ = {
|
|
||||||
isa = PBXSourcesBuildPhase;
|
|
||||||
buildActionMask = 2147483647;
|
|
||||||
files = (
|
|
||||||
);
|
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
|
||||||
};
|
|
||||||
2FB3451F2DFBE273001A8A67 /* Sources */ = {
|
|
||||||
isa = PBXSourcesBuildPhase;
|
|
||||||
buildActionMask = 2147483647;
|
|
||||||
files = (
|
|
||||||
);
|
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
|
||||||
};
|
|
||||||
/* End PBXSourcesBuildPhase section */
|
|
||||||
|
|
||||||
/* Begin PBXTargetDependency section */
|
|
||||||
2FB3451B2DFBE273001A8A67 /* PBXTargetDependency */ = {
|
|
||||||
isa = PBXTargetDependency;
|
|
||||||
target = 2FB345062DFBE270001A8A67 /* EmotionMuseum */;
|
|
||||||
targetProxy = 2FB3451A2DFBE273001A8A67 /* PBXContainerItemProxy */;
|
|
||||||
};
|
|
||||||
2FB345252DFBE273001A8A67 /* PBXTargetDependency */ = {
|
|
||||||
isa = PBXTargetDependency;
|
|
||||||
target = 2FB345062DFBE270001A8A67 /* EmotionMuseum */;
|
|
||||||
targetProxy = 2FB345242DFBE273001A8A67 /* PBXContainerItemProxy */;
|
|
||||||
};
|
|
||||||
/* End PBXTargetDependency section */
|
|
||||||
|
|
||||||
/* Begin XCBuildConfiguration section */
|
|
||||||
2FB3452B2DFBE273001A8A67 /* Debug */ = {
|
|
||||||
isa = XCBuildConfiguration;
|
|
||||||
buildSettings = {
|
|
||||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
|
||||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
|
||||||
CLANG_ANALYZER_NONNULL = YES;
|
|
||||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
|
||||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
|
||||||
CLANG_ENABLE_MODULES = YES;
|
|
||||||
CLANG_ENABLE_OBJC_ARC = YES;
|
|
||||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
|
||||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
|
||||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
|
||||||
CLANG_WARN_COMMA = YES;
|
|
||||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
|
||||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
|
||||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
|
||||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
|
||||||
CLANG_WARN_EMPTY_BODY = YES;
|
|
||||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
|
||||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
|
||||||
CLANG_WARN_INT_CONVERSION = YES;
|
|
||||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
|
||||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
|
||||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
|
||||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
|
||||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
|
||||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
|
||||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
|
||||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
|
||||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
|
||||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
|
||||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
|
||||||
COPY_PHASE_STRIP = NO;
|
|
||||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
|
||||||
DEVELOPMENT_TEAM = JA6T4PANZM;
|
|
||||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
|
||||||
ENABLE_TESTABILITY = YES;
|
|
||||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
|
||||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
|
||||||
GCC_DYNAMIC_NO_PIC = NO;
|
|
||||||
GCC_NO_COMMON_BLOCKS = YES;
|
|
||||||
GCC_OPTIMIZATION_LEVEL = 0;
|
|
||||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
|
||||||
"DEBUG=1",
|
|
||||||
"$(inherited)",
|
|
||||||
);
|
|
||||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
|
||||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
|
||||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
|
||||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
|
||||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
|
||||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 18.5;
|
|
||||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
|
||||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
|
||||||
MTL_FAST_MATH = YES;
|
|
||||||
ONLY_ACTIVE_ARCH = YES;
|
|
||||||
SDKROOT = iphoneos;
|
|
||||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
|
|
||||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
|
||||||
};
|
|
||||||
name = Debug;
|
|
||||||
};
|
|
||||||
2FB3452C2DFBE273001A8A67 /* Release */ = {
|
|
||||||
isa = XCBuildConfiguration;
|
|
||||||
buildSettings = {
|
|
||||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
|
||||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
|
||||||
CLANG_ANALYZER_NONNULL = YES;
|
|
||||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
|
||||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
|
||||||
CLANG_ENABLE_MODULES = YES;
|
|
||||||
CLANG_ENABLE_OBJC_ARC = YES;
|
|
||||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
|
||||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
|
||||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
|
||||||
CLANG_WARN_COMMA = YES;
|
|
||||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
|
||||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
|
||||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
|
||||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
|
||||||
CLANG_WARN_EMPTY_BODY = YES;
|
|
||||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
|
||||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
|
||||||
CLANG_WARN_INT_CONVERSION = YES;
|
|
||||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
|
||||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
|
||||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
|
||||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
|
||||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
|
||||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
|
||||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
|
||||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
|
||||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
|
||||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
|
||||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
|
||||||
COPY_PHASE_STRIP = NO;
|
|
||||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
|
||||||
DEVELOPMENT_TEAM = JA6T4PANZM;
|
|
||||||
ENABLE_NS_ASSERTIONS = NO;
|
|
||||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
|
||||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
|
||||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
|
||||||
GCC_NO_COMMON_BLOCKS = YES;
|
|
||||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
|
||||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
|
||||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
|
||||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
|
||||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
|
||||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 18.5;
|
|
||||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
|
||||||
MTL_ENABLE_DEBUG_INFO = NO;
|
|
||||||
MTL_FAST_MATH = YES;
|
|
||||||
SDKROOT = iphoneos;
|
|
||||||
SWIFT_COMPILATION_MODE = wholemodule;
|
|
||||||
VALIDATE_PRODUCT = YES;
|
|
||||||
};
|
|
||||||
name = Release;
|
|
||||||
};
|
|
||||||
2FB3452E2DFBE273001A8A67 /* Debug */ = {
|
|
||||||
isa = XCBuildConfiguration;
|
|
||||||
buildSettings = {
|
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
|
||||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
|
||||||
CODE_SIGN_STYLE = Automatic;
|
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
|
||||||
DEVELOPMENT_TEAM = JA6T4PANZM;
|
|
||||||
ENABLE_PREVIEWS = YES;
|
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
|
||||||
INFOPLIST_KEY_LSApplicationQueriesSchemes = iosamap;
|
|
||||||
INFOPLIST_KEY_NSLocationAlwaysAndWhenInUseUsageDescription = "需要使用您的位置信息来为您提供地图服务";
|
|
||||||
INFOPLIST_KEY_NSLocationAlwaysUsageDescription = "需要使用您的位置信息来为您提供地图服务";
|
|
||||||
INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "需要使用您的位置信息来为您提供地图服务";
|
|
||||||
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
|
|
||||||
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
|
||||||
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
|
||||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
|
||||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
|
||||||
"$(inherited)",
|
|
||||||
"@executable_path/Frameworks",
|
|
||||||
);
|
|
||||||
MARKETING_VERSION = 1.0;
|
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.dolphin.EmotionMuseum;
|
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
|
||||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
|
||||||
SWIFT_VERSION = 5.0;
|
|
||||||
TARGETED_DEVICE_FAMILY = "1,2";
|
|
||||||
};
|
|
||||||
name = Debug;
|
|
||||||
};
|
|
||||||
2FB3452F2DFBE273001A8A67 /* Release */ = {
|
|
||||||
isa = XCBuildConfiguration;
|
|
||||||
buildSettings = {
|
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
|
||||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
|
||||||
CODE_SIGN_STYLE = Automatic;
|
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
|
||||||
DEVELOPMENT_TEAM = JA6T4PANZM;
|
|
||||||
ENABLE_PREVIEWS = YES;
|
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
|
||||||
INFOPLIST_KEY_LSApplicationQueriesSchemes = iosamap;
|
|
||||||
INFOPLIST_KEY_NSLocationAlwaysAndWhenInUseUsageDescription = "需要使用您的位置信息来为您提供地图服务";
|
|
||||||
INFOPLIST_KEY_NSLocationAlwaysUsageDescription = "需要使用您的位置信息来为您提供地图服务";
|
|
||||||
INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "需要使用您的位置信息来为您提供地图服务";
|
|
||||||
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
|
|
||||||
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
|
||||||
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
|
||||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
|
||||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
|
||||||
"$(inherited)",
|
|
||||||
"@executable_path/Frameworks",
|
|
||||||
);
|
|
||||||
MARKETING_VERSION = 1.0;
|
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.dolphin.EmotionMuseum;
|
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
|
||||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
|
||||||
SWIFT_VERSION = 5.0;
|
|
||||||
TARGETED_DEVICE_FAMILY = "1,2";
|
|
||||||
};
|
|
||||||
name = Release;
|
|
||||||
};
|
|
||||||
2FB345312DFBE273001A8A67 /* Debug */ = {
|
|
||||||
isa = XCBuildConfiguration;
|
|
||||||
buildSettings = {
|
|
||||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
|
||||||
CODE_SIGN_STYLE = Automatic;
|
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
|
||||||
DEVELOPMENT_TEAM = JA6T4PANZM;
|
|
||||||
GENERATE_INFOPLIST_FILE = NO;
|
|
||||||
INFOPLIST_FILE = EmotionMuseumTests/Info.plist;
|
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 18.5;
|
|
||||||
MARKETING_VERSION = 1.0;
|
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.dolphin.EmotionMuseumTests;
|
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
|
||||||
SWIFT_EMIT_LOC_STRINGS = NO;
|
|
||||||
SWIFT_VERSION = 5.0;
|
|
||||||
TARGETED_DEVICE_FAMILY = "1,2";
|
|
||||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/EmotionMuseum.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/EmotionMuseum";
|
|
||||||
};
|
|
||||||
name = Debug;
|
|
||||||
};
|
|
||||||
2FB345322DFBE273001A8A67 /* Release */ = {
|
|
||||||
isa = XCBuildConfiguration;
|
|
||||||
buildSettings = {
|
|
||||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
|
||||||
CODE_SIGN_STYLE = Automatic;
|
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
|
||||||
DEVELOPMENT_TEAM = JA6T4PANZM;
|
|
||||||
GENERATE_INFOPLIST_FILE = NO;
|
|
||||||
INFOPLIST_FILE = EmotionMuseumTests/Info.plist;
|
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 18.5;
|
|
||||||
MARKETING_VERSION = 1.0;
|
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.dolphin.EmotionMuseumTests;
|
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
|
||||||
SWIFT_EMIT_LOC_STRINGS = NO;
|
|
||||||
SWIFT_VERSION = 5.0;
|
|
||||||
TARGETED_DEVICE_FAMILY = "1,2";
|
|
||||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/EmotionMuseum.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/EmotionMuseum";
|
|
||||||
};
|
|
||||||
name = Release;
|
|
||||||
};
|
|
||||||
2FB345342DFBE273001A8A67 /* Debug */ = {
|
|
||||||
isa = XCBuildConfiguration;
|
|
||||||
buildSettings = {
|
|
||||||
CODE_SIGN_STYLE = Automatic;
|
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
|
||||||
DEVELOPMENT_TEAM = JA6T4PANZM;
|
|
||||||
GENERATE_INFOPLIST_FILE = NO;
|
|
||||||
INFOPLIST_FILE = EmotionMuseumUITests/Info.plist;
|
|
||||||
MARKETING_VERSION = 1.0;
|
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.dolphin.EmotionMuseumUITests;
|
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
|
||||||
SWIFT_EMIT_LOC_STRINGS = NO;
|
|
||||||
SWIFT_VERSION = 5.0;
|
|
||||||
TARGETED_DEVICE_FAMILY = "1,2";
|
|
||||||
TEST_TARGET_NAME = EmotionMuseum;
|
|
||||||
};
|
|
||||||
name = Debug;
|
|
||||||
};
|
|
||||||
2FB345352DFBE273001A8A67 /* Release */ = {
|
|
||||||
isa = XCBuildConfiguration;
|
|
||||||
buildSettings = {
|
|
||||||
CODE_SIGN_STYLE = Automatic;
|
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
|
||||||
DEVELOPMENT_TEAM = JA6T4PANZM;
|
|
||||||
GENERATE_INFOPLIST_FILE = NO;
|
|
||||||
INFOPLIST_FILE = EmotionMuseumUITests/Info.plist;
|
|
||||||
MARKETING_VERSION = 1.0;
|
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.dolphin.EmotionMuseumUITests;
|
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
|
||||||
SWIFT_EMIT_LOC_STRINGS = NO;
|
|
||||||
SWIFT_VERSION = 5.0;
|
|
||||||
TARGETED_DEVICE_FAMILY = "1,2";
|
|
||||||
TEST_TARGET_NAME = EmotionMuseum;
|
|
||||||
};
|
|
||||||
name = Release;
|
|
||||||
};
|
|
||||||
/* End XCBuildConfiguration section */
|
|
||||||
|
|
||||||
/* Begin XCConfigurationList section */
|
|
||||||
2FB345022DFBE270001A8A67 /* Build configuration list for PBXProject "EmotionMuseum" */ = {
|
|
||||||
isa = XCConfigurationList;
|
|
||||||
buildConfigurations = (
|
|
||||||
2FB3452B2DFBE273001A8A67 /* Debug */,
|
|
||||||
2FB3452C2DFBE273001A8A67 /* Release */,
|
|
||||||
);
|
|
||||||
defaultConfigurationIsVisible = 0;
|
|
||||||
defaultConfigurationName = Release;
|
|
||||||
};
|
|
||||||
2FB3452D2DFBE273001A8A67 /* Build configuration list for PBXNativeTarget "EmotionMuseum" */ = {
|
|
||||||
isa = XCConfigurationList;
|
|
||||||
buildConfigurations = (
|
|
||||||
2FB3452E2DFBE273001A8A67 /* Debug */,
|
|
||||||
2FB3452F2DFBE273001A8A67 /* Release */,
|
|
||||||
);
|
|
||||||
defaultConfigurationIsVisible = 0;
|
|
||||||
defaultConfigurationName = Release;
|
|
||||||
};
|
|
||||||
2FB345302DFBE273001A8A67 /* Build configuration list for PBXNativeTarget "EmotionMuseumTests" */ = {
|
|
||||||
isa = XCConfigurationList;
|
|
||||||
buildConfigurations = (
|
|
||||||
2FB345312DFBE273001A8A67 /* Debug */,
|
|
||||||
2FB345322DFBE273001A8A67 /* Release */,
|
|
||||||
);
|
|
||||||
defaultConfigurationIsVisible = 0;
|
|
||||||
defaultConfigurationName = Release;
|
|
||||||
};
|
|
||||||
2FB345332DFBE273001A8A67 /* Build configuration list for PBXNativeTarget "EmotionMuseumUITests" */ = {
|
|
||||||
isa = XCConfigurationList;
|
|
||||||
buildConfigurations = (
|
|
||||||
2FB345342DFBE273001A8A67 /* Debug */,
|
|
||||||
2FB345352DFBE273001A8A67 /* Release */,
|
|
||||||
);
|
|
||||||
defaultConfigurationIsVisible = 0;
|
|
||||||
defaultConfigurationName = Release;
|
|
||||||
};
|
|
||||||
/* End XCConfigurationList section */
|
|
||||||
};
|
|
||||||
rootObject = 2FB344FF2DFBE270001A8A67 /* Project object */;
|
|
||||||
}
|
|
||||||
-7
@@ -1,7 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<Workspace
|
|
||||||
version = "1.0">
|
|
||||||
<FileRef
|
|
||||||
location = "self:">
|
|
||||||
</FileRef>
|
|
||||||
</Workspace>
|
|
||||||
-5
@@ -1,5 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
||||||
<plist version="1.0">
|
|
||||||
<dict/>
|
|
||||||
</plist>
|
|
||||||
-14
@@ -1,14 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
||||||
<plist version="1.0">
|
|
||||||
<dict>
|
|
||||||
<key>BuildLocationStyle</key>
|
|
||||||
<string>UseAppPreferences</string>
|
|
||||||
<key>CustomBuildLocationType</key>
|
|
||||||
<string>RelativeToDerivedData</string>
|
|
||||||
<key>DerivedDataLocationStyle</key>
|
|
||||||
<string>Default</string>
|
|
||||||
<key>ShowSharedSchemesAutomaticallyEnabled</key>
|
|
||||||
<true/>
|
|
||||||
</dict>
|
|
||||||
</plist>
|
|
||||||
-14
@@ -1,14 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
||||||
<plist version="1.0">
|
|
||||||
<dict>
|
|
||||||
<key>SchemeUserState</key>
|
|
||||||
<dict>
|
|
||||||
<key>EmotionMuseum.xcscheme_^#shared#^_</key>
|
|
||||||
<dict>
|
|
||||||
<key>orderHint</key>
|
|
||||||
<integer>0</integer>
|
|
||||||
</dict>
|
|
||||||
</dict>
|
|
||||||
</dict>
|
|
||||||
</plist>
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
{
|
|
||||||
"colors" : [
|
|
||||||
{
|
|
||||||
"color" : {
|
|
||||||
"color-space" : "srgb",
|
|
||||||
"components" : {
|
|
||||||
"alpha" : "1.000",
|
|
||||||
"blue" : "1.000",
|
|
||||||
"green" : "0.573",
|
|
||||||
"red" : "0.000"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"idiom" : "universal"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"appearances" : [
|
|
||||||
{
|
|
||||||
"appearance" : "luminosity",
|
|
||||||
"value" : "dark"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"color" : {
|
|
||||||
"color-space" : "srgb",
|
|
||||||
"components" : {
|
|
||||||
"alpha" : "1.000",
|
|
||||||
"blue" : "1.000",
|
|
||||||
"green" : "0.678",
|
|
||||||
"red" : "0.196"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"idiom" : "universal"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"info" : {
|
|
||||||
"author" : "xcode",
|
|
||||||
"version" : 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
{
|
|
||||||
"images" : [
|
|
||||||
{
|
|
||||||
"idiom" : "universal",
|
|
||||||
"platform" : "ios",
|
|
||||||
"size" : "1024x1024"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"appearances" : [
|
|
||||||
{
|
|
||||||
"appearance" : "luminosity",
|
|
||||||
"value" : "dark"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"idiom" : "universal",
|
|
||||||
"platform" : "ios",
|
|
||||||
"size" : "1024x1024"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"appearances" : [
|
|
||||||
{
|
|
||||||
"appearance" : "luminosity",
|
|
||||||
"value" : "tinted"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"idiom" : "universal",
|
|
||||||
"platform" : "ios",
|
|
||||||
"size" : "1024x1024"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"info" : {
|
|
||||||
"author" : "xcode",
|
|
||||||
"version" : 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
{
|
|
||||||
"colors" : [
|
|
||||||
{
|
|
||||||
"color" : {
|
|
||||||
"color-space" : "srgb",
|
|
||||||
"components" : {
|
|
||||||
"alpha" : "1.000",
|
|
||||||
"blue" : "1.000",
|
|
||||||
"green" : "1.000",
|
|
||||||
"red" : "1.000"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"idiom" : "universal"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"appearances" : [
|
|
||||||
{
|
|
||||||
"appearance" : "luminosity",
|
|
||||||
"value" : "dark"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"color" : {
|
|
||||||
"color-space" : "srgb",
|
|
||||||
"components" : {
|
|
||||||
"alpha" : "1.000",
|
|
||||||
"blue" : "0.118",
|
|
||||||
"green" : "0.118",
|
|
||||||
"red" : "0.118"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"idiom" : "universal"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"info" : {
|
|
||||||
"author" : "xcode",
|
|
||||||
"version" : 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
{
|
|
||||||
"colors" : [
|
|
||||||
{
|
|
||||||
"color" : {
|
|
||||||
"color-space" : "srgb",
|
|
||||||
"components" : {
|
|
||||||
"alpha" : "1.000",
|
|
||||||
"blue" : "0.700",
|
|
||||||
"green" : "0.700",
|
|
||||||
"red" : "0.700"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"idiom" : "universal"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"appearances" : [
|
|
||||||
{
|
|
||||||
"appearance" : "luminosity",
|
|
||||||
"value" : "dark"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"color" : {
|
|
||||||
"color-space" : "srgb",
|
|
||||||
"components" : {
|
|
||||||
"alpha" : "1.000",
|
|
||||||
"blue" : "0.600",
|
|
||||||
"green" : "0.600",
|
|
||||||
"red" : "0.600"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"idiom" : "universal"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"info" : {
|
|
||||||
"author" : "xcode",
|
|
||||||
"version" : 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
{
|
|
||||||
"colors" : [
|
|
||||||
{
|
|
||||||
"color" : {
|
|
||||||
"color-space" : "srgb",
|
|
||||||
"components" : {
|
|
||||||
"alpha" : "1.000",
|
|
||||||
"blue" : "1.000",
|
|
||||||
"green" : "1.000",
|
|
||||||
"red" : "1.000"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"idiom" : "universal"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"appearances" : [
|
|
||||||
{
|
|
||||||
"appearance" : "luminosity",
|
|
||||||
"value" : "dark"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"color" : {
|
|
||||||
"color-space" : "srgb",
|
|
||||||
"components" : {
|
|
||||||
"alpha" : "1.000",
|
|
||||||
"blue" : "0.196",
|
|
||||||
"green" : "0.196",
|
|
||||||
"red" : "0.196"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"idiom" : "universal"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"info" : {
|
|
||||||
"author" : "xcode",
|
|
||||||
"version" : 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
{
|
|
||||||
"info" : {
|
|
||||||
"author" : "xcode",
|
|
||||||
"version" : 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
{
|
|
||||||
"colors" : [
|
|
||||||
{
|
|
||||||
"color" : {
|
|
||||||
"color-space" : "srgb",
|
|
||||||
"components" : {
|
|
||||||
"alpha" : "1.000",
|
|
||||||
"blue" : "0.788",
|
|
||||||
"green" : "0.780",
|
|
||||||
"red" : "0.776"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"idiom" : "universal"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"appearances" : [
|
|
||||||
{
|
|
||||||
"appearance" : "luminosity",
|
|
||||||
"value" : "dark"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"color" : {
|
|
||||||
"color-space" : "srgb",
|
|
||||||
"components" : {
|
|
||||||
"alpha" : "1.000",
|
|
||||||
"blue" : "0.329",
|
|
||||||
"green" : "0.310",
|
|
||||||
"red" : "0.298"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"idiom" : "universal"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"info" : {
|
|
||||||
"author" : "xcode",
|
|
||||||
"version" : 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
{
|
|
||||||
"colors" : [
|
|
||||||
{
|
|
||||||
"color" : {
|
|
||||||
"color-space" : "srgb",
|
|
||||||
"components" : {
|
|
||||||
"alpha" : "1.000",
|
|
||||||
"blue" : "0.300",
|
|
||||||
"green" : "0.200",
|
|
||||||
"red" : "0.900"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"idiom" : "universal"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"appearances" : [
|
|
||||||
{
|
|
||||||
"appearance" : "luminosity",
|
|
||||||
"value" : "dark"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"color" : {
|
|
||||||
"color-space" : "srgb",
|
|
||||||
"components" : {
|
|
||||||
"alpha" : "1.000",
|
|
||||||
"blue" : "0.400",
|
|
||||||
"green" : "0.300",
|
|
||||||
"red" : "1.000"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"idiom" : "universal"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"info" : {
|
|
||||||
"author" : "xcode",
|
|
||||||
"version" : 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
{
|
|
||||||
"colors" : [
|
|
||||||
{
|
|
||||||
"color" : {
|
|
||||||
"color-space" : "srgb",
|
|
||||||
"components" : {
|
|
||||||
"alpha" : "1.000",
|
|
||||||
"blue" : "0.929",
|
|
||||||
"green" : "0.569",
|
|
||||||
"red" : "0.416"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"idiom" : "universal"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"appearances" : [
|
|
||||||
{
|
|
||||||
"appearance" : "luminosity",
|
|
||||||
"value" : "dark"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"color" : {
|
|
||||||
"color-space" : "srgb",
|
|
||||||
"components" : {
|
|
||||||
"alpha" : "1.000",
|
|
||||||
"blue" : "0.929",
|
|
||||||
"green" : "0.569",
|
|
||||||
"red" : "0.416"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"idiom" : "universal"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"info" : {
|
|
||||||
"author" : "xcode",
|
|
||||||
"version" : 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
{
|
|
||||||
"colors" : [
|
|
||||||
{
|
|
||||||
"color" : {
|
|
||||||
"color-space" : "srgb",
|
|
||||||
"components" : {
|
|
||||||
"alpha" : "1.000",
|
|
||||||
"blue" : "0.000",
|
|
||||||
"green" : "0.000",
|
|
||||||
"red" : "0.000"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"idiom" : "universal"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"appearances" : [
|
|
||||||
{
|
|
||||||
"appearance" : "luminosity",
|
|
||||||
"value" : "dark"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"color" : {
|
|
||||||
"color-space" : "srgb",
|
|
||||||
"components" : {
|
|
||||||
"alpha" : "1.000",
|
|
||||||
"blue" : "1.000",
|
|
||||||
"green" : "1.000",
|
|
||||||
"red" : "1.000"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"idiom" : "universal"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"info" : {
|
|
||||||
"author" : "xcode",
|
|
||||||
"version" : 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
{
|
|
||||||
"colors" : [
|
|
||||||
{
|
|
||||||
"color" : {
|
|
||||||
"color-space" : "srgb",
|
|
||||||
"components" : {
|
|
||||||
"alpha" : "1.000",
|
|
||||||
"blue" : "0.980",
|
|
||||||
"green" : "0.780",
|
|
||||||
"red" : "0.310"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"idiom" : "universal"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"appearances" : [
|
|
||||||
{
|
|
||||||
"appearance" : "luminosity",
|
|
||||||
"value" : "dark"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"color" : {
|
|
||||||
"color-space" : "srgb",
|
|
||||||
"components" : {
|
|
||||||
"alpha" : "1.000",
|
|
||||||
"blue" : "0.780",
|
|
||||||
"green" : "0.580",
|
|
||||||
"red" : "0.210"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"idiom" : "universal"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"info" : {
|
|
||||||
"author" : "xcode",
|
|
||||||
"version" : 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
{
|
|
||||||
"colors" : [
|
|
||||||
{
|
|
||||||
"color" : {
|
|
||||||
"color-space" : "srgb",
|
|
||||||
"components" : {
|
|
||||||
"alpha" : "1.000",
|
|
||||||
"blue" : "0.600",
|
|
||||||
"green" : "0.600",
|
|
||||||
"red" : "0.600"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"idiom" : "universal"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"appearances" : [
|
|
||||||
{
|
|
||||||
"appearance" : "luminosity",
|
|
||||||
"value" : "dark"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"color" : {
|
|
||||||
"color-space" : "srgb",
|
|
||||||
"components" : {
|
|
||||||
"alpha" : "1.000",
|
|
||||||
"blue" : "0.780",
|
|
||||||
"green" : "0.780",
|
|
||||||
"red" : "0.780"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"idiom" : "universal"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"info" : {
|
|
||||||
"author" : "xcode",
|
|
||||||
"version" : 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
{
|
|
||||||
"colors" : [
|
|
||||||
{
|
|
||||||
"color" : {
|
|
||||||
"color-space" : "srgb",
|
|
||||||
"components" : {
|
|
||||||
"alpha" : "1.000",
|
|
||||||
"blue" : "0.925",
|
|
||||||
"green" : "0.925",
|
|
||||||
"red" : "0.925"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"idiom" : "universal"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"appearances" : [
|
|
||||||
{
|
|
||||||
"appearance" : "luminosity",
|
|
||||||
"value" : "dark"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"color" : {
|
|
||||||
"color-space" : "srgb",
|
|
||||||
"components" : {
|
|
||||||
"alpha" : "1.000",
|
|
||||||
"blue" : "0.294",
|
|
||||||
"green" : "0.294",
|
|
||||||
"red" : "0.294"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"idiom" : "universal"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"info" : {
|
|
||||||
"author" : "xcode",
|
|
||||||
"version" : 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
{
|
|
||||||
"colors" : [
|
|
||||||
{
|
|
||||||
"color" : {
|
|
||||||
"color-space" : "srgb",
|
|
||||||
"components" : {
|
|
||||||
"alpha" : "1.000",
|
|
||||||
"blue" : "0.980",
|
|
||||||
"green" : "0.980",
|
|
||||||
"red" : "0.980"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"idiom" : "universal"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"appearances" : [
|
|
||||||
{
|
|
||||||
"appearance" : "luminosity",
|
|
||||||
"value" : "dark"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"color" : {
|
|
||||||
"color-space" : "srgb",
|
|
||||||
"components" : {
|
|
||||||
"alpha" : "1.000",
|
|
||||||
"blue" : "0.392",
|
|
||||||
"green" : "0.392",
|
|
||||||
"red" : "0.392"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"idiom" : "universal"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"info" : {
|
|
||||||
"author" : "xcode",
|
|
||||||
"version" : 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
{
|
|
||||||
"colors" : [
|
|
||||||
{
|
|
||||||
"color" : {
|
|
||||||
"color-space" : "srgb",
|
|
||||||
"components" : {
|
|
||||||
"alpha" : "1.000",
|
|
||||||
"blue" : "0.380",
|
|
||||||
"green" : "0.780",
|
|
||||||
"red" : "0.200"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"idiom" : "universal"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"appearances" : [
|
|
||||||
{
|
|
||||||
"appearance" : "luminosity",
|
|
||||||
"value" : "dark"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"color" : {
|
|
||||||
"color-space" : "srgb",
|
|
||||||
"components" : {
|
|
||||||
"alpha" : "1.000",
|
|
||||||
"blue" : "0.480",
|
|
||||||
"green" : "0.880",
|
|
||||||
"red" : "0.300"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"idiom" : "universal"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"info" : {
|
|
||||||
"author" : "xcode",
|
|
||||||
"version" : 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
{
|
|
||||||
"colors" : [
|
|
||||||
{
|
|
||||||
"color" : {
|
|
||||||
"color-space" : "srgb",
|
|
||||||
"components" : {
|
|
||||||
"alpha" : "1.000",
|
|
||||||
"blue" : "0.961",
|
|
||||||
"green" : "0.961",
|
|
||||||
"red" : "0.961"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"idiom" : "universal"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"appearances" : [
|
|
||||||
{
|
|
||||||
"appearance" : "luminosity",
|
|
||||||
"value" : "dark"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"color" : {
|
|
||||||
"color-space" : "srgb",
|
|
||||||
"components" : {
|
|
||||||
"alpha" : "1.000",
|
|
||||||
"blue" : "0.173",
|
|
||||||
"green" : "0.169",
|
|
||||||
"red" : "0.165"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"idiom" : "universal"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"info" : {
|
|
||||||
"author" : "xcode",
|
|
||||||
"version" : 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
{
|
|
||||||
"colors" : [
|
|
||||||
{
|
|
||||||
"color" : {
|
|
||||||
"color-space" : "srgb",
|
|
||||||
"components" : {
|
|
||||||
"alpha" : "1.000",
|
|
||||||
"blue" : "0.700",
|
|
||||||
"green" : "0.700",
|
|
||||||
"red" : "0.700"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"idiom" : "universal"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"appearances" : [
|
|
||||||
{
|
|
||||||
"appearance" : "luminosity",
|
|
||||||
"value" : "dark"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"color" : {
|
|
||||||
"color-space" : "srgb",
|
|
||||||
"components" : {
|
|
||||||
"alpha" : "1.000",
|
|
||||||
"blue" : "0.600",
|
|
||||||
"green" : "0.600",
|
|
||||||
"red" : "0.600"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"idiom" : "universal"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"info" : {
|
|
||||||
"author" : "xcode",
|
|
||||||
"version" : 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
{
|
|
||||||
"colors" : [
|
|
||||||
{
|
|
||||||
"color" : {
|
|
||||||
"color-space" : "srgb",
|
|
||||||
"components" : {
|
|
||||||
"alpha" : "1.000",
|
|
||||||
"blue" : "0.200",
|
|
||||||
"green" : "0.700",
|
|
||||||
"red" : "1.000"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"idiom" : "universal"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"appearances" : [
|
|
||||||
{
|
|
||||||
"appearance" : "luminosity",
|
|
||||||
"value" : "dark"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"color" : {
|
|
||||||
"color-space" : "srgb",
|
|
||||||
"components" : {
|
|
||||||
"alpha" : "1.000",
|
|
||||||
"blue" : "0.300",
|
|
||||||
"green" : "0.800",
|
|
||||||
"red" : "1.000"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"idiom" : "universal"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"info" : {
|
|
||||||
"author" : "xcode",
|
|
||||||
"version" : 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
//
|
|
||||||
// ContentView.swift
|
|
||||||
// EmotionMuseum
|
|
||||||
//
|
|
||||||
// Created by 华中敏 on 2025/6/13.
|
|
||||||
//
|
|
||||||
|
|
||||||
import SwiftUI
|
|
||||||
import CoreData
|
|
||||||
|
|
||||||
struct ContentView: View {
|
|
||||||
@EnvironmentObject var themeManager: ThemeManager
|
|
||||||
@EnvironmentObject var mockDataManager: MockDataManager
|
|
||||||
@EnvironmentObject var navigationManager: NavigationManager
|
|
||||||
@Environment(\.managedObjectContext) private var viewContext
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
ZStack {
|
|
||||||
// 主要内容
|
|
||||||
TabView(selection: $navigationManager.currentTab) {
|
|
||||||
RecordView()
|
|
||||||
.tabItem {
|
|
||||||
Image(systemName: "heart.text.square")
|
|
||||||
Text("记录")
|
|
||||||
}
|
|
||||||
.tag(MainTab.record)
|
|
||||||
|
|
||||||
GrowthView()
|
|
||||||
.tabItem {
|
|
||||||
Image(systemName: "leaf.arrow.circlepath")
|
|
||||||
Text("治愈")
|
|
||||||
}
|
|
||||||
.tag(MainTab.growth)
|
|
||||||
|
|
||||||
ExploreView()
|
|
||||||
.tabItem {
|
|
||||||
Image(systemName: "map")
|
|
||||||
Text("探索")
|
|
||||||
}
|
|
||||||
.tag(MainTab.explore)
|
|
||||||
|
|
||||||
UniverseView()
|
|
||||||
.tabItem {
|
|
||||||
Image(systemName: "person.circle")
|
|
||||||
Text("我的")
|
|
||||||
}
|
|
||||||
.tag(MainTab.insight)
|
|
||||||
}
|
|
||||||
.accentColor(Color("AccentColor"))
|
|
||||||
|
|
||||||
// 全局加载覆盖层
|
|
||||||
if navigationManager.isLoading {
|
|
||||||
LoadingOverlay(message: navigationManager.loadingMessage)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.preferredColorScheme(themeManager.isDarkMode ? .dark : .light)
|
|
||||||
.onAppear {
|
|
||||||
// 初始化应用状态
|
|
||||||
setupInitialState()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func setupInitialState() {
|
|
||||||
// 设置初始状态
|
|
||||||
navigationManager.currentTab = .record
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#Preview {
|
|
||||||
ContentView()
|
|
||||||
.environment(\.managedObjectContext, PersistenceController.preview.container.viewContext)
|
|
||||||
.environmentObject(ThemeManager())
|
|
||||||
.environmentObject(MockDataManager.shared)
|
|
||||||
.environmentObject(NavigationManager())
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
||||||
<plist version="1.0">
|
|
||||||
<dict>
|
|
||||||
<key>_XCCurrentVersionName</key>
|
|
||||||
<string>EmotionMuseum.xcdatamodel</string>
|
|
||||||
</dict>
|
|
||||||
</plist>
|
|
||||||
-9
@@ -1,9 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
||||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="1" systemVersion="11A491" minimumToolsVersion="Automatic" sourceLanguage="Swift" usedWithCloudKit="false" userDefinedModelVersionIdentifier="">
|
|
||||||
<entity name="Item" representedClassName="Item" syncable="YES" codeGenerationType="class">
|
|
||||||
<attribute name="timestamp" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
|
||||||
</entity>
|
|
||||||
<elements>
|
|
||||||
<element name="Item" positionX="-63" positionY="-18" width="128" height="44"/>
|
|
||||||
</elements>
|
|
||||||
</model>
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
//
|
|
||||||
// EmotionMuseumApp.swift
|
|
||||||
// EmotionMuseum
|
|
||||||
//
|
|
||||||
// Created by 华中敏 on 2025/6/13.
|
|
||||||
//
|
|
||||||
|
|
||||||
import SwiftUI
|
|
||||||
|
|
||||||
@main
|
|
||||||
struct EmotionMuseumApp: App {
|
|
||||||
let persistenceController = PersistenceController.shared
|
|
||||||
@StateObject private var navigationManager = NavigationManager()
|
|
||||||
@StateObject private var themeManager = ThemeManager()
|
|
||||||
@StateObject private var mockDataManager = MockDataManager.shared
|
|
||||||
|
|
||||||
init() {
|
|
||||||
// 初始化高德地图SDK
|
|
||||||
MapManager.shared.configure()
|
|
||||||
}
|
|
||||||
|
|
||||||
var body: some Scene {
|
|
||||||
WindowGroup {
|
|
||||||
ContentView()
|
|
||||||
.environment(\.managedObjectContext, persistenceController.container.viewContext)
|
|
||||||
.environmentObject(navigationManager)
|
|
||||||
.environmentObject(themeManager)
|
|
||||||
.environmentObject(mockDataManager)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,189 +0,0 @@
|
|||||||
//
|
|
||||||
// ChakraType.swift
|
|
||||||
// EmotionMuseum
|
|
||||||
//
|
|
||||||
// Created by 华中敏 on 2025/6/13.
|
|
||||||
//
|
|
||||||
|
|
||||||
import SwiftUI
|
|
||||||
|
|
||||||
enum ChakraType: String, CaseIterable {
|
|
||||||
case root = "海底轮"
|
|
||||||
case sacral = "脐轮"
|
|
||||||
case solarPlexus = "太阳轮"
|
|
||||||
case heart = "心轮"
|
|
||||||
case throat = "喉轮"
|
|
||||||
case thirdEye = "眉心轮"
|
|
||||||
case crown = "顶轮"
|
|
||||||
|
|
||||||
var color: Color {
|
|
||||||
switch self {
|
|
||||||
case .root:
|
|
||||||
return .red
|
|
||||||
case .sacral:
|
|
||||||
return .orange
|
|
||||||
case .solarPlexus:
|
|
||||||
return .yellow
|
|
||||||
case .heart:
|
|
||||||
return .green
|
|
||||||
case .throat:
|
|
||||||
return .blue
|
|
||||||
case .thirdEye:
|
|
||||||
return .indigo
|
|
||||||
case .crown:
|
|
||||||
return .purple
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var position: CGPoint {
|
|
||||||
switch self {
|
|
||||||
case .root:
|
|
||||||
return CGPoint(x: 0.5, y: 0.9)
|
|
||||||
case .sacral:
|
|
||||||
return CGPoint(x: 0.5, y: 0.8)
|
|
||||||
case .solarPlexus:
|
|
||||||
return CGPoint(x: 0.5, y: 0.65)
|
|
||||||
case .heart:
|
|
||||||
return CGPoint(x: 0.5, y: 0.5)
|
|
||||||
case .throat:
|
|
||||||
return CGPoint(x: 0.5, y: 0.35)
|
|
||||||
case .thirdEye:
|
|
||||||
return CGPoint(x: 0.5, y: 0.2)
|
|
||||||
case .crown:
|
|
||||||
return CGPoint(x: 0.5, y: 0.05)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var description: String {
|
|
||||||
switch self {
|
|
||||||
case .root:
|
|
||||||
return "安全感、稳定性、生存本能"
|
|
||||||
case .sacral:
|
|
||||||
return "创造力、性能量、情感流动"
|
|
||||||
case .solarPlexus:
|
|
||||||
return "个人力量、自信、意志力"
|
|
||||||
case .heart:
|
|
||||||
return "爱、同情心、人际关系"
|
|
||||||
case .throat:
|
|
||||||
return "沟通、表达、真实性"
|
|
||||||
case .thirdEye:
|
|
||||||
return "直觉、洞察力、智慧"
|
|
||||||
case .crown:
|
|
||||||
return "灵性连接、觉知、超越"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var audioFileName: String {
|
|
||||||
switch self {
|
|
||||||
case .root:
|
|
||||||
return "root_chakra_healing"
|
|
||||||
case .sacral:
|
|
||||||
return "sacral_chakra_healing"
|
|
||||||
case .solarPlexus:
|
|
||||||
return "solar_plexus_chakra_healing"
|
|
||||||
case .heart:
|
|
||||||
return "heart_chakra_healing"
|
|
||||||
case .throat:
|
|
||||||
return "throat_chakra_healing"
|
|
||||||
case .thirdEye:
|
|
||||||
return "third_eye_chakra_healing"
|
|
||||||
case .crown:
|
|
||||||
return "crown_chakra_healing"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var frequency: String {
|
|
||||||
switch self {
|
|
||||||
case .root:
|
|
||||||
return "396 Hz"
|
|
||||||
case .sacral:
|
|
||||||
return "417 Hz"
|
|
||||||
case .solarPlexus:
|
|
||||||
return "528 Hz"
|
|
||||||
case .heart:
|
|
||||||
return "639 Hz"
|
|
||||||
case .throat:
|
|
||||||
return "741 Hz"
|
|
||||||
case .thirdEye:
|
|
||||||
return "852 Hz"
|
|
||||||
case .crown:
|
|
||||||
return "963 Hz"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var mantra: String {
|
|
||||||
switch self {
|
|
||||||
case .root:
|
|
||||||
return "LAM"
|
|
||||||
case .sacral:
|
|
||||||
return "VAM"
|
|
||||||
case .solarPlexus:
|
|
||||||
return "RAM"
|
|
||||||
case .heart:
|
|
||||||
return "YAM"
|
|
||||||
case .throat:
|
|
||||||
return "HAM"
|
|
||||||
case .thirdEye:
|
|
||||||
return "OM"
|
|
||||||
case .crown:
|
|
||||||
return "AH"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var element: String {
|
|
||||||
switch self {
|
|
||||||
case .root:
|
|
||||||
return "土"
|
|
||||||
case .sacral:
|
|
||||||
return "水"
|
|
||||||
case .solarPlexus:
|
|
||||||
return "火"
|
|
||||||
case .heart:
|
|
||||||
return "风"
|
|
||||||
case .throat:
|
|
||||||
return "空"
|
|
||||||
case .thirdEye:
|
|
||||||
return "光"
|
|
||||||
case .crown:
|
|
||||||
return "思想"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var keywords: [String] {
|
|
||||||
switch self {
|
|
||||||
case .root:
|
|
||||||
return ["安全感", "稳定", "生存", "根基", "物质"]
|
|
||||||
case .sacral:
|
|
||||||
return ["创造力", "性能量", "情感", "流动", "享受"]
|
|
||||||
case .solarPlexus:
|
|
||||||
return ["自信", "力量", "意志", "控制", "个性"]
|
|
||||||
case .heart:
|
|
||||||
return ["爱", "同情", "宽恕", "连接", "和谐"]
|
|
||||||
case .throat:
|
|
||||||
return ["表达", "沟通", "真实", "创意", "声音"]
|
|
||||||
case .thirdEye:
|
|
||||||
return ["直觉", "洞察", "智慧", "想象", "觉知"]
|
|
||||||
case .crown:
|
|
||||||
return ["灵性", "觉醒", "超越", "统一", "神圣"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var healingBenefits: [String] {
|
|
||||||
switch self {
|
|
||||||
case .root:
|
|
||||||
return ["增强安全感", "改善焦虑", "提升专注力", "增强体力"]
|
|
||||||
case .sacral:
|
|
||||||
return ["激发创造力", "改善人际关系", "增强活力", "平衡情绪"]
|
|
||||||
case .solarPlexus:
|
|
||||||
return ["提升自信", "增强意志力", "改善消化", "释放压力"]
|
|
||||||
case .heart:
|
|
||||||
return ["开放心扉", "增强同理心", "改善关系", "释放怨恨"]
|
|
||||||
case .throat:
|
|
||||||
return ["提升表达能力", "增强创造力", "改善沟通", "释放恐惧"]
|
|
||||||
case .thirdEye:
|
|
||||||
return ["增强直觉", "提升洞察力", "改善专注", "开发智慧"]
|
|
||||||
case .crown:
|
|
||||||
return ["提升觉知", "增强灵性连接", "获得内在平静", "超越自我"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,23 +0,0 @@
|
|||||||
import Foundation
|
|
||||||
// import AMapFoundationKit // 临时注释,需要安装CocoaPods依赖
|
|
||||||
import CoreLocation
|
|
||||||
|
|
||||||
/// 地图管理器
|
|
||||||
/// @Author huazhongmin
|
|
||||||
/// @Time 2024-03-24
|
|
||||||
/// @Description 管理高德地图SDK的配置和初始化
|
|
||||||
class MapManager {
|
|
||||||
static let shared = MapManager()
|
|
||||||
|
|
||||||
private init() {}
|
|
||||||
|
|
||||||
func configure() {
|
|
||||||
// TODO: 安装CocoaPods依赖后取消注释
|
|
||||||
// 设置高德地图的AppKey
|
|
||||||
// AMapServices.shared().apiKey = "bb63ae64d651624f3673d61b47b45435"
|
|
||||||
|
|
||||||
// 配置定位权限说明
|
|
||||||
let locationManager = CLLocationManager()
|
|
||||||
locationManager.requestWhenInUseAuthorization()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
//
|
|
||||||
// Persistence.swift
|
|
||||||
// EmotionMuseum
|
|
||||||
//
|
|
||||||
// Created by 华中敏 on 2025/6/13.
|
|
||||||
//
|
|
||||||
|
|
||||||
import CoreData
|
|
||||||
|
|
||||||
struct PersistenceController {
|
|
||||||
static let shared = PersistenceController()
|
|
||||||
|
|
||||||
@MainActor
|
|
||||||
static let preview: PersistenceController = {
|
|
||||||
let result = PersistenceController(inMemory: true)
|
|
||||||
let viewContext = result.container.viewContext
|
|
||||||
for _ in 0..<10 {
|
|
||||||
let newItem = Item(context: viewContext)
|
|
||||||
newItem.timestamp = Date()
|
|
||||||
}
|
|
||||||
do {
|
|
||||||
try viewContext.save()
|
|
||||||
} catch {
|
|
||||||
// Replace this implementation with code to handle the error appropriately.
|
|
||||||
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
|
|
||||||
let nsError = error as NSError
|
|
||||||
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}()
|
|
||||||
|
|
||||||
let container: NSPersistentContainer
|
|
||||||
|
|
||||||
init(inMemory: Bool = false) {
|
|
||||||
container = NSPersistentContainer(name: "EmotionMuseum")
|
|
||||||
if inMemory {
|
|
||||||
container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
|
|
||||||
}
|
|
||||||
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
|
|
||||||
if let error = error as NSError? {
|
|
||||||
// Replace this implementation with code to handle the error appropriately.
|
|
||||||
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
|
|
||||||
|
|
||||||
/*
|
|
||||||
Typical reasons for an error here include:
|
|
||||||
* The parent directory does not exist, cannot be created, or disallows writing.
|
|
||||||
* The persistent store is not accessible, due to permissions or data protection when the device is locked.
|
|
||||||
* The device is out of space.
|
|
||||||
* The store could not be migrated to the current model version.
|
|
||||||
Check the error message to determine what the actual problem was.
|
|
||||||
*/
|
|
||||||
fatalError("Unresolved error \(error), \(error.userInfo)")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
container.viewContext.automaticallyMergesChangesFromParent = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
platform :ios, '14.0'
|
|
||||||
|
|
||||||
target 'EmotionMuseum' do
|
|
||||||
use_frameworks!
|
|
||||||
|
|
||||||
# 高德地图SDK
|
|
||||||
pod 'AMap3DMap'
|
|
||||||
pod 'AMapLocation'
|
|
||||||
pod 'AMapSearch'
|
|
||||||
|
|
||||||
end
|
|
||||||
@@ -1,549 +0,0 @@
|
|||||||
//
|
|
||||||
// AIService.swift
|
|
||||||
// EmotionMuseum
|
|
||||||
//
|
|
||||||
// Created by 华中敏 on 2025/6/13.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
import Combine
|
|
||||||
|
|
||||||
// MARK: - AI服务协议
|
|
||||||
protocol AIServiceProtocol {
|
|
||||||
func sendMessage(_ message: String, userId: UUID) async throws -> AIResponse
|
|
||||||
func analyzeEmotion(_ text: String) async throws -> EmotionAnalysis
|
|
||||||
func generateGrowthSuggestions(for stats: GrowthStats) async throws -> [GrowthTopic]
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - AI响应模型
|
|
||||||
struct AIResponse: Codable {
|
|
||||||
let messageId: UUID
|
|
||||||
let content: String
|
|
||||||
let emotionAnalysis: EmotionAnalysis
|
|
||||||
let suggestions: [String]
|
|
||||||
let followUpQuestions: [String]
|
|
||||||
let confidence: Float
|
|
||||||
let processingTime: TimeInterval
|
|
||||||
|
|
||||||
struct EmotionAnalysis: Codable {
|
|
||||||
let detectedEmotion: EmotionType
|
|
||||||
let intensity: Float
|
|
||||||
let triggers: [String]
|
|
||||||
let context: String
|
|
||||||
let recommendations: [String]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - AI服务实现
|
|
||||||
class AIService: AIServiceProtocol, ObservableObject {
|
|
||||||
static let shared = AIService()
|
|
||||||
|
|
||||||
private let baseURL = "https://api.openai.com/v1"
|
|
||||||
private let apiKey: String
|
|
||||||
|
|
||||||
@Published var isLoading = false
|
|
||||||
@Published var lastError: Error?
|
|
||||||
|
|
||||||
private init() {
|
|
||||||
// 在实际应用中,应该从安全配置文件或环境变量中读取API密钥
|
|
||||||
self.apiKey = Bundle.main.infoDictionary?["OPENAI_API_KEY"] as? String ?? ""
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - 发送消息
|
|
||||||
func sendMessage(_ message: String, userId: UUID) async throws -> AIResponse {
|
|
||||||
let startTime = Date()
|
|
||||||
isLoading = true
|
|
||||||
defer { isLoading = false }
|
|
||||||
|
|
||||||
let prompt = buildEmotionAnalysisPrompt(message: message)
|
|
||||||
let requestBody = OpenAIRequest(
|
|
||||||
model: "gpt-4",
|
|
||||||
messages: [
|
|
||||||
OpenAIMessage(role: "system", content: getSystemPrompt()),
|
|
||||||
OpenAIMessage(role: "user", content: prompt)
|
|
||||||
],
|
|
||||||
temperature: 0.7,
|
|
||||||
maxTokens: 500
|
|
||||||
)
|
|
||||||
|
|
||||||
do {
|
|
||||||
let response = try await sendOpenAIRequest(requestBody)
|
|
||||||
let processingTime = Date().timeIntervalSince(startTime)
|
|
||||||
|
|
||||||
return try parseAIResponse(response, messageId: UUID(), processingTime: processingTime)
|
|
||||||
} catch {
|
|
||||||
lastError = error
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - 情绪分析
|
|
||||||
func analyzeEmotion(_ text: String) async throws -> EmotionAnalysis {
|
|
||||||
let prompt = """
|
|
||||||
分析以下文本的情绪:"\(text)"
|
|
||||||
|
|
||||||
请返回JSON格式的分析结果,包含:
|
|
||||||
- summary: 简要总结
|
|
||||||
- keywords: 关键词数组
|
|
||||||
- suggestions: 建议数组
|
|
||||||
- moodPattern: 情绪模式
|
|
||||||
- confidence: 置信度(0-1)
|
|
||||||
"""
|
|
||||||
|
|
||||||
let requestBody = OpenAIRequest(
|
|
||||||
model: "gpt-4",
|
|
||||||
messages: [
|
|
||||||
OpenAIMessage(role: "system", content: "你是一个专业的情绪分析师,请用中文回答。"),
|
|
||||||
OpenAIMessage(role: "user", content: prompt)
|
|
||||||
],
|
|
||||||
temperature: 0.3,
|
|
||||||
maxTokens: 300
|
|
||||||
)
|
|
||||||
|
|
||||||
let response = try await sendOpenAIRequest(requestBody)
|
|
||||||
return try parseEmotionAnalysis(response)
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - 生成成长建议
|
|
||||||
func generateGrowthSuggestions(for stats: GrowthStats) async throws -> [GrowthTopic] {
|
|
||||||
let prompt = """
|
|
||||||
基于用户的五维人格画像生成个性化成长建议:
|
|
||||||
- 自我感知: \(stats.selfAwareness)
|
|
||||||
- 情绪韧性: \(stats.emotionalResilience)
|
|
||||||
- 行动力: \(stats.actionPower)
|
|
||||||
- 共情力: \(stats.empathy)
|
|
||||||
- 生活热度: \(stats.lifeEnthusiasm)
|
|
||||||
|
|
||||||
请推荐3个最适合的成长课题,返回JSON格式。
|
|
||||||
"""
|
|
||||||
|
|
||||||
let requestBody = OpenAIRequest(
|
|
||||||
model: "gpt-4",
|
|
||||||
messages: [
|
|
||||||
OpenAIMessage(role: "system", content: "你是一个专业的心理成长顾问。"),
|
|
||||||
OpenAIMessage(role: "user", content: prompt)
|
|
||||||
],
|
|
||||||
temperature: 0.5,
|
|
||||||
maxTokens: 400
|
|
||||||
)
|
|
||||||
|
|
||||||
let response = try await sendOpenAIRequest(requestBody)
|
|
||||||
return try parseGrowthTopics(response)
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - 私有方法
|
|
||||||
|
|
||||||
private func getSystemPrompt() -> String {
|
|
||||||
return """
|
|
||||||
你是情绪博物馆的AI情绪陪伴师,具有以下特质:
|
|
||||||
|
|
||||||
1. 专业且温暖:具备心理学知识,但表达温和亲切
|
|
||||||
2. 情绪敏感:能准确识别和回应用户的情绪状态
|
|
||||||
3. 个性化关怀:根据用户的话语提供针对性建议
|
|
||||||
4. 积极导向:引导用户朝着更健康的情绪状态发展
|
|
||||||
5. 边界清晰:不提供医疗建议,必要时建议寻求专业帮助
|
|
||||||
|
|
||||||
请用中文回答,保持对话自然流畅。
|
|
||||||
"""
|
|
||||||
}
|
|
||||||
|
|
||||||
private func buildEmotionAnalysisPrompt(message: String) -> String {
|
|
||||||
return """
|
|
||||||
用户消息:"\(message)"
|
|
||||||
|
|
||||||
请分析这条消息的情绪状态,并提供适当的回应。包含:
|
|
||||||
1. 检测到的主要情绪
|
|
||||||
2. 情绪强度(0-1)
|
|
||||||
3. 可能的触发因素
|
|
||||||
4. 温暖的回应内容
|
|
||||||
5. 具体的情绪调节建议
|
|
||||||
6. 后续探索问题
|
|
||||||
|
|
||||||
请以JSON格式返回分析结果。
|
|
||||||
"""
|
|
||||||
}
|
|
||||||
|
|
||||||
private func sendOpenAIRequest(_ request: OpenAIRequest) async throws -> OpenAIResponse {
|
|
||||||
guard !apiKey.isEmpty else {
|
|
||||||
throw AIServiceError.missingAPIKey
|
|
||||||
}
|
|
||||||
|
|
||||||
let url = URL(string: "\(baseURL)/chat/completions")!
|
|
||||||
var urlRequest = URLRequest(url: url)
|
|
||||||
urlRequest.httpMethod = "POST"
|
|
||||||
urlRequest.setValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization")
|
|
||||||
urlRequest.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
|
||||||
|
|
||||||
let encoder = JSONEncoder()
|
|
||||||
urlRequest.httpBody = try encoder.encode(request)
|
|
||||||
|
|
||||||
let (data, response) = try await URLSession.shared.data(for: urlRequest)
|
|
||||||
|
|
||||||
guard let httpResponse = response as? HTTPURLResponse,
|
|
||||||
httpResponse.statusCode == 200 else {
|
|
||||||
throw AIServiceError.apiError("请求失败")
|
|
||||||
}
|
|
||||||
|
|
||||||
let decoder = JSONDecoder()
|
|
||||||
return try decoder.decode(OpenAIResponse.self, from: data)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func parseAIResponse(_ response: OpenAIResponse, messageId: UUID, processingTime: TimeInterval) throws -> AIResponse {
|
|
||||||
guard let choice = response.choices.first else {
|
|
||||||
throw AIServiceError.parseError("无法解析AI响应")
|
|
||||||
}
|
|
||||||
|
|
||||||
let content = choice.message.content
|
|
||||||
|
|
||||||
// 尝试解析JSON格式的响应
|
|
||||||
if let jsonData = content.data(using: .utf8),
|
|
||||||
let parsed = try? JSONSerialization.jsonObject(with: jsonData) as? [String: Any] {
|
|
||||||
|
|
||||||
let emotion = parseEmotionFromString(parsed["emotion"] as? String ?? "neutral")
|
|
||||||
let intensity = parsed["intensity"] as? Float ?? 0.5
|
|
||||||
let triggers = parsed["triggers"] as? [String] ?? []
|
|
||||||
let context = parsed["context"] as? String ?? ""
|
|
||||||
let recommendations = parsed["recommendations"] as? [String] ?? []
|
|
||||||
let suggestions = parsed["suggestions"] as? [String] ?? []
|
|
||||||
let followUpQuestions = parsed["followUpQuestions"] as? [String] ?? []
|
|
||||||
let responseContent = parsed["response"] as? String ?? content
|
|
||||||
|
|
||||||
let emotionAnalysis = AIResponse.EmotionAnalysis(
|
|
||||||
detectedEmotion: emotion,
|
|
||||||
intensity: intensity,
|
|
||||||
triggers: triggers,
|
|
||||||
context: context,
|
|
||||||
recommendations: recommendations
|
|
||||||
)
|
|
||||||
|
|
||||||
return AIResponse(
|
|
||||||
messageId: messageId,
|
|
||||||
content: responseContent,
|
|
||||||
emotionAnalysis: emotionAnalysis,
|
|
||||||
suggestions: suggestions,
|
|
||||||
followUpQuestions: followUpQuestions,
|
|
||||||
confidence: 0.8,
|
|
||||||
processingTime: processingTime
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
// 如果不是JSON格式,返回基础响应
|
|
||||||
return AIResponse(
|
|
||||||
messageId: messageId,
|
|
||||||
content: content,
|
|
||||||
emotionAnalysis: AIResponse.EmotionAnalysis(
|
|
||||||
detectedEmotion: .neutral,
|
|
||||||
intensity: 0.5,
|
|
||||||
triggers: [],
|
|
||||||
context: "基础对话",
|
|
||||||
recommendations: ["继续分享您的感受"]
|
|
||||||
),
|
|
||||||
suggestions: ["继续对话"],
|
|
||||||
followUpQuestions: ["还有什么想分享的吗?"],
|
|
||||||
confidence: 0.6,
|
|
||||||
processingTime: processingTime
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func parseEmotionAnalysis(_ response: OpenAIResponse) throws -> EmotionAnalysis {
|
|
||||||
guard response.choices.first != nil else {
|
|
||||||
throw AIServiceError.parseError("无法解析情绪分析响应")
|
|
||||||
}
|
|
||||||
|
|
||||||
// 模拟解析,实际应用中需要更复杂的JSON解析
|
|
||||||
return EmotionAnalysis(
|
|
||||||
primaryEmotion: .neutral,
|
|
||||||
emotionIntensity: 0.5,
|
|
||||||
emotionTrend: .stable,
|
|
||||||
keywords: ["关键词1", "关键词2"],
|
|
||||||
aiInsights: "情绪分析摘要",
|
|
||||||
confidence: 0.75
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func parseGrowthTopics(_ response: OpenAIResponse) throws -> [GrowthTopic] {
|
|
||||||
// 模拟返回成长课题,实际应用中需要解析AI响应
|
|
||||||
return [
|
|
||||||
GrowthTopic(
|
|
||||||
title: "增强自我认知",
|
|
||||||
description: "通过反思练习提高自我觉察能力",
|
|
||||||
category: .selfAwareness,
|
|
||||||
difficulty: .beginner,
|
|
||||||
progress: 0.0
|
|
||||||
),
|
|
||||||
GrowthTopic(
|
|
||||||
title: "情绪调节技巧",
|
|
||||||
description: "学习有效的情绪管理方法",
|
|
||||||
category: .emotionRegulation,
|
|
||||||
difficulty: .intermediate,
|
|
||||||
progress: 0.0
|
|
||||||
),
|
|
||||||
GrowthTopic(
|
|
||||||
title: "提升沟通能力",
|
|
||||||
description: "改善人际关系和沟通技巧",
|
|
||||||
category: .relationships,
|
|
||||||
difficulty: .beginner,
|
|
||||||
progress: 0.0
|
|
||||||
)
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
private func parseEmotionFromString(_ emotionString: String) -> EmotionType {
|
|
||||||
switch emotionString.lowercased() {
|
|
||||||
case "joy", "happy", "开心", "喜悦": return .joy
|
|
||||||
case "sad", "sadness", "悲伤", "难过": return .sadness
|
|
||||||
case "angry", "anger", "愤怒", "生气": return .anger
|
|
||||||
case "fear", "scared", "恐惧", "害怕": return .fear
|
|
||||||
case "surprise", "surprised", "惊讶", "意外": return .surprise
|
|
||||||
default: return .neutral
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - OpenAI API模型
|
|
||||||
private struct OpenAIRequest: Codable {
|
|
||||||
let model: String
|
|
||||||
let messages: [OpenAIMessage]
|
|
||||||
let temperature: Float
|
|
||||||
let maxTokens: Int?
|
|
||||||
|
|
||||||
enum CodingKeys: String, CodingKey {
|
|
||||||
case model, messages, temperature
|
|
||||||
case maxTokens = "max_tokens"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private struct OpenAIMessage: Codable {
|
|
||||||
let role: String
|
|
||||||
let content: String
|
|
||||||
}
|
|
||||||
|
|
||||||
private struct OpenAIResponse: Codable {
|
|
||||||
let choices: [OpenAIChoice]
|
|
||||||
}
|
|
||||||
|
|
||||||
private struct OpenAIChoice: Codable {
|
|
||||||
let message: OpenAIMessage
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - 错误类型
|
|
||||||
enum AIServiceError: LocalizedError {
|
|
||||||
case missingAPIKey
|
|
||||||
case apiError(String)
|
|
||||||
case parseError(String)
|
|
||||||
case networkError(Error)
|
|
||||||
|
|
||||||
var errorDescription: String? {
|
|
||||||
switch self {
|
|
||||||
case .missingAPIKey:
|
|
||||||
return "缺少API密钥"
|
|
||||||
case .apiError(let message):
|
|
||||||
return "API错误: \(message)"
|
|
||||||
case .parseError(let message):
|
|
||||||
return "解析错误: \(message)"
|
|
||||||
case .networkError(let error):
|
|
||||||
return "网络错误: \(error.localizedDescription)"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - 模拟AI服务(用于开发和测试)
|
|
||||||
class MockAIService: AIServiceProtocol, ObservableObject {
|
|
||||||
@Published var isLoading = false
|
|
||||||
|
|
||||||
func sendMessage(_ message: String, userId: UUID) async throws -> AIResponse {
|
|
||||||
isLoading = true
|
|
||||||
|
|
||||||
// 模拟网络延迟
|
|
||||||
try await Task.sleep(nanoseconds: 1_000_000_000) // 1秒
|
|
||||||
|
|
||||||
isLoading = false
|
|
||||||
|
|
||||||
let emotion = analyzeEmotionFromMessage(message)
|
|
||||||
let response = generateMockResponse(for: emotion, message: message)
|
|
||||||
|
|
||||||
return AIResponse(
|
|
||||||
messageId: UUID(),
|
|
||||||
content: response,
|
|
||||||
emotionAnalysis: AIResponse.EmotionAnalysis(
|
|
||||||
detectedEmotion: emotion,
|
|
||||||
intensity: Float.random(in: 0.3...0.9),
|
|
||||||
triggers: extractTriggers(from: message),
|
|
||||||
context: "日常对话",
|
|
||||||
recommendations: generateRecommendations(for: emotion)
|
|
||||||
),
|
|
||||||
suggestions: generateSuggestions(for: emotion),
|
|
||||||
followUpQuestions: generateFollowUpQuestions(for: emotion),
|
|
||||||
confidence: Float.random(in: 0.6...0.9),
|
|
||||||
processingTime: 1.0
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func analyzeEmotion(_ text: String) async throws -> EmotionAnalysis {
|
|
||||||
try await Task.sleep(nanoseconds: 500_000_000) // 0.5秒
|
|
||||||
|
|
||||||
return EmotionAnalysis(
|
|
||||||
primaryEmotion: analyzeEmotionFromMessage(text),
|
|
||||||
emotionIntensity: Float.random(in: 0.3...0.9),
|
|
||||||
emotionTrend: .stable,
|
|
||||||
keywords: ["情绪", "感受", "心情"],
|
|
||||||
aiInsights: "检测到\(analyzeEmotionFromMessage(text).rawValue)情绪",
|
|
||||||
confidence: Float.random(in: 0.7...0.95)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func generateGrowthSuggestions(for stats: GrowthStats) async throws -> [GrowthTopic] {
|
|
||||||
try await Task.sleep(nanoseconds: 800_000_000) // 0.8秒
|
|
||||||
|
|
||||||
return [
|
|
||||||
GrowthTopic(
|
|
||||||
title: "自我认知提升",
|
|
||||||
description: "通过日记和反思提高自我觉察",
|
|
||||||
category: .selfAwareness,
|
|
||||||
difficulty: .beginner,
|
|
||||||
progress: 0.0
|
|
||||||
),
|
|
||||||
GrowthTopic(
|
|
||||||
title: "情绪管理训练",
|
|
||||||
description: "学习情绪调节和压力缓解技巧",
|
|
||||||
category: .emotionRegulation,
|
|
||||||
difficulty: .intermediate,
|
|
||||||
progress: 0.0
|
|
||||||
)
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - 私有辅助方法
|
|
||||||
|
|
||||||
private func analyzeEmotionFromMessage(_ message: String) -> EmotionType {
|
|
||||||
let lowerMessage = message.lowercased()
|
|
||||||
|
|
||||||
if lowerMessage.contains("开心") || lowerMessage.contains("高兴") || lowerMessage.contains("快乐") {
|
|
||||||
return .joy
|
|
||||||
} else if lowerMessage.contains("难过") || lowerMessage.contains("悲伤") || lowerMessage.contains("伤心") {
|
|
||||||
return .sadness
|
|
||||||
} else if lowerMessage.contains("生气") || lowerMessage.contains("愤怒") || lowerMessage.contains("烦躁") {
|
|
||||||
return .anger
|
|
||||||
} else if lowerMessage.contains("害怕") || lowerMessage.contains("恐惧") || lowerMessage.contains("紧张") {
|
|
||||||
return .fear
|
|
||||||
} else if lowerMessage.contains("惊讶") || lowerMessage.contains("意外") {
|
|
||||||
return .surprise
|
|
||||||
} else {
|
|
||||||
return .neutral
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func generateMockResponse(for emotion: EmotionType, message: String) -> String {
|
|
||||||
switch emotion {
|
|
||||||
case .joy:
|
|
||||||
return "我能感受到你的开心!这种积极的情绪很珍贵,记得把这份快乐分享给身边的人。是什么让你感到如此愉悦呢?"
|
|
||||||
case .sadness:
|
|
||||||
return "我理解你现在的感受,难过是正常的情绪反应。允许自己感受这些情绪,同时也要温柔地照顾自己。你愿意分享更多吗?"
|
|
||||||
case .anger:
|
|
||||||
return "我能察觉到你的愤怒情绪。愤怒往往是其他情绪的表达,比如受伤或失望。深呼吸一下,我们一起探索这种感受的根源。"
|
|
||||||
case .fear:
|
|
||||||
return "恐惧感让人不安,这是很自然的反应。记住,你有能力面对困难。让我们一起分析一下你担心的事情,也许并不像想象中那么可怕。"
|
|
||||||
case .surprise:
|
|
||||||
return "意外的事情总是让人印象深刻!无论是好的惊喜还是让人措手不及的情况,都是生活的一部分。告诉我更多细节吧。"
|
|
||||||
case .neutral:
|
|
||||||
return "我在聆听你的分享。有时候平静也是一种很好的状态。如果你想深入探讨任何感受或想法,我都愿意陪伴你。"
|
|
||||||
case .anxiety:
|
|
||||||
return "我能感受到你的焦虑情绪。焦虑是很常见的感受,让我们一起找到缓解的方法。"
|
|
||||||
case .excitement:
|
|
||||||
return "你的兴奋情绪很有感染力!这种充满活力的状态真的很棒。告诉我是什么让你如此激动,我也想分享你的喜悦!"
|
|
||||||
case .contentment:
|
|
||||||
return "你的满足感让我很欣慰。这种内心的平和与充实是生活中最珍贵的状态之一。珍惜这份宁静,它会给你力量。"
|
|
||||||
case .confusion:
|
|
||||||
return "我感受到你内心的困惑。困惑是思考和成长的开始,让我们一起理清思路。"
|
|
||||||
case .melancholy:
|
|
||||||
return "我感受到你内心的忧郁。这种淡淡的愁绪有时候也是一种美,它让我们更深刻地感受生活。愿意和我分享你的思绪吗?"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func extractTriggers(from message: String) -> [String] {
|
|
||||||
// 简单的关键词提取
|
|
||||||
let keywords = ["工作", "家庭", "朋友", "健康", "学习", "感情", "压力", "变化"]
|
|
||||||
return keywords.filter { message.contains($0) }
|
|
||||||
}
|
|
||||||
|
|
||||||
private func generateRecommendations(for emotion: EmotionType) -> [String] {
|
|
||||||
switch emotion {
|
|
||||||
case .joy:
|
|
||||||
return ["记录这个美好时刻", "分享你的快乐", "感恩当下"]
|
|
||||||
case .sadness:
|
|
||||||
return ["允许自己哭泣", "寻求朋友支持", "进行轻柔运动"]
|
|
||||||
case .anger:
|
|
||||||
return ["深呼吸放松", "写下愤怒的原因", "进行体力活动"]
|
|
||||||
case .fear:
|
|
||||||
return ["面对恐惧", "寻求专业建议", "制定应对计划"]
|
|
||||||
case .surprise:
|
|
||||||
return ["接受变化", "保持开放心态", "记录感受"]
|
|
||||||
case .neutral:
|
|
||||||
return ["享受平静", "进行自我反思", "设定新目标"]
|
|
||||||
case .anxiety:
|
|
||||||
return ["深呼吸练习", "寻求支持", "制定应对计划"]
|
|
||||||
case .excitement:
|
|
||||||
return ["合理安排时间", "保持专注", "与他人分享喜悦"]
|
|
||||||
case .contentment:
|
|
||||||
return ["珍惜当下", "保持感恩心", "分享你的平和"]
|
|
||||||
case .confusion:
|
|
||||||
return ["整理思路", "寻求建议", "分步骤思考"]
|
|
||||||
case .melancholy:
|
|
||||||
return ["接受这种情绪", "寻找美好的事物", "与朋友交流"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func generateSuggestions(for emotion: EmotionType) -> [String] {
|
|
||||||
switch emotion {
|
|
||||||
case .joy:
|
|
||||||
return ["继续保持积极心态", "做些让你快乐的事"]
|
|
||||||
case .sadness:
|
|
||||||
return ["给自己一些时间", "考虑专业帮助"]
|
|
||||||
case .anger:
|
|
||||||
return ["找到健康的发泄方式", "思考问题的解决方案"]
|
|
||||||
case .fear:
|
|
||||||
return ["一步步面对恐惧", "建立支持系统"]
|
|
||||||
case .surprise:
|
|
||||||
return ["适应新情况", "保持灵活性"]
|
|
||||||
case .neutral:
|
|
||||||
return ["探索新的兴趣", "建立日常习惯"]
|
|
||||||
case .anxiety:
|
|
||||||
return ["学习放松技巧", "寻求专业帮助"]
|
|
||||||
case .excitement:
|
|
||||||
return ["制定行动计划", "保持理性思考"]
|
|
||||||
case .contentment:
|
|
||||||
return ["维持内心平衡", "继续当前的生活方式"]
|
|
||||||
case .confusion:
|
|
||||||
return ["寻找清晰的方向", "与他人交流想法"]
|
|
||||||
case .melancholy:
|
|
||||||
return ["接受当下的感受", "寻找内心的平静"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func generateFollowUpQuestions(for emotion: EmotionType) -> [String] {
|
|
||||||
switch emotion {
|
|
||||||
case .joy:
|
|
||||||
return ["是什么特别的事情让你如此开心?", "你想如何延续这种快乐?"]
|
|
||||||
case .sadness:
|
|
||||||
return ["这种感受持续多久了?", "有什么可以帮助你感觉好一些?"]
|
|
||||||
case .anger:
|
|
||||||
return ["什么事情触发了这种愤怒?", "你通常如何处理愤怒情绪?"]
|
|
||||||
case .fear:
|
|
||||||
return ["你最担心的是什么?", "有什么可以让你感到更安全?"]
|
|
||||||
case .surprise:
|
|
||||||
return ["这个意外对你意味着什么?", "你如何适应这个变化?"]
|
|
||||||
case .neutral:
|
|
||||||
return ["最近有什么新的想法吗?", "有什么目标想要实现?"]
|
|
||||||
case .anxiety:
|
|
||||||
return ["这种焦虑从什么时候开始的?", "有什么特别担心的事情吗?"]
|
|
||||||
case .excitement:
|
|
||||||
return ["是什么让你如此兴奋?", "你计划如何行动?"]
|
|
||||||
case .contentment:
|
|
||||||
return ["是什么让你感到如此满足?", "这种状态对你意味着什么?"]
|
|
||||||
case .confusion:
|
|
||||||
return ["什么让你感到困惑?", "需要帮助理清哪些思路?"]
|
|
||||||
case .melancholy:
|
|
||||||
return ["这种忧郁感从何而来?", "有什么特别的回忆或想法吗?"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,701 +0,0 @@
|
|||||||
//
|
|
||||||
// MockDataManager.swift
|
|
||||||
// EmotionMuseum
|
|
||||||
//
|
|
||||||
// Created by 华中敏 on 2025/7/5.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
import SwiftUI
|
|
||||||
|
|
||||||
class MockDataManager: ObservableObject {
|
|
||||||
static let shared = MockDataManager()
|
|
||||||
|
|
||||||
// MARK: - Published Properties
|
|
||||||
@Published var currentUser: User
|
|
||||||
@Published var conversations: [Conversation] = []
|
|
||||||
@Published var growthTopics: [GrowthTopic] = []
|
|
||||||
@Published var locationPins: [LocationPin] = []
|
|
||||||
@Published var communityPosts: [CommunityPost] = []
|
|
||||||
@Published var emotionRecords: [EmotionRecord] = []
|
|
||||||
@Published var achievements: [Achievement] = []
|
|
||||||
@Published var userStats: UserStats = UserStats()
|
|
||||||
@Published var weeklyStats: WeeklyStats
|
|
||||||
|
|
||||||
private init() {
|
|
||||||
// 初始化当前用户
|
|
||||||
self.currentUser = User(
|
|
||||||
username: "emotion_explorer",
|
|
||||||
email: "user@example.com",
|
|
||||||
profile: UserProfile(
|
|
||||||
nickname: "情绪探索者",
|
|
||||||
birthDate: Calendar.current.date(byAdding: .year, value: -25, to: Date()),
|
|
||||||
location: "北京市",
|
|
||||||
bio: "在情绪的海洋中寻找内心的平静",
|
|
||||||
memberLevel: .premium,
|
|
||||||
totalDays: 127,
|
|
||||||
growthStats: GrowthStats(
|
|
||||||
selfAwareness: 78.5,
|
|
||||||
emotionalResilience: 65.2,
|
|
||||||
actionPower: 72.8,
|
|
||||||
empathy: 85.3,
|
|
||||||
lifeEnthusiasm: 69.7
|
|
||||||
)
|
|
||||||
),
|
|
||||||
createdAt: Calendar.current.date(byAdding: .day, value: -127, to: Date()) ?? Date()
|
|
||||||
)
|
|
||||||
|
|
||||||
// 初始化本周统计
|
|
||||||
let weekStart = Calendar.current.dateInterval(of: .weekOfYear, for: Date())?.start ?? Date()
|
|
||||||
self.weeklyStats = WeeklyStats(weekStartDate: weekStart)
|
|
||||||
|
|
||||||
// 生成所有模拟数据
|
|
||||||
generateAllMockData()
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Public Methods
|
|
||||||
|
|
||||||
func generateAllMockData() {
|
|
||||||
generateMockEmotionRecords()
|
|
||||||
generateMockConversations()
|
|
||||||
generateMockGrowthTopics()
|
|
||||||
generateMockLocationPins()
|
|
||||||
generateMockCommunityPosts()
|
|
||||||
generateMockAchievements()
|
|
||||||
updateUserStats()
|
|
||||||
updateWeeklyStats()
|
|
||||||
}
|
|
||||||
|
|
||||||
func refreshData() async {
|
|
||||||
await MainActor.run {
|
|
||||||
generateAllMockData()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Conversation Methods
|
|
||||||
|
|
||||||
func addMessage(to conversationId: UUID, content: String, sender: MessageSender) {
|
|
||||||
if let index = conversations.firstIndex(where: { $0.id == conversationId }) {
|
|
||||||
let message = Message(
|
|
||||||
conversationId: conversationId,
|
|
||||||
content: content,
|
|
||||||
type: .text,
|
|
||||||
sender: sender,
|
|
||||||
emotionScore: sender == .user ? Float.random(in: 0.3...0.9) : nil
|
|
||||||
)
|
|
||||||
conversations[index].messages.append(message)
|
|
||||||
|
|
||||||
if sender == .user {
|
|
||||||
// 模拟AI回复
|
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
|
|
||||||
let aiResponse = self.generateAIResponse(for: content)
|
|
||||||
self.addMessage(to: conversationId, content: aiResponse, sender: .ai)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func createNewConversation() -> Conversation {
|
|
||||||
let conversation = Conversation(
|
|
||||||
userId: currentUser.id,
|
|
||||||
title: "新对话 \(Date().shortFormat)"
|
|
||||||
)
|
|
||||||
conversations.insert(conversation, at: 0)
|
|
||||||
return conversation
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Growth Topic Methods
|
|
||||||
|
|
||||||
func updateTopicProgress(_ topicId: UUID, progress: Float) {
|
|
||||||
if let index = growthTopics.firstIndex(where: { $0.id == topicId }) {
|
|
||||||
growthTopics[index].progress = min(1.0, progress)
|
|
||||||
if growthTopics[index].progress >= 1.0 {
|
|
||||||
growthTopics[index].completedAt = Date()
|
|
||||||
// 解锁奖励
|
|
||||||
let reward = Reward(
|
|
||||||
type: .points,
|
|
||||||
title: "课题完成",
|
|
||||||
description: "完成了\(growthTopics[index].title)",
|
|
||||||
value: 100,
|
|
||||||
rarity: .common
|
|
||||||
)
|
|
||||||
growthTopics[index].rewards.append(reward)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func addTopicInteraction(_ topicId: UUID, type: InteractionType, title: String, content: String) {
|
|
||||||
if let index = growthTopics.firstIndex(where: { $0.id == topicId }) {
|
|
||||||
let interaction = TopicInteraction(
|
|
||||||
topicId: topicId,
|
|
||||||
type: type,
|
|
||||||
title: title,
|
|
||||||
content: content,
|
|
||||||
completedAt: Date(),
|
|
||||||
duration: TimeInterval.random(in: 300...1800)
|
|
||||||
)
|
|
||||||
growthTopics[index].interactions.append(interaction)
|
|
||||||
|
|
||||||
// 更新进度
|
|
||||||
let progressIncrease: Float = 0.2
|
|
||||||
updateTopicProgress(topicId, progress: growthTopics[index].progress + progressIncrease)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Location Methods
|
|
||||||
|
|
||||||
func toggleLocationBookmark(_ locationId: UUID) {
|
|
||||||
if let index = locationPins.firstIndex(where: { $0.id == locationId }) {
|
|
||||||
locationPins[index].isBookmarked.toggle()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func addLocationVisit(_ locationId: UUID) {
|
|
||||||
if let index = locationPins.firstIndex(where: { $0.id == locationId }) {
|
|
||||||
locationPins[index].visits += 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Community Methods
|
|
||||||
|
|
||||||
func togglePostLike(_ postId: UUID) {
|
|
||||||
if let index = communityPosts.firstIndex(where: { $0.id == postId }) {
|
|
||||||
communityPosts[index].isLikedByCurrentUser.toggle()
|
|
||||||
if communityPosts[index].isLikedByCurrentUser {
|
|
||||||
communityPosts[index].likes += 1
|
|
||||||
} else {
|
|
||||||
communityPosts[index].likes = max(0, communityPosts[index].likes - 1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func addComment(to postId: UUID, content: String) {
|
|
||||||
if let index = communityPosts.firstIndex(where: { $0.id == postId }) {
|
|
||||||
let comment = Comment(
|
|
||||||
postId: postId,
|
|
||||||
userId: currentUser.id,
|
|
||||||
content: content
|
|
||||||
)
|
|
||||||
communityPosts[index].comments.append(comment)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Private Data Generation Methods
|
|
||||||
|
|
||||||
private func generateMockEmotionRecords() {
|
|
||||||
emotionRecords.removeAll()
|
|
||||||
|
|
||||||
// 生成过去30天的情绪记录
|
|
||||||
for i in 0..<30 {
|
|
||||||
let date = Calendar.current.date(byAdding: .day, value: -i, to: Date()) ?? Date()
|
|
||||||
let recordCount = Int.random(in: 1...3) // 每天1-3条记录
|
|
||||||
|
|
||||||
for _ in 0..<recordCount {
|
|
||||||
let record = EmotionRecord(
|
|
||||||
userId: currentUser.id,
|
|
||||||
date: date,
|
|
||||||
emotionType: EmotionType.allCases.randomElement() ?? .neutral,
|
|
||||||
intensity: Float.random(in: 0.2...0.9),
|
|
||||||
context: generateEmotionContext(),
|
|
||||||
triggers: generateEmotionTriggers(),
|
|
||||||
location: ["家里", "公司", "咖啡厅", "公园", "地铁上"].randomElement(),
|
|
||||||
weather: ["晴天", "阴天", "雨天", "雪天"].randomElement(),
|
|
||||||
notes: generateEmotionNotes()
|
|
||||||
)
|
|
||||||
emotionRecords.append(record)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
emotionRecords.sort { $0.date > $1.date }
|
|
||||||
}
|
|
||||||
|
|
||||||
private func generateMockConversations() {
|
|
||||||
conversations.removeAll()
|
|
||||||
|
|
||||||
// 生成过去30天的对话记录
|
|
||||||
for i in 0..<15 {
|
|
||||||
let startDate = Calendar.current.date(byAdding: .day, value: -i*2, to: Date()) ?? Date()
|
|
||||||
let conversation = createMockConversation(for: startDate, index: i)
|
|
||||||
conversations.append(conversation)
|
|
||||||
}
|
|
||||||
|
|
||||||
conversations.sort { $0.startTime > $1.startTime }
|
|
||||||
}
|
|
||||||
|
|
||||||
private func generateMockGrowthTopics() {
|
|
||||||
growthTopics.removeAll()
|
|
||||||
|
|
||||||
// 为每个分类生成3-4个课题
|
|
||||||
for category in TopicCategory.allCases {
|
|
||||||
for i in 1...4 {
|
|
||||||
let topic = createMockTopic(category: category, index: i)
|
|
||||||
growthTopics.append(topic)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func generateMockLocationPins() {
|
|
||||||
locationPins.removeAll()
|
|
||||||
|
|
||||||
// 北京地区的知名地点
|
|
||||||
let beijingLocations = [
|
|
||||||
(39.9042, 116.4074, "天安门广场", "北京的心脏,见证历史的地方"),
|
|
||||||
(39.9163, 116.3972, "故宫博物院", "明清两代的皇家宫殿,文化瑰宝"),
|
|
||||||
(40.0031, 116.3272, "颐和园", "清代皇家园林,山水如画"),
|
|
||||||
(39.8844, 116.5564, "798艺术区", "现代艺术的聚集地,创意无限"),
|
|
||||||
(39.9389, 116.3467, "什刹海", "老北京的韵味,夜晚格外美丽"),
|
|
||||||
(39.9056, 116.3913, "南锣鼓巷", "胡同文化的代表,小资情调"),
|
|
||||||
(40.0090, 116.2755, "香山公园", "秋天赏红叶的绝佳去处"),
|
|
||||||
(39.8838, 116.4649, "朝阳公园", "都市中的绿洲,休闲好去处"),
|
|
||||||
(39.9280, 116.3835, "后海", "酒吧一条街,夜生活的天堂"),
|
|
||||||
(39.9170, 116.3970, "景山公园", "俯瞰紫禁城的最佳位置")
|
|
||||||
]
|
|
||||||
|
|
||||||
for (_, location) in beijingLocations.enumerated() {
|
|
||||||
let pin = LocationPin(
|
|
||||||
coordinate: Coordinate(latitude: location.0, longitude: location.1),
|
|
||||||
title: location.2,
|
|
||||||
description: location.3,
|
|
||||||
type: LocationType.allCases.randomElement() ?? .community,
|
|
||||||
emotionTags: Array(EmotionType.allCases.shuffled().prefix(Int.random(in: 1...3))),
|
|
||||||
photos: generateLocationPhotos(),
|
|
||||||
createdBy: Bool.random() ? currentUser.id : UUID(),
|
|
||||||
createdAt: Calendar.current.date(byAdding: .day, value: -Int.random(in: 1...90), to: Date()) ?? Date(),
|
|
||||||
likes: Int.random(in: 5...200),
|
|
||||||
visits: Int.random(in: 10...500),
|
|
||||||
address: "北京市" + ["东城区", "西城区", "朝阳区", "海淀区"].randomElement()!,
|
|
||||||
category: LocationCategory.allCases.randomElement() ?? .other,
|
|
||||||
isBookmarked: Bool.random()
|
|
||||||
)
|
|
||||||
locationPins.append(pin)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func generateMockCommunityPosts() {
|
|
||||||
communityPosts.removeAll()
|
|
||||||
|
|
||||||
for _ in 0..<20 {
|
|
||||||
let post = CommunityPost(
|
|
||||||
userId: Bool.random() ? currentUser.id : UUID(),
|
|
||||||
locationId: locationPins.randomElement()?.id,
|
|
||||||
content: generatePostContent(),
|
|
||||||
photos: generatePostPhotos(),
|
|
||||||
tags: generatePostTags(),
|
|
||||||
likes: Int.random(in: 0...100),
|
|
||||||
comments: generatePostComments(),
|
|
||||||
createdAt: Calendar.current.date(byAdding: .hour, value: -Int.random(in: 1...168), to: Date()) ?? Date(),
|
|
||||||
isPrivate: Bool.random(),
|
|
||||||
viewCount: Int.random(in: 10...1000),
|
|
||||||
type: PostType.allCases.randomElement() ?? .general,
|
|
||||||
isLikedByCurrentUser: Bool.random()
|
|
||||||
)
|
|
||||||
communityPosts.append(post)
|
|
||||||
}
|
|
||||||
|
|
||||||
communityPosts.sort { $0.createdAt > $1.createdAt }
|
|
||||||
}
|
|
||||||
|
|
||||||
private func generateMockAchievements() {
|
|
||||||
achievements.removeAll()
|
|
||||||
|
|
||||||
let achievementData: [(String, String, AchievementCategory, String, RewardRarity, AchievementRequirement, Int, Bool)] = [
|
|
||||||
("初次对话", "完成第一次AI对话", .conversation, "message.circle", .common, .conversationCount(1), 1, false),
|
|
||||||
("话痨达人", "累计对话100次", .conversation, "message.badge", .rare, .conversationCount(100), 45, false),
|
|
||||||
("情绪记录者", "记录50次情绪", .emotion, "heart.circle", .common, .emotionRecordCount(50), 32, false),
|
|
||||||
("成长新手", "完成第一个成长课题", .growth, "arrow.up.circle", .common, .topicCompletion(1), 1, false),
|
|
||||||
("社交达人", "获得100个点赞", .social, "heart.badge", .rare, .socialInteraction(100), 67, false),
|
|
||||||
("探索者", "访问10个地点", .exploration, "map.circle", .common, .locationVisit(10), 8, false),
|
|
||||||
("坚持不懈", "连续使用30天", .consistency, "calendar.badge", .epic, .consecutiveDays(30), 25, false),
|
|
||||||
("积分大户", "累计获得1000积分", .milestone, "star.badge", .rare, .totalPoints(1000), 750, false),
|
|
||||||
("神秘成就", "发现隐藏彩蛋", .special, "sparkles", .legendary, .special("发现应用中的隐藏彩蛋"), 0, true)
|
|
||||||
]
|
|
||||||
|
|
||||||
for data in achievementData {
|
|
||||||
let achievement = Achievement(
|
|
||||||
title: data.0,
|
|
||||||
description: data.1,
|
|
||||||
category: data.2,
|
|
||||||
icon: data.3,
|
|
||||||
rarity: data.4,
|
|
||||||
requirement: data.5,
|
|
||||||
progress: data.6,
|
|
||||||
targetValue: getTargetValue(for: data.5),
|
|
||||||
unlockedAt: data.6 >= getTargetValue(for: data.5) ? Date() : nil,
|
|
||||||
isHidden: data.7
|
|
||||||
)
|
|
||||||
achievements.append(achievement)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Helper Methods
|
|
||||||
|
|
||||||
private func createMockConversation(for date: Date, index: Int) -> Conversation {
|
|
||||||
let conversation = Conversation(
|
|
||||||
userId: currentUser.id,
|
|
||||||
title: generateConversationTitle(index: index),
|
|
||||||
startTime: date,
|
|
||||||
endTime: Calendar.current.date(byAdding: .minute, value: Int.random(in: 5...60), to: date),
|
|
||||||
tags: generateConversationTags()
|
|
||||||
)
|
|
||||||
|
|
||||||
// 添加消息
|
|
||||||
var messages: [Message] = []
|
|
||||||
let messageCount = Int.random(in: 4...12)
|
|
||||||
|
|
||||||
for i in 0..<messageCount {
|
|
||||||
let isUserMessage = i % 2 == 0
|
|
||||||
let messageTime = Calendar.current.date(byAdding: .minute, value: i * 2, to: date) ?? date
|
|
||||||
|
|
||||||
let message = Message(
|
|
||||||
conversationId: conversation.id,
|
|
||||||
content: isUserMessage ? generateUserMessage() : generateAIMessage(),
|
|
||||||
type: .text,
|
|
||||||
sender: isUserMessage ? .user : .ai,
|
|
||||||
timestamp: messageTime,
|
|
||||||
emotionScore: isUserMessage ? Float.random(in: 0.2...0.9) : nil
|
|
||||||
)
|
|
||||||
messages.append(message)
|
|
||||||
}
|
|
||||||
|
|
||||||
var updatedConversation = conversation
|
|
||||||
updatedConversation.messages = messages
|
|
||||||
|
|
||||||
// 添加情绪分析
|
|
||||||
if !messages.isEmpty {
|
|
||||||
updatedConversation.emotionAnalysis = EmotionAnalysis(
|
|
||||||
primaryEmotion: EmotionType.allCases.randomElement() ?? .neutral,
|
|
||||||
emotionIntensity: Float.random(in: 0.3...0.8),
|
|
||||||
emotionTrend: EmotionTrend.allCases.randomElement() ?? .stable,
|
|
||||||
keywords: generateEmotionKeywords(),
|
|
||||||
aiInsights: generateAIInsights()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return updatedConversation
|
|
||||||
}
|
|
||||||
|
|
||||||
private func createMockTopic(category: TopicCategory, index: Int) -> GrowthTopic {
|
|
||||||
let topicTitles = getTopicTitles(for: category)
|
|
||||||
let title = topicTitles[min(index - 1, topicTitles.count - 1)]
|
|
||||||
|
|
||||||
return GrowthTopic(
|
|
||||||
title: title,
|
|
||||||
description: generateTopicDescription(for: title),
|
|
||||||
category: category,
|
|
||||||
difficulty: Difficulty.allCases.randomElement() ?? .beginner,
|
|
||||||
progress: Float.random(in: 0...1),
|
|
||||||
level: Int.random(in: 1...5),
|
|
||||||
totalLevels: 5,
|
|
||||||
isUnlocked: index <= 2 || Bool.random(), // 前两个总是解锁的
|
|
||||||
completedAt: Float.random(in: 0...1) > 0.7 ? Date() : nil,
|
|
||||||
rewards: generateTopicRewards(),
|
|
||||||
interactions: generateTopicInteractions(),
|
|
||||||
estimatedDuration: TimeInterval.random(in: 1800...7200), // 30分钟到2小时
|
|
||||||
prerequisites: index > 1 ? [UUID()] : []
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func generateAIResponse(for userMessage: String) -> String {
|
|
||||||
let responses = [
|
|
||||||
"我理解你的感受,这确实是一个值得思考的问题。",
|
|
||||||
"你提到的这个情况很常见,让我们一起来分析一下。",
|
|
||||||
"从你的描述中,我感受到了你的情绪变化。",
|
|
||||||
"这是一个很好的观察,你有什么想法吗?",
|
|
||||||
"我听到了你的担忧,我们可以一步步来解决。",
|
|
||||||
"你的感受是完全可以理解的,很多人都会有类似的经历。",
|
|
||||||
"让我们换个角度来看这个问题,可能会有新的发现。",
|
|
||||||
"你已经很勇敢地表达了自己的想法,这很棒。",
|
|
||||||
"我注意到你提到了一些关键词,我们可以深入探讨一下。",
|
|
||||||
"你的情绪管理能力在不断提升,继续保持。"
|
|
||||||
]
|
|
||||||
return responses.randomElement() ?? "谢谢你的分享。"
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Content Generation Methods
|
|
||||||
|
|
||||||
private func generateEmotionContext() -> String {
|
|
||||||
let contexts = [
|
|
||||||
"工作压力让我感到疲惫",
|
|
||||||
"和朋友聊天后心情变好了",
|
|
||||||
"看到美丽的日落感到平静",
|
|
||||||
"遇到挫折时感到沮丧",
|
|
||||||
"完成任务后有成就感",
|
|
||||||
"听音乐时情绪放松",
|
|
||||||
"运动后感到充满活力",
|
|
||||||
"独处时思考人生",
|
|
||||||
"与家人团聚很温暖",
|
|
||||||
"面对未知感到紧张"
|
|
||||||
]
|
|
||||||
return contexts.randomElement() ?? "日常生活中的情绪体验"
|
|
||||||
}
|
|
||||||
|
|
||||||
private func generateEmotionTriggers() -> [String] {
|
|
||||||
let allTriggers = ["工作", "人际关系", "健康", "家庭", "学习", "金钱", "未来", "过去", "天气", "音乐"]
|
|
||||||
return Array(allTriggers.shuffled().prefix(Int.random(in: 1...3)))
|
|
||||||
}
|
|
||||||
|
|
||||||
private func generateEmotionNotes() -> String? {
|
|
||||||
let notes = [
|
|
||||||
"今天的情绪比昨天好一些",
|
|
||||||
"需要更多的休息时间",
|
|
||||||
"和朋友的谈话很有帮助",
|
|
||||||
"运动确实能改善心情",
|
|
||||||
"要学会接受自己的情绪",
|
|
||||||
nil, nil // 有些记录没有备注
|
|
||||||
]
|
|
||||||
return notes.randomElement() ?? nil
|
|
||||||
}
|
|
||||||
|
|
||||||
private func generateConversationTitle(index: Int) -> String {
|
|
||||||
let titles = [
|
|
||||||
"今天的心情分享",
|
|
||||||
"关于压力管理的讨论",
|
|
||||||
"人际关系的困惑",
|
|
||||||
"职场焦虑的缓解",
|
|
||||||
"自我成长的反思",
|
|
||||||
"情绪调节的方法",
|
|
||||||
"生活目标的规划",
|
|
||||||
"内心平静的追求",
|
|
||||||
"人生意义的探索",
|
|
||||||
"幸福感的提升"
|
|
||||||
]
|
|
||||||
return titles[index % titles.count]
|
|
||||||
}
|
|
||||||
|
|
||||||
private func generateConversationTags() -> [String] {
|
|
||||||
let allTags = ["情绪管理", "压力缓解", "人际关系", "自我成长", "生活规划", "心理健康"]
|
|
||||||
return Array(allTags.shuffled().prefix(Int.random(in: 1...3)))
|
|
||||||
}
|
|
||||||
|
|
||||||
private func generateUserMessage() -> String {
|
|
||||||
let messages = [
|
|
||||||
"我最近感到有些焦虑,不知道该怎么办。",
|
|
||||||
"工作压力很大,总是担心做不好。",
|
|
||||||
"和朋友的关系出现了一些问题。",
|
|
||||||
"我想要改变现在的生活状态。",
|
|
||||||
"有时候感到很孤独,需要有人倾听。",
|
|
||||||
"对未来感到不确定,有些迷茫。",
|
|
||||||
"今天心情不错,想分享一下。",
|
|
||||||
"我在思考人生的意义是什么。",
|
|
||||||
"想要培养一些新的习惯。",
|
|
||||||
"感觉自己需要更多的自信。"
|
|
||||||
]
|
|
||||||
return messages.randomElement() ?? "你好"
|
|
||||||
}
|
|
||||||
|
|
||||||
private func generateAIMessage() -> String {
|
|
||||||
let messages = [
|
|
||||||
"我理解你的感受,焦虑是很正常的情绪反应。",
|
|
||||||
"工作压力确实会影响我们的心情,不妨试试放松技巧。",
|
|
||||||
"人际关系需要时间和耐心来维护,你做得很好。",
|
|
||||||
"改变需要勇气,你已经迈出了第一步。",
|
|
||||||
"孤独感是人类共同的体验,你并不孤单。",
|
|
||||||
"对未来的不确定感是成长的一部分。",
|
|
||||||
"很高兴听到你今天心情不错!",
|
|
||||||
"人生意义的探索是一个持续的过程。",
|
|
||||||
"培养新习惯需要时间,要对自己有耐心。",
|
|
||||||
"自信是可以通过练习来培养的。"
|
|
||||||
]
|
|
||||||
return messages.randomElement() ?? "谢谢你的分享"
|
|
||||||
}
|
|
||||||
|
|
||||||
private func generateEmotionKeywords() -> [String] {
|
|
||||||
let keywords = ["压力", "焦虑", "快乐", "悲伤", "希望", "困惑", "平静", "兴奋", "担忧", "满足"]
|
|
||||||
return Array(keywords.shuffled().prefix(Int.random(in: 2...4)))
|
|
||||||
}
|
|
||||||
|
|
||||||
private func generateAIInsights() -> String {
|
|
||||||
let insights = [
|
|
||||||
"你的情绪表达能力在不断提升,这是很好的进步。",
|
|
||||||
"从对话中可以看出你对自我成长很有意识。",
|
|
||||||
"你善于反思,这有助于情绪的自我调节。",
|
|
||||||
"你的积极态度值得赞赏,继续保持。",
|
|
||||||
"建议多关注自己的情绪变化模式。",
|
|
||||||
"你的表达很真诚,这有助于深入的自我探索。"
|
|
||||||
]
|
|
||||||
return insights.randomElement() ?? "继续保持这种开放的态度"
|
|
||||||
}
|
|
||||||
|
|
||||||
private func getTopicTitles(for category: TopicCategory) -> [String] {
|
|
||||||
switch category {
|
|
||||||
case .selfAwareness:
|
|
||||||
return ["认识真实的自己", "探索内在价值观", "发现个人优势", "理解情绪模式"]
|
|
||||||
case .emotionRegulation:
|
|
||||||
return ["压力管理技巧", "愤怒情绪调节", "焦虑缓解方法", "悲伤情绪处理"]
|
|
||||||
case .socialSkills:
|
|
||||||
return ["有效沟通技巧", "建立良好关系", "冲突解决能力", "团队合作精神"]
|
|
||||||
case .stressManagement:
|
|
||||||
return ["工作压力应对", "时间管理技能", "放松训练方法", "心理韧性建设"]
|
|
||||||
case .lifeGoals:
|
|
||||||
return ["目标设定方法", "人生规划技巧", "价值观澄清", "意义感培养"]
|
|
||||||
case .mindfulness:
|
|
||||||
return ["正念冥想入门", "专注力训练", "当下觉察练习", "内心平静修炼"]
|
|
||||||
case .relationships:
|
|
||||||
return ["亲密关系维护", "友谊经营之道", "家庭和谐相处", "社交边界设定"]
|
|
||||||
case .creativity:
|
|
||||||
return ["创意思维开发", "艺术表达练习", "问题解决创新", "想象力激发"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func generateTopicDescription(for title: String) -> String {
|
|
||||||
return "通过系统化的学习和练习,帮助你在\(title)方面获得提升。包含理论知识、实践练习和个人反思,让你在成长的道路上更进一步。"
|
|
||||||
}
|
|
||||||
|
|
||||||
private func generateTopicRewards() -> [Reward] {
|
|
||||||
let rewardCount = Int.random(in: 0...2)
|
|
||||||
var rewards: [Reward] = []
|
|
||||||
|
|
||||||
for _ in 0..<rewardCount {
|
|
||||||
let reward = Reward(
|
|
||||||
type: RewardType.allCases.randomElement() ?? .points,
|
|
||||||
title: "课题奖励",
|
|
||||||
description: "完成课题获得的奖励",
|
|
||||||
value: Int.random(in: 50...200),
|
|
||||||
rarity: RewardRarity.allCases.randomElement() ?? .common
|
|
||||||
)
|
|
||||||
rewards.append(reward)
|
|
||||||
}
|
|
||||||
|
|
||||||
return rewards
|
|
||||||
}
|
|
||||||
|
|
||||||
private func generateTopicInteractions() -> [TopicInteraction] {
|
|
||||||
let interactionCount = Int.random(in: 0...3)
|
|
||||||
var interactions: [TopicInteraction] = []
|
|
||||||
|
|
||||||
for i in 0..<interactionCount {
|
|
||||||
let interaction = TopicInteraction(
|
|
||||||
topicId: UUID(),
|
|
||||||
type: InteractionType.allCases.randomElement() ?? .aiChat,
|
|
||||||
title: "互动\(i+1)",
|
|
||||||
content: "这是一个有意义的互动内容",
|
|
||||||
completedAt: Bool.random() ? Date() : nil,
|
|
||||||
duration: TimeInterval.random(in: 300...1800),
|
|
||||||
rating: Bool.random() ? Int.random(in: 3...5) : nil
|
|
||||||
)
|
|
||||||
interactions.append(interaction)
|
|
||||||
}
|
|
||||||
|
|
||||||
return interactions
|
|
||||||
}
|
|
||||||
|
|
||||||
private func generateLocationPhotos() -> [String] {
|
|
||||||
let photoCount = Int.random(in: 1...4)
|
|
||||||
return Array(repeating: "location_photo", count: photoCount)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func generatePostContent() -> String {
|
|
||||||
let contents = [
|
|
||||||
"今天在这个美丽的地方找到了内心的平静 ✨",
|
|
||||||
"和朋友一起度过了愉快的下午时光 😊",
|
|
||||||
"这里的风景让我想起了童年的回忆",
|
|
||||||
"在这个安静的角落里思考人生的意义",
|
|
||||||
"发现了一个治愈心灵的好地方",
|
|
||||||
"阳光透过树叶洒下来,心情瞬间明亮了",
|
|
||||||
"和陌生人的一次偶遇,让我对生活有了新的感悟",
|
|
||||||
"在这里感受到了城市中难得的宁静",
|
|
||||||
"每次来这里都能获得新的启发",
|
|
||||||
"分享一个让我感到温暖的瞬间"
|
|
||||||
]
|
|
||||||
return contents.randomElement() ?? "分享今天的美好时光"
|
|
||||||
}
|
|
||||||
|
|
||||||
private func generatePostPhotos() -> [String] {
|
|
||||||
let photoCount = Int.random(in: 0...3)
|
|
||||||
return Array(repeating: "post_photo", count: photoCount)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func generatePostTags() -> [String] {
|
|
||||||
let allTags = ["治愈", "美好", "分享", "生活", "感悟", "风景", "友谊", "成长", "平静", "温暖"]
|
|
||||||
return Array(allTags.shuffled().prefix(Int.random(in: 1...3)))
|
|
||||||
}
|
|
||||||
|
|
||||||
private func generatePostComments() -> [Comment] {
|
|
||||||
let commentCount = Int.random(in: 0...5)
|
|
||||||
var comments: [Comment] = []
|
|
||||||
|
|
||||||
let commentContents = [
|
|
||||||
"太美了!",
|
|
||||||
"我也想去这个地方",
|
|
||||||
"感谢分享 ❤️",
|
|
||||||
"很有感触",
|
|
||||||
"下次一起去吧",
|
|
||||||
"照片拍得很棒",
|
|
||||||
"这个地方我也去过",
|
|
||||||
"很治愈的分享"
|
|
||||||
]
|
|
||||||
|
|
||||||
for _ in 0..<commentCount {
|
|
||||||
let comment = Comment(
|
|
||||||
postId: UUID(),
|
|
||||||
userId: UUID(),
|
|
||||||
content: commentContents.randomElement() ?? "很棒的分享",
|
|
||||||
createdAt: Calendar.current.date(byAdding: .hour, value: -Int.random(in: 1...24), to: Date()) ?? Date(),
|
|
||||||
likes: Int.random(in: 0...10),
|
|
||||||
isLikedByCurrentUser: Bool.random()
|
|
||||||
)
|
|
||||||
comments.append(comment)
|
|
||||||
}
|
|
||||||
|
|
||||||
return comments
|
|
||||||
}
|
|
||||||
|
|
||||||
private func getTargetValue(for requirement: AchievementRequirement) -> Int {
|
|
||||||
switch requirement {
|
|
||||||
case .conversationCount(let count): return count
|
|
||||||
case .emotionRecordCount(let count): return count
|
|
||||||
case .topicCompletion(let count): return count
|
|
||||||
case .socialInteraction(let count): return count
|
|
||||||
case .locationVisit(let count): return count
|
|
||||||
case .consecutiveDays(let days): return days
|
|
||||||
case .totalPoints(let points): return points
|
|
||||||
case .special(_): return 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func updateUserStats() {
|
|
||||||
userStats.totalConversations = conversations.count
|
|
||||||
userStats.totalMessages = conversations.reduce(0) { $0 + $1.messageCount }
|
|
||||||
userStats.totalEmotionRecords = emotionRecords.count
|
|
||||||
userStats.completedTopics = growthTopics.filter { $0.isCompleted }.count
|
|
||||||
userStats.totalPoints = achievements.reduce(0) { $0 + ($1.isUnlocked ? 100 : 0) }
|
|
||||||
userStats.consecutiveDays = 25 // 模拟连续使用天数
|
|
||||||
userStats.maxConsecutiveDays = 30
|
|
||||||
userStats.socialInteractions = communityPosts.reduce(0) { $0 + $1.likes + $1.commentCount }
|
|
||||||
userStats.locationsVisited = locationPins.filter { $0.visits > 0 }.count
|
|
||||||
userStats.postsCreated = communityPosts.filter { $0.userId == currentUser.id }.count
|
|
||||||
userStats.likesReceived = communityPosts.filter { $0.userId == currentUser.id }.reduce(0) { $0 + $1.likes }
|
|
||||||
userStats.commentsReceived = communityPosts.filter { $0.userId == currentUser.id }.reduce(0) { $0 + $1.commentCount }
|
|
||||||
}
|
|
||||||
|
|
||||||
private func updateWeeklyStats() {
|
|
||||||
let weekStart = weeklyStats.weekStartDate
|
|
||||||
let weekEnd = Calendar.current.date(byAdding: .day, value: 6, to: weekStart) ?? weekStart
|
|
||||||
|
|
||||||
// 本周情绪记录
|
|
||||||
weeklyStats.emotionRecords = emotionRecords.filter { record in
|
|
||||||
record.date >= weekStart && record.date <= weekEnd
|
|
||||||
}
|
|
||||||
|
|
||||||
// 本周对话
|
|
||||||
weeklyStats.conversations = conversations.filter { conversation in
|
|
||||||
conversation.startTime >= weekStart && conversation.startTime <= weekEnd
|
|
||||||
}
|
|
||||||
|
|
||||||
// 计算平均情绪分数
|
|
||||||
if !weeklyStats.emotionRecords.isEmpty {
|
|
||||||
weeklyStats.averageMoodScore = weeklyStats.emotionRecords.averageIntensity()
|
|
||||||
weeklyStats.dominantEmotion = weeklyStats.emotionRecords.dominantEmotion()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 确定情绪趋势
|
|
||||||
weeklyStats.moodTrend = EmotionTrend.allCases.randomElement() ?? .stable
|
|
||||||
|
|
||||||
// 最活跃的一天
|
|
||||||
let dailyActivity = Dictionary(grouping: weeklyStats.emotionRecords + weeklyStats.conversations.map { conversation in
|
|
||||||
EmotionRecord(userId: currentUser.id, date: conversation.startTime, emotionType: .neutral, intensity: 0.5, context: "对话活动")
|
|
||||||
}, by: { Calendar.current.startOfDay(for: $0.date) })
|
|
||||||
|
|
||||||
weeklyStats.mostActiveDay = dailyActivity.max(by: { $0.value.count < $1.value.count })?.key
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,411 +0,0 @@
|
|||||||
//
|
|
||||||
// NavigationManager.swift
|
|
||||||
// EmotionMuseum
|
|
||||||
//
|
|
||||||
// Created by 华中敏 on 2025/7/5.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
import SwiftUI
|
|
||||||
|
|
||||||
class NavigationManager: ObservableObject {
|
|
||||||
// MARK: - Tab Navigation
|
|
||||||
@Published var currentTab: MainTab = .record
|
|
||||||
|
|
||||||
// MARK: - Navigation Paths for each tab
|
|
||||||
@Published var recordNavigation = NavigationPath()
|
|
||||||
@Published var growthNavigation = NavigationPath()
|
|
||||||
@Published var exploreNavigation = NavigationPath()
|
|
||||||
@Published var insightNavigation = NavigationPath()
|
|
||||||
|
|
||||||
// MARK: - Global Modal States
|
|
||||||
@Published var showingChatHistory = false
|
|
||||||
@Published var showingFullScreenChat = false
|
|
||||||
@Published var showingSettings = false
|
|
||||||
@Published var showingProfile = false
|
|
||||||
@Published var showingThemeSettings = false
|
|
||||||
@Published var showingAddLocation = false
|
|
||||||
@Published var showingTopicDetail = false
|
|
||||||
@Published var showingLocationDetail = false
|
|
||||||
@Published var showingPostDetail = false
|
|
||||||
@Published var showingAchievements = false
|
|
||||||
@Published var showingMemberCenter = false
|
|
||||||
|
|
||||||
// MARK: - Current Selection States
|
|
||||||
@Published var selectedConversation: Conversation?
|
|
||||||
@Published var selectedTopic: GrowthTopic?
|
|
||||||
@Published var selectedLocation: LocationPin?
|
|
||||||
@Published var selectedPost: CommunityPost?
|
|
||||||
@Published var selectedAchievement: Achievement?
|
|
||||||
|
|
||||||
// MARK: - Chat States
|
|
||||||
@Published var currentChatConversation: Conversation?
|
|
||||||
@Published var isVoiceMode = false
|
|
||||||
@Published var chatInputText = ""
|
|
||||||
|
|
||||||
// MARK: - Loading States
|
|
||||||
@Published var isLoading = false
|
|
||||||
@Published var loadingMessage = ""
|
|
||||||
|
|
||||||
// MARK: - Navigation Methods
|
|
||||||
|
|
||||||
func navigateToTab(_ tab: MainTab) {
|
|
||||||
withAnimation(.easeInOut(duration: 0.3)) {
|
|
||||||
currentTab = tab
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func navigateToChat(conversation: Conversation? = nil) {
|
|
||||||
selectedConversation = conversation
|
|
||||||
currentChatConversation = conversation
|
|
||||||
showingFullScreenChat = true
|
|
||||||
}
|
|
||||||
|
|
||||||
func navigateToTopic(_ topic: GrowthTopic) {
|
|
||||||
selectedTopic = topic
|
|
||||||
showingTopicDetail = true
|
|
||||||
}
|
|
||||||
|
|
||||||
func navigateToLocation(_ location: LocationPin) {
|
|
||||||
selectedLocation = location
|
|
||||||
showingLocationDetail = true
|
|
||||||
}
|
|
||||||
|
|
||||||
func navigateToPost(_ post: CommunityPost) {
|
|
||||||
selectedPost = post
|
|
||||||
showingPostDetail = true
|
|
||||||
}
|
|
||||||
|
|
||||||
func navigateToProfile() {
|
|
||||||
showingProfile = true
|
|
||||||
}
|
|
||||||
|
|
||||||
func navigateToSettings() {
|
|
||||||
showingSettings = true
|
|
||||||
}
|
|
||||||
|
|
||||||
func navigateToThemeSettings() {
|
|
||||||
showingThemeSettings = true
|
|
||||||
}
|
|
||||||
|
|
||||||
func navigateToAchievements() {
|
|
||||||
showingAchievements = true
|
|
||||||
}
|
|
||||||
|
|
||||||
func navigateToMemberCenter() {
|
|
||||||
showingMemberCenter = true
|
|
||||||
}
|
|
||||||
|
|
||||||
func showAddLocation() {
|
|
||||||
showingAddLocation = true
|
|
||||||
}
|
|
||||||
|
|
||||||
func showChatHistory() {
|
|
||||||
showingChatHistory = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Dismiss Methods
|
|
||||||
|
|
||||||
func dismissAllModals() {
|
|
||||||
showingChatHistory = false
|
|
||||||
showingFullScreenChat = false
|
|
||||||
showingSettings = false
|
|
||||||
showingProfile = false
|
|
||||||
showingThemeSettings = false
|
|
||||||
showingAddLocation = false
|
|
||||||
showingTopicDetail = false
|
|
||||||
showingLocationDetail = false
|
|
||||||
showingPostDetail = false
|
|
||||||
showingAchievements = false
|
|
||||||
showingMemberCenter = false
|
|
||||||
|
|
||||||
selectedConversation = nil
|
|
||||||
selectedTopic = nil
|
|
||||||
selectedLocation = nil
|
|
||||||
selectedPost = nil
|
|
||||||
selectedAchievement = nil
|
|
||||||
currentChatConversation = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func dismissCurrentModal() {
|
|
||||||
if showingFullScreenChat {
|
|
||||||
showingFullScreenChat = false
|
|
||||||
currentChatConversation = nil
|
|
||||||
} else if showingChatHistory {
|
|
||||||
showingChatHistory = false
|
|
||||||
} else if showingSettings {
|
|
||||||
showingSettings = false
|
|
||||||
} else if showingProfile {
|
|
||||||
showingProfile = false
|
|
||||||
} else if showingThemeSettings {
|
|
||||||
showingThemeSettings = false
|
|
||||||
} else if showingAddLocation {
|
|
||||||
showingAddLocation = false
|
|
||||||
} else if showingTopicDetail {
|
|
||||||
showingTopicDetail = false
|
|
||||||
selectedTopic = nil
|
|
||||||
} else if showingLocationDetail {
|
|
||||||
showingLocationDetail = false
|
|
||||||
selectedLocation = nil
|
|
||||||
} else if showingPostDetail {
|
|
||||||
showingPostDetail = false
|
|
||||||
selectedPost = nil
|
|
||||||
} else if showingAchievements {
|
|
||||||
showingAchievements = false
|
|
||||||
} else if showingMemberCenter {
|
|
||||||
showingMemberCenter = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Deep Link Methods
|
|
||||||
|
|
||||||
func handleDeepLink(url: URL) {
|
|
||||||
// 处理深度链接,例如从通知或分享链接打开特定页面
|
|
||||||
guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false) else { return }
|
|
||||||
|
|
||||||
switch components.host {
|
|
||||||
case "conversation":
|
|
||||||
if let conversationId = components.queryItems?.first(where: { $0.name == "id" })?.value,
|
|
||||||
let uuid = UUID(uuidString: conversationId) {
|
|
||||||
// 查找并打开对应的对话
|
|
||||||
let conversation = MockDataManager.shared.conversations.first { $0.id == uuid }
|
|
||||||
navigateToChat(conversation: conversation)
|
|
||||||
}
|
|
||||||
|
|
||||||
case "topic":
|
|
||||||
if let topicId = components.queryItems?.first(where: { $0.name == "id" })?.value,
|
|
||||||
let uuid = UUID(uuidString: topicId) {
|
|
||||||
// 查找并打开对应的成长课题
|
|
||||||
let topic = MockDataManager.shared.growthTopics.first { $0.id == uuid }
|
|
||||||
if let topic = topic {
|
|
||||||
navigateToTab(.growth)
|
|
||||||
navigateToTopic(topic)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
case "location":
|
|
||||||
if let locationId = components.queryItems?.first(where: { $0.name == "id" })?.value,
|
|
||||||
let uuid = UUID(uuidString: locationId) {
|
|
||||||
// 查找并打开对应的地点
|
|
||||||
let location = MockDataManager.shared.locationPins.first { $0.id == uuid }
|
|
||||||
if let location = location {
|
|
||||||
navigateToTab(.explore)
|
|
||||||
navigateToLocation(location)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
case "post":
|
|
||||||
if let postId = components.queryItems?.first(where: { $0.name == "id" })?.value,
|
|
||||||
let uuid = UUID(uuidString: postId) {
|
|
||||||
// 查找并打开对应的帖子
|
|
||||||
let post = MockDataManager.shared.communityPosts.first { $0.id == uuid }
|
|
||||||
if let post = post {
|
|
||||||
navigateToTab(.explore)
|
|
||||||
navigateToPost(post)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
default:
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Loading Management
|
|
||||||
|
|
||||||
func showLoading(_ message: String = "加载中...") {
|
|
||||||
loadingMessage = message
|
|
||||||
withAnimation(.easeInOut) {
|
|
||||||
isLoading = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func hideLoading() {
|
|
||||||
withAnimation(.easeInOut) {
|
|
||||||
isLoading = false
|
|
||||||
}
|
|
||||||
loadingMessage = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Chat Management
|
|
||||||
|
|
||||||
func sendMessage(_ content: String, to conversation: Conversation? = nil) {
|
|
||||||
guard !content.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { return }
|
|
||||||
|
|
||||||
let targetConversation = conversation ?? currentChatConversation
|
|
||||||
|
|
||||||
if let conv = targetConversation {
|
|
||||||
MockDataManager.shared.addMessage(to: conv.id, content: content, sender: .user)
|
|
||||||
} else {
|
|
||||||
// 创建新对话
|
|
||||||
let newConversation = MockDataManager.shared.createNewConversation()
|
|
||||||
currentChatConversation = newConversation
|
|
||||||
MockDataManager.shared.addMessage(to: newConversation.id, content: content, sender: .user)
|
|
||||||
}
|
|
||||||
|
|
||||||
chatInputText = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func toggleVoiceMode() {
|
|
||||||
withAnimation(.easeInOut(duration: 0.2)) {
|
|
||||||
isVoiceMode.toggle()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Utility Methods
|
|
||||||
|
|
||||||
func clearNavigationPath(for tab: MainTab) {
|
|
||||||
switch tab {
|
|
||||||
case .record:
|
|
||||||
recordNavigation = NavigationPath()
|
|
||||||
case .growth:
|
|
||||||
growthNavigation = NavigationPath()
|
|
||||||
case .explore:
|
|
||||||
exploreNavigation = NavigationPath()
|
|
||||||
case .insight:
|
|
||||||
insightNavigation = NavigationPath()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func popToRoot(for tab: MainTab) {
|
|
||||||
clearNavigationPath(for: tab)
|
|
||||||
dismissAllModals()
|
|
||||||
}
|
|
||||||
|
|
||||||
func canGoBack(for tab: MainTab) -> Bool {
|
|
||||||
switch tab {
|
|
||||||
case .record:
|
|
||||||
return !recordNavigation.isEmpty
|
|
||||||
case .growth:
|
|
||||||
return !growthNavigation.isEmpty
|
|
||||||
case .explore:
|
|
||||||
return !exploreNavigation.isEmpty
|
|
||||||
case .insight:
|
|
||||||
return !insightNavigation.isEmpty
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func goBack(for tab: MainTab) {
|
|
||||||
switch tab {
|
|
||||||
case .record:
|
|
||||||
if !recordNavigation.isEmpty {
|
|
||||||
recordNavigation.removeLast()
|
|
||||||
}
|
|
||||||
case .growth:
|
|
||||||
if !growthNavigation.isEmpty {
|
|
||||||
growthNavigation.removeLast()
|
|
||||||
}
|
|
||||||
case .explore:
|
|
||||||
if !exploreNavigation.isEmpty {
|
|
||||||
exploreNavigation.removeLast()
|
|
||||||
}
|
|
||||||
case .insight:
|
|
||||||
if !insightNavigation.isEmpty {
|
|
||||||
insightNavigation.removeLast()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Main Tab Enum
|
|
||||||
|
|
||||||
enum MainTab: String, CaseIterable {
|
|
||||||
case record = "记录"
|
|
||||||
case growth = "治愈"
|
|
||||||
case explore = "探索"
|
|
||||||
case insight = "我的"
|
|
||||||
|
|
||||||
var title: String {
|
|
||||||
return self.rawValue
|
|
||||||
}
|
|
||||||
|
|
||||||
var icon: String {
|
|
||||||
switch self {
|
|
||||||
case .record: return "brain.head.profile"
|
|
||||||
case .growth: return "heart"
|
|
||||||
case .explore: return "map"
|
|
||||||
case .insight: return "person"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var selectedIcon: String {
|
|
||||||
switch self {
|
|
||||||
case .record: return "brain.head.profile.fill"
|
|
||||||
case .growth: return "heart.fill"
|
|
||||||
case .explore: return "map.fill"
|
|
||||||
case .insight: return "person.fill"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var color: Color {
|
|
||||||
switch self {
|
|
||||||
case .record: return .blue
|
|
||||||
case .growth: return .pink
|
|
||||||
case .explore: return .green
|
|
||||||
case .insight: return .orange
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Navigation Destination Types
|
|
||||||
|
|
||||||
enum NavigationDestination: Hashable {
|
|
||||||
case conversation(Conversation)
|
|
||||||
case topic(GrowthTopic)
|
|
||||||
case location(LocationPin)
|
|
||||||
case post(CommunityPost)
|
|
||||||
case achievement(Achievement)
|
|
||||||
case settings
|
|
||||||
case profile
|
|
||||||
case chatHistory
|
|
||||||
case addLocation
|
|
||||||
|
|
||||||
static func == (lhs: NavigationDestination, rhs: NavigationDestination) -> Bool {
|
|
||||||
switch (lhs, rhs) {
|
|
||||||
case (.conversation(let lhsConv), .conversation(let rhsConv)):
|
|
||||||
return lhsConv.id == rhsConv.id
|
|
||||||
case (.topic(let lhsTopic), .topic(let rhsTopic)):
|
|
||||||
return lhsTopic.id == rhsTopic.id
|
|
||||||
case (.location(let lhsLoc), .location(let rhsLoc)):
|
|
||||||
return lhsLoc.id == rhsLoc.id
|
|
||||||
case (.post(let lhsPost), .post(let rhsPost)):
|
|
||||||
return lhsPost.id == rhsPost.id
|
|
||||||
case (.achievement(let lhsAch), .achievement(let rhsAch)):
|
|
||||||
return lhsAch.id == rhsAch.id
|
|
||||||
case (.settings, .settings),
|
|
||||||
(.profile, .profile),
|
|
||||||
(.chatHistory, .chatHistory),
|
|
||||||
(.addLocation, .addLocation):
|
|
||||||
return true
|
|
||||||
default:
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func hash(into hasher: inout Hasher) {
|
|
||||||
switch self {
|
|
||||||
case .conversation(let conv):
|
|
||||||
hasher.combine("conversation")
|
|
||||||
hasher.combine(conv.id)
|
|
||||||
case .topic(let topic):
|
|
||||||
hasher.combine("topic")
|
|
||||||
hasher.combine(topic.id)
|
|
||||||
case .location(let location):
|
|
||||||
hasher.combine("location")
|
|
||||||
hasher.combine(location.id)
|
|
||||||
case .post(let post):
|
|
||||||
hasher.combine("post")
|
|
||||||
hasher.combine(post.id)
|
|
||||||
case .achievement(let achievement):
|
|
||||||
hasher.combine("achievement")
|
|
||||||
hasher.combine(achievement.id)
|
|
||||||
case .settings:
|
|
||||||
hasher.combine("settings")
|
|
||||||
case .profile:
|
|
||||||
hasher.combine("profile")
|
|
||||||
case .chatHistory:
|
|
||||||
hasher.combine("chatHistory")
|
|
||||||
case .addLocation:
|
|
||||||
hasher.combine("addLocation")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,565 +0,0 @@
|
|||||||
//
|
|
||||||
// AnimationComponents.swift
|
|
||||||
// EmotionMuseum
|
|
||||||
//
|
|
||||||
// Created by 华中敏 on 2025/6/13.
|
|
||||||
//
|
|
||||||
|
|
||||||
import SwiftUI
|
|
||||||
|
|
||||||
// MARK: - 动画配置
|
|
||||||
struct AnimationConfig {
|
|
||||||
static let springy = Animation.spring(response: 0.6, dampingFraction: 0.8)
|
|
||||||
static let bouncy = Animation.spring(response: 0.4, dampingFraction: 0.6)
|
|
||||||
static let smooth = Animation.easeInOut(duration: 0.3)
|
|
||||||
static let quick = Animation.easeOut(duration: 0.2)
|
|
||||||
static let slow = Animation.easeInOut(duration: 0.8)
|
|
||||||
static let elastic = Animation.interpolatingSpring(stiffness: 300, damping: 15)
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - 动画视图修饰符
|
|
||||||
struct AnimatedAppearance: ViewModifier {
|
|
||||||
@State private var isVisible = false
|
|
||||||
let delay: Double
|
|
||||||
let animation: Animation
|
|
||||||
|
|
||||||
init(delay: Double = 0, animation: Animation = AnimationConfig.smooth) {
|
|
||||||
self.delay = delay
|
|
||||||
self.animation = animation
|
|
||||||
}
|
|
||||||
|
|
||||||
func body(content: Content) -> some View {
|
|
||||||
content
|
|
||||||
.scaleEffect(isVisible ? 1 : 0.8)
|
|
||||||
.opacity(isVisible ? 1 : 0)
|
|
||||||
.offset(y: isVisible ? 0 : 20)
|
|
||||||
.onAppear {
|
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + delay) {
|
|
||||||
withAnimation(animation) {
|
|
||||||
isVisible = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct AnimatedCounter: ViewModifier {
|
|
||||||
@State private var currentValue: Int = 0
|
|
||||||
let targetValue: Int
|
|
||||||
let duration: Double
|
|
||||||
|
|
||||||
init(targetValue: Int, duration: Double = 1.0) {
|
|
||||||
self.targetValue = targetValue
|
|
||||||
self.duration = duration
|
|
||||||
}
|
|
||||||
|
|
||||||
func body(content: Content) -> some View {
|
|
||||||
Text("\(currentValue)")
|
|
||||||
.onAppear {
|
|
||||||
animateCounter()
|
|
||||||
}
|
|
||||||
.onChange(of: targetValue) { _ in
|
|
||||||
animateCounter()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func animateCounter() {
|
|
||||||
currentValue = 0
|
|
||||||
let stepDuration = duration / Double(targetValue)
|
|
||||||
|
|
||||||
for i in 1...targetValue {
|
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + stepDuration * Double(i)) {
|
|
||||||
withAnimation(.easeOut(duration: 0.1)) {
|
|
||||||
currentValue = i
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - 动画按钮
|
|
||||||
struct AnimatedButton<Content: View>: View {
|
|
||||||
let action: () -> Void
|
|
||||||
let content: () -> Content
|
|
||||||
@State private var isPressed = false
|
|
||||||
@State private var scale: CGFloat = 1.0
|
|
||||||
|
|
||||||
init(action: @escaping () -> Void, @ViewBuilder content: @escaping () -> Content) {
|
|
||||||
self.action = action
|
|
||||||
self.content = content
|
|
||||||
}
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
Button(action: {
|
|
||||||
impactFeedback()
|
|
||||||
action()
|
|
||||||
}) {
|
|
||||||
content()
|
|
||||||
}
|
|
||||||
.scaleEffect(scale)
|
|
||||||
.onLongPressGesture(minimumDuration: 0, maximumDistance: .infinity) { isPressing in
|
|
||||||
withAnimation(AnimationConfig.quick) {
|
|
||||||
scale = isPressing ? 0.95 : 1.0
|
|
||||||
isPressed = isPressing
|
|
||||||
}
|
|
||||||
} perform: {
|
|
||||||
// 长按完成时的动作
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func impactFeedback() {
|
|
||||||
let impactFeedback = UIImpactFeedbackGenerator(style: .light)
|
|
||||||
impactFeedback.impactOccurred()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - 波纹动画
|
|
||||||
struct RippleEffect: View {
|
|
||||||
@State private var animationAmount = 1.0
|
|
||||||
let color: Color
|
|
||||||
let size: CGFloat
|
|
||||||
|
|
||||||
init(color: Color = .blue, size: CGFloat = 100) {
|
|
||||||
self.color = color
|
|
||||||
self.size = size
|
|
||||||
}
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
Circle()
|
|
||||||
.fill(color.opacity(0.3))
|
|
||||||
.frame(width: size, height: size)
|
|
||||||
.scaleEffect(animationAmount)
|
|
||||||
.opacity(2 - animationAmount)
|
|
||||||
.animation(
|
|
||||||
Animation.easeOut(duration: 1.5)
|
|
||||||
.repeatForever(autoreverses: false),
|
|
||||||
value: animationAmount
|
|
||||||
)
|
|
||||||
.onAppear {
|
|
||||||
animationAmount = 2
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - 脉冲动画
|
|
||||||
struct PulseEffect: ViewModifier {
|
|
||||||
@State private var isAnimating = false
|
|
||||||
let color: Color
|
|
||||||
let size: CGFloat
|
|
||||||
|
|
||||||
init(color: Color = .blue, size: CGFloat = 1.2) {
|
|
||||||
self.color = color
|
|
||||||
self.size = size
|
|
||||||
}
|
|
||||||
|
|
||||||
func body(content: Content) -> some View {
|
|
||||||
content
|
|
||||||
.scaleEffect(isAnimating ? size : 1.0)
|
|
||||||
.animation(
|
|
||||||
Animation.easeInOut(duration: 1.0)
|
|
||||||
.repeatForever(autoreverses: true),
|
|
||||||
value: isAnimating
|
|
||||||
)
|
|
||||||
.onAppear {
|
|
||||||
isAnimating = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - 摇摆动画
|
|
||||||
struct ShakeEffect: ViewModifier {
|
|
||||||
@State private var shakeOffset: CGFloat = 0
|
|
||||||
let intensity: CGFloat
|
|
||||||
|
|
||||||
init(intensity: CGFloat = 10) {
|
|
||||||
self.intensity = intensity
|
|
||||||
}
|
|
||||||
|
|
||||||
func body(content: Content) -> some View {
|
|
||||||
content
|
|
||||||
.offset(x: shakeOffset)
|
|
||||||
.onAppear {
|
|
||||||
withAnimation(
|
|
||||||
Animation.easeInOut(duration: 0.1)
|
|
||||||
.repeatCount(4, autoreverses: true)
|
|
||||||
) {
|
|
||||||
shakeOffset = intensity
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - 流体动画背景
|
|
||||||
struct FluidBackground: View {
|
|
||||||
@State private var animationOffset = 0.0
|
|
||||||
let colors: [Color]
|
|
||||||
|
|
||||||
init(colors: [Color] = [.blue, .purple, .pink]) {
|
|
||||||
self.colors = colors
|
|
||||||
}
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
GeometryReader { geometry in
|
|
||||||
ZStack {
|
|
||||||
ForEach(0..<colors.count, id: \.self) { index in
|
|
||||||
Circle()
|
|
||||||
.fill(colors[index].opacity(0.3))
|
|
||||||
.frame(width: geometry.size.width * 0.8)
|
|
||||||
.offset(
|
|
||||||
x: sin(animationOffset + Double(index) * 2) * 50,
|
|
||||||
y: cos(animationOffset + Double(index) * 1.5) * 30
|
|
||||||
)
|
|
||||||
.blur(radius: 20)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.onAppear {
|
|
||||||
withAnimation(
|
|
||||||
Animation.linear(duration: 20)
|
|
||||||
.repeatForever(autoreverses: false)
|
|
||||||
) {
|
|
||||||
animationOffset = .pi * 2
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - 弹跳列表项
|
|
||||||
struct BounceListItem<Content: View>: View {
|
|
||||||
let content: () -> Content
|
|
||||||
@State private var isVisible = false
|
|
||||||
let index: Int
|
|
||||||
|
|
||||||
init(index: Int, @ViewBuilder content: @escaping () -> Content) {
|
|
||||||
self.index = index
|
|
||||||
self.content = content
|
|
||||||
}
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
content()
|
|
||||||
.scaleEffect(isVisible ? 1 : 0.8)
|
|
||||||
.opacity(isVisible ? 1 : 0)
|
|
||||||
.offset(x: isVisible ? 0 : -50)
|
|
||||||
.onAppear {
|
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + Double(index) * 0.1) {
|
|
||||||
withAnimation(AnimationConfig.bouncy) {
|
|
||||||
isVisible = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - 卡片翻转动画
|
|
||||||
struct FlipCard<Front: View, Back: View>: View {
|
|
||||||
let front: () -> Front
|
|
||||||
let back: () -> Back
|
|
||||||
@State private var isFlipped = false
|
|
||||||
@State private var rotation = 0.0
|
|
||||||
|
|
||||||
init(@ViewBuilder front: @escaping () -> Front, @ViewBuilder back: @escaping () -> Back) {
|
|
||||||
self.front = front
|
|
||||||
self.back = back
|
|
||||||
}
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
ZStack {
|
|
||||||
if !isFlipped {
|
|
||||||
front()
|
|
||||||
} else {
|
|
||||||
back()
|
|
||||||
.rotation3DEffect(.degrees(180), axis: (x: 0, y: 1, z: 0))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.rotation3DEffect(.degrees(rotation), axis: (x: 0, y: 1, z: 0))
|
|
||||||
.onTapGesture {
|
|
||||||
withAnimation(.easeInOut(duration: 0.6)) {
|
|
||||||
rotation += 180
|
|
||||||
isFlipped.toggle()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - 进度条动画
|
|
||||||
struct AnimatedProgressBar: View {
|
|
||||||
let progress: Double
|
|
||||||
let height: CGFloat
|
|
||||||
let backgroundColor: Color
|
|
||||||
let fillColor: Color
|
|
||||||
@State private var animatedProgress: Double = 0
|
|
||||||
|
|
||||||
init(
|
|
||||||
progress: Double,
|
|
||||||
height: CGFloat = 8,
|
|
||||||
backgroundColor: Color = Color.gray.opacity(0.3),
|
|
||||||
fillColor: Color = .blue
|
|
||||||
) {
|
|
||||||
self.progress = progress
|
|
||||||
self.height = height
|
|
||||||
self.backgroundColor = backgroundColor
|
|
||||||
self.fillColor = fillColor
|
|
||||||
}
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
GeometryReader { geometry in
|
|
||||||
ZStack(alignment: .leading) {
|
|
||||||
Rectangle()
|
|
||||||
.fill(backgroundColor)
|
|
||||||
.frame(height: height)
|
|
||||||
.cornerRadius(height / 2)
|
|
||||||
|
|
||||||
Rectangle()
|
|
||||||
.fill(fillColor)
|
|
||||||
.frame(
|
|
||||||
width: geometry.size.width * animatedProgress,
|
|
||||||
height: height
|
|
||||||
)
|
|
||||||
.cornerRadius(height / 2)
|
|
||||||
.animation(AnimationConfig.smooth, value: animatedProgress)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.frame(height: height)
|
|
||||||
.onAppear {
|
|
||||||
withAnimation(.easeOut(duration: 1.0)) {
|
|
||||||
animatedProgress = progress
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.onChange(of: progress) { newValue in
|
|
||||||
withAnimation(AnimationConfig.smooth) {
|
|
||||||
animatedProgress = newValue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - 浮动动作按钮
|
|
||||||
struct FloatingActionButton: View {
|
|
||||||
let action: () -> Void
|
|
||||||
let icon: String
|
|
||||||
let color: Color
|
|
||||||
@State private var isPressed = false
|
|
||||||
@State private var rotation = 0.0
|
|
||||||
|
|
||||||
init(icon: String, color: Color = .blue, action: @escaping () -> Void) {
|
|
||||||
self.icon = icon
|
|
||||||
self.color = color
|
|
||||||
self.action = action
|
|
||||||
}
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
Button(action: {
|
|
||||||
withAnimation(AnimationConfig.bouncy) {
|
|
||||||
rotation += 360
|
|
||||||
}
|
|
||||||
action()
|
|
||||||
}) {
|
|
||||||
Image(systemName: icon)
|
|
||||||
.font(.title2)
|
|
||||||
.foregroundColor(.white)
|
|
||||||
.frame(width: 56, height: 56)
|
|
||||||
.background(color)
|
|
||||||
.clipShape(Circle())
|
|
||||||
.shadow(color: color.opacity(0.4), radius: 8, x: 0, y: 4)
|
|
||||||
}
|
|
||||||
.rotationEffect(.degrees(rotation))
|
|
||||||
.scaleEffect(isPressed ? 0.9 : 1.0)
|
|
||||||
.onLongPressGesture(minimumDuration: 0, maximumDistance: .infinity) { isPressing in
|
|
||||||
withAnimation(AnimationConfig.quick) {
|
|
||||||
isPressed = isPressing
|
|
||||||
}
|
|
||||||
} perform: {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - 粒子动画
|
|
||||||
struct ParticleSystem: View {
|
|
||||||
@State private var particles: [AnimationParticle] = []
|
|
||||||
let particleCount: Int
|
|
||||||
let colors: [Color]
|
|
||||||
|
|
||||||
init(particleCount: Int = 20, colors: [Color] = [.blue, .purple, .pink]) {
|
|
||||||
self.particleCount = particleCount
|
|
||||||
self.colors = colors
|
|
||||||
}
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
GeometryReader { geometry in
|
|
||||||
ZStack {
|
|
||||||
ForEach(particles) { particle in
|
|
||||||
Circle()
|
|
||||||
.fill(particle.color)
|
|
||||||
.frame(width: particle.size, height: particle.size)
|
|
||||||
.position(particle.position)
|
|
||||||
.opacity(particle.opacity)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.onAppear {
|
|
||||||
createParticles()
|
|
||||||
animateParticles()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func createParticles() {
|
|
||||||
particles = (0..<particleCount).map { _ in
|
|
||||||
AnimationParticle(
|
|
||||||
id: UUID(),
|
|
||||||
position: CGPoint(
|
|
||||||
x: CGFloat.random(in: 0...UIScreen.main.bounds.width),
|
|
||||||
y: CGFloat.random(in: 0...UIScreen.main.bounds.height)
|
|
||||||
),
|
|
||||||
size: CGFloat.random(in: 2...8),
|
|
||||||
color: colors.randomElement() ?? .blue,
|
|
||||||
opacity: Double.random(in: 0.3...1.0)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func animateParticles() {
|
|
||||||
Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { _ in
|
|
||||||
withAnimation(.linear(duration: 0.1)) {
|
|
||||||
for i in particles.indices {
|
|
||||||
particles[i].position.x += CGFloat.random(in: -2...2)
|
|
||||||
particles[i].position.y += CGFloat.random(in: -2...2)
|
|
||||||
particles[i].opacity = Double.random(in: 0.3...1.0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct AnimationParticle: Identifiable {
|
|
||||||
let id: UUID
|
|
||||||
var position: CGPoint
|
|
||||||
var size: CGFloat
|
|
||||||
var color: Color
|
|
||||||
var opacity: Double
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - 渐变动画文本
|
|
||||||
struct AnimatedGradientText: View {
|
|
||||||
let text: String
|
|
||||||
let colors: [Color]
|
|
||||||
@State private var animationOffset = 0.0
|
|
||||||
|
|
||||||
init(_ text: String, colors: [Color] = [.blue, .purple, .pink]) {
|
|
||||||
self.text = text
|
|
||||||
self.colors = colors
|
|
||||||
}
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
Text(text)
|
|
||||||
.font(.title)
|
|
||||||
.fontWeight(.bold)
|
|
||||||
.foregroundColor(.primary)
|
|
||||||
.background(
|
|
||||||
LinearGradient(
|
|
||||||
colors: colors,
|
|
||||||
startPoint: .leading,
|
|
||||||
endPoint: .trailing
|
|
||||||
)
|
|
||||||
.rotationEffect(.degrees(animationOffset))
|
|
||||||
.mask(
|
|
||||||
Text(text)
|
|
||||||
.font(.title)
|
|
||||||
.fontWeight(.bold)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.onAppear {
|
|
||||||
withAnimation(
|
|
||||||
Animation.linear(duration: 3)
|
|
||||||
.repeatForever(autoreverses: false)
|
|
||||||
) {
|
|
||||||
animationOffset = 360
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - 视图修饰符扩展
|
|
||||||
extension View {
|
|
||||||
func animatedAppearance(delay: Double = 0, animation: Animation = AnimationConfig.smooth) -> some View {
|
|
||||||
modifier(AnimatedAppearance(delay: delay, animation: animation))
|
|
||||||
}
|
|
||||||
|
|
||||||
func pulseEffect(color: Color = .blue, size: CGFloat = 1.2) -> some View {
|
|
||||||
modifier(PulseEffect(color: color, size: size))
|
|
||||||
}
|
|
||||||
|
|
||||||
func shakeEffect(intensity: CGFloat = 10) -> some View {
|
|
||||||
modifier(ShakeEffect(intensity: intensity))
|
|
||||||
}
|
|
||||||
|
|
||||||
func bounceOnTap(scale: CGFloat = 0.95) -> some View {
|
|
||||||
scaleEffect(1.0)
|
|
||||||
.onTapGesture {
|
|
||||||
withAnimation(AnimationConfig.bouncy) {
|
|
||||||
// 实现点击反弹效果
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func cardTransition() -> some View {
|
|
||||||
transition(
|
|
||||||
.asymmetric(
|
|
||||||
insertion: .scale(scale: 0.8).combined(with: .opacity),
|
|
||||||
removal: .scale(scale: 1.2).combined(with: .opacity)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func slideTransition(edge: Edge = .bottom) -> some View {
|
|
||||||
transition(.move(edge: edge).combined(with: .opacity))
|
|
||||||
}
|
|
||||||
|
|
||||||
func rotateTransition() -> some View {
|
|
||||||
transition(.scale(scale: 0.1).combined(with: .opacity))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - 页面过渡动画
|
|
||||||
struct PageTransition<Content: View>: View {
|
|
||||||
let content: () -> Content
|
|
||||||
@State private var isVisible = false
|
|
||||||
let transitionType: TransitionType
|
|
||||||
|
|
||||||
enum TransitionType {
|
|
||||||
case slide, fade, scale, rotate
|
|
||||||
}
|
|
||||||
|
|
||||||
init(type: TransitionType = .fade, @ViewBuilder content: @escaping () -> Content) {
|
|
||||||
self.transitionType = type
|
|
||||||
self.content = content
|
|
||||||
}
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
content()
|
|
||||||
.opacity(isVisible ? 1 : 0)
|
|
||||||
.scaleEffect(transitionType == .scale ? (isVisible ? 1 : 0.8) : 1)
|
|
||||||
.offset(y: transitionType == .slide ? (isVisible ? 0 : 50) : 0)
|
|
||||||
.rotationEffect(.degrees(transitionType == .rotate ? (isVisible ? 0 : 90) : 0))
|
|
||||||
.onAppear {
|
|
||||||
withAnimation(AnimationConfig.smooth) {
|
|
||||||
isVisible = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - 预览
|
|
||||||
#Preview("动画组件") {
|
|
||||||
VStack(spacing: 20) {
|
|
||||||
AnimatedGradientText("情绪博物馆")
|
|
||||||
|
|
||||||
AnimatedProgressBar(progress: 0.7)
|
|
||||||
.frame(height: 10)
|
|
||||||
|
|
||||||
FloatingActionButton(icon: "plus") {
|
|
||||||
print("FAB tapped")
|
|
||||||
}
|
|
||||||
|
|
||||||
RippleEffect(color: .blue, size: 60)
|
|
||||||
}
|
|
||||||
.padding()
|
|
||||||
}
|
|
||||||
@@ -1,395 +0,0 @@
|
|||||||
//
|
|
||||||
// ChakraHealingView.swift
|
|
||||||
// EmotionMuseum
|
|
||||||
//
|
|
||||||
// Created by 华中敏 on 2025/6/13.
|
|
||||||
//
|
|
||||||
|
|
||||||
import SwiftUI
|
|
||||||
import AVFoundation
|
|
||||||
|
|
||||||
struct ChakraHealingView: View {
|
|
||||||
let chakra: ChakraType
|
|
||||||
let onComplete: () -> Void
|
|
||||||
|
|
||||||
@State private var isPlaying = false
|
|
||||||
@State private var progress: Double = 0.0
|
|
||||||
@State private var timer: Timer? = nil
|
|
||||||
@State private var breathingPhase: BreathingPhase = .inhale
|
|
||||||
@State private var breathCount = 0
|
|
||||||
@State private var beforeScore: Int? = nil
|
|
||||||
@State private var afterScore: Int? = nil
|
|
||||||
@State private var showingCompletionSheet = false
|
|
||||||
|
|
||||||
// 粒子系统状态
|
|
||||||
@State private var particles: [Particle] = []
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
ZStack {
|
|
||||||
// 背景色彩
|
|
||||||
chakra.color.opacity(0.2)
|
|
||||||
.ignoresSafeArea()
|
|
||||||
|
|
||||||
// 动态背景
|
|
||||||
ZStack {
|
|
||||||
// 呼吸引导圆圈
|
|
||||||
Circle()
|
|
||||||
.fill(
|
|
||||||
RadialGradient(
|
|
||||||
gradient: Gradient(colors: [chakra.color, chakra.color.opacity(0.5)]),
|
|
||||||
center: .center,
|
|
||||||
startRadius: 10,
|
|
||||||
endRadius: 150
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.frame(width: breathingPhase == .inhale ? 200 : 150,
|
|
||||||
height: breathingPhase == .inhale ? 200 : 150)
|
|
||||||
.opacity(0.7)
|
|
||||||
.animation(.easeInOut(duration: breathingPhase == .inhale ? 4 : 4), value: breathingPhase)
|
|
||||||
|
|
||||||
// 粒子效果
|
|
||||||
ForEach(particles) { particle in
|
|
||||||
Circle()
|
|
||||||
.fill(chakra.color.opacity(particle.opacity))
|
|
||||||
.frame(width: particle.size, height: particle.size)
|
|
||||||
.position(particle.position)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 内容
|
|
||||||
VStack(spacing: 30) {
|
|
||||||
// 顶部信息
|
|
||||||
VStack(spacing: 8) {
|
|
||||||
Text(chakra.rawValue)
|
|
||||||
.font(.largeTitle)
|
|
||||||
.fontWeight(.bold)
|
|
||||||
.foregroundColor(chakra.color)
|
|
||||||
|
|
||||||
Text(chakra.description)
|
|
||||||
.font(.subheadline)
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
}
|
|
||||||
.padding(.top, 40)
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
|
|
||||||
// 呼吸引导文字
|
|
||||||
Text(breathingPhase == .inhale ? "吸气..." : "呼气...")
|
|
||||||
.font(.title)
|
|
||||||
.fontWeight(.medium)
|
|
||||||
.foregroundColor(chakra.color)
|
|
||||||
.opacity(isPlaying ? 1 : 0)
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
|
|
||||||
// 控制区域
|
|
||||||
VStack(spacing: 20) {
|
|
||||||
// 进度条
|
|
||||||
ProgressView(value: progress)
|
|
||||||
.progressViewStyle(LinearProgressViewStyle(tint: chakra.color))
|
|
||||||
.padding(.horizontal)
|
|
||||||
|
|
||||||
// 播放控制
|
|
||||||
HStack(spacing: 40) {
|
|
||||||
Button(action: {
|
|
||||||
if beforeScore == nil {
|
|
||||||
// 如果还没开始,先评分
|
|
||||||
showBeforeScorePrompt()
|
|
||||||
} else {
|
|
||||||
togglePlayback()
|
|
||||||
}
|
|
||||||
}) {
|
|
||||||
Image(systemName: isPlaying ? "pause.circle.fill" : "play.circle.fill")
|
|
||||||
.font(.system(size: 60))
|
|
||||||
.foregroundColor(chakra.color)
|
|
||||||
}
|
|
||||||
|
|
||||||
Button(action: {
|
|
||||||
completeSession()
|
|
||||||
}) {
|
|
||||||
Image(systemName: "xmark.circle.fill")
|
|
||||||
.font(.system(size: 40))
|
|
||||||
.foregroundColor(.gray)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding(.bottom, 30)
|
|
||||||
}
|
|
||||||
.background(
|
|
||||||
Rectangle()
|
|
||||||
.fill(Color(.systemBackground))
|
|
||||||
.cornerRadius(30, corners: [.topLeft, .topRight])
|
|
||||||
.shadow(color: Color.black.opacity(0.1), radius: 10, x: 0, y: -5)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.onAppear {
|
|
||||||
setupParticles()
|
|
||||||
}
|
|
||||||
.onDisappear {
|
|
||||||
stopSession()
|
|
||||||
}
|
|
||||||
.sheet(isPresented: $showingCompletionSheet) {
|
|
||||||
SessionCompletionView(chakra: chakra, beforeScore: beforeScore ?? 5, afterScore: afterScore ?? 5) {
|
|
||||||
onComplete()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - 私有方法
|
|
||||||
|
|
||||||
private func setupParticles() {
|
|
||||||
// 创建初始粒子
|
|
||||||
for _ in 0..<20 {
|
|
||||||
particles.append(Particle.random(in: UIScreen.main.bounds, color: chakra.color))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func togglePlayback() {
|
|
||||||
isPlaying.toggle()
|
|
||||||
|
|
||||||
if isPlaying {
|
|
||||||
startSession()
|
|
||||||
} else {
|
|
||||||
pauseSession()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func startSession() {
|
|
||||||
// 开始呼吸动画
|
|
||||||
startBreathingAnimation()
|
|
||||||
|
|
||||||
// 开始粒子动画
|
|
||||||
animateParticles()
|
|
||||||
|
|
||||||
// 模拟进度
|
|
||||||
timer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { _ in
|
|
||||||
if progress < 1.0 {
|
|
||||||
progress += 0.0005 // 大约需要30分钟完成
|
|
||||||
} else {
|
|
||||||
completeSession()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func pauseSession() {
|
|
||||||
timer?.invalidate()
|
|
||||||
}
|
|
||||||
|
|
||||||
private func stopSession() {
|
|
||||||
timer?.invalidate()
|
|
||||||
timer = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
private func startBreathingAnimation() {
|
|
||||||
// 呼吸动画循环
|
|
||||||
Timer.scheduledTimer(withTimeInterval: 4, repeats: true) { _ in
|
|
||||||
withAnimation {
|
|
||||||
breathingPhase = breathingPhase == .inhale ? .exhale : .inhale
|
|
||||||
}
|
|
||||||
|
|
||||||
breathCount += 1
|
|
||||||
if breathCount >= 100 {
|
|
||||||
completeSession()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func animateParticles() {
|
|
||||||
// 粒子动画
|
|
||||||
Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { _ in
|
|
||||||
for i in 0..<particles.count {
|
|
||||||
if i < particles.count {
|
|
||||||
particles[i].move()
|
|
||||||
|
|
||||||
// 如果粒子移出屏幕,重新生成
|
|
||||||
if !UIScreen.main.bounds.contains(particles[i].position) {
|
|
||||||
particles[i] = Particle.random(in: UIScreen.main.bounds, color: chakra.color)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func showBeforeScorePrompt() {
|
|
||||||
// 在实际应用中,这里应该显示一个评分对话框
|
|
||||||
// 简化起见,我们直接设置一个默认值
|
|
||||||
beforeScore = 5
|
|
||||||
togglePlayback()
|
|
||||||
}
|
|
||||||
|
|
||||||
private func completeSession() {
|
|
||||||
stopSession()
|
|
||||||
isPlaying = false
|
|
||||||
|
|
||||||
// 设置完成后评分(实际应用中应该弹出评分对话框)
|
|
||||||
afterScore = 8
|
|
||||||
|
|
||||||
// 显示完成页面
|
|
||||||
showingCompletionSheet = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - 呼吸阶段枚举
|
|
||||||
enum BreathingPhase {
|
|
||||||
case inhale
|
|
||||||
case exhale
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - 粒子模型
|
|
||||||
struct Particle: Identifiable {
|
|
||||||
let id = UUID()
|
|
||||||
var position: CGPoint
|
|
||||||
var direction: CGVector
|
|
||||||
var speed: CGFloat
|
|
||||||
var size: CGFloat
|
|
||||||
var opacity: Double
|
|
||||||
|
|
||||||
mutating func move() {
|
|
||||||
position.x += direction.dx * speed
|
|
||||||
position.y += direction.dy * speed
|
|
||||||
opacity = max(0, opacity - 0.001)
|
|
||||||
|
|
||||||
if opacity <= 0.1 {
|
|
||||||
opacity = Double.random(in: 0.3...0.7)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static func random(in rect: CGRect, color: Color) -> Particle {
|
|
||||||
let position = CGPoint(
|
|
||||||
x: CGFloat.random(in: rect.minX...rect.maxX),
|
|
||||||
y: CGFloat.random(in: rect.minY...rect.maxY)
|
|
||||||
)
|
|
||||||
|
|
||||||
let angle = CGFloat.random(in: 0...(2 * .pi))
|
|
||||||
let direction = CGVector(
|
|
||||||
dx: cos(angle),
|
|
||||||
dy: sin(angle)
|
|
||||||
)
|
|
||||||
|
|
||||||
return Particle(
|
|
||||||
position: position,
|
|
||||||
direction: direction,
|
|
||||||
speed: CGFloat.random(in: 0.5...2.0),
|
|
||||||
size: CGFloat.random(in: 3...8),
|
|
||||||
opacity: Double.random(in: 0.3...0.7)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - 会话完成视图
|
|
||||||
struct SessionCompletionView: View {
|
|
||||||
let chakra: ChakraType
|
|
||||||
let beforeScore: Int
|
|
||||||
let afterScore: Int
|
|
||||||
let onDismiss: () -> Void
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
VStack(spacing: 20) {
|
|
||||||
// 标题
|
|
||||||
Text("疗愈完成")
|
|
||||||
.font(.title)
|
|
||||||
.fontWeight(.bold)
|
|
||||||
|
|
||||||
// 脉轮信息
|
|
||||||
HStack(spacing: 12) {
|
|
||||||
Circle()
|
|
||||||
.fill(chakra.color)
|
|
||||||
.frame(width: 30, height: 30)
|
|
||||||
|
|
||||||
Text(chakra.rawValue)
|
|
||||||
.font(.headline)
|
|
||||||
}
|
|
||||||
|
|
||||||
Divider()
|
|
||||||
|
|
||||||
// 疗愈效果
|
|
||||||
VStack(alignment: .leading, spacing: 16) {
|
|
||||||
Text("疗愈效果")
|
|
||||||
.font(.headline)
|
|
||||||
|
|
||||||
HStack(spacing: 30) {
|
|
||||||
VStack {
|
|
||||||
Text("疗愈前")
|
|
||||||
.font(.subheadline)
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
|
|
||||||
Text("\(beforeScore)")
|
|
||||||
.font(.title)
|
|
||||||
.fontWeight(.bold)
|
|
||||||
}
|
|
||||||
|
|
||||||
Image(systemName: "arrow.right")
|
|
||||||
.font(.title2)
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
|
|
||||||
VStack {
|
|
||||||
Text("疗愈后")
|
|
||||||
.font(.subheadline)
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
|
|
||||||
Text("\(afterScore)")
|
|
||||||
.font(.title)
|
|
||||||
.fontWeight(.bold)
|
|
||||||
.foregroundColor(.green)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding()
|
|
||||||
.background(Color(.systemGray6))
|
|
||||||
.cornerRadius(12)
|
|
||||||
}
|
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
|
||||||
.padding(.horizontal)
|
|
||||||
|
|
||||||
// 反馈信息
|
|
||||||
VStack(alignment: .leading, spacing: 12) {
|
|
||||||
Text("反馈")
|
|
||||||
.font(.headline)
|
|
||||||
|
|
||||||
Text("你的\(chakra.rawValue)能量已得到提升,情绪状态有所改善。建议每天进行一次疗愈,保持能量平衡。")
|
|
||||||
.font(.body)
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
}
|
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
|
||||||
.padding(.horizontal)
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
|
|
||||||
// 按钮
|
|
||||||
Button(action: {
|
|
||||||
onDismiss()
|
|
||||||
}) {
|
|
||||||
Text("返回")
|
|
||||||
.font(.headline)
|
|
||||||
.foregroundColor(.white)
|
|
||||||
.frame(maxWidth: .infinity)
|
|
||||||
.padding()
|
|
||||||
.background(chakra.color)
|
|
||||||
.cornerRadius(12)
|
|
||||||
}
|
|
||||||
.padding(.horizontal)
|
|
||||||
.padding(.bottom)
|
|
||||||
}
|
|
||||||
.padding(.top, 40)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - 扩展
|
|
||||||
extension View {
|
|
||||||
func cornerRadius(_ radius: CGFloat, corners: UIRectCorner) -> some View {
|
|
||||||
clipShape(RoundedCorner(radius: radius, corners: corners))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct RoundedCorner: Shape {
|
|
||||||
var radius: CGFloat = .infinity
|
|
||||||
var corners: UIRectCorner = .allCorners
|
|
||||||
|
|
||||||
func path(in rect: CGRect) -> Path {
|
|
||||||
let path = UIBezierPath(roundedRect: rect, byRoundingCorners: corners, cornerRadii: CGSize(width: radius, height: radius))
|
|
||||||
return Path(path.cgPath)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#Preview {
|
|
||||||
ChakraHealingView(chakra: .heart, onComplete: {})
|
|
||||||
}
|
|
||||||
@@ -1,115 +0,0 @@
|
|||||||
//
|
|
||||||
// ConversationPreviewCard.swift
|
|
||||||
// EmotionMuseum
|
|
||||||
//
|
|
||||||
// Created by 华中敏 on 2025/7/5.
|
|
||||||
//
|
|
||||||
|
|
||||||
import SwiftUI
|
|
||||||
|
|
||||||
struct ConversationPreviewCard: View {
|
|
||||||
let conversation: Conversation
|
|
||||||
let onTap: () -> Void
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
Button(action: onTap) {
|
|
||||||
HStack(spacing: 12) {
|
|
||||||
// 对话类型图标
|
|
||||||
Image(systemName: "message.circle.fill")
|
|
||||||
.font(.title2)
|
|
||||||
.foregroundColor(Color("AccentColor"))
|
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 4) {
|
|
||||||
// 对话标题
|
|
||||||
Text(conversation.title)
|
|
||||||
.font(.headline)
|
|
||||||
.foregroundColor(Color("PrimaryText"))
|
|
||||||
.lineLimit(1)
|
|
||||||
|
|
||||||
// 最后一条消息或摘要
|
|
||||||
if let lastMessage = conversation.lastMessage {
|
|
||||||
Text(lastMessage.content)
|
|
||||||
.font(.subheadline)
|
|
||||||
.foregroundColor(Color("SecondaryText"))
|
|
||||||
.lineLimit(2)
|
|
||||||
} else if let summary = conversation.summary {
|
|
||||||
Text(summary)
|
|
||||||
.font(.subheadline)
|
|
||||||
.foregroundColor(Color("SecondaryText"))
|
|
||||||
.lineLimit(2)
|
|
||||||
} else {
|
|
||||||
Text("点击查看对话详情")
|
|
||||||
.font(.subheadline)
|
|
||||||
.foregroundColor(Color("TertiaryText"))
|
|
||||||
}
|
|
||||||
|
|
||||||
// 时间和消息数量
|
|
||||||
HStack {
|
|
||||||
Text(conversation.startTime.timeAgo)
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundColor(Color("TertiaryText"))
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
|
|
||||||
Text("\(conversation.messageCount) 条消息")
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundColor(Color("TertiaryText"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
|
|
||||||
// 情绪分析指示器
|
|
||||||
if let emotion = conversation.emotionAnalysis?.primaryEmotion {
|
|
||||||
VStack {
|
|
||||||
Text(emotion.emoji)
|
|
||||||
.font(.title2)
|
|
||||||
|
|
||||||
Text(emotion.rawValue)
|
|
||||||
.font(.caption2)
|
|
||||||
.foregroundColor(emotion.color)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding()
|
|
||||||
.background(
|
|
||||||
RoundedRectangle(cornerRadius: 12)
|
|
||||||
.fill(Color("CardBackground"))
|
|
||||||
.shadow(
|
|
||||||
color: Color.black.opacity(0.05),
|
|
||||||
radius: 3,
|
|
||||||
x: 0,
|
|
||||||
y: 2
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
.buttonStyle(PlainButtonStyle())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#Preview {
|
|
||||||
ConversationPreviewCard(
|
|
||||||
conversation: Conversation(
|
|
||||||
userId: UUID(),
|
|
||||||
title: "今天的心情分享",
|
|
||||||
messages: [
|
|
||||||
Message(
|
|
||||||
conversationId: UUID(),
|
|
||||||
content: "我今天感觉有点焦虑,不知道该怎么办。",
|
|
||||||
type: .text,
|
|
||||||
sender: .user
|
|
||||||
)
|
|
||||||
],
|
|
||||||
emotionAnalysis: EmotionAnalysis(
|
|
||||||
primaryEmotion: .anxiety,
|
|
||||||
emotionIntensity: 0.7,
|
|
||||||
emotionTrend: .stable,
|
|
||||||
keywords: ["焦虑", "压力"],
|
|
||||||
aiInsights: "用户表现出一定程度的焦虑情绪"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
print("Tapped conversation")
|
|
||||||
}
|
|
||||||
.padding()
|
|
||||||
}
|
|
||||||
@@ -1,226 +0,0 @@
|
|||||||
//
|
|
||||||
// ExploreView.swift
|
|
||||||
// EmotionMuseum
|
|
||||||
//
|
|
||||||
// Created by 华中敏 on 2025/6/13.
|
|
||||||
//
|
|
||||||
|
|
||||||
import SwiftUI
|
|
||||||
import MapKit
|
|
||||||
import CoreLocation
|
|
||||||
|
|
||||||
// MARK: - 探索页面主视图
|
|
||||||
struct ExploreView: View {
|
|
||||||
@EnvironmentObject var mockDataManager: MockDataManager
|
|
||||||
@EnvironmentObject var navigationManager: NavigationManager
|
|
||||||
@State private var selectedLocation: LocationPin?
|
|
||||||
@State private var showingLocationDetail = false
|
|
||||||
@State private var showingCommunityFeed = false
|
|
||||||
@State private var savedLocations: [LocationPin] = []
|
|
||||||
@State private var showingMapView = true
|
|
||||||
@State private var showingLocationPicker = false
|
|
||||||
@State private var shouldMoveToLocationPin = false
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
NavigationView {
|
|
||||||
VStack(spacing: 0) {
|
|
||||||
// 顶部切换按钮
|
|
||||||
HStack {
|
|
||||||
Button(action: {
|
|
||||||
showingMapView = true
|
|
||||||
showingCommunityFeed = false
|
|
||||||
}) {
|
|
||||||
HStack {
|
|
||||||
Image(systemName: "map")
|
|
||||||
Text("情绪地图")
|
|
||||||
}
|
|
||||||
.foregroundColor(showingMapView ? .white : .primary)
|
|
||||||
.padding(.horizontal, 16)
|
|
||||||
.padding(.vertical, 8)
|
|
||||||
.background(showingMapView ? Color.blue : Color.clear)
|
|
||||||
.cornerRadius(20)
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
|
|
||||||
Button(action: {
|
|
||||||
showingMapView = false
|
|
||||||
showingCommunityFeed = true
|
|
||||||
}) {
|
|
||||||
HStack {
|
|
||||||
Image(systemName: "person.3")
|
|
||||||
Text("社区分享")
|
|
||||||
}
|
|
||||||
.foregroundColor(showingCommunityFeed ? .white : .primary)
|
|
||||||
.padding(.horizontal, 16)
|
|
||||||
.padding(.vertical, 8)
|
|
||||||
.background(showingCommunityFeed ? Color.green : Color.clear)
|
|
||||||
.cornerRadius(20)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding()
|
|
||||||
.background(Color(.systemGray6))
|
|
||||||
|
|
||||||
// 主要内容区域
|
|
||||||
if showingMapView {
|
|
||||||
mapViewSection
|
|
||||||
} else {
|
|
||||||
communityFeedSection
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.navigationTitle("探索")
|
|
||||||
.navigationBarTitleDisplayMode(.large)
|
|
||||||
.onAppear {
|
|
||||||
loadSavedLocations()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.sheet(isPresented: $showingLocationDetail) {
|
|
||||||
if let location = selectedLocation {
|
|
||||||
LocationDetailView(location: location)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - 地图视图部分
|
|
||||||
private var mapViewSection: some View {
|
|
||||||
VStack {
|
|
||||||
// 地图区域
|
|
||||||
ZStack {
|
|
||||||
MapView()
|
|
||||||
.frame(height: 300)
|
|
||||||
.cornerRadius(12)
|
|
||||||
.padding()
|
|
||||||
}
|
|
||||||
// 推荐位置列表
|
|
||||||
VStack(alignment: .leading, spacing: 12) {
|
|
||||||
HStack {
|
|
||||||
Text("推荐地点")
|
|
||||||
.font(.headline)
|
|
||||||
Spacer()
|
|
||||||
Button("查看全部") {
|
|
||||||
// 查看全部推荐地点
|
|
||||||
}
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundColor(.blue)
|
|
||||||
}
|
|
||||||
ScrollView(.horizontal, showsIndicators: false) {
|
|
||||||
HStack(spacing: 12) {
|
|
||||||
ForEach(Array(savedLocations.prefix(5))) { location in
|
|
||||||
LocationCard(location: location) {
|
|
||||||
selectedLocation = location
|
|
||||||
showingLocationDetail = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding(.horizontal)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding()
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - 社区动态部分
|
|
||||||
private var communityFeedSection: some View {
|
|
||||||
VStack {
|
|
||||||
ScrollView {
|
|
||||||
LazyVStack(spacing: 16) {
|
|
||||||
ForEach(mockDataManager.communityPosts) { post in
|
|
||||||
CommunityPostCard(post: post)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - 私有方法
|
|
||||||
private func loadSavedLocations() {
|
|
||||||
// 创建一些示例位置数据
|
|
||||||
savedLocations = [
|
|
||||||
LocationPin(
|
|
||||||
coordinate: Coordinate(latitude: 39.9042, longitude: 116.4074),
|
|
||||||
title: "天安门广场",
|
|
||||||
description: "庄严肃穆的历史地标",
|
|
||||||
type: .popular,
|
|
||||||
emotionTags: [.neutral],
|
|
||||||
category: .other
|
|
||||||
),
|
|
||||||
LocationPin(
|
|
||||||
coordinate: Coordinate(latitude: 39.9163, longitude: 116.3972),
|
|
||||||
title: "故宫博物院",
|
|
||||||
description: "深厚的历史文化底蕴",
|
|
||||||
type: .aiRecommended,
|
|
||||||
emotionTags: [.surprise],
|
|
||||||
category: .museum
|
|
||||||
),
|
|
||||||
LocationPin(
|
|
||||||
coordinate: Coordinate(latitude: 39.9925, longitude: 116.3135),
|
|
||||||
title: "颐和园",
|
|
||||||
description: "宁静优美的皇家园林",
|
|
||||||
type: .personal,
|
|
||||||
emotionTags: [.contentment],
|
|
||||||
category: .garden
|
|
||||||
),
|
|
||||||
LocationPin(
|
|
||||||
coordinate: Coordinate(latitude: 40.0090, longitude: 116.3348),
|
|
||||||
title: "圆明园",
|
|
||||||
description: "历史的见证与思考",
|
|
||||||
type: .community,
|
|
||||||
emotionTags: [.sadness],
|
|
||||||
category: .park
|
|
||||||
),
|
|
||||||
LocationPin(
|
|
||||||
coordinate: Coordinate(latitude: 39.9059, longitude: 116.3913),
|
|
||||||
title: "北海公园",
|
|
||||||
description: "古典园林的宁静之美",
|
|
||||||
type: .popular,
|
|
||||||
emotionTags: [.joy],
|
|
||||||
category: .park
|
|
||||||
)
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - 位置卡片组件
|
|
||||||
struct LocationCard: View {
|
|
||||||
let location: LocationPin
|
|
||||||
let onTap: () -> Void
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
Button(action: onTap) {
|
|
||||||
VStack(alignment: .leading, spacing: 8) {
|
|
||||||
// 位置图片占位符
|
|
||||||
Rectangle()
|
|
||||||
.fill(Color(.systemGray5))
|
|
||||||
.frame(width: 120, height: 80)
|
|
||||||
.overlay(
|
|
||||||
Image(systemName: location.category.icon)
|
|
||||||
.font(.title)
|
|
||||||
.foregroundColor(.gray)
|
|
||||||
)
|
|
||||||
.cornerRadius(8)
|
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 4) {
|
|
||||||
Text(location.title)
|
|
||||||
.font(.caption)
|
|
||||||
.fontWeight(.medium)
|
|
||||||
.lineLimit(1)
|
|
||||||
|
|
||||||
Text(location.description)
|
|
||||||
.font(.caption2)
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
.lineLimit(2)
|
|
||||||
}
|
|
||||||
.frame(width: 120, alignment: .leading)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.buttonStyle(PlainButtonStyle())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#Preview {
|
|
||||||
ExploreView()
|
|
||||||
.environmentObject(MockDataManager.shared)
|
|
||||||
.environmentObject(NavigationManager())
|
|
||||||
}
|
|
||||||
@@ -1,808 +0,0 @@
|
|||||||
//
|
|
||||||
// GrowthView.swift
|
|
||||||
// EmotionMuseum
|
|
||||||
//
|
|
||||||
// Created by 华中敏 on 2025/6/13.
|
|
||||||
//
|
|
||||||
|
|
||||||
import SwiftUI
|
|
||||||
|
|
||||||
// MARK: - 治愈页面主视图
|
|
||||||
struct GrowthView: View {
|
|
||||||
@StateObject private var themeManager = ThemeManager()
|
|
||||||
@State private var showingTopicDetail = false
|
|
||||||
@State private var selectedTopic: GrowthTopic?
|
|
||||||
@State private var showingInsights = false
|
|
||||||
@State private var showingRadarChart = false
|
|
||||||
@State private var loadingState: LoadingState = .idle
|
|
||||||
@State private var isInitialLoading = true
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
NavigationView {
|
|
||||||
LoadingStateView(loadingState: isInitialLoading ? .loading : .loaded) {
|
|
||||||
ScrollView {
|
|
||||||
VStack(spacing: 24) {
|
|
||||||
// 情绪洞察卡片
|
|
||||||
EmotionalInsightCard {
|
|
||||||
showingInsights = true
|
|
||||||
}
|
|
||||||
.transition(.scale(scale: 0.8).combined(with: .opacity))
|
|
||||||
|
|
||||||
// 用户画像五维图
|
|
||||||
UserProfileRadarCard {
|
|
||||||
showingRadarChart = true
|
|
||||||
}
|
|
||||||
.transition(.scale(scale: 0.8).combined(with: .opacity))
|
|
||||||
|
|
||||||
// 成长课题系统
|
|
||||||
GrowthTopicsSection { topic in
|
|
||||||
selectedTopic = topic
|
|
||||||
showingTopicDetail = true
|
|
||||||
}
|
|
||||||
.transition(.scale(scale: 0.8).combined(with: .opacity))
|
|
||||||
|
|
||||||
// 今日推荐行动
|
|
||||||
TodayActionCard()
|
|
||||||
.transition(.scale(scale: 0.8).combined(with: .opacity))
|
|
||||||
|
|
||||||
// 成长历程
|
|
||||||
GrowthTimelineCard()
|
|
||||||
.transition(.scale(scale: 0.8).combined(with: .opacity))
|
|
||||||
}
|
|
||||||
.padding(.horizontal)
|
|
||||||
.padding(.vertical)
|
|
||||||
}
|
|
||||||
.refreshable {
|
|
||||||
await refreshData()
|
|
||||||
}
|
|
||||||
} loadingView: {
|
|
||||||
AnyView(growthViewSkeleton)
|
|
||||||
}
|
|
||||||
.navigationTitle("成长治愈")
|
|
||||||
.navigationBarTitleDisplayMode(.large)
|
|
||||||
}
|
|
||||||
.environmentObject(themeManager)
|
|
||||||
.preferredColorScheme(themeManager.systemFollowsDeviceTheme ? nil : (themeManager.isDarkMode ? .dark : .light))
|
|
||||||
.sheet(isPresented: $showingTopicDetail) {
|
|
||||||
if let topic = selectedTopic {
|
|
||||||
TopicDetailView(topic: topic)
|
|
||||||
.environmentObject(themeManager)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.sheet(isPresented: $showingInsights) {
|
|
||||||
EmotionalInsightsView()
|
|
||||||
.environmentObject(themeManager)
|
|
||||||
}
|
|
||||||
.sheet(isPresented: $showingRadarChart) {
|
|
||||||
NavigationView {
|
|
||||||
VStack {
|
|
||||||
Text("用户画像雷达图")
|
|
||||||
.font(.title)
|
|
||||||
.padding()
|
|
||||||
|
|
||||||
Text("这里显示用户的多维度能力雷达图")
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
.padding()
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
.navigationTitle("用户画像")
|
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
|
||||||
.toolbar {
|
|
||||||
ToolbarItem(placement: .navigationBarTrailing) {
|
|
||||||
Button("关闭") {
|
|
||||||
showingRadarChart = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.environmentObject(themeManager)
|
|
||||||
}
|
|
||||||
.onAppear {
|
|
||||||
simulateInitialLoading()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - 私有方法
|
|
||||||
private func simulateInitialLoading() {
|
|
||||||
loadingState = .loading
|
|
||||||
|
|
||||||
// 模拟异步加载过程
|
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 1.2) {
|
|
||||||
withAnimation(.easeOut(duration: 0.8)) {
|
|
||||||
isInitialLoading = false
|
|
||||||
loadingState = .loaded
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func refreshData() async {
|
|
||||||
loadingState = .loading
|
|
||||||
|
|
||||||
// 模拟数据刷新
|
|
||||||
try? await Task.sleep(nanoseconds: 800_000_000) // 0.8秒
|
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
withAnimation(.easeOut(duration: 0.6)) {
|
|
||||||
loadingState = .loaded
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - 骨架屏视图
|
|
||||||
private var growthViewSkeleton: some View {
|
|
||||||
ScrollView {
|
|
||||||
VStack(spacing: 24) {
|
|
||||||
// 情绪洞察卡片骨架屏
|
|
||||||
insightCardSkeleton
|
|
||||||
|
|
||||||
// 用户画像卡片骨架屏
|
|
||||||
radarCardSkeleton
|
|
||||||
|
|
||||||
// 成长课题骨架屏
|
|
||||||
topicsGridSkeleton
|
|
||||||
|
|
||||||
// 今日推荐骨架屏
|
|
||||||
actionCardSkeleton
|
|
||||||
|
|
||||||
// 成长历程骨架屏
|
|
||||||
timelineCardSkeleton
|
|
||||||
}
|
|
||||||
.padding(.horizontal)
|
|
||||||
.padding(.vertical)
|
|
||||||
}
|
|
||||||
.background(Color.theme.background)
|
|
||||||
}
|
|
||||||
|
|
||||||
private var insightCardSkeleton: some View {
|
|
||||||
VStack(alignment: .leading, spacing: 16) {
|
|
||||||
HStack {
|
|
||||||
VStack(alignment: .leading, spacing: 4) {
|
|
||||||
SkeletonView(width: 80, height: 20, cornerRadius: 6)
|
|
||||||
SkeletonView(width: 140, height: 12, cornerRadius: 3)
|
|
||||||
}
|
|
||||||
Spacer()
|
|
||||||
SkeletonView(width: 24, height: 24, cornerRadius: 12)
|
|
||||||
}
|
|
||||||
|
|
||||||
VStack(spacing: 12) {
|
|
||||||
ForEach(0..<3, id: \.self) { _ in
|
|
||||||
HStack(spacing: 12) {
|
|
||||||
SkeletonView(width: 20, height: 16, cornerRadius: 4)
|
|
||||||
SkeletonView(width: 80, height: 14, cornerRadius: 3)
|
|
||||||
Spacer()
|
|
||||||
SkeletonView(width: 60, height: 14, cornerRadius: 3)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
HStack {
|
|
||||||
SkeletonView(width: 100, height: 12, cornerRadius: 3)
|
|
||||||
Spacer()
|
|
||||||
SkeletonView(width: 12, height: 12, cornerRadius: 3)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding()
|
|
||||||
.background(Color.theme.cardBackground)
|
|
||||||
.cornerRadius(16)
|
|
||||||
}
|
|
||||||
|
|
||||||
private var radarCardSkeleton: some View {
|
|
||||||
VStack(spacing: 16) {
|
|
||||||
HStack {
|
|
||||||
SkeletonView(width: 80, height: 20, cornerRadius: 6)
|
|
||||||
Spacer()
|
|
||||||
SkeletonView(width: 24, height: 24, cornerRadius: 12)
|
|
||||||
}
|
|
||||||
|
|
||||||
VStack(spacing: 8) {
|
|
||||||
ForEach(0..<5, id: \.self) { _ in
|
|
||||||
HStack {
|
|
||||||
SkeletonView(width: 60, height: 12, cornerRadius: 3)
|
|
||||||
SkeletonView(height: 8, cornerRadius: 4)
|
|
||||||
SkeletonView(width: 40, height: 12, cornerRadius: 3)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
HStack {
|
|
||||||
SkeletonView(width: 120, height: 12, cornerRadius: 3)
|
|
||||||
Spacer()
|
|
||||||
SkeletonView(width: 12, height: 12, cornerRadius: 3)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding()
|
|
||||||
.background(Color.theme.cardBackground)
|
|
||||||
.cornerRadius(16)
|
|
||||||
}
|
|
||||||
|
|
||||||
private var topicsGridSkeleton: some View {
|
|
||||||
VStack(alignment: .leading, spacing: 16) {
|
|
||||||
HStack {
|
|
||||||
SkeletonView(width: 80, height: 20, cornerRadius: 6)
|
|
||||||
Spacer()
|
|
||||||
SkeletonView(width: 60, height: 12, cornerRadius: 3)
|
|
||||||
}
|
|
||||||
|
|
||||||
LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: 2), spacing: 12) {
|
|
||||||
ForEach(0..<4, id: \.self) { _ in
|
|
||||||
VStack(alignment: .leading, spacing: 12) {
|
|
||||||
HStack {
|
|
||||||
SkeletonView(width: 20, height: 20, cornerRadius: 10)
|
|
||||||
Spacer()
|
|
||||||
SkeletonView(width: 30, height: 12, cornerRadius: 3)
|
|
||||||
}
|
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 4) {
|
|
||||||
SkeletonView(width: 100, height: 14, cornerRadius: 3)
|
|
||||||
SkeletonView(width: 120, height: 12, cornerRadius: 3)
|
|
||||||
SkeletonView(width: 80, height: 12, cornerRadius: 3)
|
|
||||||
}
|
|
||||||
|
|
||||||
VStack(spacing: 4) {
|
|
||||||
HStack {
|
|
||||||
SkeletonView(width: 30, height: 10, cornerRadius: 2)
|
|
||||||
Spacer()
|
|
||||||
SkeletonView(width: 30, height: 10, cornerRadius: 2)
|
|
||||||
}
|
|
||||||
SkeletonView(height: 4, cornerRadius: 2)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding()
|
|
||||||
.background(Color.theme.cardBackground)
|
|
||||||
.cornerRadius(12)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private var actionCardSkeleton: some View {
|
|
||||||
VStack(alignment: .leading, spacing: 16) {
|
|
||||||
HStack {
|
|
||||||
SkeletonView(width: 80, height: 20, cornerRadius: 6)
|
|
||||||
Spacer()
|
|
||||||
SkeletonView(width: 24, height: 24, cornerRadius: 12)
|
|
||||||
}
|
|
||||||
|
|
||||||
VStack(spacing: 12) {
|
|
||||||
ForEach(0..<3, id: \.self) { _ in
|
|
||||||
HStack(spacing: 12) {
|
|
||||||
SkeletonView(width: 24, height: 24, cornerRadius: 12)
|
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 2) {
|
|
||||||
SkeletonView(width: 160, height: 14, cornerRadius: 3)
|
|
||||||
HStack(spacing: 8) {
|
|
||||||
SkeletonView(width: 60, height: 16, cornerRadius: 8)
|
|
||||||
SkeletonView(width: 40, height: 12, cornerRadius: 3)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding()
|
|
||||||
.background(Color.theme.cardBackground)
|
|
||||||
.cornerRadius(16)
|
|
||||||
}
|
|
||||||
|
|
||||||
private var timelineCardSkeleton: some View {
|
|
||||||
VStack(alignment: .leading, spacing: 16) {
|
|
||||||
SkeletonView(width: 80, height: 20, cornerRadius: 6)
|
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 12) {
|
|
||||||
ForEach(0..<4, id: \.self) { _ in
|
|
||||||
HStack(spacing: 12) {
|
|
||||||
VStack {
|
|
||||||
SkeletonView(width: 8, height: 8, cornerRadius: 4)
|
|
||||||
SkeletonView(width: 1, height: 20, cornerRadius: 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 2) {
|
|
||||||
HStack {
|
|
||||||
SkeletonView(width: 100, height: 14, cornerRadius: 3)
|
|
||||||
Spacer()
|
|
||||||
SkeletonView(width: 40, height: 12, cornerRadius: 3)
|
|
||||||
}
|
|
||||||
SkeletonView(width: 180, height: 12, cornerRadius: 3)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding()
|
|
||||||
.background(Color.theme.cardBackground)
|
|
||||||
.cornerRadius(16)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - 情绪洞察卡片
|
|
||||||
struct EmotionalInsightCard: View {
|
|
||||||
let onTap: () -> Void
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
Button(action: onTap) {
|
|
||||||
VStack(alignment: .leading, spacing: 16) {
|
|
||||||
HStack {
|
|
||||||
VStack(alignment: .leading, spacing: 4) {
|
|
||||||
Text("情绪洞察")
|
|
||||||
.font(.headline)
|
|
||||||
.fontWeight(.semibold)
|
|
||||||
.foregroundColor(Color.theme.primaryText)
|
|
||||||
|
|
||||||
Text("基于你的对话记录生成")
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundColor(Color.theme.secondaryText)
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
|
|
||||||
Image(systemName: "brain.head.profile")
|
|
||||||
.font(.title2)
|
|
||||||
.foregroundColor(.purple)
|
|
||||||
}
|
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 12) {
|
|
||||||
InsightRow(
|
|
||||||
icon: "heart.fill",
|
|
||||||
title: "主要情绪状态",
|
|
||||||
value: "平静自省",
|
|
||||||
color: .blue
|
|
||||||
)
|
|
||||||
|
|
||||||
InsightRow(
|
|
||||||
icon: "target",
|
|
||||||
title: "成长焦点",
|
|
||||||
value: "人际关系",
|
|
||||||
color: .green
|
|
||||||
)
|
|
||||||
|
|
||||||
InsightRow(
|
|
||||||
icon: "chart.line.uptrend.xyaxis",
|
|
||||||
title: "进步指数",
|
|
||||||
value: "↗️ 稳步提升",
|
|
||||||
color: .orange
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
HStack {
|
|
||||||
Text("点击查看详细分析")
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundColor(Color.theme.secondaryText)
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
|
|
||||||
Image(systemName: "chevron.right")
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundColor(Color.theme.secondaryText)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding()
|
|
||||||
.background(
|
|
||||||
RoundedRectangle(cornerRadius: 16)
|
|
||||||
.fill(Color(.systemGray6))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
.buttonStyle(PlainButtonStyle())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct InsightRow: View {
|
|
||||||
let icon: String
|
|
||||||
let title: String
|
|
||||||
let value: String
|
|
||||||
let color: Color
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
HStack(spacing: 12) {
|
|
||||||
Image(systemName: icon)
|
|
||||||
.font(.subheadline)
|
|
||||||
.foregroundColor(color)
|
|
||||||
.frame(width: 20)
|
|
||||||
|
|
||||||
Text(title)
|
|
||||||
.font(.subheadline)
|
|
||||||
.foregroundColor(Color.theme.secondaryText)
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
|
|
||||||
Text(value)
|
|
||||||
.font(.subheadline)
|
|
||||||
.fontWeight(.medium)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - 用户画像雷达图卡片
|
|
||||||
struct UserProfileRadarCard: View {
|
|
||||||
let onTap: () -> Void
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
Button(action: onTap) {
|
|
||||||
VStack(spacing: 16) {
|
|
||||||
HStack {
|
|
||||||
Text("成长画像")
|
|
||||||
.font(.headline)
|
|
||||||
.fontWeight(.semibold)
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
|
|
||||||
Image(systemName: "person.crop.circle.badge.checkmark")
|
|
||||||
.font(.title2)
|
|
||||||
.foregroundColor(.green)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 简化的五维显示
|
|
||||||
VStack(spacing: 8) {
|
|
||||||
ProfileDimensionRow(name: "自我感知", value: 0.8, color: .blue)
|
|
||||||
ProfileDimensionRow(name: "情绪韧性", value: 0.7, color: .purple)
|
|
||||||
ProfileDimensionRow(name: "行动力", value: 0.6, color: .orange)
|
|
||||||
ProfileDimensionRow(name: "共情力", value: 0.9, color: .green)
|
|
||||||
ProfileDimensionRow(name: "生活热度", value: 0.7, color: .red)
|
|
||||||
}
|
|
||||||
|
|
||||||
HStack {
|
|
||||||
Text("点击查看完整雷达图")
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
|
|
||||||
Image(systemName: "chevron.right")
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding()
|
|
||||||
.background(
|
|
||||||
RoundedRectangle(cornerRadius: 16)
|
|
||||||
.fill(Color(.systemGray6))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
.buttonStyle(PlainButtonStyle())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct ProfileDimensionRow: View {
|
|
||||||
let name: String
|
|
||||||
let value: Double
|
|
||||||
let color: Color
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
HStack {
|
|
||||||
Text(name)
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundColor(.primary)
|
|
||||||
.frame(width: 60, alignment: .leading)
|
|
||||||
|
|
||||||
ProgressView(value: value)
|
|
||||||
.progressViewStyle(LinearProgressViewStyle(tint: color))
|
|
||||||
|
|
||||||
Text("\(Int(value * 100))%")
|
|
||||||
.font(.caption)
|
|
||||||
.fontWeight(.medium)
|
|
||||||
.foregroundColor(color)
|
|
||||||
.frame(width: 40, alignment: .trailing)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - 成长课题系统
|
|
||||||
struct GrowthTopicsSection: View {
|
|
||||||
let onTopicTap: (GrowthTopic) -> Void
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
VStack(alignment: .leading, spacing: 16) {
|
|
||||||
HStack {
|
|
||||||
Text("成长课题")
|
|
||||||
.font(.headline)
|
|
||||||
.fontWeight(.semibold)
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
|
|
||||||
Text("3个进行中")
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
}
|
|
||||||
|
|
||||||
LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: 2), spacing: 12) {
|
|
||||||
ForEach(sampleGrowthTopics) { topic in
|
|
||||||
TopicCard(topic: topic) {
|
|
||||||
onTopicTap(topic)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct TopicCard: View {
|
|
||||||
let topic: GrowthTopic
|
|
||||||
let onTap: () -> Void
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
Button(action: onTap) {
|
|
||||||
VStack(alignment: .leading, spacing: 12) {
|
|
||||||
HStack {
|
|
||||||
Image(systemName: topic.icon)
|
|
||||||
.font(.title3)
|
|
||||||
.foregroundColor(topic.color)
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
|
|
||||||
Text("Lv.\(topic.level)")
|
|
||||||
.font(.caption)
|
|
||||||
.fontWeight(.bold)
|
|
||||||
.foregroundColor(topic.color)
|
|
||||||
}
|
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 4) {
|
|
||||||
Text(topic.title)
|
|
||||||
.font(.subheadline)
|
|
||||||
.fontWeight(.medium)
|
|
||||||
.multilineTextAlignment(.leading)
|
|
||||||
|
|
||||||
Text(topic.description)
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
.lineLimit(2)
|
|
||||||
.multilineTextAlignment(.leading)
|
|
||||||
}
|
|
||||||
|
|
||||||
VStack(spacing: 4) {
|
|
||||||
HStack {
|
|
||||||
Text("进度")
|
|
||||||
.font(.caption2)
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
|
|
||||||
Text("\(Int(topic.progress * 100))%")
|
|
||||||
.font(.caption2)
|
|
||||||
.fontWeight(.medium)
|
|
||||||
}
|
|
||||||
|
|
||||||
ProgressView(value: topic.progress)
|
|
||||||
.progressViewStyle(LinearProgressViewStyle(tint: topic.color))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding()
|
|
||||||
.background(
|
|
||||||
RoundedRectangle(cornerRadius: 12)
|
|
||||||
.fill(Color(.systemGray6))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
.buttonStyle(PlainButtonStyle())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - 今日推荐行动
|
|
||||||
struct TodayActionCard: View {
|
|
||||||
var body: some View {
|
|
||||||
VStack(alignment: .leading, spacing: 16) {
|
|
||||||
HStack {
|
|
||||||
Text("今日推荐")
|
|
||||||
.font(.headline)
|
|
||||||
.fontWeight(.semibold)
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
|
|
||||||
Image(systemName: "lightbulb.fill")
|
|
||||||
.font(.title3)
|
|
||||||
.foregroundColor(.yellow)
|
|
||||||
}
|
|
||||||
|
|
||||||
VStack(spacing: 12) {
|
|
||||||
ActionItemView(
|
|
||||||
title: "与朋友分享一件开心的事",
|
|
||||||
category: "人际关系",
|
|
||||||
duration: "5分钟",
|
|
||||||
color: .blue
|
|
||||||
)
|
|
||||||
|
|
||||||
ActionItemView(
|
|
||||||
title: "写下今天的三个感恩点",
|
|
||||||
category: "自我觉察",
|
|
||||||
duration: "10分钟",
|
|
||||||
color: .green
|
|
||||||
)
|
|
||||||
|
|
||||||
ActionItemView(
|
|
||||||
title: "深呼吸练习",
|
|
||||||
category: "情绪调节",
|
|
||||||
duration: "3分钟",
|
|
||||||
color: .purple
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding()
|
|
||||||
.background(
|
|
||||||
RoundedRectangle(cornerRadius: 16)
|
|
||||||
.fill(Color(.systemGray6))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct ActionItemView: View {
|
|
||||||
let title: String
|
|
||||||
let category: String
|
|
||||||
let duration: String
|
|
||||||
let color: Color
|
|
||||||
@State private var isCompleted = false
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
HStack(spacing: 12) {
|
|
||||||
Button(action: { isCompleted.toggle() }) {
|
|
||||||
Image(systemName: isCompleted ? "checkmark.circle.fill" : "circle")
|
|
||||||
.font(.title3)
|
|
||||||
.foregroundColor(isCompleted ? color : .gray)
|
|
||||||
}
|
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 2) {
|
|
||||||
Text(title)
|
|
||||||
.font(.subheadline)
|
|
||||||
.fontWeight(.medium)
|
|
||||||
.strikethrough(isCompleted)
|
|
||||||
|
|
||||||
HStack(spacing: 8) {
|
|
||||||
Text(category)
|
|
||||||
.font(.caption)
|
|
||||||
.padding(.horizontal, 8)
|
|
||||||
.padding(.vertical, 2)
|
|
||||||
.background(color.opacity(0.2))
|
|
||||||
.foregroundColor(color)
|
|
||||||
.cornerRadius(8)
|
|
||||||
|
|
||||||
Text(duration)
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
.opacity(isCompleted ? 0.6 : 1.0)
|
|
||||||
.animation(.easeInOut(duration: 0.2), value: isCompleted)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - 成长历程
|
|
||||||
struct GrowthTimelineCard: View {
|
|
||||||
var body: some View {
|
|
||||||
VStack(alignment: .leading, spacing: 16) {
|
|
||||||
Text("成长历程")
|
|
||||||
.font(.headline)
|
|
||||||
.fontWeight(.semibold)
|
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 12) {
|
|
||||||
TimelineItem(
|
|
||||||
date: "今天",
|
|
||||||
title: "完成情绪记录",
|
|
||||||
description: "记录了平静的心情状态",
|
|
||||||
color: .blue,
|
|
||||||
isRecent: true
|
|
||||||
)
|
|
||||||
|
|
||||||
TimelineItem(
|
|
||||||
date: "昨天",
|
|
||||||
title: "课题升级",
|
|
||||||
description: "人际关系课题提升到Lv.2",
|
|
||||||
color: .green,
|
|
||||||
isRecent: true
|
|
||||||
)
|
|
||||||
|
|
||||||
TimelineItem(
|
|
||||||
date: "3天前",
|
|
||||||
title: "解锁新课题",
|
|
||||||
description: "开始学习情绪调节技巧",
|
|
||||||
color: .purple,
|
|
||||||
isRecent: false
|
|
||||||
)
|
|
||||||
|
|
||||||
TimelineItem(
|
|
||||||
date: "1周前",
|
|
||||||
title: "达成里程碑",
|
|
||||||
description: "连续7天完成情绪记录",
|
|
||||||
color: .orange,
|
|
||||||
isRecent: false
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding()
|
|
||||||
.background(
|
|
||||||
RoundedRectangle(cornerRadius: 16)
|
|
||||||
.fill(Color(.systemGray6))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct TimelineItem: View {
|
|
||||||
let date: String
|
|
||||||
let title: String
|
|
||||||
let description: String
|
|
||||||
let color: Color
|
|
||||||
let isRecent: Bool
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
HStack(spacing: 12) {
|
|
||||||
VStack {
|
|
||||||
Circle()
|
|
||||||
.fill(color)
|
|
||||||
.frame(width: 8, height: 8)
|
|
||||||
|
|
||||||
if !isRecent {
|
|
||||||
Rectangle()
|
|
||||||
.fill(Color(.systemGray4))
|
|
||||||
.frame(width: 1, height: 20)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 2) {
|
|
||||||
HStack {
|
|
||||||
Text(title)
|
|
||||||
.font(.subheadline)
|
|
||||||
.fontWeight(.medium)
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
|
|
||||||
Text(date)
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
}
|
|
||||||
|
|
||||||
Text(description)
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
.lineLimit(2)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.opacity(isRecent ? 1.0 : 0.7)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - GrowthTopic UI扩展
|
|
||||||
extension GrowthTopic {
|
|
||||||
var icon: String {
|
|
||||||
category.icon
|
|
||||||
}
|
|
||||||
|
|
||||||
var color: Color {
|
|
||||||
category.color
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - 模拟数据
|
|
||||||
let sampleGrowthTopics = [
|
|
||||||
GrowthTopic(
|
|
||||||
title: "人际关系边界",
|
|
||||||
description: "学习建立健康的人际关系边界",
|
|
||||||
category: .relationships,
|
|
||||||
difficulty: .intermediate,
|
|
||||||
progress: 0.7,
|
|
||||||
level: 2
|
|
||||||
),
|
|
||||||
GrowthTopic(
|
|
||||||
title: "情绪调节技能",
|
|
||||||
description: "掌握情绪识别和调节的核心技能",
|
|
||||||
category: .emotionRegulation,
|
|
||||||
difficulty: .beginner,
|
|
||||||
progress: 0.3,
|
|
||||||
level: 1
|
|
||||||
),
|
|
||||||
GrowthTopic(
|
|
||||||
title: "深度自我认知",
|
|
||||||
description: "提升对内在世界的认知和理解",
|
|
||||||
category: .selfAwareness,
|
|
||||||
difficulty: .advanced,
|
|
||||||
progress: 0.9,
|
|
||||||
level: 3
|
|
||||||
),
|
|
||||||
GrowthTopic(
|
|
||||||
title: "行动力提升",
|
|
||||||
description: "培养执行力和目标达成能力",
|
|
||||||
category: .lifeGoals,
|
|
||||||
difficulty: .beginner,
|
|
||||||
progress: 0.4,
|
|
||||||
level: 1
|
|
||||||
)
|
|
||||||
]
|
|
||||||
@@ -1,311 +0,0 @@
|
|||||||
//
|
|
||||||
// HealingView.swift
|
|
||||||
// EmotionMuseum
|
|
||||||
//
|
|
||||||
// Created by 华中敏 on 2025/6/13.
|
|
||||||
//
|
|
||||||
|
|
||||||
import SwiftUI
|
|
||||||
import AVFoundation
|
|
||||||
|
|
||||||
struct HealingView: View {
|
|
||||||
@State private var selectedChakra: ChakraType? = nil
|
|
||||||
@State private var showingHealingSession = false
|
|
||||||
@State private var chakraStates: [ChakraType: ChakraState] = [:]
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
NavigationView {
|
|
||||||
ZStack {
|
|
||||||
// 背景渐变
|
|
||||||
LinearGradient(
|
|
||||||
gradient: Gradient(colors: [.purple.opacity(0.1), .blue.opacity(0.1)]),
|
|
||||||
startPoint: .topLeading,
|
|
||||||
endPoint: .bottomTrailing
|
|
||||||
)
|
|
||||||
.ignoresSafeArea()
|
|
||||||
|
|
||||||
ScrollView {
|
|
||||||
VStack(spacing: 30) {
|
|
||||||
// 标题和说明
|
|
||||||
VStack(spacing: 8) {
|
|
||||||
Text("脉轮疗愈")
|
|
||||||
.font(.largeTitle)
|
|
||||||
.fontWeight(.bold)
|
|
||||||
|
|
||||||
Text("点击脉轮区域开始疗愈之旅")
|
|
||||||
.font(.subheadline)
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
}
|
|
||||||
.padding(.top)
|
|
||||||
|
|
||||||
// 人形脉轮图
|
|
||||||
VStack(spacing: 0) {
|
|
||||||
// 顶轮
|
|
||||||
ChakraButton(
|
|
||||||
chakra: .crown,
|
|
||||||
state: chakraStates[.crown] ?? .normal,
|
|
||||||
action: { selectChakra(.crown) }
|
|
||||||
)
|
|
||||||
.offset(y: 10)
|
|
||||||
|
|
||||||
Spacer().frame(height: 20)
|
|
||||||
|
|
||||||
// 眉心轮
|
|
||||||
ChakraButton(
|
|
||||||
chakra: .thirdEye,
|
|
||||||
state: chakraStates[.thirdEye] ?? .normal,
|
|
||||||
action: { selectChakra(.thirdEye) }
|
|
||||||
)
|
|
||||||
|
|
||||||
Spacer().frame(height: 30)
|
|
||||||
|
|
||||||
// 喉轮
|
|
||||||
ChakraButton(
|
|
||||||
chakra: .throat,
|
|
||||||
state: chakraStates[.throat] ?? .weak,
|
|
||||||
action: { selectChakra(.throat) }
|
|
||||||
)
|
|
||||||
|
|
||||||
Spacer().frame(height: 40)
|
|
||||||
|
|
||||||
// 心轮
|
|
||||||
ChakraButton(
|
|
||||||
chakra: .heart,
|
|
||||||
state: chakraStates[.heart] ?? .normal,
|
|
||||||
action: { selectChakra(.heart) }
|
|
||||||
)
|
|
||||||
|
|
||||||
Spacer().frame(height: 40)
|
|
||||||
|
|
||||||
// 太阳轮
|
|
||||||
ChakraButton(
|
|
||||||
chakra: .solarPlexus,
|
|
||||||
state: chakraStates[.solarPlexus] ?? .normal,
|
|
||||||
action: { selectChakra(.solarPlexus) }
|
|
||||||
)
|
|
||||||
|
|
||||||
Spacer().frame(height: 40)
|
|
||||||
|
|
||||||
// 脐轮
|
|
||||||
ChakraButton(
|
|
||||||
chakra: .sacral,
|
|
||||||
state: chakraStates[.sacral] ?? .weak,
|
|
||||||
action: { selectChakra(.sacral) }
|
|
||||||
)
|
|
||||||
|
|
||||||
Spacer().frame(height: 40)
|
|
||||||
|
|
||||||
// 海底轮
|
|
||||||
ChakraButton(
|
|
||||||
chakra: .root,
|
|
||||||
state: chakraStates[.root] ?? .normal,
|
|
||||||
action: { selectChakra(.root) }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
.frame(maxWidth: 200)
|
|
||||||
|
|
||||||
// 脉轮状态说明
|
|
||||||
VStack(alignment: .leading, spacing: 12) {
|
|
||||||
Text("脉轮状态说明")
|
|
||||||
.font(.headline)
|
|
||||||
|
|
||||||
HStack(spacing: 16) {
|
|
||||||
HStack {
|
|
||||||
Circle()
|
|
||||||
.fill(Color.green)
|
|
||||||
.frame(width: 12, height: 12)
|
|
||||||
Text("健康")
|
|
||||||
.font(.caption)
|
|
||||||
}
|
|
||||||
|
|
||||||
HStack {
|
|
||||||
Circle()
|
|
||||||
.fill(Color.orange.opacity(0.6))
|
|
||||||
.frame(width: 12, height: 12)
|
|
||||||
Text("疲弱")
|
|
||||||
.font(.caption)
|
|
||||||
}
|
|
||||||
|
|
||||||
HStack {
|
|
||||||
Circle()
|
|
||||||
.fill(Color.red.opacity(0.6))
|
|
||||||
.frame(width: 12, height: 12)
|
|
||||||
Text("受阻")
|
|
||||||
.font(.caption)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding()
|
|
||||||
.background(Color(.systemGray6))
|
|
||||||
.cornerRadius(12)
|
|
||||||
.padding(.horizontal)
|
|
||||||
|
|
||||||
// 快速疗愈选项
|
|
||||||
VStack(alignment: .leading, spacing: 12) {
|
|
||||||
Text("快速疗愈")
|
|
||||||
.font(.headline)
|
|
||||||
|
|
||||||
LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: 2), spacing: 12) {
|
|
||||||
QuickHealingCard(title: "全身平衡", icon: "figure.mind.and.body", color: .purple)
|
|
||||||
QuickHealingCard(title: "情绪释放", icon: "heart.fill", color: .pink)
|
|
||||||
QuickHealingCard(title: "能量充电", icon: "bolt.fill", color: .yellow)
|
|
||||||
QuickHealingCard(title: "深度放松", icon: "moon.fill", color: .blue)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding()
|
|
||||||
.background(Color(.systemGray6))
|
|
||||||
.cornerRadius(12)
|
|
||||||
.padding(.horizontal)
|
|
||||||
}
|
|
||||||
.padding(.bottom, 30)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.navigationBarHidden(true)
|
|
||||||
}
|
|
||||||
.sheet(item: $selectedChakra) { chakra in
|
|
||||||
ChakraHealingView(chakra: chakra) {
|
|
||||||
// 疗愈完成回调
|
|
||||||
updateChakraState(chakra, newState: .normal)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.onAppear {
|
|
||||||
initializeChakraStates()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func selectChakra(_ chakra: ChakraType) {
|
|
||||||
selectedChakra = chakra
|
|
||||||
}
|
|
||||||
|
|
||||||
private func initializeChakraStates() {
|
|
||||||
// 初始化脉轮状态(模拟数据)
|
|
||||||
chakraStates = [
|
|
||||||
.root: .normal,
|
|
||||||
.sacral: .weak,
|
|
||||||
.solarPlexus: .normal,
|
|
||||||
.heart: .normal,
|
|
||||||
.throat: .weak,
|
|
||||||
.thirdEye: .normal,
|
|
||||||
.crown: .normal
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
private func updateChakraState(_ chakra: ChakraType, newState: ChakraState) {
|
|
||||||
withAnimation {
|
|
||||||
chakraStates[chakra] = newState
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - 脉轮类型扩展
|
|
||||||
extension ChakraType: Identifiable {
|
|
||||||
var id: String { rawValue }
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - 脉轮状态枚举
|
|
||||||
enum ChakraState {
|
|
||||||
case normal // 健康
|
|
||||||
case weak // 疲弱
|
|
||||||
case blocked // 受阻
|
|
||||||
|
|
||||||
var opacity: Double {
|
|
||||||
switch self {
|
|
||||||
case .normal: return 1.0
|
|
||||||
case .weak: return 0.6
|
|
||||||
case .blocked: return 0.3
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - 脉轮按钮组件
|
|
||||||
struct ChakraButton: View {
|
|
||||||
let chakra: ChakraType
|
|
||||||
let state: ChakraState
|
|
||||||
let action: () -> Void
|
|
||||||
|
|
||||||
@State private var isAnimating = false
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
Button(action: action) {
|
|
||||||
ZStack {
|
|
||||||
// 外圈光晕效果
|
|
||||||
Circle()
|
|
||||||
.fill(
|
|
||||||
RadialGradient(
|
|
||||||
gradient: Gradient(colors: [chakra.color.opacity(0.3), Color.clear]),
|
|
||||||
center: .center,
|
|
||||||
startRadius: 20,
|
|
||||||
endRadius: 40
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.frame(width: 80, height: 80)
|
|
||||||
.scaleEffect(isAnimating ? 1.2 : 1.0)
|
|
||||||
.opacity(state == .weak ? 0.8 : 1.0)
|
|
||||||
|
|
||||||
// 主圆圈
|
|
||||||
Circle()
|
|
||||||
.fill(
|
|
||||||
RadialGradient(
|
|
||||||
gradient: Gradient(colors: [chakra.color, chakra.color.opacity(0.7)]),
|
|
||||||
center: .center,
|
|
||||||
startRadius: 5,
|
|
||||||
endRadius: 25
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.frame(width: 50, height: 50)
|
|
||||||
.opacity(state.opacity)
|
|
||||||
.overlay(
|
|
||||||
Circle()
|
|
||||||
.stroke(Color.white.opacity(0.8), lineWidth: 2)
|
|
||||||
)
|
|
||||||
|
|
||||||
// 脉轮名称
|
|
||||||
Text(chakra.rawValue)
|
|
||||||
.font(.caption2)
|
|
||||||
.fontWeight(.semibold)
|
|
||||||
.foregroundColor(.white)
|
|
||||||
.shadow(radius: 1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.onAppear {
|
|
||||||
if state == .weak {
|
|
||||||
withAnimation(Animation.easeInOut(duration: 2).repeatForever(autoreverses: true)) {
|
|
||||||
isAnimating = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - 快速疗愈卡片
|
|
||||||
struct QuickHealingCard: View {
|
|
||||||
let title: String
|
|
||||||
let icon: String
|
|
||||||
let color: Color
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
Button(action: {
|
|
||||||
// 快速疗愈功能
|
|
||||||
}) {
|
|
||||||
VStack(spacing: 8) {
|
|
||||||
Image(systemName: icon)
|
|
||||||
.font(.title2)
|
|
||||||
.foregroundColor(color)
|
|
||||||
|
|
||||||
Text(title)
|
|
||||||
.font(.subheadline)
|
|
||||||
.fontWeight(.medium)
|
|
||||||
.foregroundColor(.primary)
|
|
||||||
}
|
|
||||||
.frame(maxWidth: .infinity)
|
|
||||||
.padding()
|
|
||||||
.background(Color(.systemBackground))
|
|
||||||
.cornerRadius(12)
|
|
||||||
.shadow(color: color.opacity(0.3), radius: 4, x: 0, y: 2)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#Preview {
|
|
||||||
HealingView()
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,438 +0,0 @@
|
|||||||
//
|
|
||||||
// LoadingComponents.swift
|
|
||||||
// EmotionMuseum
|
|
||||||
//
|
|
||||||
// Created by 华中敏 on 2025/6/13.
|
|
||||||
//
|
|
||||||
|
|
||||||
import SwiftUI
|
|
||||||
|
|
||||||
// MARK: - 加载状态枚举
|
|
||||||
enum LoadingState {
|
|
||||||
case idle
|
|
||||||
case loading
|
|
||||||
case loaded
|
|
||||||
case error(String)
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - 主题管理器
|
|
||||||
class ThemeManager: ObservableObject {
|
|
||||||
@Published var isDarkMode: Bool = false
|
|
||||||
@Published var systemFollowsDeviceTheme: Bool = true
|
|
||||||
|
|
||||||
init() {
|
|
||||||
// 从用户偏好设置中读取主题设置
|
|
||||||
self.isDarkMode = UserDefaults.standard.bool(forKey: "darkMode")
|
|
||||||
self.systemFollowsDeviceTheme = UserDefaults.standard.bool(forKey: "systemFollowsDeviceTheme")
|
|
||||||
}
|
|
||||||
|
|
||||||
func toggleTheme() {
|
|
||||||
isDarkMode.toggle()
|
|
||||||
UserDefaults.standard.set(isDarkMode, forKey: "darkMode")
|
|
||||||
}
|
|
||||||
|
|
||||||
func setSystemFollowing(_ follows: Bool) {
|
|
||||||
systemFollowsDeviceTheme = follows
|
|
||||||
UserDefaults.standard.set(follows, forKey: "systemFollowsDeviceTheme")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - 主题颜色扩展
|
|
||||||
extension Color {
|
|
||||||
static let theme = ColorTheme()
|
|
||||||
}
|
|
||||||
|
|
||||||
struct ColorTheme {
|
|
||||||
// 主色调
|
|
||||||
let primary = Color("PrimaryColor")
|
|
||||||
let secondary = Color("SecondaryColor")
|
|
||||||
let accent = Color("AccentColor")
|
|
||||||
|
|
||||||
// 背景色
|
|
||||||
let background = Color("BackgroundColor")
|
|
||||||
let cardBackground = Color("CardBackground")
|
|
||||||
let surfaceBackground = Color("SurfaceBackground")
|
|
||||||
|
|
||||||
// 文字颜色
|
|
||||||
let primaryText = Color("PrimaryText")
|
|
||||||
let secondaryText = Color("SecondaryText")
|
|
||||||
let tertiaryText = Color("TertiaryText")
|
|
||||||
|
|
||||||
// 边框颜色
|
|
||||||
let border = Color("BorderColor")
|
|
||||||
let divider = Color("DividerColor")
|
|
||||||
|
|
||||||
// 状态颜色
|
|
||||||
let success = Color("SuccessColor")
|
|
||||||
let warning = Color("WarningColor")
|
|
||||||
let error = Color("ErrorColor")
|
|
||||||
|
|
||||||
// 骨架屏颜色
|
|
||||||
let skeleton = Color("SkeletonColor")
|
|
||||||
let skeletonHighlight = Color("SkeletonHighlight")
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - 基础骨架屏组件
|
|
||||||
struct SkeletonView: View {
|
|
||||||
@State private var isAnimating = false
|
|
||||||
let width: CGFloat?
|
|
||||||
let height: CGFloat
|
|
||||||
let cornerRadius: CGFloat
|
|
||||||
|
|
||||||
init(width: CGFloat? = nil, height: CGFloat = 20, cornerRadius: CGFloat = 8) {
|
|
||||||
self.width = width
|
|
||||||
self.height = height
|
|
||||||
self.cornerRadius = cornerRadius
|
|
||||||
}
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
Rectangle()
|
|
||||||
.fill(
|
|
||||||
LinearGradient(
|
|
||||||
colors: [
|
|
||||||
Color.theme.skeleton,
|
|
||||||
Color.theme.skeletonHighlight,
|
|
||||||
Color.theme.skeleton
|
|
||||||
],
|
|
||||||
startPoint: .leading,
|
|
||||||
endPoint: .trailing
|
|
||||||
)
|
|
||||||
.opacity(isAnimating ? 0.6 : 1.0)
|
|
||||||
)
|
|
||||||
.frame(width: width, height: height)
|
|
||||||
.cornerRadius(cornerRadius)
|
|
||||||
.onAppear {
|
|
||||||
withAnimation(
|
|
||||||
Animation
|
|
||||||
.easeInOut(duration: 1.2)
|
|
||||||
.repeatForever(autoreverses: true)
|
|
||||||
) {
|
|
||||||
isAnimating = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - 文本骨架屏
|
|
||||||
struct SkeletonText: View {
|
|
||||||
let lineCount: Int
|
|
||||||
let spacing: CGFloat
|
|
||||||
|
|
||||||
init(lineCount: Int = 3, spacing: CGFloat = 8) {
|
|
||||||
self.lineCount = lineCount
|
|
||||||
self.spacing = spacing
|
|
||||||
}
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
VStack(alignment: .leading, spacing: spacing) {
|
|
||||||
ForEach(0..<lineCount, id: \.self) { index in
|
|
||||||
SkeletonView(
|
|
||||||
width: index == lineCount - 1 ? CGFloat.random(in: 100...200) : nil,
|
|
||||||
height: 16,
|
|
||||||
cornerRadius: 4
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - 卡片骨架屏
|
|
||||||
struct SkeletonCard: View {
|
|
||||||
let showImage: Bool
|
|
||||||
let showButton: Bool
|
|
||||||
|
|
||||||
init(showImage: Bool = true, showButton: Bool = false) {
|
|
||||||
self.showImage = showImage
|
|
||||||
self.showButton = showButton
|
|
||||||
}
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
VStack(alignment: .leading, spacing: 12) {
|
|
||||||
if showImage {
|
|
||||||
SkeletonView(height: 120, cornerRadius: 12)
|
|
||||||
}
|
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 8) {
|
|
||||||
SkeletonView(height: 20, cornerRadius: 6)
|
|
||||||
SkeletonText(lineCount: 2, spacing: 6)
|
|
||||||
|
|
||||||
if showButton {
|
|
||||||
HStack {
|
|
||||||
Spacer()
|
|
||||||
SkeletonView(width: 80, height: 32, cornerRadius: 16)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding(.horizontal, showImage ? 0 : 16)
|
|
||||||
}
|
|
||||||
.padding(16)
|
|
||||||
.background(Color.theme.cardBackground)
|
|
||||||
.cornerRadius(16)
|
|
||||||
.shadow(color: Color.black.opacity(0.05), radius: 8, x: 0, y: 2)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - 列表项骨架屏
|
|
||||||
struct SkeletonListItem: View {
|
|
||||||
let showAvatar: Bool
|
|
||||||
let showTrailing: Bool
|
|
||||||
|
|
||||||
init(showAvatar: Bool = true, showTrailing: Bool = true) {
|
|
||||||
self.showAvatar = showAvatar
|
|
||||||
self.showTrailing = showTrailing
|
|
||||||
}
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
HStack(spacing: 12) {
|
|
||||||
if showAvatar {
|
|
||||||
SkeletonView(width: 50, height: 50, cornerRadius: 25)
|
|
||||||
}
|
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 6) {
|
|
||||||
SkeletonView(width: 120, height: 16, cornerRadius: 4)
|
|
||||||
SkeletonView(width: 200, height: 14, cornerRadius: 4)
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
|
|
||||||
if showTrailing {
|
|
||||||
SkeletonView(width: 60, height: 20, cornerRadius: 6)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding(.horizontal, 16)
|
|
||||||
.padding(.vertical, 12)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - 加载状态视图
|
|
||||||
struct LoadingStateView<Content: View>: View {
|
|
||||||
let loadingState: LoadingState
|
|
||||||
let content: () -> Content
|
|
||||||
let loadingView: (() -> AnyView)?
|
|
||||||
let errorView: ((String) -> AnyView)?
|
|
||||||
|
|
||||||
init(
|
|
||||||
loadingState: LoadingState,
|
|
||||||
@ViewBuilder content: @escaping () -> Content,
|
|
||||||
loadingView: (() -> AnyView)? = nil,
|
|
||||||
errorView: ((String) -> AnyView)? = nil
|
|
||||||
) {
|
|
||||||
self.loadingState = loadingState
|
|
||||||
self.content = content
|
|
||||||
self.loadingView = loadingView
|
|
||||||
self.errorView = errorView
|
|
||||||
}
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
switch loadingState {
|
|
||||||
case .idle:
|
|
||||||
Color.clear
|
|
||||||
.onAppear {
|
|
||||||
// 可以在这里触发初始加载
|
|
||||||
}
|
|
||||||
|
|
||||||
case .loading:
|
|
||||||
if let loadingView = loadingView {
|
|
||||||
loadingView()
|
|
||||||
} else {
|
|
||||||
defaultLoadingView
|
|
||||||
}
|
|
||||||
|
|
||||||
case .loaded:
|
|
||||||
content()
|
|
||||||
.transition(.opacity.combined(with: .scale(scale: 0.95)))
|
|
||||||
|
|
||||||
case .error(let message):
|
|
||||||
if let errorView = errorView {
|
|
||||||
errorView(message)
|
|
||||||
} else {
|
|
||||||
defaultErrorView(message: message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private var defaultLoadingView: some View {
|
|
||||||
VStack(spacing: 20) {
|
|
||||||
ProgressView()
|
|
||||||
.scaleEffect(1.5)
|
|
||||||
.progressViewStyle(CircularProgressViewStyle(tint: Color.theme.accent))
|
|
||||||
|
|
||||||
Text("加载中...")
|
|
||||||
.font(.subheadline)
|
|
||||||
.foregroundColor(Color.theme.secondaryText)
|
|
||||||
}
|
|
||||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
|
||||||
.background(Color.theme.background)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func defaultErrorView(message: String) -> some View {
|
|
||||||
VStack(spacing: 16) {
|
|
||||||
Image(systemName: "exclamationmark.triangle")
|
|
||||||
.font(.system(size: 48))
|
|
||||||
.foregroundColor(Color.theme.error)
|
|
||||||
|
|
||||||
Text("加载失败")
|
|
||||||
.font(.headline)
|
|
||||||
.foregroundColor(Color.theme.primaryText)
|
|
||||||
|
|
||||||
Text(message)
|
|
||||||
.font(.subheadline)
|
|
||||||
.foregroundColor(Color.theme.secondaryText)
|
|
||||||
.multilineTextAlignment(.center)
|
|
||||||
|
|
||||||
Button("重试") {
|
|
||||||
// 重试逻辑需要由父视图处理
|
|
||||||
}
|
|
||||||
.buttonStyle(PrimaryButtonStyle())
|
|
||||||
}
|
|
||||||
.padding(32)
|
|
||||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
|
||||||
.background(Color.theme.background)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - 按钮样式
|
|
||||||
struct PrimaryButtonStyle: ButtonStyle {
|
|
||||||
func makeBody(configuration: Configuration) -> some View {
|
|
||||||
configuration.label
|
|
||||||
.padding(.horizontal, 24)
|
|
||||||
.padding(.vertical, 12)
|
|
||||||
.background(Color.theme.accent)
|
|
||||||
.foregroundColor(.white)
|
|
||||||
.cornerRadius(12)
|
|
||||||
.scaleEffect(configuration.isPressed ? 0.95 : 1.0)
|
|
||||||
.animation(.easeInOut(duration: 0.1), value: configuration.isPressed)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - 列表加载视图
|
|
||||||
struct LoadingList: View {
|
|
||||||
let itemCount: Int
|
|
||||||
let showAvatar: Bool
|
|
||||||
let showTrailing: Bool
|
|
||||||
|
|
||||||
init(itemCount: Int = 5, showAvatar: Bool = true, showTrailing: Bool = true) {
|
|
||||||
self.itemCount = itemCount
|
|
||||||
self.showAvatar = showAvatar
|
|
||||||
self.showTrailing = showTrailing
|
|
||||||
}
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
LazyVStack(spacing: 0) {
|
|
||||||
ForEach(0..<itemCount, id: \.self) { _ in
|
|
||||||
SkeletonListItem(showAvatar: showAvatar, showTrailing: showTrailing)
|
|
||||||
Divider()
|
|
||||||
.background(Color.theme.divider)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - 卡片网格加载视图
|
|
||||||
struct LoadingCardGrid: View {
|
|
||||||
let columns: Int
|
|
||||||
let itemCount: Int
|
|
||||||
let showImage: Bool
|
|
||||||
let showButton: Bool
|
|
||||||
|
|
||||||
init(columns: Int = 2, itemCount: Int = 6, showImage: Bool = true, showButton: Bool = false) {
|
|
||||||
self.columns = columns
|
|
||||||
self.itemCount = itemCount
|
|
||||||
self.showImage = showImage
|
|
||||||
self.showButton = showButton
|
|
||||||
}
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
LazyVGrid(
|
|
||||||
columns: Array(repeating: GridItem(.flexible(), spacing: 16), count: columns),
|
|
||||||
spacing: 16
|
|
||||||
) {
|
|
||||||
ForEach(0..<itemCount, id: \.self) { _ in
|
|
||||||
SkeletonCard(showImage: showImage, showButton: showButton)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding(.horizontal, 16)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - 智能刷新控制器
|
|
||||||
struct SmartRefreshView<Content: View>: View {
|
|
||||||
@State private var isRefreshing = false
|
|
||||||
let onRefresh: () async -> Void
|
|
||||||
let content: () -> Content
|
|
||||||
|
|
||||||
init(
|
|
||||||
onRefresh: @escaping () async -> Void,
|
|
||||||
@ViewBuilder content: @escaping () -> Content
|
|
||||||
) {
|
|
||||||
self.onRefresh = onRefresh
|
|
||||||
self.content = content
|
|
||||||
}
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
ScrollView {
|
|
||||||
content()
|
|
||||||
}
|
|
||||||
.refreshable {
|
|
||||||
await onRefresh()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - 加载更多组件
|
|
||||||
struct LoadMoreView: View {
|
|
||||||
@State private var isLoading = false
|
|
||||||
let onLoadMore: () async -> Void
|
|
||||||
let hasMore: Bool
|
|
||||||
|
|
||||||
init(hasMore: Bool = true, onLoadMore: @escaping () async -> Void) {
|
|
||||||
self.hasMore = hasMore
|
|
||||||
self.onLoadMore = onLoadMore
|
|
||||||
}
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
HStack {
|
|
||||||
Spacer()
|
|
||||||
|
|
||||||
if hasMore {
|
|
||||||
if isLoading {
|
|
||||||
HStack(spacing: 8) {
|
|
||||||
ProgressView()
|
|
||||||
.scaleEffect(0.8)
|
|
||||||
Text("加载中...")
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundColor(Color.theme.secondaryText)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Button("加载更多") {
|
|
||||||
Task {
|
|
||||||
isLoading = true
|
|
||||||
await onLoadMore()
|
|
||||||
isLoading = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundColor(Color.theme.accent)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Text("没有更多内容了")
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundColor(Color.theme.tertiaryText)
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
.padding(.vertical, 16)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - 预览
|
|
||||||
#Preview("骨架屏组件") {
|
|
||||||
VStack(spacing: 20) {
|
|
||||||
SkeletonCard()
|
|
||||||
SkeletonListItem()
|
|
||||||
SkeletonText()
|
|
||||||
}
|
|
||||||
.padding()
|
|
||||||
.background(Color.theme.background)
|
|
||||||
}
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
//
|
|
||||||
// LoadingOverlay.swift
|
|
||||||
// EmotionMuseum
|
|
||||||
//
|
|
||||||
// Created by 华中敏 on 2025/7/5.
|
|
||||||
//
|
|
||||||
|
|
||||||
import SwiftUI
|
|
||||||
|
|
||||||
struct LoadingOverlay: View {
|
|
||||||
let message: String
|
|
||||||
@State private var rotationAngle: Double = 0
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
ZStack {
|
|
||||||
// 半透明背景
|
|
||||||
Color.black
|
|
||||||
.opacity(0.3)
|
|
||||||
.ignoresSafeArea()
|
|
||||||
|
|
||||||
// 加载内容
|
|
||||||
VStack(spacing: 20) {
|
|
||||||
// 旋转的加载指示器
|
|
||||||
Image(systemName: "brain.head.profile")
|
|
||||||
.font(.system(size: 40))
|
|
||||||
.foregroundColor(Color("AccentColor"))
|
|
||||||
.rotationEffect(.degrees(rotationAngle))
|
|
||||||
.onAppear {
|
|
||||||
withAnimation(.linear(duration: 2).repeatForever(autoreverses: false)) {
|
|
||||||
rotationAngle = 360
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 加载文字
|
|
||||||
Text(message)
|
|
||||||
.font(.subheadline)
|
|
||||||
.foregroundColor(Color("PrimaryText"))
|
|
||||||
.multilineTextAlignment(.center)
|
|
||||||
}
|
|
||||||
.padding(30)
|
|
||||||
.background(
|
|
||||||
RoundedRectangle(cornerRadius: 16)
|
|
||||||
.fill(Color("CardBackground"))
|
|
||||||
.shadow(color: .black.opacity(0.1), radius: 10, x: 0, y: 5)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
.transition(.opacity)
|
|
||||||
.zIndex(999) // 确保在最顶层
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#Preview {
|
|
||||||
LoadingOverlay(message: "正在加载数据...")
|
|
||||||
.background(Color.gray.opacity(0.3))
|
|
||||||
}
|
|
||||||
@@ -1,232 +0,0 @@
|
|||||||
import SwiftUI
|
|
||||||
// import AMapFoundationKit // 临时注释,需要安装CocoaPods依赖
|
|
||||||
// import MAMapKit // 临时注释,需要安装CocoaPods依赖
|
|
||||||
import UIKit
|
|
||||||
import MapKit
|
|
||||||
import CoreLocation
|
|
||||||
|
|
||||||
/// 地图视图
|
|
||||||
/// @Author huazhongmin
|
|
||||||
/// @Time 2024-03-24
|
|
||||||
/// @Description 使用系统地图展示地图内容,默认定位到用户当前位置
|
|
||||||
struct MapView: View {
|
|
||||||
@Binding var shouldMoveToUserLocation: Bool
|
|
||||||
|
|
||||||
init(shouldMoveToUserLocation: Binding<Bool> = .constant(false)) {
|
|
||||||
self._shouldMoveToUserLocation = shouldMoveToUserLocation
|
|
||||||
}
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
MapViewRepresentable(shouldMoveToUserLocation: $shouldMoveToUserLocation)
|
|
||||||
.edgesIgnoringSafeArea(.all)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 系统地图SwiftUI包装器
|
|
||||||
struct MapViewRepresentable: UIViewRepresentable {
|
|
||||||
@Binding var shouldMoveToUserLocation: Bool
|
|
||||||
|
|
||||||
// 默认位置(天安门坐标)
|
|
||||||
let defaultLocation = CLLocationCoordinate2D(
|
|
||||||
latitude: 39.908823,
|
|
||||||
longitude: 116.397470
|
|
||||||
)
|
|
||||||
|
|
||||||
// 创建地图视图的协调器
|
|
||||||
func makeCoordinator() -> Coordinator {
|
|
||||||
Coordinator(self)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建地图视图
|
|
||||||
func makeUIView(context: Context) -> MKMapView {
|
|
||||||
let mapView = MKMapView(frame: .zero)
|
|
||||||
mapView.delegate = context.coordinator
|
|
||||||
|
|
||||||
// 显示用户位置
|
|
||||||
mapView.showsUserLocation = true
|
|
||||||
mapView.userTrackingMode = .none // 不自动跟踪,手动控制
|
|
||||||
|
|
||||||
// 设置地图类型和控件
|
|
||||||
mapView.mapType = .standard
|
|
||||||
mapView.showsCompass = true
|
|
||||||
mapView.showsScale = true
|
|
||||||
mapView.showsTraffic = false
|
|
||||||
|
|
||||||
// 允许缩放和滚动
|
|
||||||
mapView.isZoomEnabled = true
|
|
||||||
mapView.isScrollEnabled = true
|
|
||||||
mapView.isRotateEnabled = true
|
|
||||||
mapView.isPitchEnabled = true
|
|
||||||
|
|
||||||
// 设置初始区域(在获取用户位置前显示默认位置)
|
|
||||||
let initialRegion = MKCoordinateRegion(
|
|
||||||
center: defaultLocation,
|
|
||||||
span: MKCoordinateSpan(latitudeDelta: 0.05, longitudeDelta: 0.05)
|
|
||||||
)
|
|
||||||
mapView.setRegion(initialRegion, animated: false)
|
|
||||||
|
|
||||||
// 保存mapView引用到coordinator并开始定位
|
|
||||||
context.coordinator.mapView = mapView
|
|
||||||
context.coordinator.startLocationUpdates()
|
|
||||||
|
|
||||||
return mapView
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新地图视图
|
|
||||||
func updateUIView(_ mapView: MKMapView, context: Context) {
|
|
||||||
// 检查是否需要重新请求定位权限
|
|
||||||
context.coordinator.checkLocationPermission()
|
|
||||||
|
|
||||||
// 检查是否需要移动到用户位置
|
|
||||||
if shouldMoveToUserLocation {
|
|
||||||
context.coordinator.moveToUserLocation()
|
|
||||||
// 重置状态
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
shouldMoveToUserLocation = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 协调器类,用于处理地图代理事件和位置管理
|
|
||||||
class Coordinator: NSObject, MKMapViewDelegate, CLLocationManagerDelegate {
|
|
||||||
var parent: MapViewRepresentable
|
|
||||||
var mapView: MKMapView?
|
|
||||||
var locationManager: CLLocationManager
|
|
||||||
var hasInitialLocationSet = false
|
|
||||||
|
|
||||||
init(_ parent: MapViewRepresentable) {
|
|
||||||
self.parent = parent
|
|
||||||
self.locationManager = CLLocationManager()
|
|
||||||
super.init()
|
|
||||||
|
|
||||||
// 配置位置管理器
|
|
||||||
locationManager.delegate = self
|
|
||||||
locationManager.desiredAccuracy = kCLLocationAccuracyBest
|
|
||||||
locationManager.distanceFilter = 10 // 移动10米以上才更新
|
|
||||||
}
|
|
||||||
|
|
||||||
// 开始位置更新
|
|
||||||
func startLocationUpdates() {
|
|
||||||
checkLocationPermission()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查定位权限并请求权限
|
|
||||||
func checkLocationPermission() {
|
|
||||||
guard CLLocationManager.locationServicesEnabled() else {
|
|
||||||
print("定位服务未启用")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let authorizationStatus = locationManager.authorizationStatus
|
|
||||||
|
|
||||||
switch authorizationStatus {
|
|
||||||
case .notDetermined:
|
|
||||||
// 首次使用,请求定位权限
|
|
||||||
locationManager.requestWhenInUseAuthorization()
|
|
||||||
|
|
||||||
case .authorizedWhenInUse, .authorizedAlways:
|
|
||||||
// 已授权,开始定位
|
|
||||||
locationManager.startUpdatingLocation()
|
|
||||||
|
|
||||||
case .denied, .restricted:
|
|
||||||
// 被拒绝或受限,显示默认位置
|
|
||||||
print("定位权限被拒绝,显示默认位置")
|
|
||||||
setDefaultLocation()
|
|
||||||
|
|
||||||
@unknown default:
|
|
||||||
print("未知的定位权限状态")
|
|
||||||
setDefaultLocation()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 设置默认位置
|
|
||||||
func setDefaultLocation() {
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
guard let mapView = self.mapView else { return }
|
|
||||||
|
|
||||||
let region = MKCoordinateRegion(
|
|
||||||
center: self.parent.defaultLocation,
|
|
||||||
span: MKCoordinateSpan(latitudeDelta: 0.01, longitudeDelta: 0.01)
|
|
||||||
)
|
|
||||||
mapView.setRegion(region, animated: true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - CLLocationManagerDelegate
|
|
||||||
|
|
||||||
// 权限状态变化
|
|
||||||
func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
|
|
||||||
checkLocationPermission()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 位置更新成功
|
|
||||||
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
|
|
||||||
guard let location = locations.last else { return }
|
|
||||||
|
|
||||||
// 只在首次获取位置时自动移动地图到用户位置
|
|
||||||
if !hasInitialLocationSet {
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
guard let mapView = self.mapView else { return }
|
|
||||||
|
|
||||||
let userRegion = MKCoordinateRegion(
|
|
||||||
center: location.coordinate,
|
|
||||||
span: MKCoordinateSpan(latitudeDelta: 0.01, longitudeDelta: 0.01)
|
|
||||||
)
|
|
||||||
mapView.setRegion(userRegion, animated: true)
|
|
||||||
self.hasInitialLocationSet = true
|
|
||||||
|
|
||||||
print("已定位到用户位置: \(location.coordinate)")
|
|
||||||
}
|
|
||||||
|
|
||||||
// 定位成功后可以停止持续更新,节省电量
|
|
||||||
locationManager.stopUpdatingLocation()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 位置更新失败
|
|
||||||
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
|
|
||||||
print("定位失败: \(error.localizedDescription)")
|
|
||||||
|
|
||||||
// 定位失败时显示默认位置
|
|
||||||
if !hasInitialLocationSet {
|
|
||||||
setDefaultLocation()
|
|
||||||
hasInitialLocationSet = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - MKMapViewDelegate
|
|
||||||
|
|
||||||
// 用户位置更新(地图上的蓝点)
|
|
||||||
func mapView(_ mapView: MKMapView, didUpdate userLocation: MKUserLocation) {
|
|
||||||
// 这里不自动移动地图,避免干扰用户操作
|
|
||||||
// 用户位置的蓝点会自动显示在地图上
|
|
||||||
}
|
|
||||||
|
|
||||||
// 地图区域变化
|
|
||||||
func mapView(_ mapView: MKMapView, regionDidChangeAnimated animated: Bool) {
|
|
||||||
// 可以在这里处理地图区域变化事件
|
|
||||||
}
|
|
||||||
|
|
||||||
// 手动回到用户位置的方法(供外部调用)
|
|
||||||
func moveToUserLocation() {
|
|
||||||
guard let mapView = self.mapView,
|
|
||||||
let userLocation = mapView.userLocation.location else {
|
|
||||||
// 如果没有用户位置,重新开始定位
|
|
||||||
checkLocationPermission()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let userRegion = MKCoordinateRegion(
|
|
||||||
center: userLocation.coordinate,
|
|
||||||
span: MKCoordinateSpan(latitudeDelta: 0.01, longitudeDelta: 0.01)
|
|
||||||
)
|
|
||||||
mapView.setRegion(userRegion, animated: true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct MapView_Previews: PreviewProvider {
|
|
||||||
static var previews: some View {
|
|
||||||
MapView()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,662 +0,0 @@
|
|||||||
//
|
|
||||||
// RecordView.swift
|
|
||||||
// EmotionMuseum
|
|
||||||
//
|
|
||||||
// Created by 华中敏 on 2025/6/13.
|
|
||||||
//
|
|
||||||
|
|
||||||
import SwiftUI
|
|
||||||
|
|
||||||
// MARK: - 记录页面主视图
|
|
||||||
struct RecordView: View {
|
|
||||||
@EnvironmentObject var navigationManager: NavigationManager
|
|
||||||
@EnvironmentObject var themeManager: ThemeManager
|
|
||||||
@EnvironmentObject var mockData: MockDataManager
|
|
||||||
@StateObject private var aiService = MockAIService()
|
|
||||||
@State private var selectedDate = Date()
|
|
||||||
@State private var inputText = ""
|
|
||||||
@State private var showingMoodPicker = false
|
|
||||||
@State private var selectedMood = ""
|
|
||||||
@State private var loadingState: LoadingState = .idle
|
|
||||||
@State private var isInitialLoading = true
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
LoadingStateView(loadingState: isInitialLoading ? .loading : .loaded) {
|
|
||||||
VStack(spacing: 0) {
|
|
||||||
// 可滚动的主要内容区域
|
|
||||||
ScrollView {
|
|
||||||
LazyVStack(spacing: 16) {
|
|
||||||
// 顶部导航栏
|
|
||||||
topNavigationBar
|
|
||||||
.padding(.horizontal, 16)
|
|
||||||
.padding(.top, 8)
|
|
||||||
.transition(.move(edge: .top).combined(with: .opacity))
|
|
||||||
|
|
||||||
// 日历组件
|
|
||||||
emotionCalendar
|
|
||||||
.padding(.horizontal, 16)
|
|
||||||
.transition(.scale(scale: 0.8).combined(with: .opacity))
|
|
||||||
|
|
||||||
// AI助手头像
|
|
||||||
aiAvatarSection
|
|
||||||
.padding(.horizontal, 16)
|
|
||||||
.transition(.scale(scale: 0.9).combined(with: .opacity))
|
|
||||||
|
|
||||||
// 聊天区域
|
|
||||||
chatArea
|
|
||||||
.padding(.horizontal, 16)
|
|
||||||
.transition(.opacity.combined(with: .slide))
|
|
||||||
}
|
|
||||||
.padding(.bottom, 10) // 为输入区域留出空间
|
|
||||||
}
|
|
||||||
.refreshable {
|
|
||||||
await simulateRefresh()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 固定在底部的输入区域
|
|
||||||
inputArea
|
|
||||||
.background(Color.theme.cardBackground)
|
|
||||||
.shadow(color: .black.opacity(0.1), radius: 8, y: -4)
|
|
||||||
.transition(.move(edge: .bottom).combined(with: .opacity))
|
|
||||||
}
|
|
||||||
} loadingView: {
|
|
||||||
AnyView(recordViewSkeleton)
|
|
||||||
}
|
|
||||||
.background(Color.theme.background)
|
|
||||||
.environmentObject(themeManager)
|
|
||||||
.preferredColorScheme(themeManager.systemFollowsDeviceTheme ? nil : (themeManager.isDarkMode ? .dark : .light))
|
|
||||||
|
|
||||||
.ignoresSafeArea(.keyboard, edges: .bottom) // 键盘出现时不影响布局
|
|
||||||
.sheet(isPresented: $showingMoodPicker) {
|
|
||||||
MoodPickerView(
|
|
||||||
selectedDate: selectedDate,
|
|
||||||
selectedMood: $selectedMood,
|
|
||||||
isPresented: $showingMoodPicker
|
|
||||||
)
|
|
||||||
}
|
|
||||||
.onAppear {
|
|
||||||
simulateInitialLoading()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - 顶部导航栏
|
|
||||||
private var topNavigationBar: some View {
|
|
||||||
HStack {
|
|
||||||
// 左上角 - 聊天记录图标
|
|
||||||
Button(action: { navigationManager.showChatHistory() }) {
|
|
||||||
Image(systemName: "bubble.left.and.bubble.right.fill")
|
|
||||||
.font(.title3)
|
|
||||||
.foregroundColor(.blue)
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
|
|
||||||
// 中间 - 页面标题
|
|
||||||
Text("情绪记录")
|
|
||||||
.font(.title2)
|
|
||||||
.fontWeight(.semibold)
|
|
||||||
.foregroundColor(.primary)
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
|
|
||||||
// 右上角 - 设置图标
|
|
||||||
Button(action: { navigationManager.navigateToSettings() }) {
|
|
||||||
Image(systemName: "gearshape.fill")
|
|
||||||
.font(.title3)
|
|
||||||
.foregroundColor(.gray)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - 情绪日历
|
|
||||||
private var emotionCalendar: some View {
|
|
||||||
VStack(spacing: 12) {
|
|
||||||
HStack {
|
|
||||||
Text(selectedDate.formatted(.dateTime.month(.wide)))
|
|
||||||
.font(.headline)
|
|
||||||
.foregroundColor(.primary)
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
|
|
||||||
Button(action: { showingMoodPicker = true }) {
|
|
||||||
HStack(spacing: 4) {
|
|
||||||
Text(selectedMood.isEmpty ? "记录心情" : selectedMood)
|
|
||||||
.font(.caption)
|
|
||||||
Image(systemName: "plus.circle")
|
|
||||||
.font(.caption)
|
|
||||||
}
|
|
||||||
.foregroundColor(.blue)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 7天日历视图
|
|
||||||
ScrollView(.horizontal, showsIndicators: false) {
|
|
||||||
HStack(spacing: 16) {
|
|
||||||
ForEach(-3...3, id: \.self) { dayOffset in
|
|
||||||
let date = Calendar.current.date(byAdding: .day, value: dayOffset, to: selectedDate) ?? selectedDate
|
|
||||||
let isToday = Calendar.current.isDate(date, inSameDayAs: Date())
|
|
||||||
let isSelected = Calendar.current.isDate(date, inSameDayAs: selectedDate)
|
|
||||||
|
|
||||||
VStack(spacing: 4) {
|
|
||||||
Text(DateFormatter.weekdayShort.string(from: date))
|
|
||||||
.font(.caption2)
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
|
|
||||||
Button(action: { selectedDate = date }) {
|
|
||||||
VStack(spacing: 2) {
|
|
||||||
Text("\(Calendar.current.component(.day, from: date))")
|
|
||||||
.font(.system(size: 16, weight: .medium))
|
|
||||||
.foregroundColor(isSelected ? .white : .primary)
|
|
||||||
|
|
||||||
// 情绪点
|
|
||||||
Circle()
|
|
||||||
.fill(emotionColorForDate(date))
|
|
||||||
.frame(width: 6, height: 6)
|
|
||||||
.opacity(hasEmotionRecord(for: date) ? 1 : 0)
|
|
||||||
}
|
|
||||||
.frame(width: 40, height: 44)
|
|
||||||
.background(
|
|
||||||
RoundedRectangle(cornerRadius: 12)
|
|
||||||
.fill(isSelected ? Color.blue : Color.clear)
|
|
||||||
)
|
|
||||||
.overlay(
|
|
||||||
RoundedRectangle(cornerRadius: 12)
|
|
||||||
.stroke(isToday ? Color.blue : Color.clear, lineWidth: 1)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding(.horizontal, 16)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding()
|
|
||||||
.background(Color.theme.cardBackground)
|
|
||||||
.cornerRadius(16)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// MARK: - AI助手头像区域
|
|
||||||
private var aiAvatarSection: some View {
|
|
||||||
HStack {
|
|
||||||
Spacer()
|
|
||||||
|
|
||||||
// AI助手形象 - 紧凑版本
|
|
||||||
ZStack {
|
|
||||||
// 背景渐变
|
|
||||||
Circle()
|
|
||||||
.fill(
|
|
||||||
LinearGradient(
|
|
||||||
colors: [Color.blue.opacity(0.1), Color.purple.opacity(0.1)],
|
|
||||||
startPoint: .topLeading,
|
|
||||||
endPoint: .bottomTrailing
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.frame(width: 80, height: 80)
|
|
||||||
|
|
||||||
// AI助手图标
|
|
||||||
Image(systemName: "brain.head.profile")
|
|
||||||
.font(.system(size: 35))
|
|
||||||
.foregroundColor(.blue)
|
|
||||||
.scaleEffect(aiService.isLoading ? 1.1 : 1.0)
|
|
||||||
.animation(.easeInOut(duration: 1).repeatForever(), value: aiService.isLoading)
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
.padding(.vertical, 10)
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - 聊天区域
|
|
||||||
private var chatArea: some View {
|
|
||||||
VStack(spacing: 0) {
|
|
||||||
if mockData.conversations.isEmpty {
|
|
||||||
// 默认状态:显示问候语和快捷回复
|
|
||||||
defaultChatContent
|
|
||||||
} else {
|
|
||||||
// 有聊天记录时显示对话列表
|
|
||||||
chatMessagesList
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.frame(minHeight: 300) // 设置最小高度确保有足够的聊天空间
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - 默认聊天内容
|
|
||||||
private var defaultChatContent: some View {
|
|
||||||
VStack(spacing: 20) {
|
|
||||||
// AI问候消息气泡
|
|
||||||
HStack {
|
|
||||||
// AI头像
|
|
||||||
Circle()
|
|
||||||
.fill(Color.blue.opacity(0.1))
|
|
||||||
.frame(width: 32, height: 32)
|
|
||||||
.overlay(
|
|
||||||
Image(systemName: "brain.head.profile")
|
|
||||||
.font(.system(size: 16))
|
|
||||||
.foregroundColor(.blue)
|
|
||||||
)
|
|
||||||
|
|
||||||
// 问候消息气泡
|
|
||||||
VStack(alignment: .leading, spacing: 8) {
|
|
||||||
Text("你好!我是你的情绪陪伴师")
|
|
||||||
.font(.system(size: 16, weight: .medium))
|
|
||||||
.foregroundColor(Color.theme.primaryText)
|
|
||||||
|
|
||||||
Text(getGreetingText())
|
|
||||||
.font(.system(size: 14))
|
|
||||||
.foregroundColor(Color.theme.secondaryText)
|
|
||||||
}
|
|
||||||
.padding(.horizontal, 16)
|
|
||||||
.padding(.vertical, 12)
|
|
||||||
.background(
|
|
||||||
RoundedRectangle(cornerRadius: 18)
|
|
||||||
.fill(Color.theme.surfaceBackground)
|
|
||||||
.shadow(color: Color.black.opacity(0.05), radius: 2, x: 0, y: 1)
|
|
||||||
)
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
.padding(.horizontal, 4)
|
|
||||||
|
|
||||||
// 快捷回复提示
|
|
||||||
HStack {
|
|
||||||
Spacer()
|
|
||||||
Text("你可以这样开始对话")
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundColor(Color.theme.tertiaryText)
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
.padding(.top, 10)
|
|
||||||
|
|
||||||
// 快捷回复卡片
|
|
||||||
chatQuickReplyCards
|
|
||||||
}
|
|
||||||
.padding(.vertical, 20)
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - 聊天消息列表
|
|
||||||
private var chatMessagesList: some View {
|
|
||||||
LazyVStack(spacing: 12) {
|
|
||||||
ForEach(mockData.conversations.prefix(3), id: \.id) { conversation in
|
|
||||||
ConversationPreviewCard(conversation: conversation) {
|
|
||||||
navigationManager.navigateToChat(conversation: conversation)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding(.vertical, 10)
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - 聊天样式的快捷回复卡片
|
|
||||||
private var chatQuickReplyCards: some View {
|
|
||||||
LazyVGrid(columns: [
|
|
||||||
GridItem(.flexible(), spacing: 8),
|
|
||||||
GridItem(.flexible(), spacing: 8)
|
|
||||||
], spacing: 12) {
|
|
||||||
ForEach(quickReplies, id: \.self) { reply in
|
|
||||||
Button(action: { sendQuickReply(reply) }) {
|
|
||||||
Text(reply)
|
|
||||||
.font(.system(size: 14, weight: .medium))
|
|
||||||
.foregroundColor(Color("AccentColor"))
|
|
||||||
.multilineTextAlignment(.center)
|
|
||||||
.lineLimit(2)
|
|
||||||
.padding(.horizontal, 12)
|
|
||||||
.padding(.vertical, 14)
|
|
||||||
.frame(maxWidth: .infinity)
|
|
||||||
.frame(minHeight: 70)
|
|
||||||
.background(
|
|
||||||
RoundedRectangle(cornerRadius: 12)
|
|
||||||
.fill(Color.theme.cardBackground)
|
|
||||||
.overlay(
|
|
||||||
RoundedRectangle(cornerRadius: 12)
|
|
||||||
.stroke(Color("AccentColor").opacity(0.3), lineWidth: 1)
|
|
||||||
)
|
|
||||||
.shadow(
|
|
||||||
color: Color.black.opacity(0.05),
|
|
||||||
radius: 3,
|
|
||||||
x: 0,
|
|
||||||
y: 2
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
.buttonStyle(PlainButtonStyle())
|
|
||||||
.scaleEffect(1.0)
|
|
||||||
.animation(.spring(response: 0.3, dampingFraction: 0.8), value: false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
private let quickReplies = [
|
|
||||||
"我今天感觉有点焦虑",
|
|
||||||
"想和你聊聊最近的压力",
|
|
||||||
"今天发生了一些开心的事",
|
|
||||||
"我需要一些建议"
|
|
||||||
]
|
|
||||||
|
|
||||||
// MARK: - 输入区域
|
|
||||||
private var inputArea: some View {
|
|
||||||
VStack(spacing: 0) {
|
|
||||||
// 渐变分隔线
|
|
||||||
LinearGradient(
|
|
||||||
colors: [Color.theme.divider.opacity(0), Color.theme.divider, Color.theme.divider.opacity(0)],
|
|
||||||
startPoint: .leading,
|
|
||||||
endPoint: .trailing
|
|
||||||
)
|
|
||||||
.frame(height: 1)
|
|
||||||
|
|
||||||
// 紧凑的输入区域
|
|
||||||
HStack(spacing: 12) {
|
|
||||||
// 图片按钮
|
|
||||||
Button(action: { }) {
|
|
||||||
Image(systemName: "photo.circle.fill")
|
|
||||||
.font(.title2)
|
|
||||||
.foregroundColor(Color.theme.secondaryText)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 主输入框容器
|
|
||||||
HStack(spacing: 8) {
|
|
||||||
TextField("说说你的感受...", text: $inputText)
|
|
||||||
.textFieldStyle(PlainTextFieldStyle())
|
|
||||||
.foregroundColor(Color.theme.primaryText)
|
|
||||||
.padding(.vertical, 12)
|
|
||||||
.padding(.leading, 16)
|
|
||||||
|
|
||||||
// 语音输入按钮
|
|
||||||
Button(action: { }) {
|
|
||||||
Image(systemName: "mic.circle.fill")
|
|
||||||
.font(.title2)
|
|
||||||
.foregroundColor(Color("AccentColor"))
|
|
||||||
}
|
|
||||||
.padding(.trailing, 8)
|
|
||||||
}
|
|
||||||
.background(
|
|
||||||
RoundedRectangle(cornerRadius: 24)
|
|
||||||
.fill(Color.theme.surfaceBackground)
|
|
||||||
.shadow(
|
|
||||||
color: Color.black.opacity(0.05),
|
|
||||||
radius: 2,
|
|
||||||
x: 0,
|
|
||||||
y: 1
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
// 发送按钮
|
|
||||||
Button(action: sendMessage) {
|
|
||||||
Image(systemName: inputText.isEmpty ? "arrow.up.circle" : "arrow.up.circle.fill")
|
|
||||||
.font(.title2)
|
|
||||||
.foregroundColor(inputText.isEmpty ? Color.theme.secondaryText : Color("AccentColor"))
|
|
||||||
.scaleEffect(inputText.isEmpty ? 0.9 : 1.0)
|
|
||||||
.animation(.spring(response: 0.3, dampingFraction: 0.8), value: inputText.isEmpty)
|
|
||||||
}
|
|
||||||
.disabled(inputText.isEmpty)
|
|
||||||
}
|
|
||||||
.padding(.horizontal, 16)
|
|
||||||
.padding(.top, 12)
|
|
||||||
.padding(.bottom, 12) // 适中的底部间距
|
|
||||||
.background(
|
|
||||||
Color.theme.cardBackground
|
|
||||||
.overlay(
|
|
||||||
// 顶部高光效果
|
|
||||||
LinearGradient(
|
|
||||||
colors: [
|
|
||||||
Color.white.opacity(themeManager.isDarkMode ? 0.05 : 0.4),
|
|
||||||
Color.clear
|
|
||||||
],
|
|
||||||
startPoint: .top,
|
|
||||||
endPoint: .bottom
|
|
||||||
)
|
|
||||||
.frame(height: 1),
|
|
||||||
alignment: .top
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
// AI状态指示器(如果需要的话)
|
|
||||||
if aiService.isLoading {
|
|
||||||
HStack(spacing: 4) {
|
|
||||||
ProgressView()
|
|
||||||
.scaleEffect(0.7)
|
|
||||||
.tint(Color("AccentColor"))
|
|
||||||
Text("AI思考中...")
|
|
||||||
.font(.caption2)
|
|
||||||
.foregroundColor(Color.theme.secondaryText)
|
|
||||||
}
|
|
||||||
.padding(.horizontal, 16)
|
|
||||||
.padding(.bottom, 12)
|
|
||||||
.background(Color.theme.cardBackground)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - 私有方法
|
|
||||||
|
|
||||||
private func getGreetingText() -> String {
|
|
||||||
let hour = Calendar.current.component(.hour, from: Date())
|
|
||||||
switch hour {
|
|
||||||
case 5..<12:
|
|
||||||
return "早上好!新的一天,新的开始。今天感觉怎么样?"
|
|
||||||
case 12..<17:
|
|
||||||
return "下午好!工作辛苦了,有什么想聊的吗?"
|
|
||||||
case 17..<22:
|
|
||||||
return "晚上好!一天结束了,让我们聊聊今天的感受吧。"
|
|
||||||
default:
|
|
||||||
return "夜深了,如果睡不着,我可以陪你聊聊。"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func emotionColorForDate(_ date: Date) -> Color {
|
|
||||||
// 模拟数据,实际应该从数据库获取
|
|
||||||
let day = Calendar.current.component(.day, from: date)
|
|
||||||
switch day % 6 {
|
|
||||||
case 0: return .red
|
|
||||||
case 1: return .blue
|
|
||||||
case 2: return .green
|
|
||||||
case 3: return .yellow
|
|
||||||
case 4: return .purple
|
|
||||||
default: return .orange
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func hasEmotionRecord(for date: Date) -> Bool {
|
|
||||||
// 模拟数据,实际应该查询数据库
|
|
||||||
let day = Calendar.current.component(.day, from: date)
|
|
||||||
return day % 3 != 0
|
|
||||||
}
|
|
||||||
|
|
||||||
private func sendQuickReply(_ reply: String) {
|
|
||||||
inputText = reply
|
|
||||||
sendMessage()
|
|
||||||
}
|
|
||||||
|
|
||||||
private func sendMessage() {
|
|
||||||
guard !inputText.isEmpty else { return }
|
|
||||||
|
|
||||||
let messageContent = inputText
|
|
||||||
inputText = ""
|
|
||||||
|
|
||||||
// 使用导航管理器发送消息
|
|
||||||
navigationManager.sendMessage(messageContent)
|
|
||||||
|
|
||||||
// 如果没有当前对话,自动进入全屏聊天模式
|
|
||||||
if navigationManager.currentChatConversation == nil {
|
|
||||||
navigationManager.navigateToChat()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func loadConversations() {
|
|
||||||
// 从数据库或本地存储加载对话历史
|
|
||||||
// mockData.conversations = [] // 如果需要清空对话历史
|
|
||||||
}
|
|
||||||
|
|
||||||
private func simulateInitialLoading() {
|
|
||||||
loadingState = .loading
|
|
||||||
|
|
||||||
// 模拟异步加载过程
|
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) {
|
|
||||||
withAnimation(.easeOut(duration: 0.6)) {
|
|
||||||
isInitialLoading = false
|
|
||||||
loadingState = .loaded
|
|
||||||
}
|
|
||||||
loadConversations()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func simulateRefresh() async {
|
|
||||||
// 模拟刷新延迟
|
|
||||||
try? await Task.sleep(nanoseconds: 1_000_000_000)
|
|
||||||
await MainActor.run {
|
|
||||||
loadConversations()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - 骨架屏视图
|
|
||||||
private var recordViewSkeleton: some View {
|
|
||||||
VStack(spacing: 0) {
|
|
||||||
// 顶部导航栏骨架屏
|
|
||||||
HStack {
|
|
||||||
SkeletonView(width: 24, height: 24, cornerRadius: 12)
|
|
||||||
Spacer()
|
|
||||||
SkeletonView(width: 100, height: 20, cornerRadius: 6)
|
|
||||||
Spacer()
|
|
||||||
SkeletonView(width: 24, height: 24, cornerRadius: 12)
|
|
||||||
}
|
|
||||||
.padding(.horizontal, 16)
|
|
||||||
.padding(.top, 8)
|
|
||||||
|
|
||||||
// 日历组件骨架屏
|
|
||||||
VStack(spacing: 12) {
|
|
||||||
HStack {
|
|
||||||
SkeletonView(width: 80, height: 20, cornerRadius: 6)
|
|
||||||
Spacer()
|
|
||||||
SkeletonView(width: 60, height: 16, cornerRadius: 4)
|
|
||||||
}
|
|
||||||
|
|
||||||
HStack(spacing: 16) {
|
|
||||||
ForEach(0..<7, id: \.self) { _ in
|
|
||||||
VStack(spacing: 4) {
|
|
||||||
SkeletonView(width: 20, height: 12, cornerRadius: 3)
|
|
||||||
SkeletonView(width: 40, height: 44, cornerRadius: 12)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding()
|
|
||||||
.background(Color.theme.cardBackground)
|
|
||||||
.cornerRadius(16)
|
|
||||||
.padding(.horizontal, 16)
|
|
||||||
.padding(.vertical, 8)
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
|
|
||||||
// AI助手区域骨架屏
|
|
||||||
VStack(spacing: 24) {
|
|
||||||
SkeletonView(width: 200, height: 200, cornerRadius: 100)
|
|
||||||
|
|
||||||
VStack(spacing: 8) {
|
|
||||||
SkeletonView(width: 200, height: 20, cornerRadius: 6)
|
|
||||||
SkeletonView(width: 250, height: 16, cornerRadius: 4)
|
|
||||||
SkeletonView(width: 180, height: 16, cornerRadius: 4)
|
|
||||||
}
|
|
||||||
|
|
||||||
VStack(spacing: 8) {
|
|
||||||
SkeletonView(width: 120, height: 12, cornerRadius: 3)
|
|
||||||
ForEach(0..<3, id: \.self) { _ in
|
|
||||||
SkeletonView(width: 160, height: 32, cornerRadius: 16)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding(.horizontal, 24)
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
|
|
||||||
// 输入区域骨架屏
|
|
||||||
VStack(spacing: 12) {
|
|
||||||
HStack(spacing: 12) {
|
|
||||||
SkeletonView(height: 48, cornerRadius: 24)
|
|
||||||
SkeletonView(width: 48, height: 48, cornerRadius: 24)
|
|
||||||
}
|
|
||||||
|
|
||||||
HStack(spacing: 24) {
|
|
||||||
ForEach(0..<2, id: \.self) { _ in
|
|
||||||
VStack(spacing: 4) {
|
|
||||||
SkeletonView(width: 24, height: 24, cornerRadius: 12)
|
|
||||||
SkeletonView(width: 30, height: 12, cornerRadius: 3)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding(.horizontal, 16)
|
|
||||||
.padding(.top, 16)
|
|
||||||
.padding(.bottom, 24)
|
|
||||||
.background(Color.theme.cardBackground)
|
|
||||||
}
|
|
||||||
.background(Color.theme.background)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - 辅助视图
|
|
||||||
|
|
||||||
// 心情选择视图
|
|
||||||
struct MoodPickerView: View {
|
|
||||||
let selectedDate: Date
|
|
||||||
@Binding var selectedMood: String
|
|
||||||
@Binding var isPresented: Bool
|
|
||||||
|
|
||||||
let moods = [
|
|
||||||
("😊", "开心"), ("😢", "难过"), ("😡", "愤怒"),
|
|
||||||
("😰", "焦虑"), ("😌", "平静"), ("🤔", "思考")
|
|
||||||
]
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
NavigationView {
|
|
||||||
VStack(spacing: 24) {
|
|
||||||
Text("选择今日心情")
|
|
||||||
.font(.title2)
|
|
||||||
.fontWeight(.semibold)
|
|
||||||
|
|
||||||
LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: 3), spacing: 16) {
|
|
||||||
ForEach(moods, id: \.0) { emoji, name in
|
|
||||||
Button(action: {
|
|
||||||
selectedMood = emoji
|
|
||||||
isPresented = false
|
|
||||||
}) {
|
|
||||||
VStack(spacing: 8) {
|
|
||||||
Text(emoji)
|
|
||||||
.font(.system(size: 40))
|
|
||||||
Text(name)
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundColor(.primary)
|
|
||||||
}
|
|
||||||
.frame(width: 80, height: 80)
|
|
||||||
.background(Color(.systemGray6))
|
|
||||||
.cornerRadius(16)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding()
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
.navigationTitle("心情记录")
|
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
|
||||||
.toolbar {
|
|
||||||
ToolbarItem(placement: .cancellationAction) {
|
|
||||||
Button("取消") { isPresented = false }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - 日期格式化扩展
|
|
||||||
extension DateFormatter {
|
|
||||||
static let weekdayShort: DateFormatter = {
|
|
||||||
let formatter = DateFormatter()
|
|
||||||
formatter.dateFormat = "E"
|
|
||||||
return formatter
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
@@ -1,875 +0,0 @@
|
|||||||
//
|
|
||||||
// SupportViews.swift
|
|
||||||
// EmotionMuseum
|
|
||||||
//
|
|
||||||
// Created by huazhongmin on 2024/01/01.
|
|
||||||
//
|
|
||||||
|
|
||||||
import SwiftUI
|
|
||||||
import MapKit
|
|
||||||
|
|
||||||
// MARK: - 占星分析视图
|
|
||||||
struct AstroAnalysisView: View {
|
|
||||||
@State private var selectedDate = Date()
|
|
||||||
@State private var selectedTime = Date()
|
|
||||||
@State private var birthLocation = ""
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
NavigationView {
|
|
||||||
ScrollView {
|
|
||||||
VStack(spacing: 24) {
|
|
||||||
Text("通过占星学了解你的内在特质")
|
|
||||||
.font(.headline)
|
|
||||||
.multilineTextAlignment(.center)
|
|
||||||
.padding()
|
|
||||||
|
|
||||||
VStack(spacing: 16) {
|
|
||||||
DatePicker("出生日期", selection: $selectedDate, displayedComponents: .date)
|
|
||||||
DatePicker("出生时间", selection: $selectedTime, displayedComponents: .hourAndMinute)
|
|
||||||
TextField("出生地点", text: $birthLocation)
|
|
||||||
.textFieldStyle(RoundedBorderTextFieldStyle())
|
|
||||||
}
|
|
||||||
.padding()
|
|
||||||
|
|
||||||
Button("生成星盘分析") {
|
|
||||||
// 分析逻辑
|
|
||||||
}
|
|
||||||
.buttonStyle(.borderedProminent)
|
|
||||||
.padding()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.navigationTitle("占星分析")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - 地点标记卡片
|
|
||||||
struct LocationMarkerCard: View {
|
|
||||||
let location: LocationPin
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
VStack(alignment: .leading, spacing: 8) {
|
|
||||||
HStack {
|
|
||||||
Image(systemName: location.category.icon)
|
|
||||||
.foregroundColor(location.emotion.color)
|
|
||||||
Text(location.name)
|
|
||||||
.font(.headline)
|
|
||||||
Spacer()
|
|
||||||
Text(location.emotion.emoji)
|
|
||||||
.font(.title2)
|
|
||||||
}
|
|
||||||
|
|
||||||
Text(location.description)
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
.lineLimit(2)
|
|
||||||
}
|
|
||||||
.padding()
|
|
||||||
.background(Color(.systemBackground))
|
|
||||||
.cornerRadius(12)
|
|
||||||
.shadow(radius: 2)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - 推荐地点卡片
|
|
||||||
struct RecommendedLocationCard: View {
|
|
||||||
let location: LocationPin
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
VStack(alignment: .leading, spacing: 12) {
|
|
||||||
HStack {
|
|
||||||
VStack(alignment: .leading, spacing: 4) {
|
|
||||||
Text(location.name)
|
|
||||||
.font(.headline)
|
|
||||||
.fontWeight(.semibold)
|
|
||||||
|
|
||||||
Text(location.category.rawValue)
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
|
|
||||||
VStack(spacing: 4) {
|
|
||||||
Text(location.emotion.emoji)
|
|
||||||
.font(.title)
|
|
||||||
|
|
||||||
Text(location.emotion.displayName)
|
|
||||||
.font(.caption2)
|
|
||||||
.foregroundColor(location.emotion.color)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Text(location.description)
|
|
||||||
.font(.subheadline)
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
.lineLimit(2)
|
|
||||||
|
|
||||||
HStack {
|
|
||||||
Label("\(location.visitCount)", systemImage: "person.2")
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
|
|
||||||
if !location.tags.isEmpty {
|
|
||||||
Text("#\(location.tags.first ?? "")")
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundColor(.blue)
|
|
||||||
.padding(.horizontal, 8)
|
|
||||||
.padding(.vertical, 2)
|
|
||||||
.background(Color.blue.opacity(0.1))
|
|
||||||
.cornerRadius(4)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding()
|
|
||||||
.background(Color(.systemBackground))
|
|
||||||
.cornerRadius(16)
|
|
||||||
.shadow(color: .black.opacity(0.05), radius: 8, y: 2)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - 社区动态卡片
|
|
||||||
struct CommunityPostCard: View {
|
|
||||||
let post: CommunityPost
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
VStack(alignment: .leading, spacing: 12) {
|
|
||||||
HStack {
|
|
||||||
Circle()
|
|
||||||
.fill(Color.blue)
|
|
||||||
.frame(width: 40, height: 40)
|
|
||||||
.overlay(
|
|
||||||
Text(String(post.authorName.prefix(1)))
|
|
||||||
.foregroundColor(.white)
|
|
||||||
.fontWeight(.semibold)
|
|
||||||
)
|
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 2) {
|
|
||||||
Text(post.authorName)
|
|
||||||
.font(.subheadline)
|
|
||||||
.fontWeight(.medium)
|
|
||||||
|
|
||||||
Text(DateFormatter.shortRelative.string(from: post.createdAt))
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
|
|
||||||
Text("💭")
|
|
||||||
.font(.title2)
|
|
||||||
}
|
|
||||||
|
|
||||||
Text(post.content)
|
|
||||||
.font(.body)
|
|
||||||
.lineLimit(3)
|
|
||||||
|
|
||||||
if !post.tags.isEmpty {
|
|
||||||
ScrollView(.horizontal, showsIndicators: false) {
|
|
||||||
HStack(spacing: 8) {
|
|
||||||
ForEach(post.tags, id: \.self) { tag in
|
|
||||||
Text("#\(tag)")
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundColor(.blue)
|
|
||||||
.padding(.horizontal, 8)
|
|
||||||
.padding(.vertical, 4)
|
|
||||||
.background(Color.blue.opacity(0.1))
|
|
||||||
.cornerRadius(8)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding(.horizontal, 4)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
HStack {
|
|
||||||
Button(action: {}) {
|
|
||||||
HStack(spacing: 4) {
|
|
||||||
Image(systemName: post.isLikedByCurrentUser ? "heart.fill" : "heart")
|
|
||||||
.foregroundColor(post.isLikedByCurrentUser ? .red : .gray)
|
|
||||||
Text("\(post.likes)")
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Button(action: {}) {
|
|
||||||
HStack(spacing: 4) {
|
|
||||||
Image(systemName: "message")
|
|
||||||
.foregroundColor(.gray)
|
|
||||||
Text("\(post.comments.count)")
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
|
|
||||||
Button(action: {}) {
|
|
||||||
Image(systemName: "square.and.arrow.up")
|
|
||||||
.foregroundColor(.gray)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding()
|
|
||||||
.background(Color(.systemBackground))
|
|
||||||
.cornerRadius(16)
|
|
||||||
.shadow(color: .black.opacity(0.05), radius: 8, y: 2)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - 所有地点视图
|
|
||||||
struct AllLocationsView: View {
|
|
||||||
@EnvironmentObject var dataManager: MockDataManager
|
|
||||||
@State private var searchText = ""
|
|
||||||
@State private var selectedCategory: LocationCategory?
|
|
||||||
@State private var sortOption: SortOption = .recent
|
|
||||||
|
|
||||||
enum SortOption: String, CaseIterable {
|
|
||||||
case recent = "最近访问"
|
|
||||||
case popular = "最受欢迎"
|
|
||||||
case nearby = "距离最近"
|
|
||||||
case alphabetical = "按名称"
|
|
||||||
}
|
|
||||||
|
|
||||||
var filteredLocations: [LocationPin] {
|
|
||||||
let filtered = dataManager.locationPins.filter { location in
|
|
||||||
let matchesSearch = searchText.isEmpty ||
|
|
||||||
location.name.localizedCaseInsensitiveContains(searchText) ||
|
|
||||||
location.description.localizedCaseInsensitiveContains(searchText) ||
|
|
||||||
location.tags.contains { $0.localizedCaseInsensitiveContains(searchText) }
|
|
||||||
|
|
||||||
let matchesCategory = selectedCategory == nil || location.category == selectedCategory
|
|
||||||
|
|
||||||
return matchesSearch && matchesCategory
|
|
||||||
}
|
|
||||||
|
|
||||||
switch sortOption {
|
|
||||||
case .recent:
|
|
||||||
return filtered.sorted { ($0.lastVisitAt ?? Date.distantPast) > ($1.lastVisitAt ?? Date.distantPast) }
|
|
||||||
case .popular:
|
|
||||||
return filtered.sorted { $0.visitCount > $1.visitCount }
|
|
||||||
case .nearby:
|
|
||||||
return filtered // 实际应用中需要根据用户位置排序
|
|
||||||
case .alphabetical:
|
|
||||||
return filtered.sorted { $0.name < $1.name }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
NavigationView {
|
|
||||||
VStack(spacing: 0) {
|
|
||||||
// 搜索栏
|
|
||||||
SearchBar(text: $searchText)
|
|
||||||
.padding()
|
|
||||||
|
|
||||||
// 筛选和排序
|
|
||||||
ScrollView(.horizontal, showsIndicators: false) {
|
|
||||||
HStack(spacing: 12) {
|
|
||||||
// 分类筛选
|
|
||||||
Button(action: { selectedCategory = nil }) {
|
|
||||||
Text("全部")
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundColor(selectedCategory == nil ? .white : .primary)
|
|
||||||
.padding(.horizontal, 12)
|
|
||||||
.padding(.vertical, 6)
|
|
||||||
.background(selectedCategory == nil ? Color.blue : Color(.systemGray6))
|
|
||||||
.cornerRadius(16)
|
|
||||||
}
|
|
||||||
|
|
||||||
ForEach(LocationCategory.allCases, id: \.self) { category in
|
|
||||||
Button(action: { selectedCategory = category }) {
|
|
||||||
Text(category.rawValue)
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundColor(selectedCategory == category ? .white : .primary)
|
|
||||||
.padding(.horizontal, 12)
|
|
||||||
.padding(.vertical, 6)
|
|
||||||
.background(selectedCategory == category ? Color.blue : Color(.systemGray6))
|
|
||||||
.cornerRadius(16)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding(.horizontal)
|
|
||||||
}
|
|
||||||
.padding(.bottom)
|
|
||||||
|
|
||||||
// 排序选项
|
|
||||||
Picker("排序", selection: $sortOption) {
|
|
||||||
ForEach(SortOption.allCases, id: \.self) { option in
|
|
||||||
Text(option.rawValue).tag(option)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.pickerStyle(SegmentedPickerStyle())
|
|
||||||
.padding(.horizontal)
|
|
||||||
|
|
||||||
// 地点列表
|
|
||||||
ScrollView {
|
|
||||||
LazyVStack(spacing: 16) {
|
|
||||||
ForEach(filteredLocations) { location in
|
|
||||||
AllLocationCard(location: location)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.navigationTitle("所有地点")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - 搜索栏
|
|
||||||
struct SearchBar: View {
|
|
||||||
@Binding var text: String
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
HStack {
|
|
||||||
Image(systemName: "magnifyingglass")
|
|
||||||
.foregroundColor(.gray)
|
|
||||||
|
|
||||||
TextField("搜索地点、标签...", text: $text)
|
|
||||||
.textFieldStyle(PlainTextFieldStyle())
|
|
||||||
|
|
||||||
if !text.isEmpty {
|
|
||||||
Button(action: { text = "" }) {
|
|
||||||
Image(systemName: "xmark.circle.fill")
|
|
||||||
.foregroundColor(.gray)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding(.horizontal, 12)
|
|
||||||
.padding(.vertical, 8)
|
|
||||||
.background(Color(.systemGray6))
|
|
||||||
.cornerRadius(10)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - 地点卡片
|
|
||||||
struct AllLocationCard: View {
|
|
||||||
let location: LocationPin
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
HStack(spacing: 16) {
|
|
||||||
// 地点图标和情绪
|
|
||||||
VStack(spacing: 8) {
|
|
||||||
Image(systemName: location.category.icon)
|
|
||||||
.font(.title2)
|
|
||||||
.foregroundColor(location.emotion.color)
|
|
||||||
.frame(width: 40, height: 40)
|
|
||||||
.background(location.emotion.color.opacity(0.2))
|
|
||||||
.cornerRadius(12)
|
|
||||||
|
|
||||||
Text(location.emotion.emoji)
|
|
||||||
.font(.title3)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 地点信息
|
|
||||||
VStack(alignment: .leading, spacing: 4) {
|
|
||||||
Text(location.name)
|
|
||||||
.font(.headline)
|
|
||||||
.fontWeight(.semibold)
|
|
||||||
|
|
||||||
Text(location.description)
|
|
||||||
.font(.subheadline)
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
.lineLimit(2)
|
|
||||||
|
|
||||||
HStack {
|
|
||||||
Label("\(location.visitCount)次访问", systemImage: "clock")
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
|
|
||||||
if let lastVisit = location.lastVisitAt {
|
|
||||||
Text(DateFormatter.shortRelative.string(from: lastVisit))
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
|
|
||||||
// 操作按钮
|
|
||||||
VStack(spacing: 8) {
|
|
||||||
Button(action: {}) {
|
|
||||||
Image(systemName: location.isBookmarked ? "bookmark.fill" : "bookmark")
|
|
||||||
.foregroundColor(location.isBookmarked ? .blue : .gray)
|
|
||||||
}
|
|
||||||
|
|
||||||
Button(action: {}) {
|
|
||||||
Image(systemName: "square.and.arrow.up")
|
|
||||||
.foregroundColor(.gray)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding()
|
|
||||||
.background(Color(.systemBackground))
|
|
||||||
.cornerRadius(16)
|
|
||||||
.shadow(color: .black.opacity(0.05), radius: 8, y: 2)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - 创建分享视图
|
|
||||||
struct CreatePostView: View {
|
|
||||||
let selectedLocation: LocationPin?
|
|
||||||
@Environment(\.dismiss) private var dismiss
|
|
||||||
@State private var postContent = ""
|
|
||||||
@State private var selectedEmotion: EmotionType = .neutral
|
|
||||||
@State private var selectedTags: Set<String> = []
|
|
||||||
|
|
||||||
let availableTags = ["推荐", "美食", "风景", "心情", "感悟", "治愈", "安静", "热闹"]
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
NavigationView {
|
|
||||||
ScrollView {
|
|
||||||
VStack(spacing: 20) {
|
|
||||||
// 内容输入
|
|
||||||
VStack(alignment: .leading, spacing: 8) {
|
|
||||||
Text("分享你的感受")
|
|
||||||
.font(.headline)
|
|
||||||
|
|
||||||
TextEditor(text: $postContent)
|
|
||||||
.frame(height: 120)
|
|
||||||
.padding(8)
|
|
||||||
.background(Color(.systemGray6))
|
|
||||||
.cornerRadius(12)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 情绪选择
|
|
||||||
VStack(alignment: .leading, spacing: 8) {
|
|
||||||
Text("当前情绪")
|
|
||||||
.font(.headline)
|
|
||||||
|
|
||||||
ScrollView(.horizontal, showsIndicators: false) {
|
|
||||||
HStack(spacing: 12) {
|
|
||||||
ForEach(EmotionType.allCases, id: \.self) { emotion in
|
|
||||||
Button(action: { selectedEmotion = emotion }) {
|
|
||||||
VStack(spacing: 4) {
|
|
||||||
Text(emotion.emoji)
|
|
||||||
.font(.title2)
|
|
||||||
Text(emotion.rawValue)
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundColor(.primary)
|
|
||||||
}
|
|
||||||
.padding(8)
|
|
||||||
.background(
|
|
||||||
RoundedRectangle(cornerRadius: 12)
|
|
||||||
.fill(selectedEmotion == emotion ? emotion.color.opacity(0.3) : Color(.systemGray6))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding(.horizontal, 4)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 标签选择
|
|
||||||
VStack(alignment: .leading, spacing: 8) {
|
|
||||||
Text("添加标签")
|
|
||||||
.font(.headline)
|
|
||||||
|
|
||||||
LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: 4), spacing: 8) {
|
|
||||||
ForEach(availableTags, id: \.self) { tag in
|
|
||||||
Button(action: {
|
|
||||||
if selectedTags.contains(tag) {
|
|
||||||
selectedTags.remove(tag)
|
|
||||||
} else {
|
|
||||||
selectedTags.insert(tag)
|
|
||||||
}
|
|
||||||
}) {
|
|
||||||
Text("#\(tag)")
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundColor(selectedTags.contains(tag) ? .white : .primary)
|
|
||||||
.padding(.horizontal, 8)
|
|
||||||
.padding(.vertical, 4)
|
|
||||||
.background(
|
|
||||||
RoundedRectangle(cornerRadius: 8)
|
|
||||||
.fill(selectedTags.contains(tag) ? Color.blue : Color(.systemGray6))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 位置信息
|
|
||||||
if let location = selectedLocation {
|
|
||||||
VStack(alignment: .leading, spacing: 8) {
|
|
||||||
Text("位置")
|
|
||||||
.font(.headline)
|
|
||||||
|
|
||||||
HStack(spacing: 12) {
|
|
||||||
Image(systemName: location.category.icon)
|
|
||||||
.foregroundColor(.blue)
|
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 2) {
|
|
||||||
Text(location.name)
|
|
||||||
.font(.subheadline)
|
|
||||||
.fontWeight(.medium)
|
|
||||||
|
|
||||||
if let address = location.address {
|
|
||||||
Text(address)
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
.padding()
|
|
||||||
.background(Color(.systemGray6))
|
|
||||||
.cornerRadius(12)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding()
|
|
||||||
}
|
|
||||||
.navigationTitle("分享动态")
|
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
|
||||||
.toolbar {
|
|
||||||
ToolbarItem(placement: .cancellationAction) {
|
|
||||||
Button("取消") { dismiss() }
|
|
||||||
}
|
|
||||||
|
|
||||||
ToolbarItem(placement: .confirmationAction) {
|
|
||||||
Button("发布") {
|
|
||||||
// 发布逻辑
|
|
||||||
dismiss()
|
|
||||||
}
|
|
||||||
.disabled(postContent.isEmpty)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - 地点详情视图
|
|
||||||
struct LocationDetailView: View {
|
|
||||||
let location: LocationPin
|
|
||||||
@Environment(\.dismiss) private var dismiss
|
|
||||||
@State private var showingShareView = false
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
NavigationView {
|
|
||||||
ScrollView {
|
|
||||||
VStack(spacing: 24) {
|
|
||||||
// 头部信息
|
|
||||||
VStack(spacing: 16) {
|
|
||||||
HStack(spacing: 20) {
|
|
||||||
Image(systemName: location.category.icon)
|
|
||||||
.font(.system(size: 48))
|
|
||||||
.foregroundColor(location.emotion.color)
|
|
||||||
.frame(width: 80, height: 80)
|
|
||||||
.background(location.emotion.color.opacity(0.2))
|
|
||||||
.cornerRadius(20)
|
|
||||||
|
|
||||||
VStack(spacing: 8) {
|
|
||||||
Text(location.emotion.emoji)
|
|
||||||
.font(.system(size: 60))
|
|
||||||
|
|
||||||
Text(location.emotion.displayName)
|
|
||||||
.font(.headline)
|
|
||||||
.foregroundColor(location.emotion.color)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Text(location.name)
|
|
||||||
.font(.title)
|
|
||||||
.fontWeight(.bold)
|
|
||||||
.multilineTextAlignment(.center)
|
|
||||||
|
|
||||||
Text(location.description)
|
|
||||||
.font(.body)
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
.multilineTextAlignment(.center)
|
|
||||||
.padding(.horizontal)
|
|
||||||
}
|
|
||||||
.padding(.vertical, 20)
|
|
||||||
.frame(maxWidth: .infinity)
|
|
||||||
.background(
|
|
||||||
LinearGradient(
|
|
||||||
colors: [location.emotion.color.opacity(0.1), location.emotion.color.opacity(0.05)],
|
|
||||||
startPoint: .topLeading,
|
|
||||||
endPoint: .bottomTrailing
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.cornerRadius(20)
|
|
||||||
|
|
||||||
// 基本信息
|
|
||||||
VStack(alignment: .leading, spacing: 12) {
|
|
||||||
Text("基本信息")
|
|
||||||
.font(.headline)
|
|
||||||
|
|
||||||
InfoRow(icon: "location", title: "地址", value: location.address ?? "未设置")
|
|
||||||
InfoRow(icon: "list.bullet", title: "类别", value: location.category.rawValue)
|
|
||||||
InfoRow(icon: "number", title: "访问次数", value: "\(location.visitCount) 次")
|
|
||||||
}
|
|
||||||
.padding()
|
|
||||||
.background(Color(.systemBackground))
|
|
||||||
.cornerRadius(16)
|
|
||||||
.shadow(radius: 2)
|
|
||||||
|
|
||||||
// 操作按钮
|
|
||||||
VStack(spacing: 12) {
|
|
||||||
Button(action: { showingShareView = true }) {
|
|
||||||
HStack {
|
|
||||||
Image(systemName: "square.and.pencil")
|
|
||||||
Text("在这里分享心情")
|
|
||||||
}
|
|
||||||
.font(.headline)
|
|
||||||
.foregroundColor(.white)
|
|
||||||
.frame(maxWidth: .infinity)
|
|
||||||
.padding()
|
|
||||||
.background(location.emotion.color)
|
|
||||||
.cornerRadius(12)
|
|
||||||
}
|
|
||||||
|
|
||||||
HStack(spacing: 12) {
|
|
||||||
Button(action: {}) {
|
|
||||||
HStack {
|
|
||||||
Image(systemName: "bookmark")
|
|
||||||
Text("收藏")
|
|
||||||
}
|
|
||||||
.frame(maxWidth: .infinity)
|
|
||||||
.padding()
|
|
||||||
.background(Color(.systemGray6))
|
|
||||||
.cornerRadius(12)
|
|
||||||
}
|
|
||||||
|
|
||||||
Button(action: {}) {
|
|
||||||
HStack {
|
|
||||||
Image(systemName: "square.and.arrow.up")
|
|
||||||
Text("分享")
|
|
||||||
}
|
|
||||||
.frame(maxWidth: .infinity)
|
|
||||||
.padding()
|
|
||||||
.background(Color(.systemGray6))
|
|
||||||
.cornerRadius(12)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding(.horizontal)
|
|
||||||
}
|
|
||||||
.padding()
|
|
||||||
}
|
|
||||||
.navigationTitle("")
|
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
|
||||||
.navigationBarBackButtonHidden(true)
|
|
||||||
.toolbar {
|
|
||||||
ToolbarItem(placement: .navigationBarLeading) {
|
|
||||||
Button(action: { dismiss() }) {
|
|
||||||
HStack(spacing: 4) {
|
|
||||||
Image(systemName: "xmark.circle.fill")
|
|
||||||
.foregroundColor(.gray)
|
|
||||||
Text("关闭")
|
|
||||||
.foregroundColor(.primary)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.sheet(isPresented: $showingShareView) {
|
|
||||||
CreatePostView(selectedLocation: location)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - 信息行组件
|
|
||||||
struct InfoRow: View {
|
|
||||||
let icon: String
|
|
||||||
let title: String
|
|
||||||
let value: String
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
HStack {
|
|
||||||
Image(systemName: icon)
|
|
||||||
.foregroundColor(.blue)
|
|
||||||
.frame(width: 20)
|
|
||||||
|
|
||||||
Text(title)
|
|
||||||
.font(.subheadline)
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
|
|
||||||
Text(value)
|
|
||||||
.font(.subheadline)
|
|
||||||
.fontWeight(.medium)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - 访问记录模型和视图
|
|
||||||
struct VisitRecord: Identifiable, Codable {
|
|
||||||
let id: UUID
|
|
||||||
let date: Date
|
|
||||||
let emotion: EmotionType
|
|
||||||
let notes: String
|
|
||||||
|
|
||||||
init(id: UUID = UUID(), date: Date, emotion: EmotionType, notes: String) {
|
|
||||||
self.id = id
|
|
||||||
self.date = date
|
|
||||||
self.emotion = emotion
|
|
||||||
self.notes = notes
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct VisitHistoryRow: View {
|
|
||||||
let visit: VisitRecord
|
|
||||||
let isLatest: Bool
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
HStack(spacing: 12) {
|
|
||||||
VStack(spacing: 4) {
|
|
||||||
Text(visit.emotion.emoji)
|
|
||||||
.font(.title3)
|
|
||||||
|
|
||||||
if isLatest {
|
|
||||||
Circle()
|
|
||||||
.fill(Color.green)
|
|
||||||
.frame(width: 6, height: 6)
|
|
||||||
} else {
|
|
||||||
Circle()
|
|
||||||
.fill(Color.gray.opacity(0.3))
|
|
||||||
.frame(width: 4, height: 4)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 4) {
|
|
||||||
HStack {
|
|
||||||
Text(visit.emotion.displayName)
|
|
||||||
.font(.subheadline)
|
|
||||||
.fontWeight(.medium)
|
|
||||||
.foregroundColor(visit.emotion.color)
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
|
|
||||||
Text(DateFormatter.localizedDate.string(from: visit.date))
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
}
|
|
||||||
|
|
||||||
Text(visit.notes)
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
.lineLimit(2)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding(.vertical, 4)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - 日期格式化扩展
|
|
||||||
extension DateFormatter {
|
|
||||||
static let localizedDate: DateFormatter = {
|
|
||||||
let formatter = DateFormatter()
|
|
||||||
formatter.dateStyle = .medium
|
|
||||||
formatter.timeStyle = .none
|
|
||||||
return formatter
|
|
||||||
}()
|
|
||||||
|
|
||||||
static let shortRelative: DateFormatter = {
|
|
||||||
let formatter = DateFormatter()
|
|
||||||
formatter.dateStyle = .short
|
|
||||||
formatter.timeStyle = .none
|
|
||||||
return formatter
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - 生成访问记录的辅助函数
|
|
||||||
func generateVisitNote(for location: LocationPin, emotion: EmotionType) -> String {
|
|
||||||
let notes = [
|
|
||||||
"在这里度过了美好的时光",
|
|
||||||
"心情得到了很好的调节",
|
|
||||||
"这个地方让我感到平静",
|
|
||||||
"和朋友一起来的,很开心",
|
|
||||||
"独自一人,享受安静的时光",
|
|
||||||
"记录了这次特别的体验"
|
|
||||||
]
|
|
||||||
return notes.randomElement() ?? "记录了这次访问"
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - 简化的成长相关视图
|
|
||||||
struct GuidedSelectionView: View {
|
|
||||||
var body: some View {
|
|
||||||
VStack {
|
|
||||||
Text("引导选择")
|
|
||||||
.font(.title)
|
|
||||||
.padding()
|
|
||||||
|
|
||||||
Text("这里是引导用户进行选择的界面")
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
.padding()
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct AstroAnalysisInputView: View {
|
|
||||||
var body: some View {
|
|
||||||
VStack {
|
|
||||||
Text("占星分析输入")
|
|
||||||
.font(.title)
|
|
||||||
.padding()
|
|
||||||
|
|
||||||
Text("这里是占星分析的输入界面")
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
.padding()
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - 其他支持视图的简化版本
|
|
||||||
struct TopicDetailView: View {
|
|
||||||
let topic: GrowthTopic
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
VStack {
|
|
||||||
Text(topic.title)
|
|
||||||
.font(.title)
|
|
||||||
.padding()
|
|
||||||
|
|
||||||
Text(topic.description)
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
.padding()
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct EmotionalInsightsView: View {
|
|
||||||
var body: some View {
|
|
||||||
VStack {
|
|
||||||
Text("情绪洞察")
|
|
||||||
.font(.title)
|
|
||||||
.padding()
|
|
||||||
|
|
||||||
Text("这里显示用户的情绪分析和洞察")
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
.padding()
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct ChatHistoryView: View {
|
|
||||||
var body: some View {
|
|
||||||
VStack {
|
|
||||||
Text("对话历史")
|
|
||||||
.font(.title)
|
|
||||||
.padding()
|
|
||||||
|
|
||||||
Text("这里显示与AI的对话历史")
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
.padding()
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,415 +0,0 @@
|
|||||||
//
|
|
||||||
// ThemeAdapter.swift
|
|
||||||
// EmotionMuseum
|
|
||||||
//
|
|
||||||
// Created by 华中敏 on 2025/6/13.
|
|
||||||
//
|
|
||||||
|
|
||||||
import SwiftUI
|
|
||||||
|
|
||||||
// MARK: - 主题适配器
|
|
||||||
struct ThemedCard<Content: View>: View {
|
|
||||||
let content: () -> Content
|
|
||||||
let padding: CGFloat
|
|
||||||
let cornerRadius: CGFloat
|
|
||||||
let shadowEnabled: Bool
|
|
||||||
|
|
||||||
init(
|
|
||||||
padding: CGFloat = 16,
|
|
||||||
cornerRadius: CGFloat = 16,
|
|
||||||
shadowEnabled: Bool = true,
|
|
||||||
@ViewBuilder content: @escaping () -> Content
|
|
||||||
) {
|
|
||||||
self.padding = padding
|
|
||||||
self.cornerRadius = cornerRadius
|
|
||||||
self.shadowEnabled = shadowEnabled
|
|
||||||
self.content = content
|
|
||||||
}
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
content()
|
|
||||||
.padding(padding)
|
|
||||||
.background(Color.theme.cardBackground)
|
|
||||||
.cornerRadius(cornerRadius)
|
|
||||||
.shadow(
|
|
||||||
color: shadowEnabled ? Color.black.opacity(0.05) : Color.clear,
|
|
||||||
radius: shadowEnabled ? 8 : 0,
|
|
||||||
x: 0,
|
|
||||||
y: shadowEnabled ? 2 : 0
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - 主题文本组件
|
|
||||||
struct ThemedText: View {
|
|
||||||
let text: String
|
|
||||||
let style: TextStyle
|
|
||||||
let alignment: TextAlignment
|
|
||||||
|
|
||||||
enum TextStyle {
|
|
||||||
case title, headline, subheadline, body, caption
|
|
||||||
case primary, secondary, tertiary
|
|
||||||
|
|
||||||
var font: Font {
|
|
||||||
switch self {
|
|
||||||
case .title: return .title
|
|
||||||
case .headline: return .headline
|
|
||||||
case .subheadline: return .subheadline
|
|
||||||
case .body: return .body
|
|
||||||
case .caption: return .caption
|
|
||||||
case .primary, .secondary, .tertiary: return .body
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var color: Color {
|
|
||||||
switch self {
|
|
||||||
case .title, .headline, .subheadline, .body, .primary:
|
|
||||||
return Color.theme.primaryText
|
|
||||||
case .secondary:
|
|
||||||
return Color.theme.secondaryText
|
|
||||||
case .tertiary, .caption:
|
|
||||||
return Color.theme.tertiaryText
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
init(_ text: String, style: TextStyle = .body, alignment: TextAlignment = .leading) {
|
|
||||||
self.text = text
|
|
||||||
self.style = style
|
|
||||||
self.alignment = alignment
|
|
||||||
}
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
Text(text)
|
|
||||||
.font(style.font)
|
|
||||||
.foregroundColor(style.color)
|
|
||||||
.multilineTextAlignment(alignment)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - 主题按钮
|
|
||||||
struct ThemedButton: View {
|
|
||||||
let title: String
|
|
||||||
let style: ButtonStyle
|
|
||||||
let size: ButtonSize
|
|
||||||
let action: () -> Void
|
|
||||||
|
|
||||||
enum ButtonStyle {
|
|
||||||
case primary, secondary, outline, text
|
|
||||||
|
|
||||||
var backgroundColor: Color {
|
|
||||||
switch self {
|
|
||||||
case .primary: return Color.theme.accent
|
|
||||||
case .secondary: return Color.theme.secondary
|
|
||||||
case .outline, .text: return Color.clear
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var foregroundColor: Color {
|
|
||||||
switch self {
|
|
||||||
case .primary: return .white
|
|
||||||
case .secondary: return Color.theme.primaryText
|
|
||||||
case .outline: return Color.theme.accent
|
|
||||||
case .text: return Color.theme.accent
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var borderColor: Color {
|
|
||||||
switch self {
|
|
||||||
case .outline: return Color.theme.accent
|
|
||||||
default: return Color.clear
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
enum ButtonSize {
|
|
||||||
case small, medium, large
|
|
||||||
|
|
||||||
var padding: EdgeInsets {
|
|
||||||
switch self {
|
|
||||||
case .small: return EdgeInsets(top: 8, leading: 12, bottom: 8, trailing: 12)
|
|
||||||
case .medium: return EdgeInsets(top: 12, leading: 16, bottom: 12, trailing: 16)
|
|
||||||
case .large: return EdgeInsets(top: 16, leading: 24, bottom: 16, trailing: 24)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var cornerRadius: CGFloat {
|
|
||||||
switch self {
|
|
||||||
case .small: return 8
|
|
||||||
case .medium: return 12
|
|
||||||
case .large: return 16
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
init(_ title: String, style: ButtonStyle = .primary, size: ButtonSize = .medium, action: @escaping () -> Void) {
|
|
||||||
self.title = title
|
|
||||||
self.style = style
|
|
||||||
self.size = size
|
|
||||||
self.action = action
|
|
||||||
}
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
Button(action: action) {
|
|
||||||
Text(title)
|
|
||||||
.font(.subheadline)
|
|
||||||
.fontWeight(.medium)
|
|
||||||
.foregroundColor(style.foregroundColor)
|
|
||||||
.padding(size.padding)
|
|
||||||
.background(style.backgroundColor)
|
|
||||||
.overlay(
|
|
||||||
RoundedRectangle(cornerRadius: size.cornerRadius)
|
|
||||||
.stroke(style.borderColor, lineWidth: style == .outline ? 1 : 0)
|
|
||||||
)
|
|
||||||
.cornerRadius(size.cornerRadius)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - 主题分隔线
|
|
||||||
struct ThemedDivider: View {
|
|
||||||
let thickness: CGFloat
|
|
||||||
let color: Color?
|
|
||||||
|
|
||||||
init(thickness: CGFloat = 1, color: Color? = nil) {
|
|
||||||
self.thickness = thickness
|
|
||||||
self.color = color
|
|
||||||
}
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
Rectangle()
|
|
||||||
.fill(color ?? Color.theme.divider)
|
|
||||||
.frame(height: thickness)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - 主题进度条
|
|
||||||
struct ThemedProgressView: View {
|
|
||||||
let value: Double
|
|
||||||
let total: Double
|
|
||||||
let height: CGFloat
|
|
||||||
let backgroundColor: Color?
|
|
||||||
let foregroundColor: Color?
|
|
||||||
|
|
||||||
init(
|
|
||||||
value: Double,
|
|
||||||
total: Double = 1.0,
|
|
||||||
height: CGFloat = 8,
|
|
||||||
backgroundColor: Color? = nil,
|
|
||||||
foregroundColor: Color? = nil
|
|
||||||
) {
|
|
||||||
self.value = value
|
|
||||||
self.total = total
|
|
||||||
self.height = height
|
|
||||||
self.backgroundColor = backgroundColor
|
|
||||||
self.foregroundColor = foregroundColor
|
|
||||||
}
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
ProgressView(value: value, total: total)
|
|
||||||
.progressViewStyle(
|
|
||||||
ThemedLinearProgressViewStyle(
|
|
||||||
height: height,
|
|
||||||
backgroundColor: backgroundColor ?? Color.theme.skeleton,
|
|
||||||
foregroundColor: foregroundColor ?? Color.theme.accent
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct ThemedLinearProgressViewStyle: ProgressViewStyle {
|
|
||||||
let height: CGFloat
|
|
||||||
let backgroundColor: Color
|
|
||||||
let foregroundColor: Color
|
|
||||||
|
|
||||||
func makeBody(configuration: Configuration) -> some View {
|
|
||||||
GeometryReader { geometry in
|
|
||||||
ZStack(alignment: .leading) {
|
|
||||||
Rectangle()
|
|
||||||
.fill(backgroundColor)
|
|
||||||
.frame(height: height)
|
|
||||||
.cornerRadius(height / 2)
|
|
||||||
|
|
||||||
Rectangle()
|
|
||||||
.fill(foregroundColor)
|
|
||||||
.frame(
|
|
||||||
width: geometry.size.width * CGFloat(configuration.fractionCompleted ?? 0),
|
|
||||||
height: height
|
|
||||||
)
|
|
||||||
.cornerRadius(height / 2)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.frame(height: height)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - 主题输入框
|
|
||||||
struct ThemedTextField: View {
|
|
||||||
let placeholder: String
|
|
||||||
@Binding var text: String
|
|
||||||
let style: TextFieldStyle
|
|
||||||
|
|
||||||
enum TextFieldStyle {
|
|
||||||
case standard, rounded, outline
|
|
||||||
|
|
||||||
var backgroundColor: Color {
|
|
||||||
switch self {
|
|
||||||
case .standard: return Color.clear
|
|
||||||
case .rounded: return Color.theme.surfaceBackground
|
|
||||||
case .outline: return Color.theme.background
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var borderColor: Color {
|
|
||||||
switch self {
|
|
||||||
case .outline: return Color.theme.border
|
|
||||||
default: return Color.clear
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var cornerRadius: CGFloat {
|
|
||||||
switch self {
|
|
||||||
case .rounded: return 12
|
|
||||||
case .outline: return 8
|
|
||||||
default: return 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
init(_ placeholder: String, text: Binding<String>, style: TextFieldStyle = .standard) {
|
|
||||||
self.placeholder = placeholder
|
|
||||||
self._text = text
|
|
||||||
self.style = style
|
|
||||||
}
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
TextField(placeholder, text: $text)
|
|
||||||
.padding(.horizontal, style == .standard ? 0 : 12)
|
|
||||||
.padding(.vertical, style == .standard ? 0 : 10)
|
|
||||||
.background(style.backgroundColor)
|
|
||||||
.foregroundColor(Color.theme.primaryText)
|
|
||||||
.overlay(
|
|
||||||
RoundedRectangle(cornerRadius: style.cornerRadius)
|
|
||||||
.stroke(style.borderColor, lineWidth: style == .outline ? 1 : 0)
|
|
||||||
)
|
|
||||||
.cornerRadius(style.cornerRadius)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - 主题图标
|
|
||||||
struct ThemedIcon: View {
|
|
||||||
let systemName: String
|
|
||||||
let style: IconStyle
|
|
||||||
let size: IconSize
|
|
||||||
|
|
||||||
enum IconStyle {
|
|
||||||
case primary, secondary, accent, custom(Color)
|
|
||||||
|
|
||||||
var color: Color {
|
|
||||||
switch self {
|
|
||||||
case .primary: return Color.theme.primaryText
|
|
||||||
case .secondary: return Color.theme.secondaryText
|
|
||||||
case .accent: return Color.theme.accent
|
|
||||||
case .custom(let color): return color
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
enum IconSize {
|
|
||||||
case small, medium, large, custom(CGFloat)
|
|
||||||
|
|
||||||
var font: Font {
|
|
||||||
switch self {
|
|
||||||
case .small: return .caption
|
|
||||||
case .medium: return .body
|
|
||||||
case .large: return .title2
|
|
||||||
case .custom(let size): return .system(size: size)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
init(_ systemName: String, style: IconStyle = .primary, size: IconSize = .medium) {
|
|
||||||
self.systemName = systemName
|
|
||||||
self.style = style
|
|
||||||
self.size = size
|
|
||||||
}
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
Image(systemName: systemName)
|
|
||||||
.font(size.font)
|
|
||||||
.foregroundColor(style.color)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - 主题列表行
|
|
||||||
struct ThemedListRow<Content: View>: View {
|
|
||||||
let content: () -> Content
|
|
||||||
let showSeparator: Bool
|
|
||||||
let padding: EdgeInsets
|
|
||||||
|
|
||||||
init(
|
|
||||||
showSeparator: Bool = true,
|
|
||||||
padding: EdgeInsets = EdgeInsets(top: 12, leading: 16, bottom: 12, trailing: 16),
|
|
||||||
@ViewBuilder content: @escaping () -> Content
|
|
||||||
) {
|
|
||||||
self.showSeparator = showSeparator
|
|
||||||
self.padding = padding
|
|
||||||
self.content = content
|
|
||||||
}
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
VStack(spacing: 0) {
|
|
||||||
content()
|
|
||||||
.padding(padding)
|
|
||||||
.background(Color.theme.cardBackground)
|
|
||||||
|
|
||||||
if showSeparator {
|
|
||||||
ThemedDivider()
|
|
||||||
.padding(.leading, padding.leading)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - 视图扩展
|
|
||||||
extension View {
|
|
||||||
func themedCard(
|
|
||||||
padding: CGFloat = 16,
|
|
||||||
cornerRadius: CGFloat = 16,
|
|
||||||
shadowEnabled: Bool = true
|
|
||||||
) -> some View {
|
|
||||||
ThemedCard(
|
|
||||||
padding: padding,
|
|
||||||
cornerRadius: cornerRadius,
|
|
||||||
shadowEnabled: shadowEnabled
|
|
||||||
) {
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func themedBackground() -> some View {
|
|
||||||
background(Color.theme.background)
|
|
||||||
}
|
|
||||||
|
|
||||||
func themedSurface() -> some View {
|
|
||||||
background(Color.theme.surfaceBackground)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - 预览
|
|
||||||
#Preview("主题组件") {
|
|
||||||
VStack(spacing: 20) {
|
|
||||||
ThemedText("主标题", style: .title)
|
|
||||||
ThemedText("副标题文本", style: .secondary)
|
|
||||||
|
|
||||||
ThemedButton("主要按钮") {}
|
|
||||||
ThemedButton("次要按钮", style: .secondary) {}
|
|
||||||
|
|
||||||
ThemedProgressView(value: 0.6)
|
|
||||||
.frame(height: 8)
|
|
||||||
|
|
||||||
ThemedTextField("请输入内容", text: .constant(""))
|
|
||||||
}
|
|
||||||
.padding()
|
|
||||||
.themedBackground()
|
|
||||||
}
|
|
||||||
@@ -1,367 +0,0 @@
|
|||||||
//
|
|
||||||
// ThemeSettingsView.swift
|
|
||||||
// EmotionMuseum
|
|
||||||
//
|
|
||||||
// Created by 华中敏 on 2025/6/13.
|
|
||||||
//
|
|
||||||
|
|
||||||
import SwiftUI
|
|
||||||
|
|
||||||
// MARK: - 主题设置页面
|
|
||||||
struct ThemeSettingsView: View {
|
|
||||||
@EnvironmentObject var themeManager: ThemeManager
|
|
||||||
@Environment(\.dismiss) var dismiss
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
NavigationView {
|
|
||||||
List {
|
|
||||||
// 主题选择区域
|
|
||||||
themeSelectionSection
|
|
||||||
|
|
||||||
// 外观预览
|
|
||||||
previewSection
|
|
||||||
|
|
||||||
// 高级设置
|
|
||||||
advancedSettingsSection
|
|
||||||
}
|
|
||||||
.listStyle(InsetGroupedListStyle())
|
|
||||||
.navigationTitle("主题设置")
|
|
||||||
.navigationBarTitleDisplayMode(.large)
|
|
||||||
.toolbar {
|
|
||||||
ToolbarItem(placement: .topBarTrailing) {
|
|
||||||
Button("完成") {
|
|
||||||
dismiss()
|
|
||||||
}
|
|
||||||
.foregroundColor(Color.theme.accent)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.themedBackground()
|
|
||||||
.preferredColorScheme(themeManager.systemFollowsDeviceTheme ? nil : (themeManager.isDarkMode ? .dark : .light))
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - 主题选择区域
|
|
||||||
private var themeSelectionSection: some View {
|
|
||||||
Section {
|
|
||||||
// 跟随系统设置
|
|
||||||
ThemedListRow {
|
|
||||||
HStack {
|
|
||||||
ThemedIcon("gear.circle.fill", style: .accent, size: .medium)
|
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 2) {
|
|
||||||
ThemedText("跟随系统", style: .headline)
|
|
||||||
ThemedText("自动适应系统的深色模式设置", style: .secondary)
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
|
|
||||||
Toggle("", isOn: $themeManager.systemFollowsDeviceTheme)
|
|
||||||
.toggleStyle(SwitchToggleStyle(tint: Color.theme.accent))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !themeManager.systemFollowsDeviceTheme {
|
|
||||||
// 浅色模式
|
|
||||||
ThemeOptionRow(
|
|
||||||
title: "浅色模式",
|
|
||||||
description: "明亮清新的视觉体验",
|
|
||||||
icon: "sun.max.fill",
|
|
||||||
isSelected: !themeManager.isDarkMode
|
|
||||||
) {
|
|
||||||
withAnimation(AnimationConfig.smooth) {
|
|
||||||
themeManager.isDarkMode = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 深色模式
|
|
||||||
ThemeOptionRow(
|
|
||||||
title: "深色模式",
|
|
||||||
description: "舒适护眼的暗色调体验",
|
|
||||||
icon: "moon.fill",
|
|
||||||
isSelected: themeManager.isDarkMode
|
|
||||||
) {
|
|
||||||
withAnimation(AnimationConfig.smooth) {
|
|
||||||
themeManager.isDarkMode = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} header: {
|
|
||||||
ThemedText("外观模式", style: .caption)
|
|
||||||
.foregroundColor(Color.theme.secondaryText)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - 预览区域
|
|
||||||
private var previewSection: some View {
|
|
||||||
Section {
|
|
||||||
VStack(spacing: 16) {
|
|
||||||
// 预览卡片
|
|
||||||
PreviewCard()
|
|
||||||
|
|
||||||
// 色彩预览
|
|
||||||
ColorPreviewGrid()
|
|
||||||
}
|
|
||||||
.padding(.vertical, 8)
|
|
||||||
} header: {
|
|
||||||
ThemedText("预览效果", style: .caption)
|
|
||||||
.foregroundColor(Color.theme.secondaryText)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - 高级设置
|
|
||||||
private var advancedSettingsSection: some View {
|
|
||||||
Section {
|
|
||||||
// 重置主题设置
|
|
||||||
ThemedListRow {
|
|
||||||
Button(action: resetThemeSettings) {
|
|
||||||
HStack {
|
|
||||||
ThemedIcon("arrow.clockwise.circle.fill", style: .custom(.orange), size: .medium)
|
|
||||||
ThemedText("重置主题设置", style: .headline)
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 关于主题
|
|
||||||
ThemedListRow(showSeparator: false) {
|
|
||||||
NavigationLink {
|
|
||||||
ThemeInfoView()
|
|
||||||
} label: {
|
|
||||||
HStack {
|
|
||||||
ThemedIcon("info.circle.fill", style: .accent, size: .medium)
|
|
||||||
ThemedText("关于主题", style: .headline)
|
|
||||||
Spacer()
|
|
||||||
ThemedIcon("chevron.right", style: .secondary, size: .small)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} header: {
|
|
||||||
ThemedText("高级选项", style: .caption)
|
|
||||||
.foregroundColor(Color.theme.secondaryText)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func resetThemeSettings() {
|
|
||||||
withAnimation(AnimationConfig.smooth) {
|
|
||||||
themeManager.systemFollowsDeviceTheme = true
|
|
||||||
themeManager.isDarkMode = false
|
|
||||||
}
|
|
||||||
|
|
||||||
// 触觉反馈
|
|
||||||
let impactFeedback = UIImpactFeedbackGenerator(style: .medium)
|
|
||||||
impactFeedback.impactOccurred()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - 主题选项行
|
|
||||||
struct ThemeOptionRow: View {
|
|
||||||
let title: String
|
|
||||||
let description: String
|
|
||||||
let icon: String
|
|
||||||
let isSelected: Bool
|
|
||||||
let action: () -> Void
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
ThemedListRow {
|
|
||||||
Button(action: action) {
|
|
||||||
HStack(spacing: 12) {
|
|
||||||
ThemedIcon(icon, style: .accent, size: .medium)
|
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 2) {
|
|
||||||
ThemedText(title, style: .headline)
|
|
||||||
ThemedText(description, style: .secondary)
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
|
|
||||||
if isSelected {
|
|
||||||
ThemedIcon("checkmark.circle.fill", style: .custom(.green), size: .medium)
|
|
||||||
.transition(.scale.combined(with: .opacity))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.buttonStyle(PlainButtonStyle())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - 预览卡片
|
|
||||||
struct PreviewCard: View {
|
|
||||||
var body: some View {
|
|
||||||
VStack(alignment: .leading, spacing: 12) {
|
|
||||||
HStack {
|
|
||||||
ThemedText("示例卡片", style: .headline)
|
|
||||||
Spacer()
|
|
||||||
ThemedIcon("heart.fill", style: .custom(.red), size: .medium)
|
|
||||||
}
|
|
||||||
|
|
||||||
ThemedText("这是一个预览卡片,展示当前主题的效果。文本清晰度和对比度都经过精心调校。", style: .secondary)
|
|
||||||
|
|
||||||
HStack {
|
|
||||||
ThemedButton("主要按钮", size: .small) {}
|
|
||||||
ThemedButton("次要按钮", style: .secondary, size: .small) {}
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
|
|
||||||
ThemedProgressView(value: 0.6)
|
|
||||||
.frame(height: 6)
|
|
||||||
}
|
|
||||||
.themedCard()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - 色彩预览网格
|
|
||||||
struct ColorPreviewGrid: View {
|
|
||||||
let colors: [(String, Color)] = [
|
|
||||||
("主色调", Color.theme.accent),
|
|
||||||
("成功", Color.theme.success),
|
|
||||||
("警告", Color.theme.warning),
|
|
||||||
("错误", Color.theme.error)
|
|
||||||
]
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: 4), spacing: 12) {
|
|
||||||
ForEach(colors, id: \.0) { name, color in
|
|
||||||
VStack(spacing: 6) {
|
|
||||||
Circle()
|
|
||||||
.fill(color)
|
|
||||||
.frame(width: 32, height: 32)
|
|
||||||
|
|
||||||
ThemedText(name, style: .caption)
|
|
||||||
.lineLimit(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.themedCard(padding: 12)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - 主题信息页面
|
|
||||||
struct ThemeInfoView: View {
|
|
||||||
@Environment(\.dismiss) var dismiss
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
ScrollView {
|
|
||||||
VStack(alignment: .leading, spacing: 24) {
|
|
||||||
// 标题区域
|
|
||||||
VStack(alignment: .leading, spacing: 8) {
|
|
||||||
ThemedText("关于主题系统", style: .title)
|
|
||||||
ThemedText("智能适配,呵护双眼", style: .secondary)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 特性介绍
|
|
||||||
VStack(alignment: .leading, spacing: 16) {
|
|
||||||
FeatureRow(
|
|
||||||
icon: "eye.fill",
|
|
||||||
title: "护眼设计",
|
|
||||||
description: "精心调校的颜色对比度,长时间使用不疲劳"
|
|
||||||
)
|
|
||||||
|
|
||||||
FeatureRow(
|
|
||||||
icon: "paintbrush.fill",
|
|
||||||
title: "精美配色",
|
|
||||||
description: "专业设计师打造的色彩方案,视觉体验更佳"
|
|
||||||
)
|
|
||||||
|
|
||||||
FeatureRow(
|
|
||||||
icon: "gear.badge.checkmark",
|
|
||||||
title: "智能适配",
|
|
||||||
description: "可跟随系统设置自动切换,也可手动调节"
|
|
||||||
)
|
|
||||||
|
|
||||||
FeatureRow(
|
|
||||||
icon: "moon.stars.fill",
|
|
||||||
title: "深色模式",
|
|
||||||
description: "夜间使用更舒适,有效减少蓝光刺激"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(minLength: 32)
|
|
||||||
|
|
||||||
// 版本信息
|
|
||||||
VStack(spacing: 8) {
|
|
||||||
ThemedText("主题系统 v1.0", style: .secondary)
|
|
||||||
ThemedText("情绪博物馆团队制作", style: .tertiary)
|
|
||||||
}
|
|
||||||
.frame(maxWidth: .infinity)
|
|
||||||
}
|
|
||||||
.padding(.horizontal, 20)
|
|
||||||
.padding(.vertical, 24)
|
|
||||||
}
|
|
||||||
.themedBackground()
|
|
||||||
.navigationTitle("主题信息")
|
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - 特性行
|
|
||||||
struct FeatureRow: View {
|
|
||||||
let icon: String
|
|
||||||
let title: String
|
|
||||||
let description: String
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
HStack(alignment: .top, spacing: 12) {
|
|
||||||
ThemedIcon(icon, style: .accent, size: .medium)
|
|
||||||
.frame(width: 24, height: 24)
|
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 4) {
|
|
||||||
ThemedText(title, style: .headline)
|
|
||||||
ThemedText(description, style: .secondary)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - 快速主题切换组件
|
|
||||||
struct QuickThemeToggle: View {
|
|
||||||
@EnvironmentObject var themeManager: ThemeManager
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
Button(action: toggleTheme) {
|
|
||||||
HStack(spacing: 8) {
|
|
||||||
ThemedIcon(
|
|
||||||
themeManager.isDarkMode ? "moon.fill" : "sun.max.fill",
|
|
||||||
style: .accent,
|
|
||||||
size: .medium
|
|
||||||
)
|
|
||||||
|
|
||||||
ThemedText(
|
|
||||||
themeManager.isDarkMode ? "深色" : "浅色",
|
|
||||||
style: .secondary
|
|
||||||
)
|
|
||||||
}
|
|
||||||
.padding(.horizontal, 12)
|
|
||||||
.padding(.vertical, 6)
|
|
||||||
.background(Color.theme.surfaceBackground)
|
|
||||||
.cornerRadius(16)
|
|
||||||
}
|
|
||||||
.buttonStyle(PlainButtonStyle())
|
|
||||||
}
|
|
||||||
|
|
||||||
private func toggleTheme() {
|
|
||||||
withAnimation(AnimationConfig.smooth) {
|
|
||||||
if themeManager.systemFollowsDeviceTheme {
|
|
||||||
themeManager.setSystemFollowing(false)
|
|
||||||
}
|
|
||||||
themeManager.toggleTheme()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 触觉反馈
|
|
||||||
let impactFeedback = UIImpactFeedbackGenerator(style: .light)
|
|
||||||
impactFeedback.impactOccurred()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - 预览
|
|
||||||
#Preview("主题设置") {
|
|
||||||
ThemeSettingsView()
|
|
||||||
.environmentObject(ThemeManager())
|
|
||||||
}
|
|
||||||
|
|
||||||
#Preview("快速切换") {
|
|
||||||
QuickThemeToggle()
|
|
||||||
.environmentObject(ThemeManager())
|
|
||||||
.padding()
|
|
||||||
.themedBackground()
|
|
||||||
}
|
|
||||||
@@ -1,622 +0,0 @@
|
|||||||
//
|
|
||||||
// UniverseView.swift
|
|
||||||
// EmotionMuseum
|
|
||||||
//
|
|
||||||
// Created by 华中敏 on 2025/6/13.
|
|
||||||
//
|
|
||||||
|
|
||||||
import SwiftUI
|
|
||||||
|
|
||||||
struct UniverseView: View {
|
|
||||||
@State private var showingProfile = false
|
|
||||||
@State private var showingSettings = false
|
|
||||||
@State private var showingAchievements = false
|
|
||||||
@State private var showingDataExport = false
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
NavigationView {
|
|
||||||
ScrollView {
|
|
||||||
VStack(spacing: 24) {
|
|
||||||
// 用户信息卡片
|
|
||||||
UserProfileCard(onEditProfile: {
|
|
||||||
showingProfile = true
|
|
||||||
})
|
|
||||||
|
|
||||||
// 成长数据概览
|
|
||||||
GrowthOverviewCard()
|
|
||||||
|
|
||||||
// 功能菜单
|
|
||||||
VStack(spacing: 16) {
|
|
||||||
MenuSection(title: "我的成长") {
|
|
||||||
VStack(spacing: 12) {
|
|
||||||
MenuRow(
|
|
||||||
icon: "trophy.fill",
|
|
||||||
title: "成就徽章",
|
|
||||||
subtitle: "查看你的成长里程碑",
|
|
||||||
color: .yellow,
|
|
||||||
action: { showingAchievements = true }
|
|
||||||
)
|
|
||||||
|
|
||||||
MenuRow(
|
|
||||||
icon: "chart.line.uptrend.xyaxis",
|
|
||||||
title: "成长报告",
|
|
||||||
subtitle: "详细的成长数据分析",
|
|
||||||
color: .blue,
|
|
||||||
action: { /* 跳转到成长报告 */ }
|
|
||||||
)
|
|
||||||
|
|
||||||
MenuRow(
|
|
||||||
icon: "calendar",
|
|
||||||
title: "情绪日历",
|
|
||||||
subtitle: "回顾你的情绪历程",
|
|
||||||
color: .green,
|
|
||||||
action: { /* 跳转到情绪日历 */ }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MenuSection(title: "数据管理") {
|
|
||||||
VStack(spacing: 12) {
|
|
||||||
MenuRow(
|
|
||||||
icon: "square.and.arrow.up",
|
|
||||||
title: "导出数据",
|
|
||||||
subtitle: "导出你的个人数据",
|
|
||||||
color: .purple,
|
|
||||||
action: { showingDataExport = true }
|
|
||||||
)
|
|
||||||
|
|
||||||
MenuRow(
|
|
||||||
icon: "icloud.and.arrow.up",
|
|
||||||
title: "云端同步",
|
|
||||||
subtitle: "同步到iCloud",
|
|
||||||
color: .cyan,
|
|
||||||
action: { /* 云端同步 */ }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MenuSection(title: "设置") {
|
|
||||||
VStack(spacing: 12) {
|
|
||||||
MenuRow(
|
|
||||||
icon: "bell.fill",
|
|
||||||
title: "通知设置",
|
|
||||||
subtitle: "管理提醒和通知",
|
|
||||||
color: .orange,
|
|
||||||
action: { /* 通知设置 */ }
|
|
||||||
)
|
|
||||||
|
|
||||||
MenuRow(
|
|
||||||
icon: "lock.fill",
|
|
||||||
title: "隐私设置",
|
|
||||||
subtitle: "数据隐私和安全",
|
|
||||||
color: .red,
|
|
||||||
action: { /* 隐私设置 */ }
|
|
||||||
)
|
|
||||||
|
|
||||||
MenuRow(
|
|
||||||
icon: "gearshape.fill",
|
|
||||||
title: "应用设置",
|
|
||||||
subtitle: "个性化设置",
|
|
||||||
color: .gray,
|
|
||||||
action: { showingSettings = true }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MenuSection(title: "帮助与支持") {
|
|
||||||
VStack(spacing: 12) {
|
|
||||||
MenuRow(
|
|
||||||
icon: "questionmark.circle.fill",
|
|
||||||
title: "使用帮助",
|
|
||||||
subtitle: "常见问题和使用指南",
|
|
||||||
color: .blue,
|
|
||||||
action: { /* 帮助页面 */ }
|
|
||||||
)
|
|
||||||
|
|
||||||
MenuRow(
|
|
||||||
icon: "envelope.fill",
|
|
||||||
title: "联系我们",
|
|
||||||
subtitle: "反馈和建议",
|
|
||||||
color: .green,
|
|
||||||
action: { /* 联系我们 */ }
|
|
||||||
)
|
|
||||||
|
|
||||||
MenuRow(
|
|
||||||
icon: "star.fill",
|
|
||||||
title: "评价应用",
|
|
||||||
subtitle: "在App Store评价",
|
|
||||||
color: .yellow,
|
|
||||||
action: { /* 跳转App Store */ }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 版本信息
|
|
||||||
VStack(spacing: 8) {
|
|
||||||
Text("情绪博物馆")
|
|
||||||
.font(.subheadline)
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
|
|
||||||
Text("版本 1.0.0")
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
}
|
|
||||||
.padding(.top, 20)
|
|
||||||
}
|
|
||||||
.padding(.horizontal)
|
|
||||||
.padding(.vertical)
|
|
||||||
}
|
|
||||||
.navigationTitle("我的宇宙")
|
|
||||||
.navigationBarTitleDisplayMode(.large)
|
|
||||||
}
|
|
||||||
.sheet(isPresented: $showingProfile) {
|
|
||||||
ProfileEditView()
|
|
||||||
}
|
|
||||||
.sheet(isPresented: $showingSettings) {
|
|
||||||
SettingsView()
|
|
||||||
}
|
|
||||||
.sheet(isPresented: $showingAchievements) {
|
|
||||||
AchievementsView()
|
|
||||||
}
|
|
||||||
.sheet(isPresented: $showingDataExport) {
|
|
||||||
DataExportView()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - 用户信息卡片
|
|
||||||
struct UserProfileCard: View {
|
|
||||||
let onEditProfile: () -> Void
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
VStack(spacing: 16) {
|
|
||||||
HStack {
|
|
||||||
// 头像
|
|
||||||
Button(action: onEditProfile) {
|
|
||||||
ZStack {
|
|
||||||
Circle()
|
|
||||||
.fill(LinearGradient(
|
|
||||||
colors: [.purple, .blue],
|
|
||||||
startPoint: .topLeading,
|
|
||||||
endPoint: .bottomTrailing
|
|
||||||
))
|
|
||||||
.frame(width: 80, height: 80)
|
|
||||||
|
|
||||||
Text("华")
|
|
||||||
.font(.title)
|
|
||||||
.fontWeight(.bold)
|
|
||||||
.foregroundColor(.white)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 4) {
|
|
||||||
Text("华中敏")
|
|
||||||
.font(.title2)
|
|
||||||
.fontWeight(.bold)
|
|
||||||
|
|
||||||
Text("成长探索者")
|
|
||||||
.font(.subheadline)
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
|
|
||||||
HStack(spacing: 4) {
|
|
||||||
Image(systemName: "calendar")
|
|
||||||
.font(.caption)
|
|
||||||
Text("加入 30 天")
|
|
||||||
.font(.caption)
|
|
||||||
}
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
|
|
||||||
Button(action: onEditProfile) {
|
|
||||||
Image(systemName: "pencil")
|
|
||||||
.font(.title3)
|
|
||||||
.foregroundColor(.blue)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 成长等级
|
|
||||||
VStack(spacing: 8) {
|
|
||||||
HStack {
|
|
||||||
Text("成长等级")
|
|
||||||
.font(.subheadline)
|
|
||||||
.fontWeight(.medium)
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
|
|
||||||
Text("Lv.5")
|
|
||||||
.font(.subheadline)
|
|
||||||
.fontWeight(.bold)
|
|
||||||
.foregroundColor(.purple)
|
|
||||||
}
|
|
||||||
|
|
||||||
ProgressView(value: 0.7)
|
|
||||||
.progressViewStyle(LinearProgressViewStyle(tint: .purple))
|
|
||||||
|
|
||||||
HStack {
|
|
||||||
Text("距离下一级还需 150 经验")
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding()
|
|
||||||
.background(
|
|
||||||
RoundedRectangle(cornerRadius: 16)
|
|
||||||
.fill(Color(UIColor.systemGray6))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - 成长数据概览
|
|
||||||
struct GrowthOverviewCard: View {
|
|
||||||
var body: some View {
|
|
||||||
VStack(alignment: .leading, spacing: 16) {
|
|
||||||
Text("本周成长数据")
|
|
||||||
.font(.headline)
|
|
||||||
.fontWeight(.semibold)
|
|
||||||
|
|
||||||
HStack(spacing: 16) {
|
|
||||||
GrowthMetricView(
|
|
||||||
title: "情绪记录",
|
|
||||||
value: "12",
|
|
||||||
unit: "次",
|
|
||||||
color: .blue,
|
|
||||||
icon: "heart.fill"
|
|
||||||
)
|
|
||||||
|
|
||||||
GrowthMetricView(
|
|
||||||
title: "疗愈时长",
|
|
||||||
value: "45",
|
|
||||||
unit: "分钟",
|
|
||||||
color: .purple,
|
|
||||||
icon: "timer.circle.fill"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
HStack(spacing: 16) {
|
|
||||||
GrowthMetricView(
|
|
||||||
title: "课题进展",
|
|
||||||
value: "3",
|
|
||||||
unit: "个",
|
|
||||||
color: .green,
|
|
||||||
icon: "checkmark.circle.fill"
|
|
||||||
)
|
|
||||||
|
|
||||||
GrowthMetricView(
|
|
||||||
title: "连续天数",
|
|
||||||
value: "7",
|
|
||||||
unit: "天",
|
|
||||||
color: .orange,
|
|
||||||
icon: "flame.fill"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding()
|
|
||||||
.background(
|
|
||||||
RoundedRectangle(cornerRadius: 16)
|
|
||||||
.fill(Color(.systemGray6))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct GrowthMetricView: View {
|
|
||||||
let title: String
|
|
||||||
let value: String
|
|
||||||
let unit: String
|
|
||||||
let color: Color
|
|
||||||
let icon: String
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
VStack(spacing: 8) {
|
|
||||||
HStack {
|
|
||||||
Image(systemName: icon)
|
|
||||||
.font(.title3)
|
|
||||||
.foregroundColor(color)
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 2) {
|
|
||||||
HStack(alignment: .bottom, spacing: 2) {
|
|
||||||
Text(value)
|
|
||||||
.font(.title2)
|
|
||||||
.fontWeight(.bold)
|
|
||||||
|
|
||||||
Text(unit)
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
}
|
|
||||||
|
|
||||||
Text(title)
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
}
|
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
|
||||||
}
|
|
||||||
.padding()
|
|
||||||
.background(
|
|
||||||
RoundedRectangle(cornerRadius: 12)
|
|
||||||
.fill(Color(.systemBackground))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - 菜单组件
|
|
||||||
struct MenuSection<Content: View>: View {
|
|
||||||
let title: String
|
|
||||||
let content: Content
|
|
||||||
|
|
||||||
init(title: String, @ViewBuilder content: () -> Content) {
|
|
||||||
self.title = title
|
|
||||||
self.content = content()
|
|
||||||
}
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
VStack(alignment: .leading, spacing: 12) {
|
|
||||||
Text(title)
|
|
||||||
.font(.headline)
|
|
||||||
.fontWeight(.semibold)
|
|
||||||
|
|
||||||
content
|
|
||||||
}
|
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct MenuRow: View {
|
|
||||||
let icon: String
|
|
||||||
let title: String
|
|
||||||
let subtitle: String
|
|
||||||
let color: Color
|
|
||||||
let action: () -> Void
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
Button(action: action) {
|
|
||||||
HStack(spacing: 16) {
|
|
||||||
ZStack {
|
|
||||||
RoundedRectangle(cornerRadius: 8)
|
|
||||||
.fill(color.opacity(0.1))
|
|
||||||
.frame(width: 40, height: 40)
|
|
||||||
|
|
||||||
Image(systemName: icon)
|
|
||||||
.font(.title3)
|
|
||||||
.foregroundColor(color)
|
|
||||||
}
|
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 2) {
|
|
||||||
Text(title)
|
|
||||||
.font(.subheadline)
|
|
||||||
.fontWeight(.medium)
|
|
||||||
.foregroundColor(.primary)
|
|
||||||
|
|
||||||
Text(subtitle)
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
|
|
||||||
Image(systemName: "chevron.right")
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
}
|
|
||||||
.padding()
|
|
||||||
.background(
|
|
||||||
RoundedRectangle(cornerRadius: 12)
|
|
||||||
.fill(Color(.systemGray6))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
.buttonStyle(PlainButtonStyle())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - 子页面视图(占位符)
|
|
||||||
struct ProfileEditView: View {
|
|
||||||
@Environment(\.dismiss) private var dismiss
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
NavigationView {
|
|
||||||
VStack {
|
|
||||||
Text("个人资料编辑")
|
|
||||||
.font(.title)
|
|
||||||
|
|
||||||
Text("这里是个人资料编辑页面")
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
}
|
|
||||||
.navigationTitle("编辑资料")
|
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
|
||||||
.toolbar {
|
|
||||||
ToolbarItem(placement: .navigationBarLeading) {
|
|
||||||
Button("取消") {
|
|
||||||
dismiss()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ToolbarItem(placement: .navigationBarTrailing) {
|
|
||||||
Button("保存") {
|
|
||||||
dismiss()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct SettingsView: View {
|
|
||||||
@Environment(\.dismiss) private var dismiss
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
NavigationView {
|
|
||||||
VStack {
|
|
||||||
Text("应用设置")
|
|
||||||
.font(.title)
|
|
||||||
|
|
||||||
Text("这里是应用设置页面")
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
}
|
|
||||||
.navigationTitle("设置")
|
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
|
||||||
.toolbar {
|
|
||||||
ToolbarItem(placement: .navigationBarTrailing) {
|
|
||||||
Button("完成") {
|
|
||||||
dismiss()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct AchievementsView: View {
|
|
||||||
@Environment(\.dismiss) private var dismiss
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
NavigationView {
|
|
||||||
ScrollView {
|
|
||||||
LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: 2), spacing: 16) {
|
|
||||||
ForEach(0..<6) { index in
|
|
||||||
AchievementCard(achievement: sampleAchievements[index])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding()
|
|
||||||
}
|
|
||||||
.navigationTitle("成就徽章")
|
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
|
||||||
.toolbar {
|
|
||||||
ToolbarItem(placement: .navigationBarTrailing) {
|
|
||||||
Button("完成") {
|
|
||||||
dismiss()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private var sampleAchievements: [Achievement] {
|
|
||||||
[
|
|
||||||
Achievement(title: "初心者", description: "完成第一次情绪记录", category: .milestone, icon: "star.fill", rarity: .common, requirement: .conversationCount(1), targetValue: 1, unlockedAt: Date()),
|
|
||||||
Achievement(title: "坚持者", description: "连续7天记录情绪", category: .consistency, icon: "flame.fill", rarity: .rare, requirement: .consecutiveDays(7), targetValue: 7, unlockedAt: Date()),
|
|
||||||
Achievement(title: "探索者", description: "开始第一个课题", category: .growth, icon: "map.fill", rarity: .common, requirement: .topicCompletion(1), targetValue: 1, unlockedAt: Date()),
|
|
||||||
Achievement(title: "疗愈师", description: "完成10次脉轮疗愈", category: .emotion, icon: "heart.fill", rarity: .epic, requirement: .emotionRecordCount(10), targetValue: 10),
|
|
||||||
Achievement(title: "成长者", description: "完成一个完整课题", category: .growth, icon: "trophy.fill", rarity: .rare, requirement: .topicCompletion(5), targetValue: 5),
|
|
||||||
Achievement(title: "大师", description: "达到10级成长等级", category: .milestone, icon: "crown.fill", rarity: .legendary, requirement: .totalPoints(10000), targetValue: 10000)
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct DataExportView: View {
|
|
||||||
@Environment(\.dismiss) private var dismiss
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
NavigationView {
|
|
||||||
VStack(spacing: 20) {
|
|
||||||
Text("数据导出")
|
|
||||||
.font(.title)
|
|
||||||
|
|
||||||
Text("选择要导出的数据类型")
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
|
|
||||||
VStack(spacing: 12) {
|
|
||||||
ExportOptionRow(title: "情绪记录", description: "所有的情绪记录数据")
|
|
||||||
ExportOptionRow(title: "疗愈记录", description: "脉轮疗愈会话记录")
|
|
||||||
ExportOptionRow(title: "课题进展", description: "成长课题和进展数据")
|
|
||||||
ExportOptionRow(title: "成就数据", description: "解锁的成就和里程碑")
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
|
|
||||||
Button("导出数据") {
|
|
||||||
// 导出逻辑
|
|
||||||
}
|
|
||||||
.buttonStyle(.borderedProminent)
|
|
||||||
}
|
|
||||||
.padding()
|
|
||||||
.navigationTitle("导出数据")
|
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
|
||||||
.toolbar {
|
|
||||||
ToolbarItem(placement: .navigationBarTrailing) {
|
|
||||||
Button("取消") {
|
|
||||||
dismiss()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct ExportOptionRow: View {
|
|
||||||
let title: String
|
|
||||||
let description: String
|
|
||||||
@State private var isSelected = false
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
HStack {
|
|
||||||
VStack(alignment: .leading, spacing: 4) {
|
|
||||||
Text(title)
|
|
||||||
.font(.subheadline)
|
|
||||||
.fontWeight(.medium)
|
|
||||||
|
|
||||||
Text(description)
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
|
|
||||||
Toggle("", isOn: $isSelected)
|
|
||||||
}
|
|
||||||
.padding()
|
|
||||||
.background(
|
|
||||||
RoundedRectangle(cornerRadius: 12)
|
|
||||||
.fill(Color(.systemGray6))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct AchievementCard: View {
|
|
||||||
let achievement: Achievement
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
VStack(spacing: 12) {
|
|
||||||
ZStack {
|
|
||||||
Circle()
|
|
||||||
.fill(achievement.isUnlocked ? achievement.rarity.color.opacity(0.2) : Color.gray.opacity(0.2))
|
|
||||||
.frame(width: 60, height: 60)
|
|
||||||
|
|
||||||
Image(systemName: achievement.icon)
|
|
||||||
.font(.title2)
|
|
||||||
.foregroundColor(achievement.isUnlocked ? achievement.rarity.color : .gray)
|
|
||||||
}
|
|
||||||
|
|
||||||
VStack(spacing: 4) {
|
|
||||||
Text(achievement.title)
|
|
||||||
.font(.subheadline)
|
|
||||||
.fontWeight(.medium)
|
|
||||||
.foregroundColor(achievement.isUnlocked ? .primary : .secondary)
|
|
||||||
|
|
||||||
Text(achievement.description)
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
.multilineTextAlignment(.center)
|
|
||||||
.lineLimit(2)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding()
|
|
||||||
.background(
|
|
||||||
RoundedRectangle(cornerRadius: 12)
|
|
||||||
.fill(Color(.systemGray6))
|
|
||||||
)
|
|
||||||
.opacity(achievement.isUnlocked ? 1.0 : 0.6)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - 数据模型(使用DataModels中的Achievement)
|
|
||||||
|
|
||||||
#Preview {
|
|
||||||
UniverseView()
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
//
|
|
||||||
// EmotionMuseumTests.swift
|
|
||||||
// EmotionMuseumTests
|
|
||||||
//
|
|
||||||
// Created by 华中敏 on 2025/6/13.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Testing
|
|
||||||
@testable import EmotionMuseum
|
|
||||||
|
|
||||||
struct EmotionMuseumTests {
|
|
||||||
|
|
||||||
@Test func example() async throws {
|
|
||||||
// Write your test here and use APIs like `#expect(...)` to check expected conditions.
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
||||||
<plist version="1.0">
|
|
||||||
<dict>
|
|
||||||
<key>CFBundleDevelopmentRegion</key>
|
|
||||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
|
||||||
<key>CFBundleExecutable</key>
|
|
||||||
<string>$(EXECUTABLE_NAME)</string>
|
|
||||||
<key>CFBundleIdentifier</key>
|
|
||||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
|
||||||
<key>CFBundleInfoDictionaryVersion</key>
|
|
||||||
<string>6.0</string>
|
|
||||||
<key>CFBundleName</key>
|
|
||||||
<string>$(PRODUCT_NAME)</string>
|
|
||||||
<key>CFBundlePackageType</key>
|
|
||||||
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
|
||||||
<key>CFBundleShortVersionString</key>
|
|
||||||
<string>1.0</string>
|
|
||||||
<key>CFBundleVersion</key>
|
|
||||||
<string>1</string>
|
|
||||||
</dict>
|
|
||||||
</plist>
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
//
|
|
||||||
// EmotionMuseumUITests.swift
|
|
||||||
// EmotionMuseumUITests
|
|
||||||
//
|
|
||||||
// Created by 华中敏 on 2025/6/13.
|
|
||||||
//
|
|
||||||
|
|
||||||
import XCTest
|
|
||||||
|
|
||||||
final class EmotionMuseumUITests: XCTestCase {
|
|
||||||
|
|
||||||
override func setUpWithError() throws {
|
|
||||||
// Put setup code here. This method is called before the invocation of each test method in the class.
|
|
||||||
|
|
||||||
// In UI tests it is usually best to stop immediately when a failure occurs.
|
|
||||||
continueAfterFailure = false
|
|
||||||
|
|
||||||
// In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this.
|
|
||||||
}
|
|
||||||
|
|
||||||
override func tearDownWithError() throws {
|
|
||||||
// Put teardown code here. This method is called after the invocation of each test method in the class.
|
|
||||||
}
|
|
||||||
|
|
||||||
@MainActor
|
|
||||||
func testExample() throws {
|
|
||||||
// UI tests must launch the application that they test.
|
|
||||||
let app = XCUIApplication()
|
|
||||||
app.launch()
|
|
||||||
|
|
||||||
// Use XCTAssert and related functions to verify your tests produce the correct results.
|
|
||||||
}
|
|
||||||
|
|
||||||
@MainActor
|
|
||||||
func testLaunchPerformance() throws {
|
|
||||||
// This measures how long it takes to launch your application.
|
|
||||||
measure(metrics: [XCTApplicationLaunchMetric()]) {
|
|
||||||
XCUIApplication().launch()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
//
|
|
||||||
// EmotionMuseumUITestsLaunchTests.swift
|
|
||||||
// EmotionMuseumUITests
|
|
||||||
//
|
|
||||||
// Created by 华中敏 on 2025/6/13.
|
|
||||||
//
|
|
||||||
|
|
||||||
import XCTest
|
|
||||||
|
|
||||||
final class EmotionMuseumUITestsLaunchTests: XCTestCase {
|
|
||||||
|
|
||||||
override class var runsForEachTargetApplicationUIConfiguration: Bool {
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
override func setUpWithError() throws {
|
|
||||||
continueAfterFailure = false
|
|
||||||
}
|
|
||||||
|
|
||||||
@MainActor
|
|
||||||
func testLaunch() throws {
|
|
||||||
let app = XCUIApplication()
|
|
||||||
app.launch()
|
|
||||||
|
|
||||||
// Insert steps here to perform after app launch but before taking a screenshot,
|
|
||||||
// such as logging into a test account or navigating somewhere in the app
|
|
||||||
|
|
||||||
let attachment = XCTAttachment(screenshot: app.screenshot())
|
|
||||||
attachment.name = "Launch Screen"
|
|
||||||
attachment.lifetime = .keepAlways
|
|
||||||
add(attachment)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
||||||
<plist version="1.0">
|
|
||||||
<dict>
|
|
||||||
<key>CFBundleDevelopmentRegion</key>
|
|
||||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
|
||||||
<key>CFBundleExecutable</key>
|
|
||||||
<string>$(EXECUTABLE_NAME)</string>
|
|
||||||
<key>CFBundleIdentifier</key>
|
|
||||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
|
||||||
<key>CFBundleInfoDictionaryVersion</key>
|
|
||||||
<string>6.0</string>
|
|
||||||
<key>CFBundleName</key>
|
|
||||||
<string>$(PRODUCT_NAME)</string>
|
|
||||||
<key>CFBundlePackageType</key>
|
|
||||||
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
|
||||||
<key>CFBundleShortVersionString</key>
|
|
||||||
<string>1.0</string>
|
|
||||||
<key>CFBundleVersion</key>
|
|
||||||
<string>1</string>
|
|
||||||
</dict>
|
|
||||||
</plist>
|
|
||||||
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 63 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 484 KiB |
@@ -0,0 +1,104 @@
|
|||||||
|
# 情感博物馆项目结构
|
||||||
|
|
||||||
|
## 📁 目录结构
|
||||||
|
|
||||||
|
```
|
||||||
|
emotion-museum/
|
||||||
|
├── 📁 backend/ # 后端微服务
|
||||||
|
│ ├── 📁 emotion-gateway/ # API网关服务
|
||||||
|
│ ├── 📁 emotion-user/ # 用户管理服务
|
||||||
|
│ ├── 📁 emotion-ai/ # AI聊天服务
|
||||||
|
│ ├── 📁 emotion-auth/ # 认证服务
|
||||||
|
│ ├── 📁 emotion-record/ # 记录管理服务
|
||||||
|
│ ├── 📁 emotion-growth/ # 成长跟踪服务
|
||||||
|
│ ├── 📁 emotion-explore/ # 探索服务
|
||||||
|
│ ├── 📁 emotion-reward/ # 奖励服务
|
||||||
|
│ ├── 📁 emotion-websocket/ # WebSocket服务
|
||||||
|
│ ├── 📁 emotion-stats/ # 统计服务
|
||||||
|
│ ├── 📁 emotion-common/ # 公共模块
|
||||||
|
│ ├── 🔧 build-all.sh # 构建脚本
|
||||||
|
│ ├── 🔧 deploy-all.sh # 综合部署脚本
|
||||||
|
│ ├── 🔧 deploy-remote.sh # 远程部署脚本
|
||||||
|
│ └── 📄 pom.xml # Maven父项目配置
|
||||||
|
├── 📁 web-flowith/ # 前端Vue项目
|
||||||
|
│ ├── 📁 src/ # 源代码
|
||||||
|
│ ├── 📁 public/ # 静态资源
|
||||||
|
│ ├── 🔧 deploy.sh # 前端部署脚本
|
||||||
|
│ └── 📄 package.json # 前端依赖配置
|
||||||
|
├── 📁 docs/ # 项目文档
|
||||||
|
│ ├── 📁 deployment/ # 部署相关文档
|
||||||
|
│ ├── 📁 architecture/ # 架构设计文档
|
||||||
|
│ └── 📁 database/ # 数据库相关文档
|
||||||
|
├── 📁 configs/ # 配置文件
|
||||||
|
│ ├── 📁 nginx/ # Nginx配置
|
||||||
|
│ ├── 📁 docker/ # Docker配置
|
||||||
|
│ └── 📁 env/ # 环境配置
|
||||||
|
├── 🔧 one-click-deploy.sh # 一键部署脚本
|
||||||
|
├── 🔧 restart-middleware.sh # 中间件重启脚本
|
||||||
|
├── 🔧 cleanup-project.sh # 项目清理脚本
|
||||||
|
└── 📄 README.md # 项目说明
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🚀 快速开始
|
||||||
|
|
||||||
|
### 1. 一键部署
|
||||||
|
```bash
|
||||||
|
# 完整部署(前端+后端)
|
||||||
|
./one-click-deploy.sh
|
||||||
|
|
||||||
|
# 仅部署后端
|
||||||
|
./one-click-deploy.sh backend
|
||||||
|
|
||||||
|
# 仅部署前端
|
||||||
|
./one-click-deploy.sh frontend
|
||||||
|
|
||||||
|
# 健康检查
|
||||||
|
./one-click-deploy.sh check
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 中间件管理
|
||||||
|
```bash
|
||||||
|
# 重启中间件(MySQL, Redis, Nacos)
|
||||||
|
./restart-middleware.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 分步部署
|
||||||
|
```bash
|
||||||
|
# 构建后端
|
||||||
|
cd backend && ./build-all.sh
|
||||||
|
|
||||||
|
# 部署后端到远程
|
||||||
|
cd backend && ./deploy-remote.sh
|
||||||
|
|
||||||
|
# 部署前端
|
||||||
|
cd web-flowith && ./deploy.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📋 服务端口
|
||||||
|
|
||||||
|
| 服务 | 端口 | 描述 |
|
||||||
|
|------|------|------|
|
||||||
|
| emotion-gateway | 19000 | API网关 |
|
||||||
|
| emotion-user | 19001 | 用户服务 |
|
||||||
|
| emotion-ai | 19002 | AI服务 |
|
||||||
|
| emotion-record | 19003 | 记录服务 |
|
||||||
|
| emotion-growth | 19004 | 成长服务 |
|
||||||
|
| emotion-explore | 19005 | 探索服务 |
|
||||||
|
| emotion-reward | 19006 | 奖励服务 |
|
||||||
|
| emotion-websocket | 19007 | WebSocket服务 |
|
||||||
|
| emotion-auth | 19008 | 认证服务 |
|
||||||
|
| emotion-stats | 19009 | 统计服务 |
|
||||||
|
|
||||||
|
## 🔧 中间件端口
|
||||||
|
|
||||||
|
| 服务 | 端口 | 描述 |
|
||||||
|
|------|------|------|
|
||||||
|
| MySQL | 3306 | 数据库 |
|
||||||
|
| Redis | 6379 | 缓存 |
|
||||||
|
| Nacos | 8848 | 注册中心 |
|
||||||
|
|
||||||
|
## 📖 文档链接
|
||||||
|
|
||||||
|
- [部署指南](docs/deployment/)
|
||||||
|
- [架构设计](docs/architecture/)
|
||||||
|
- [数据库设计](docs/database/)
|
||||||
@@ -1,175 +0,0 @@
|
|||||||
# 服务器部署检查清单
|
|
||||||
|
|
||||||
## 📋 部署前检查
|
|
||||||
|
|
||||||
### 🖥️ 服务器要求
|
|
||||||
- [ ] **操作系统**: Linux (Ubuntu 18.04+, CentOS 7+, Debian 9+)
|
|
||||||
- [ ] **CPU**: 2核心以上
|
|
||||||
- [ ] **内存**: 4GB以上(推荐8GB)
|
|
||||||
- [ ] **磁盘**: 20GB以上可用空间
|
|
||||||
- [ ] **网络**: 稳定的互联网连接
|
|
||||||
|
|
||||||
### 🔧 软件环境
|
|
||||||
- [ ] **Docker**: 20.10+ 已安装
|
|
||||||
- [ ] **Docker Compose**: 1.29+ 已安装
|
|
||||||
- [ ] **Git**: 已安装(可选)
|
|
||||||
- [ ] **Curl/Wget**: 已安装
|
|
||||||
|
|
||||||
### 🌐 网络配置
|
|
||||||
- [ ] **端口开放**: 80, 443 已开放
|
|
||||||
- [ ] **防火墙**: 已配置允许HTTP/HTTPS流量
|
|
||||||
- [ ] **域名解析**: 域名已正确解析到服务器IP(如有)
|
|
||||||
- [ ] **SSL证书**: 已准备好SSL证书文件(生产环境)
|
|
||||||
|
|
||||||
## 📦 部署包准备
|
|
||||||
|
|
||||||
### 📁 文件检查
|
|
||||||
- [ ] **部署包**: `emotion-museum-1.0.0-20250713_111829.tar.gz` 已下载
|
|
||||||
- [ ] **校验和**: SHA256校验通过
|
|
||||||
- [ ] **解压**: 部署包已成功解压
|
|
||||||
- [ ] **权限**: 脚本文件已设置执行权限
|
|
||||||
|
|
||||||
### ⚙️ 配置文件
|
|
||||||
- [ ] **环境变量**: `.env` 文件已配置
|
|
||||||
- [ ] **Coze API**: API Token 已设置
|
|
||||||
- [ ] **数据库密码**: 已修改默认密码
|
|
||||||
- [ ] **域名配置**: Nginx配置中的域名已更新(生产环境)
|
|
||||||
|
|
||||||
## 🚀 部署执行
|
|
||||||
|
|
||||||
### 🔄 部署步骤
|
|
||||||
- [ ] **1. 环境检查**: `./quick-deploy.sh` 环境检查通过
|
|
||||||
- [ ] **2. Docker安装**: Docker和Docker Compose安装成功
|
|
||||||
- [ ] **3. 配置生成**: 配置文件生成成功
|
|
||||||
- [ ] **4. 镜像构建**: Docker镜像构建成功
|
|
||||||
- [ ] **5. 服务启动**: 所有容器启动成功
|
|
||||||
|
|
||||||
### 📊 服务状态
|
|
||||||
- [ ] **MySQL**: 容器运行正常,数据库连接成功
|
|
||||||
- [ ] **Redis**: 容器运行正常,缓存服务可用
|
|
||||||
- [ ] **Nacos**: 容器运行正常,注册中心可访问
|
|
||||||
- [ ] **Gateway**: 容器运行正常,网关服务可用
|
|
||||||
- [ ] **AI Service**: 容器运行正常,AI服务可用
|
|
||||||
- [ ] **User Service**: 容器运行正常,用户服务可用
|
|
||||||
- [ ] **Frontend**: 容器运行正常,前端应用可访问
|
|
||||||
- [ ] **Nginx**: 容器运行正常,反向代理工作正常
|
|
||||||
|
|
||||||
## ✅ 部署验证
|
|
||||||
|
|
||||||
### 🌐 访问测试
|
|
||||||
- [ ] **前端首页**: http://your-domain.com 可正常访问
|
|
||||||
- [ ] **API网关**: http://your-domain.com:9000/actuator/health 返回正常
|
|
||||||
- [ ] **Nacos控制台**: http://your-domain.com:8848/nacos 可正常登录
|
|
||||||
- [ ] **API文档**: http://your-domain.com:9000/doc.html 可正常访问
|
|
||||||
|
|
||||||
### 🔍 功能测试
|
|
||||||
- [ ] **用户注册**: 新用户注册功能正常
|
|
||||||
- [ ] **用户登录**: 用户登录功能正常
|
|
||||||
- [ ] **AI对话**: AI聊天功能正常
|
|
||||||
- [ ] **数据存储**: 对话记录正常保存
|
|
||||||
- [ ] **情绪分析**: 情绪分析功能正常
|
|
||||||
|
|
||||||
### 📈 性能测试
|
|
||||||
- [ ] **响应时间**: 页面加载时间 < 3秒
|
|
||||||
- [ ] **API响应**: API接口响应时间 < 1秒
|
|
||||||
- [ ] **并发测试**: 支持预期的并发用户数
|
|
||||||
- [ ] **资源使用**: CPU和内存使用率在合理范围
|
|
||||||
|
|
||||||
## 🔒 安全配置
|
|
||||||
|
|
||||||
### 🛡️ 基础安全
|
|
||||||
- [ ] **默认密码**: 所有默认密码已修改
|
|
||||||
- [ ] **防火墙**: 只开放必要端口
|
|
||||||
- [ ] **用户权限**: 使用非root用户运行服务
|
|
||||||
- [ ] **文件权限**: 敏感文件权限设置正确
|
|
||||||
|
|
||||||
### 🔐 HTTPS配置(生产环境)
|
|
||||||
- [ ] **SSL证书**: 证书文件已正确放置
|
|
||||||
- [ ] **Nginx配置**: HTTPS配置已启用
|
|
||||||
- [ ] **HTTP重定向**: HTTP自动重定向到HTTPS
|
|
||||||
- [ ] **证书验证**: SSL证书验证通过
|
|
||||||
|
|
||||||
### 🔑 API安全
|
|
||||||
- [ ] **JWT配置**: JWT密钥已设置
|
|
||||||
- [ ] **CORS配置**: 跨域配置正确
|
|
||||||
- [ ] **限流配置**: API限流规则已启用
|
|
||||||
- [ ] **访问日志**: 访问日志记录正常
|
|
||||||
|
|
||||||
## 📊 监控配置
|
|
||||||
|
|
||||||
### 📈 服务监控
|
|
||||||
- [ ] **健康检查**: `./manage.sh health` 所有服务健康
|
|
||||||
- [ ] **日志收集**: 日志文件正常生成
|
|
||||||
- [ ] **资源监控**: `./manage.sh monitor` 监控面板正常
|
|
||||||
- [ ] **告警配置**: 异常告警机制已配置(可选)
|
|
||||||
|
|
||||||
### 💾 数据备份
|
|
||||||
- [ ] **备份脚本**: `./manage.sh backup` 备份功能正常
|
|
||||||
- [ ] **备份策略**: 定期备份计划已制定
|
|
||||||
- [ ] **恢复测试**: 数据恢复功能已测试
|
|
||||||
- [ ] **备份存储**: 备份文件存储位置已确定
|
|
||||||
|
|
||||||
## 📝 文档记录
|
|
||||||
|
|
||||||
### 📋 部署记录
|
|
||||||
- [ ] **部署时间**: 记录部署完成时间
|
|
||||||
- [ ] **版本信息**: 记录部署的版本号
|
|
||||||
- [ ] **配置信息**: 记录重要配置参数
|
|
||||||
- [ ] **访问信息**: 记录访问地址和账号
|
|
||||||
|
|
||||||
### 📖 运维文档
|
|
||||||
- [ ] **管理命令**: 熟悉 `./manage.sh` 各项命令
|
|
||||||
- [ ] **故障排除**: 了解常见问题解决方案
|
|
||||||
- [ ] **更新流程**: 了解服务更新流程
|
|
||||||
- [ ] **联系方式**: 记录技术支持联系方式
|
|
||||||
|
|
||||||
## 🎯 部署后任务
|
|
||||||
|
|
||||||
### 🔧 优化配置
|
|
||||||
- [ ] **性能调优**: 根据实际负载调整配置
|
|
||||||
- [ ] **缓存策略**: 优化Redis缓存配置
|
|
||||||
- [ ] **数据库优化**: 调整MySQL配置参数
|
|
||||||
- [ ] **网络优化**: 优化Nginx配置
|
|
||||||
|
|
||||||
### 📊 监控设置
|
|
||||||
- [ ] **日志轮转**: 配置日志文件轮转
|
|
||||||
- [ ] **磁盘清理**: 设置定期清理任务
|
|
||||||
- [ ] **性能监控**: 配置性能监控工具
|
|
||||||
- [ ] **告警通知**: 设置异常告警通知
|
|
||||||
|
|
||||||
### 🔄 维护计划
|
|
||||||
- [ ] **更新计划**: 制定定期更新计划
|
|
||||||
- [ ] **备份计划**: 制定数据备份计划
|
|
||||||
- [ ] **安全审计**: 制定安全审计计划
|
|
||||||
- [ ] **容量规划**: 制定容量扩展计划
|
|
||||||
|
|
||||||
## ⚠️ 注意事项
|
|
||||||
|
|
||||||
### 🚨 重要提醒
|
|
||||||
1. **Coze API Token**: 必须配置正确的API Token,否则AI功能无法使用
|
|
||||||
2. **数据库密码**: 生产环境必须修改默认密码
|
|
||||||
3. **防火墙配置**: 确保只开放必要的端口
|
|
||||||
4. **SSL证书**: 生产环境强烈建议使用HTTPS
|
|
||||||
5. **定期备份**: 重要数据必须定期备份
|
|
||||||
|
|
||||||
### 📞 紧急联系
|
|
||||||
- **技术支持**: support@emotion-museum.com
|
|
||||||
- **紧急热线**: 400-xxx-xxxx
|
|
||||||
- **在线文档**: https://docs.emotion-museum.com
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ✅ 部署完成确认
|
|
||||||
|
|
||||||
**部署工程师**: ________________
|
|
||||||
**部署时间**: ________________
|
|
||||||
**版本号**: emotion-museum-1.0.0-20250713_111829
|
|
||||||
**服务器IP**: ________________
|
|
||||||
**域名**: ________________
|
|
||||||
|
|
||||||
**签名确认**: ________________
|
|
||||||
**日期**: ________________
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**🎉 恭喜完成部署!请妥善保存此检查清单作为部署记录。**
|
|
||||||
@@ -1,269 +0,0 @@
|
|||||||
# Nacos配置优化总结
|
|
||||||
|
|
||||||
## 概述
|
|
||||||
|
|
||||||
根据当前更新后的模块端口号和Nacos配置要求,已完成所有后台模块的Nacos配置优化,确保所有服务可以正确注册到Nacos上,并为每个模块创建了本地、测试、生产三套环境配置文件。
|
|
||||||
|
|
||||||
## 完成的工作
|
|
||||||
|
|
||||||
### 1. 端口配置统一
|
|
||||||
|
|
||||||
| 服务名称 | 端口 | 状态 | 描述 |
|
|
||||||
|---------|------|------|------|
|
|
||||||
| emotion-gateway | 19000 | ✅ 已配置 | 网关服务 |
|
|
||||||
| emotion-user | 19001 | ✅ 已配置 | 用户服务(包含认证) |
|
|
||||||
| emotion-ai | 19002 | ✅ 已配置 | AI对话服务 |
|
|
||||||
| emotion-record | 19003 | ✅ 已修正 | 情绪记录服务 |
|
|
||||||
| emotion-growth | 19004 | ✅ 已配置 | 成长课题服务 |
|
|
||||||
| emotion-explore | 19005 | ✅ 已配置 | 地图探索服务 |
|
|
||||||
| emotion-reward | 19006 | ✅ 已配置 | 成就奖励服务 |
|
|
||||||
| emotion-websocket | 19007 | ✅ 已配置 | WebSocket聊天服务 |
|
|
||||||
| emotion-stats | 19008 | ✅ 已配置 | 统计分析服务 |
|
|
||||||
|
|
||||||
### 2. Nacos配置优化
|
|
||||||
|
|
||||||
#### 主配置文件更新 (`application.yml`)
|
|
||||||
- ✅ 启用Nacos服务发现 (`enabled: true`)
|
|
||||||
- ✅ 添加认证配置 (`username/password`)
|
|
||||||
- ✅ 配置服务元数据 (`metadata`)
|
|
||||||
- ✅ 设置心跳和超时参数
|
|
||||||
- ✅ 配置集群和权重信息
|
|
||||||
|
|
||||||
#### 环境配置文件创建
|
|
||||||
为每个服务创建了三套环境配置:
|
|
||||||
|
|
||||||
**本地环境** (`application-local.yml`)
|
|
||||||
```yaml
|
|
||||||
spring:
|
|
||||||
cloud:
|
|
||||||
nacos:
|
|
||||||
discovery:
|
|
||||||
server-addr: localhost:8848
|
|
||||||
namespace:
|
|
||||||
group: DEFAULT_GROUP
|
|
||||||
enabled: true
|
|
||||||
username: nacos
|
|
||||||
password: nacos
|
|
||||||
```
|
|
||||||
|
|
||||||
**测试环境** (`application-test.yml`)
|
|
||||||
```yaml
|
|
||||||
spring:
|
|
||||||
cloud:
|
|
||||||
nacos:
|
|
||||||
discovery:
|
|
||||||
server-addr: 47.111.10.27:8848
|
|
||||||
namespace: test
|
|
||||||
group: DEFAULT_GROUP
|
|
||||||
enabled: true
|
|
||||||
username: nacos
|
|
||||||
password: nacos
|
|
||||||
```
|
|
||||||
|
|
||||||
**生产环境** (`application-prod.yml`)
|
|
||||||
```yaml
|
|
||||||
spring:
|
|
||||||
cloud:
|
|
||||||
nacos:
|
|
||||||
discovery:
|
|
||||||
server-addr: 47.111.10.27:8848
|
|
||||||
namespace: prod
|
|
||||||
group: DEFAULT_GROUP
|
|
||||||
enabled: true
|
|
||||||
username: nacos
|
|
||||||
password: nacos
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. 网关配置优化
|
|
||||||
|
|
||||||
#### 路由配置更新
|
|
||||||
- ✅ 使用负载均衡 (`lb://service-name`)
|
|
||||||
- ✅ 启用服务发现 (`locator.enabled: true`)
|
|
||||||
- ✅ 配置跨域支持 (`globalcors`)
|
|
||||||
- ✅ 支持WebSocket路由
|
|
||||||
|
|
||||||
#### 完整路由列表
|
|
||||||
```yaml
|
|
||||||
routes:
|
|
||||||
# 用户相关服务
|
|
||||||
- /user/** → lb://emotion-user
|
|
||||||
- /captcha/** → lb://emotion-user
|
|
||||||
- /oauth/** → lb://emotion-user
|
|
||||||
|
|
||||||
# AI相关服务
|
|
||||||
- /ai/** → lb://emotion-ai
|
|
||||||
- /websocket/** → lb://emotion-websocket
|
|
||||||
- /ws/** → lb://emotion-websocket
|
|
||||||
|
|
||||||
# 业务功能服务
|
|
||||||
- /record/** → lb://emotion-record
|
|
||||||
- /growth/** → lb://emotion-growth
|
|
||||||
- /explore/** → lb://emotion-explore
|
|
||||||
- /reward/** → lb://emotion-reward
|
|
||||||
- /stats/** → lb://emotion-stats
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. 特殊服务配置
|
|
||||||
|
|
||||||
#### emotion-ai服务
|
|
||||||
- ✅ 添加Coze平台配置
|
|
||||||
- ✅ 配置功能开关
|
|
||||||
- ✅ AI模型参数配置
|
|
||||||
|
|
||||||
#### emotion-websocket服务
|
|
||||||
- ✅ 添加WebSocket配置
|
|
||||||
- ✅ 配置STOMP和SockJS
|
|
||||||
- ✅ 设置消息代理
|
|
||||||
|
|
||||||
### 5. 数据库和Redis配置
|
|
||||||
|
|
||||||
#### 环境差异化配置
|
|
||||||
**本地环境**
|
|
||||||
```yaml
|
|
||||||
datasource:
|
|
||||||
url: jdbc:mysql://localhost:3306/emotion_museum
|
|
||||||
username: root
|
|
||||||
password: 123456
|
|
||||||
|
|
||||||
redis:
|
|
||||||
host: localhost
|
|
||||||
port: 6379
|
|
||||||
password:
|
|
||||||
```
|
|
||||||
|
|
||||||
**测试/生产环境**
|
|
||||||
```yaml
|
|
||||||
datasource:
|
|
||||||
url: jdbc:mysql://47.111.10.27:3306/emotion_museum
|
|
||||||
username: root
|
|
||||||
password: EmotionMuseum2025*#
|
|
||||||
|
|
||||||
redis:
|
|
||||||
host: 47.111.10.27
|
|
||||||
port: 6379
|
|
||||||
password: EmotionMuseum2025*#
|
|
||||||
```
|
|
||||||
|
|
||||||
## 工具脚本
|
|
||||||
|
|
||||||
### 1. 批量配置生成脚本
|
|
||||||
- **文件**: `backend/update-nacos-config.sh`
|
|
||||||
- **功能**: 批量为所有服务生成Nacos配置文件
|
|
||||||
- **使用**: `./backend/update-nacos-config.sh`
|
|
||||||
|
|
||||||
### 2. 配置验证脚本
|
|
||||||
- **文件**: `backend/verify-nacos-config.sh`
|
|
||||||
- **功能**: 验证所有服务的配置文件完整性
|
|
||||||
- **使用**: `./backend/verify-nacos-config.sh`
|
|
||||||
|
|
||||||
## Nacos服务器配置
|
|
||||||
|
|
||||||
### 认证配置
|
|
||||||
根据提供的Nacos配置,已配置:
|
|
||||||
```properties
|
|
||||||
nacos.core.auth.enabled=false
|
|
||||||
nacos.core.auth.admin.enabled=true
|
|
||||||
nacos.core.auth.console.enabled=true
|
|
||||||
nacos.core.auth.server.identity.key=nacosServerIdentity
|
|
||||||
nacos.core.auth.server.identity.value=Peanut2817*#
|
|
||||||
```
|
|
||||||
|
|
||||||
### 客户端认证
|
|
||||||
所有服务配置了统一的认证信息:
|
|
||||||
- **用户名**: nacos
|
|
||||||
- **密码**: nacos
|
|
||||||
|
|
||||||
## 启动方式
|
|
||||||
|
|
||||||
### 1. 指定环境启动
|
|
||||||
```bash
|
|
||||||
# 本地环境
|
|
||||||
mvn spring-boot:run -Dspring-boot.run.profiles=local
|
|
||||||
|
|
||||||
# 测试环境
|
|
||||||
mvn spring-boot:run -Dspring-boot.run.profiles=test
|
|
||||||
|
|
||||||
# 生产环境
|
|
||||||
mvn spring-boot:run -Dspring-boot.run.profiles=prod
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. JAR包启动
|
|
||||||
```bash
|
|
||||||
# 本地环境
|
|
||||||
java -jar app.jar --spring.profiles.active=local
|
|
||||||
|
|
||||||
# 测试环境
|
|
||||||
java -jar app.jar --spring.profiles.active=test
|
|
||||||
|
|
||||||
# 生产环境
|
|
||||||
java -jar app.jar --spring.profiles.active=prod
|
|
||||||
```
|
|
||||||
|
|
||||||
## 验证结果
|
|
||||||
|
|
||||||
### 配置验证通过
|
|
||||||
```
|
|
||||||
===========================================
|
|
||||||
验证结果统计
|
|
||||||
===========================================
|
|
||||||
总服务数: 9
|
|
||||||
验证成功: 9
|
|
||||||
验证失败: 0
|
|
||||||
🎉 所有服务配置验证通过!
|
|
||||||
```
|
|
||||||
|
|
||||||
### 服务注册验证
|
|
||||||
启动服务后,可通过以下方式验证:
|
|
||||||
|
|
||||||
1. **Nacos控制台**: http://47.111.10.27:8848/nacos
|
|
||||||
2. **服务列表**: 查看所有服务是否正确注册
|
|
||||||
3. **健康检查**: 确认服务状态为UP
|
|
||||||
|
|
||||||
## 环境隔离
|
|
||||||
|
|
||||||
### 命名空间配置
|
|
||||||
- **本地环境**: 默认命名空间 (空)
|
|
||||||
- **测试环境**: test命名空间
|
|
||||||
- **生产环境**: prod命名空间
|
|
||||||
|
|
||||||
### 配置隔离
|
|
||||||
- 不同环境使用不同的数据库和Redis实例
|
|
||||||
- 日志级别按环境调整
|
|
||||||
- 服务发现配置环境隔离
|
|
||||||
|
|
||||||
## 监控和日志
|
|
||||||
|
|
||||||
### 日志配置
|
|
||||||
- **本地环境**: DEBUG级别,详细日志
|
|
||||||
- **测试环境**: INFO级别,适中日志
|
|
||||||
- **生产环境**: WARN级别,精简日志
|
|
||||||
|
|
||||||
### 日志文件
|
|
||||||
- 格式: `logs/{service-name}-{env}.log`
|
|
||||||
- 示例: `logs/emotion-user-local.log`
|
|
||||||
|
|
||||||
## 后续优化建议
|
|
||||||
|
|
||||||
1. **配置中心**: 启用Nacos配置中心功能
|
|
||||||
2. **服务监控**: 集成Prometheus和Grafana
|
|
||||||
3. **链路追踪**: 添加Sleuth和Zipkin
|
|
||||||
4. **熔断降级**: 集成Sentinel
|
|
||||||
5. **安全加固**: 配置服务间认证
|
|
||||||
|
|
||||||
## 总结
|
|
||||||
|
|
||||||
✅ **完成项目**:
|
|
||||||
- 所有9个微服务的Nacos配置已完成
|
|
||||||
- 端口配置已统一和修正
|
|
||||||
- 三套环境配置文件已创建
|
|
||||||
- 网关路由配置已优化
|
|
||||||
- 配置验证100%通过
|
|
||||||
|
|
||||||
✅ **关键特性**:
|
|
||||||
- 支持环境隔离
|
|
||||||
- 自动服务发现
|
|
||||||
- 负载均衡路由
|
|
||||||
- 跨域支持
|
|
||||||
- WebSocket支持
|
|
||||||
|
|
||||||
现在所有服务都可以正确注册到Nacos,通过网关进行统一访问,支持多环境部署!🚀
|
|
||||||
@@ -1,270 +0,0 @@
|
|||||||
# Nacos配置最终总结
|
|
||||||
|
|
||||||
## 概述
|
|
||||||
|
|
||||||
已完成所有后台模块的Nacos配置优化,包括端口统一、环境配置文件创建和密码配置更新。所有服务现在可以正确注册到Nacos并通过网关进行统一访问。
|
|
||||||
|
|
||||||
## 最终配置状态
|
|
||||||
|
|
||||||
### 1. 服务端口配置 ✅
|
|
||||||
|
|
||||||
| 服务名称 | 端口 | 状态 | 描述 |
|
|
||||||
|---------|------|------|------|
|
|
||||||
| emotion-gateway | 19000 | ✅ 已配置 | 网关服务 |
|
|
||||||
| emotion-user | 19001 | ✅ 已配置 | 用户服务(包含认证) |
|
|
||||||
| emotion-ai | 19002 | ✅ 已配置 | AI对话服务 |
|
|
||||||
| emotion-record | 19003 | ✅ 已修正 | 情绪记录服务 |
|
|
||||||
| emotion-growth | 19004 | ✅ 已配置 | 成长课题服务 |
|
|
||||||
| emotion-explore | 19005 | ✅ 已配置 | 地图探索服务 |
|
|
||||||
| emotion-reward | 19006 | ✅ 已配置 | 成就奖励服务 |
|
|
||||||
| emotion-websocket | 19007 | ✅ 已配置 | WebSocket聊天服务 |
|
|
||||||
| emotion-stats | 19008 | ✅ 已配置 | 统计分析服务 |
|
|
||||||
|
|
||||||
### 2. Nacos密码配置 ✅
|
|
||||||
|
|
||||||
#### 环境密码配置
|
|
||||||
- **本地环境** (`application-local.yml`): `Peanut2817*#`
|
|
||||||
- **测试环境** (`application-test.yml`): `EmotionMuseum2025`
|
|
||||||
- **生产环境** (`application-prod.yml`): `EmotionMuseum2025`
|
|
||||||
|
|
||||||
#### 验证结果
|
|
||||||
```bash
|
|
||||||
# 本地环境密码验证
|
|
||||||
grep "password: Peanut2817*#" backend/emotion-*/src/main/resources/application-local.yml
|
|
||||||
# 结果: 18个匹配项 (9个服务 × 2个配置项)
|
|
||||||
|
|
||||||
# 测试环境密码验证
|
|
||||||
grep "password: EmotionMuseum2025" backend/emotion-*/src/main/resources/application-test.yml
|
|
||||||
# 结果: 18个匹配项
|
|
||||||
|
|
||||||
# 生产环境密码验证
|
|
||||||
grep "password: EmotionMuseum2025" backend/emotion-*/src/main/resources/application-prod.yml
|
|
||||||
# 结果: 18个匹配项
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. 环境配置文件 ✅
|
|
||||||
|
|
||||||
每个服务都包含完整的三套环境配置:
|
|
||||||
|
|
||||||
#### 本地环境配置示例
|
|
||||||
```yaml
|
|
||||||
spring:
|
|
||||||
cloud:
|
|
||||||
nacos:
|
|
||||||
discovery:
|
|
||||||
server-addr: localhost:8848
|
|
||||||
namespace:
|
|
||||||
group: DEFAULT_GROUP
|
|
||||||
enabled: true
|
|
||||||
username: nacos
|
|
||||||
password: Peanut2817*#
|
|
||||||
metadata:
|
|
||||||
version: 1.0.0
|
|
||||||
zone: local
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 测试环境配置示例
|
|
||||||
```yaml
|
|
||||||
spring:
|
|
||||||
cloud:
|
|
||||||
nacos:
|
|
||||||
discovery:
|
|
||||||
server-addr: 47.111.10.27:8848
|
|
||||||
namespace: test
|
|
||||||
group: DEFAULT_GROUP
|
|
||||||
enabled: true
|
|
||||||
username: nacos
|
|
||||||
password: EmotionMuseum2025
|
|
||||||
metadata:
|
|
||||||
version: 1.0.0
|
|
||||||
zone: test
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 生产环境配置示例
|
|
||||||
```yaml
|
|
||||||
spring:
|
|
||||||
cloud:
|
|
||||||
nacos:
|
|
||||||
discovery:
|
|
||||||
server-addr: 47.111.10.27:8848
|
|
||||||
namespace: prod
|
|
||||||
group: DEFAULT_GROUP
|
|
||||||
enabled: true
|
|
||||||
username: nacos
|
|
||||||
password: EmotionMuseum2025
|
|
||||||
metadata:
|
|
||||||
version: 1.0.0
|
|
||||||
zone: prod
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. 网关路由配置 ✅
|
|
||||||
|
|
||||||
#### 完整路由映射
|
|
||||||
```yaml
|
|
||||||
routes:
|
|
||||||
# 用户相关服务
|
|
||||||
- /user/** → lb://emotion-user:19001
|
|
||||||
- /captcha/** → lb://emotion-user:19001
|
|
||||||
- /oauth/** → lb://emotion-user:19001
|
|
||||||
|
|
||||||
# AI相关服务
|
|
||||||
- /ai/** → lb://emotion-ai:19002
|
|
||||||
- /websocket/** → lb://emotion-websocket:19007
|
|
||||||
- /ws/** → lb://emotion-websocket:19007 (WebSocket)
|
|
||||||
|
|
||||||
# 业务功能服务
|
|
||||||
- /record/** → lb://emotion-record:19003
|
|
||||||
- /growth/** → lb://emotion-growth:19004
|
|
||||||
- /explore/** → lb://emotion-explore:19005
|
|
||||||
- /reward/** → lb://emotion-reward:19006
|
|
||||||
- /stats/** → lb://emotion-stats:19008
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 特殊配置
|
|
||||||
- ✅ 跨域支持 (`globalcors`)
|
|
||||||
- ✅ WebSocket协议升级支持
|
|
||||||
- ✅ 负载均衡 (`lb://`)
|
|
||||||
- ✅ 服务发现集成
|
|
||||||
|
|
||||||
### 5. 特殊服务配置 ✅
|
|
||||||
|
|
||||||
#### emotion-ai服务
|
|
||||||
```yaml
|
|
||||||
# Coze平台配置
|
|
||||||
coze:
|
|
||||||
base-url: https://api.coze.cn
|
|
||||||
bot-id: 7523042446285439016
|
|
||||||
workflow-id: 7523047462895796287
|
|
||||||
token: pat_GCR4qKzqpf90wMCvKsldMrB18KG3QsLDci65bZthssKsbLxu8X70BKYumleDcabO
|
|
||||||
|
|
||||||
# 功能开关
|
|
||||||
features:
|
|
||||||
emotion-analysis:
|
|
||||||
enabled: false
|
|
||||||
chat:
|
|
||||||
enabled: true
|
|
||||||
```
|
|
||||||
|
|
||||||
#### emotion-websocket服务
|
|
||||||
```yaml
|
|
||||||
# WebSocket配置
|
|
||||||
websocket:
|
|
||||||
allowed-origins: "*"
|
|
||||||
sockjs:
|
|
||||||
enabled: true
|
|
||||||
heartbeat-time: 25000
|
|
||||||
stomp:
|
|
||||||
broker:
|
|
||||||
enabled: true
|
|
||||||
destinations: ["/topic", "/queue"]
|
|
||||||
application-destination-prefixes: ["/app"]
|
|
||||||
```
|
|
||||||
|
|
||||||
## 启动方式
|
|
||||||
|
|
||||||
### 1. 环境指定启动
|
|
||||||
```bash
|
|
||||||
# 本地环境
|
|
||||||
mvn spring-boot:run -Dspring-boot.run.profiles=local
|
|
||||||
|
|
||||||
# 测试环境
|
|
||||||
mvn spring-boot:run -Dspring-boot.run.profiles=test
|
|
||||||
|
|
||||||
# 生产环境
|
|
||||||
mvn spring-boot:run -Dspring-boot.run.profiles=prod
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. JAR包启动
|
|
||||||
```bash
|
|
||||||
# 本地环境
|
|
||||||
java -jar app.jar --spring.profiles.active=local
|
|
||||||
|
|
||||||
# 测试环境
|
|
||||||
java -jar app.jar --spring.profiles.active=test
|
|
||||||
|
|
||||||
# 生产环境
|
|
||||||
java -jar app.jar --spring.profiles.active=prod
|
|
||||||
```
|
|
||||||
|
|
||||||
## 验证工具
|
|
||||||
|
|
||||||
### 1. 配置验证脚本
|
|
||||||
```bash
|
|
||||||
./backend/verify-nacos-config.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. 密码更新脚本
|
|
||||||
```bash
|
|
||||||
./backend/update-nacos-passwords.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. 网关路由测试
|
|
||||||
```bash
|
|
||||||
./backend/emotion-gateway/test-gateway-routes.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
## 环境隔离
|
|
||||||
|
|
||||||
### 命名空间配置
|
|
||||||
- **本地环境**: 默认命名空间 (空字符串)
|
|
||||||
- **测试环境**: `test` 命名空间
|
|
||||||
- **生产环境**: `prod` 命名空间
|
|
||||||
|
|
||||||
### 数据库配置
|
|
||||||
- **本地环境**: `localhost:3306`, 密码: `123456`
|
|
||||||
- **测试环境**: `47.111.10.27:3306`, 密码: `EmotionMuseum2025*#`
|
|
||||||
- **生产环境**: `47.111.10.27:3306`, 密码: `EmotionMuseum2025*#`
|
|
||||||
|
|
||||||
### Redis配置
|
|
||||||
- **本地环境**: `localhost:6379`, 无密码
|
|
||||||
- **测试环境**: `47.111.10.27:6379`, 密码: `EmotionMuseum2025*#`
|
|
||||||
- **生产环境**: `47.111.10.27:6379`, 密码: `EmotionMuseum2025*#`
|
|
||||||
|
|
||||||
## 配置文件清单
|
|
||||||
|
|
||||||
### 每个服务包含的配置文件
|
|
||||||
```
|
|
||||||
backend/{service-name}/src/main/resources/
|
|
||||||
├── application.yml # 主配置文件
|
|
||||||
├── application-local.yml # 本地环境配置
|
|
||||||
├── application-test.yml # 测试环境配置
|
|
||||||
└── application-prod.yml # 生产环境配置
|
|
||||||
```
|
|
||||||
|
|
||||||
### 总计配置文件数量
|
|
||||||
- **9个服务** × **4个配置文件** = **36个配置文件**
|
|
||||||
- 所有配置文件都已创建并验证通过 ✅
|
|
||||||
|
|
||||||
## 最终验证结果
|
|
||||||
|
|
||||||
```
|
|
||||||
===========================================
|
|
||||||
验证结果统计
|
|
||||||
===========================================
|
|
||||||
总服务数: 9
|
|
||||||
验证成功: 9
|
|
||||||
验证失败: 0
|
|
||||||
🎉 所有服务配置验证通过!
|
|
||||||
```
|
|
||||||
|
|
||||||
## 总结
|
|
||||||
|
|
||||||
✅ **已完成的工作**:
|
|
||||||
1. 统一了所有9个微服务的端口配置
|
|
||||||
2. 为每个服务创建了完整的三套环境配置文件
|
|
||||||
3. 配置了正确的Nacos认证密码
|
|
||||||
4. 优化了网关路由配置,支持负载均衡和WebSocket
|
|
||||||
5. 添加了特殊服务的专用配置
|
|
||||||
6. 创建了验证和管理工具脚本
|
|
||||||
|
|
||||||
✅ **关键特性**:
|
|
||||||
- 完整的环境隔离 (local/test/prod)
|
|
||||||
- 统一的服务发现和注册
|
|
||||||
- 负载均衡路由
|
|
||||||
- WebSocket支持
|
|
||||||
- 跨域配置
|
|
||||||
- 安全认证
|
|
||||||
|
|
||||||
现在所有服务都可以正确注册到Nacos,通过网关进行统一访问,支持多环境部署!🚀
|
|
||||||
|
|
||||||
**下一步**: 提交代码到远程仓库
|
|
||||||
@@ -1,156 +0,0 @@
|
|||||||
# 情感博物馆项目优化总结
|
|
||||||
|
|
||||||
## 已完成的工作
|
|
||||||
|
|
||||||
### 1. 创建了emotion-auth统一认证模块
|
|
||||||
- ✅ 创建了emotion-auth模块的基础结构
|
|
||||||
- ✅ 移动了认证相关的代码到auth模块
|
|
||||||
- ✅ 配置了auth模块的pom.xml和Dockerfile
|
|
||||||
- ✅ 更新了父pom.xml包含auth模块
|
|
||||||
|
|
||||||
### 2. 优化了配置文件管理
|
|
||||||
- ✅ 统一了所有服务的application.yml配置
|
|
||||||
- ✅ 添加了环境变量支持:`spring.profiles.active: ${SPRING_PROFILES_ACTIVE:local}`
|
|
||||||
- ✅ 创建了application-local.yml本地环境配置文件
|
|
||||||
- ✅ 配置了Bean覆盖和循环引用支持
|
|
||||||
|
|
||||||
### 3. 优化了启动脚本
|
|
||||||
- ✅ 创建了支持环境参数的启动脚本:`./start-services.sh [env]`
|
|
||||||
- ✅ 默认使用local环境,支持dev、prod等环境切换
|
|
||||||
- ✅ 添加了基础服务检查(MySQL、Redis)
|
|
||||||
- ✅ 添加了服务状态检查和日志输出
|
|
||||||
|
|
||||||
### 4. 统一了Coze API配置
|
|
||||||
- ✅ 在AI服务中配置了统一的Coze API参数
|
|
||||||
- ✅ 所有环境使用相同的Coze配置
|
|
||||||
|
|
||||||
### 5. 清理了无用文件
|
|
||||||
- ✅ 删除了测试文件和重复的配置文件
|
|
||||||
- ✅ 删除了不再使用的启动脚本
|
|
||||||
|
|
||||||
### 6. 前端网关配置
|
|
||||||
- ✅ 更新了前端vite.config.js,统一通过网关访问后端
|
|
||||||
- ✅ 更新了网关路由配置,支持所有服务的路由转发
|
|
||||||
- ✅ 创建了.env.local环境配置文件
|
|
||||||
|
|
||||||
## 已解决的问题
|
|
||||||
|
|
||||||
### 1. 循环依赖问题 ✅
|
|
||||||
**问题描述:** 用户服务存在Bean循环依赖问题
|
|
||||||
```
|
|
||||||
jwtAuthenticationFilter -> userDetailsServiceImpl -> userServiceImpl -> securityConfig -> jwtAuthenticationFilter
|
|
||||||
```
|
|
||||||
|
|
||||||
**解决方案:**
|
|
||||||
1. ✅ 创建独立的AuthenticationConfig类,分离认证相关Bean配置
|
|
||||||
2. ✅ 使用@Lazy注解延迟加载关键依赖
|
|
||||||
3. ✅ 通过ApplicationContext获取Bean,避免直接依赖
|
|
||||||
4. ✅ 修复验证码配置中的初始化问题
|
|
||||||
|
|
||||||
### 2. 服务启动成功 ✅
|
|
||||||
所有核心服务现在都能正常启动:
|
|
||||||
- ✅ 用户服务 (19001) - 运行正常
|
|
||||||
- ✅ AI服务 (19002) - 运行正常
|
|
||||||
- ✅ 网关服务 (19000) - 运行正常
|
|
||||||
|
|
||||||
### 3. 配置文件问题 ✅
|
|
||||||
- ✅ 修复了重复的profiles配置
|
|
||||||
- ✅ 统一了环境变量配置格式
|
|
||||||
- ✅ 修复了pom.xml中缺失的主类配置
|
|
||||||
|
|
||||||
## 下一步工作计划
|
|
||||||
|
|
||||||
### 优先级1:完善网关路由和安全配置 🔄
|
|
||||||
1. **修复网关路由问题**
|
|
||||||
- 配置用户服务的安全白名单,允许actuator端点访问
|
|
||||||
- 为AI服务添加actuator端点配置
|
|
||||||
- 测试网关路由功能
|
|
||||||
|
|
||||||
2. **完善前端集成**
|
|
||||||
- 测试前端通过网关访问后端API
|
|
||||||
- 验证用户注册登录功能
|
|
||||||
- 测试AI对话功能
|
|
||||||
|
|
||||||
### 优先级2:完善emotion-auth模块
|
|
||||||
1. **实现认证服务接口**
|
|
||||||
- 完成AuthService实现类
|
|
||||||
- 实现JWT Token管理
|
|
||||||
- 实现用户认证逻辑
|
|
||||||
|
|
||||||
2. **配置服务间调用**
|
|
||||||
- 配置Feign客户端
|
|
||||||
- 实现服务间认证传递
|
|
||||||
- 配置统一的异常处理
|
|
||||||
|
|
||||||
### 优先级3:扩展其他微服务
|
|
||||||
1. **启动其他微服务**
|
|
||||||
- emotion-record (记录服务)
|
|
||||||
- emotion-growth (成长服务)
|
|
||||||
- emotion-explore (探索服务)
|
|
||||||
- emotion-reward (奖励服务)
|
|
||||||
- emotion-stats (统计服务)
|
|
||||||
|
|
||||||
2. **完整功能测试**
|
|
||||||
- 测试所有微服务的启动和通信
|
|
||||||
- 验证完整的业务流程
|
|
||||||
- 性能测试和优化
|
|
||||||
|
|
||||||
## 使用说明
|
|
||||||
|
|
||||||
### 启动服务
|
|
||||||
```bash
|
|
||||||
# 使用默认local环境启动
|
|
||||||
./start-services.sh
|
|
||||||
|
|
||||||
# 使用指定环境启动
|
|
||||||
./start-services.sh dev
|
|
||||||
./start-services.sh prod
|
|
||||||
```
|
|
||||||
|
|
||||||
### 停止服务
|
|
||||||
```bash
|
|
||||||
./stop-services.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
### 查看日志
|
|
||||||
```bash
|
|
||||||
# 查看用户服务日志
|
|
||||||
tail -f logs/emotion-user-local.log
|
|
||||||
|
|
||||||
# 查看AI服务日志
|
|
||||||
tail -f logs/emotion-ai-local.log
|
|
||||||
|
|
||||||
# 查看网关服务日志
|
|
||||||
tail -f logs/emotion-gateway-local.log
|
|
||||||
```
|
|
||||||
|
|
||||||
### 环境变量配置
|
|
||||||
可以通过环境变量覆盖默认配置:
|
|
||||||
- `SPRING_PROFILES_ACTIVE`: 指定激活的配置文件
|
|
||||||
- `MYSQL_HOST`, `MYSQL_PORT`, `MYSQL_USERNAME`, `MYSQL_PASSWORD`: 数据库配置
|
|
||||||
- `REDIS_HOST`, `REDIS_PORT`, `REDIS_PASSWORD`: Redis配置
|
|
||||||
- `COZE_API_TOKEN`: Coze API令牌
|
|
||||||
|
|
||||||
## 项目结构
|
|
||||||
```
|
|
||||||
backend/
|
|
||||||
├── emotion-common/ # 公共模块
|
|
||||||
├── emotion-auth/ # 认证授权服务 (新增)
|
|
||||||
├── emotion-user/ # 用户服务
|
|
||||||
├── emotion-ai/ # AI服务
|
|
||||||
├── emotion-gateway/ # 网关服务
|
|
||||||
├── emotion-record/ # 记录服务
|
|
||||||
├── emotion-growth/ # 成长服务
|
|
||||||
├── emotion-explore/ # 探索服务
|
|
||||||
├── emotion-reward/ # 奖励服务
|
|
||||||
├── emotion-stats/ # 统计服务
|
|
||||||
├── start-services.sh # 启动脚本
|
|
||||||
├── stop-services.sh # 停止脚本
|
|
||||||
└── logs/ # 日志目录
|
|
||||||
```
|
|
||||||
|
|
||||||
## 注意事项
|
|
||||||
1. 确保MySQL和Redis服务已启动
|
|
||||||
2. 首次启动可能需要较长时间进行编译
|
|
||||||
3. 如遇到端口占用,脚本会自动跳过该服务
|
|
||||||
4. 建议在解决循环依赖问题后再进行完整的功能测试
|
|
||||||
@@ -192,10 +192,15 @@ generate_build_report() {
|
|||||||
for service_info in "${SERVICES[@]}"; do
|
for service_info in "${SERVICES[@]}"; do
|
||||||
service_name=$(echo $service_info | cut -d':' -f1)
|
service_name=$(echo $service_info | cut -d':' -f1)
|
||||||
jar_file="${service_name}/target/${service_name}-1.0.0.jar"
|
jar_file="${service_name}/target/${service_name}-1.0.0.jar"
|
||||||
|
|
||||||
if [ -f "$jar_file" ]; then
|
if [ -f "$jar_file" ]; then
|
||||||
jar_size=$(du -h "$jar_file" | cut -f1)
|
jar_size=$(du -h "$jar_file" | cut -f1)
|
||||||
jar_bytes=$(du -b "$jar_file" | cut -f1)
|
# 兼容macOS和Linux的文件大小获取
|
||||||
|
if [[ "$OSTYPE" == "darwin"* ]]; then
|
||||||
|
jar_bytes=$(stat -f%z "$jar_file" 2>/dev/null || echo "0")
|
||||||
|
else
|
||||||
|
jar_bytes=$(stat -c%s "$jar_file" 2>/dev/null || echo "0")
|
||||||
|
fi
|
||||||
total_size=$((total_size + jar_bytes))
|
total_size=$((total_size + jar_bytes))
|
||||||
printf "%-20s ${GREEN}%-10s${NC} %-10s %s\n" "$service_name" "✅ 成功" "$jar_size" "$jar_file"
|
printf "%-20s ${GREEN}%-10s${NC} %-10s %s\n" "$service_name" "✅ 成功" "$jar_size" "$jar_file"
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -73,9 +73,6 @@ if [ -n "$TEST_SINGLE_SERVICE" ]; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# 部署状态跟踪
|
# 部署状态跟踪
|
||||||
declare -A DEPLOYMENT_STATUS
|
|
||||||
declare -A DEPLOYMENT_ERRORS
|
|
||||||
declare -A DEPLOYMENT_TIMES
|
|
||||||
TOTAL_SERVICES=${#SERVICES[@]}
|
TOTAL_SERVICES=${#SERVICES[@]}
|
||||||
SUCCESSFUL_DEPLOYMENTS=0
|
SUCCESSFUL_DEPLOYMENTS=0
|
||||||
FAILED_DEPLOYMENTS=0
|
FAILED_DEPLOYMENTS=0
|
||||||
@@ -214,13 +211,10 @@ deploy_service() {
|
|||||||
local start_time=$(date +%s)
|
local start_time=$(date +%s)
|
||||||
|
|
||||||
log_info "开始部署服务到远程服务器: $service_name"
|
log_info "开始部署服务到远程服务器: $service_name"
|
||||||
DEPLOYMENT_STATUS[$service_name]="DEPLOYING"
|
|
||||||
|
|
||||||
# 先传输jar包
|
# 先传输jar包
|
||||||
if ! transfer_jar_to_remote $service_name; then
|
if ! transfer_jar_to_remote $service_name; then
|
||||||
local error_msg="jar包传输失败"
|
log_error "jar包传输失败"
|
||||||
DEPLOYMENT_STATUS[$service_name]="FAILED"
|
|
||||||
DEPLOYMENT_ERRORS[$service_name]="$error_msg"
|
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
@@ -58,9 +58,6 @@ SERVICES=(
|
|||||||
)
|
)
|
||||||
|
|
||||||
# 部署状态跟踪
|
# 部署状态跟踪
|
||||||
declare -A DEPLOYMENT_STATUS
|
|
||||||
declare -A DEPLOYMENT_ERRORS
|
|
||||||
declare -A DEPLOYMENT_TIMES
|
|
||||||
TOTAL_SERVICES=${#SERVICES[@]}
|
TOTAL_SERVICES=${#SERVICES[@]}
|
||||||
SUCCESSFUL_DEPLOYMENTS=0
|
SUCCESSFUL_DEPLOYMENTS=0
|
||||||
FAILED_DEPLOYMENTS=0
|
FAILED_DEPLOYMENTS=0
|
||||||
@@ -177,14 +174,11 @@ deploy_service_to_remote() {
|
|||||||
local start_time=$(date +%s)
|
local start_time=$(date +%s)
|
||||||
|
|
||||||
log_info "部署服务到远程: $service_name"
|
log_info "部署服务到远程: $service_name"
|
||||||
DEPLOYMENT_STATUS[$service_name]="DEPLOYING"
|
|
||||||
|
|
||||||
# 验证远程jar包存在
|
# 验证远程jar包存在
|
||||||
if ! ssh 'root@47.111.10.27' "test -f $REMOTE_BUILD_DIR/${service_name}-1.0.0.jar"; then
|
if ! ssh 'root@47.111.10.27' "test -f $REMOTE_BUILD_DIR/${service_name}-1.0.0.jar"; then
|
||||||
local error_msg="远程jar包不存在"
|
local error_msg="远程jar包不存在"
|
||||||
log_error "$error_msg"
|
log_error "$error_msg"
|
||||||
DEPLOYMENT_STATUS[$service_name]="FAILED"
|
|
||||||
DEPLOYMENT_ERRORS[$service_name]="$error_msg"
|
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -253,8 +247,7 @@ deploy_service_to_remote() {
|
|||||||
# 记录成功状态
|
# 记录成功状态
|
||||||
local end_time=$(date +%s)
|
local end_time=$(date +%s)
|
||||||
local duration=$((end_time - start_time))
|
local duration=$((end_time - start_time))
|
||||||
DEPLOYMENT_STATUS[$service_name]="SUCCESS"
|
log_info "服务 $service_name 部署成功,耗时: ${duration}s"
|
||||||
DEPLOYMENT_TIMES[$service_name]="${duration}s"
|
|
||||||
return 0
|
return 0
|
||||||
else
|
else
|
||||||
local error_msg="服务启动失败"
|
local error_msg="服务启动失败"
|
||||||
@@ -265,9 +258,7 @@ deploy_service_to_remote() {
|
|||||||
# 记录失败状态
|
# 记录失败状态
|
||||||
local end_time=$(date +%s)
|
local end_time=$(date +%s)
|
||||||
local duration=$((end_time - start_time))
|
local duration=$((end_time - start_time))
|
||||||
DEPLOYMENT_STATUS[$service_name]="FAILED"
|
log_error "服务 $service_name 部署失败,耗时: ${duration}s"
|
||||||
DEPLOYMENT_ERRORS[$service_name]="$error_msg: $error_logs"
|
|
||||||
DEPLOYMENT_TIMES[$service_name]="${duration}s"
|
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,594 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# 情感博物馆 - 全服务容器化部署脚本
|
|
||||||
# 作者: emotion-museum
|
|
||||||
# 日期: 2025-07-18
|
|
||||||
# 支持Jenkins CI/CD部署
|
|
||||||
|
|
||||||
# 不要在遇到错误时立即退出,让所有模块都尝试部署
|
|
||||||
set +e
|
|
||||||
|
|
||||||
# 配置变量 - 支持Jenkins环境变量覆盖
|
|
||||||
REMOTE_HOST="${DEPLOY_HOST:-'root@47.111.10.27'}"
|
|
||||||
REMOTE_BUILD_DIR="${REMOTE_BUILD_DIR:-/data/builds}"
|
|
||||||
REMOTE_DOCKER_COMPOSE_DIR="${REMOTE_DOCKER_DIR:-/data/docker}"
|
|
||||||
PROFILE="${DEPLOY_ENV:-test}"
|
|
||||||
PROJECT_NAME="${PROJECT_NAME:-emotion-museum}"
|
|
||||||
|
|
||||||
# Jenkins构建信息
|
|
||||||
BUILD_NUMBER="${BUILD_NUMBER:-manual}"
|
|
||||||
JOB_NAME="${JOB_NAME:-local-deploy}"
|
|
||||||
BUILD_URL="${BUILD_URL:-}"
|
|
||||||
|
|
||||||
# 部署模式配置
|
|
||||||
DEPLOY_MODE="${DEPLOY_MODE:-full}" # full: 完整部署, build: 仅构建, deploy: 仅部署
|
|
||||||
|
|
||||||
# 颜色输出
|
|
||||||
RED='\033[0;31m'
|
|
||||||
GREEN='\033[0;32m'
|
|
||||||
YELLOW='\033[1;33m'
|
|
||||||
BLUE='\033[0;34m'
|
|
||||||
NC='\033[0m' # No Color
|
|
||||||
|
|
||||||
# 日志函数
|
|
||||||
log_info() {
|
|
||||||
echo -e "${BLUE}[INFO]${NC} $(date '+%Y-%m-%d %H:%M:%S') - $1"
|
|
||||||
}
|
|
||||||
|
|
||||||
log_success() {
|
|
||||||
echo -e "${GREEN}[SUCCESS]${NC} $(date '+%Y-%m-%d %H:%M:%S') - $1"
|
|
||||||
}
|
|
||||||
|
|
||||||
log_warning() {
|
|
||||||
echo -e "${YELLOW}[WARNING]${NC} $(date '+%Y-%m-%d %H:%M:%S') - $1"
|
|
||||||
}
|
|
||||||
|
|
||||||
log_error() {
|
|
||||||
echo -e "${RED}[ERROR]${NC} $(date '+%Y-%m-%d %H:%M:%S') - $1"
|
|
||||||
}
|
|
||||||
|
|
||||||
# 服务列表
|
|
||||||
SERVICES=(
|
|
||||||
"emotion-gateway:19000"
|
|
||||||
"emotion-user:19001"
|
|
||||||
"emotion-ai:19002"
|
|
||||||
"emotion-record:19003"
|
|
||||||
"emotion-growth:19004"
|
|
||||||
"emotion-explore:19005"
|
|
||||||
"emotion-reward:19006"
|
|
||||||
"emotion-websocket:19007"
|
|
||||||
"emotion-auth:19008"
|
|
||||||
"emotion-stats:19009"
|
|
||||||
)
|
|
||||||
|
|
||||||
# 部署状态跟踪
|
|
||||||
declare -A DEPLOYMENT_STATUS
|
|
||||||
declare -A DEPLOYMENT_ERRORS
|
|
||||||
declare -A DEPLOYMENT_TIMES
|
|
||||||
TOTAL_SERVICES=${#SERVICES[@]}
|
|
||||||
SUCCESSFUL_DEPLOYMENTS=0
|
|
||||||
FAILED_DEPLOYMENTS=0
|
|
||||||
|
|
||||||
# 检查远程服务器连接
|
|
||||||
check_remote_connection() {
|
|
||||||
log_info "检查远程服务器连接..."
|
|
||||||
if ssh -o ConnectTimeout=10 'root@47.111.10.27' "echo 'Connection successful'" > /dev/null 2>&1; then
|
|
||||||
log_success "远程服务器连接正常"
|
|
||||||
else
|
|
||||||
log_error "无法连接到远程服务器 'root@47.111.10.27'"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# 创建远程目录
|
|
||||||
create_remote_directories() {
|
|
||||||
log_info "创建远程目录结构..."
|
|
||||||
ssh 'root@47.111.10.27' "
|
|
||||||
mkdir -p $REMOTE_BUILD_DIR
|
|
||||||
mkdir -p $REMOTE_DOCKER_COMPOSE_DIR
|
|
||||||
mkdir -p /data/logs/emotion-museum
|
|
||||||
mkdir -p /data/config/emotion-museum
|
|
||||||
"
|
|
||||||
log_success "远程目录创建完成"
|
|
||||||
}
|
|
||||||
|
|
||||||
# 构建所有服务 (Jenkins阶段)
|
|
||||||
build_all_services() {
|
|
||||||
log_info "开始在Jenkins服务器上构建所有微服务..."
|
|
||||||
|
|
||||||
# 检查是否在Jenkins环境中
|
|
||||||
if [ -n "$JENKINS_HOME" ] || [ -n "$BUILD_NUMBER" ]; then
|
|
||||||
log_info "检测到Jenkins环境,执行完整构建流程"
|
|
||||||
else
|
|
||||||
log_info "本地环境,执行构建流程"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 先构建父项目
|
|
||||||
log_info "构建父项目..."
|
|
||||||
if mvn clean install -DskipTests -q; then
|
|
||||||
log_success "父项目构建成功"
|
|
||||||
else
|
|
||||||
log_error "父项目构建失败"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 构建各个微服务
|
|
||||||
for service_info in "${SERVICES[@]}"; do
|
|
||||||
service_name=$(echo $service_info | cut -d':' -f1)
|
|
||||||
log_info "构建服务: $service_name"
|
|
||||||
|
|
||||||
cd $service_name
|
|
||||||
if mvn clean package -DskipTests -P${PROFILE} -q; then
|
|
||||||
# 检查jar包是否生成
|
|
||||||
if [ -f "target/${service_name}-1.0.0.jar" ]; then
|
|
||||||
local jar_size=$(du -h "target/${service_name}-1.0.0.jar" | cut -f1)
|
|
||||||
log_success "服务 $service_name 构建成功 (大小: $jar_size)"
|
|
||||||
else
|
|
||||||
log_error "服务 $service_name jar包未生成"
|
|
||||||
cd ..
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
log_error "服务 $service_name 构建失败"
|
|
||||||
cd ..
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
cd ..
|
|
||||||
done
|
|
||||||
|
|
||||||
log_success "所有服务在Jenkins服务器构建完成"
|
|
||||||
}
|
|
||||||
|
|
||||||
# 部署所有服务到远程服务器
|
|
||||||
deploy_all_services_to_remote() {
|
|
||||||
log_info "开始逐个部署服务到远程服务器..."
|
|
||||||
|
|
||||||
for service_info in "${SERVICES[@]}"; do
|
|
||||||
service_name=$(echo $service_info | cut -d':' -f1)
|
|
||||||
service_port=$(echo $service_info | cut -d':' -f2)
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
log_info "[$((SUCCESSFUL_DEPLOYMENTS + FAILED_DEPLOYMENTS + 1))/$TOTAL_SERVICES] 部署服务: $service_name"
|
|
||||||
|
|
||||||
if deploy_service $service_name $service_port; then
|
|
||||||
SUCCESSFUL_DEPLOYMENTS=$((SUCCESSFUL_DEPLOYMENTS + 1))
|
|
||||||
log_success "✅ 服务 $service_name 部署成功"
|
|
||||||
else
|
|
||||||
FAILED_DEPLOYMENTS=$((FAILED_DEPLOYMENTS + 1))
|
|
||||||
log_error "❌ 服务 $service_name 部署失败,继续部署其他服务..."
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
}
|
|
||||||
|
|
||||||
# 传输jar包到远程服务器
|
|
||||||
transfer_jar_to_remote() {
|
|
||||||
local service_name=$1
|
|
||||||
|
|
||||||
log_info "传输jar包到远程服务器: $service_name"
|
|
||||||
|
|
||||||
# 检查本地jar包是否存在
|
|
||||||
local jar_file="${service_name}/target/${service_name}-1.0.0.jar"
|
|
||||||
if [ ! -f "$jar_file" ]; then
|
|
||||||
log_error "本地JAR包不存在: $jar_file"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 显示jar包信息
|
|
||||||
local jar_size=$(du -h "$jar_file" | cut -f1)
|
|
||||||
log_info "准备传输jar包: $jar_file (大小: $jar_size)"
|
|
||||||
|
|
||||||
# 删除远程旧jar包
|
|
||||||
log_info "清理远程旧jar包: $service_name"
|
|
||||||
ssh 'root@47.111.10.27' "rm -f $REMOTE_BUILD_DIR/${service_name}-*.jar"
|
|
||||||
|
|
||||||
# 上传新jar包
|
|
||||||
log_info "上传jar包到远程服务器..."
|
|
||||||
if scp "$jar_file" 'root@47.111.10.27':$REMOTE_BUILD_DIR/${service_name}-1.0.0.jar; then
|
|
||||||
log_success "jar包传输成功: $service_name"
|
|
||||||
|
|
||||||
# 验证远程jar包
|
|
||||||
local remote_size=$(ssh 'root@47.111.10.27' "du -h $REMOTE_BUILD_DIR/${service_name}-1.0.0.jar | cut -f1")
|
|
||||||
log_info "远程jar包大小: $remote_size"
|
|
||||||
return 0
|
|
||||||
else
|
|
||||||
log_error "jar包传输失败: $service_name"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# 部署单个服务 (远程服务器阶段)
|
|
||||||
deploy_service() {
|
|
||||||
local service_name=$1
|
|
||||||
local service_port=$2
|
|
||||||
local start_time=$(date +%s)
|
|
||||||
|
|
||||||
log_info "开始部署服务到远程服务器: $service_name"
|
|
||||||
DEPLOYMENT_STATUS[$service_name]="DEPLOYING"
|
|
||||||
|
|
||||||
# 先传输jar包
|
|
||||||
if ! transfer_jar_to_remote $service_name; then
|
|
||||||
local error_msg="jar包传输失败"
|
|
||||||
DEPLOYMENT_STATUS[$service_name]="FAILED"
|
|
||||||
DEPLOYMENT_ERRORS[$service_name]="$error_msg"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 验证远程jar包存在
|
|
||||||
log_info "验证远程jar包: $service_name"
|
|
||||||
if ! ssh 'root@47.111.10.27' "test -f $REMOTE_BUILD_DIR/${service_name}-1.0.0.jar"; then
|
|
||||||
local error_msg="远程jar包不存在,请先执行构建和传输"
|
|
||||||
log_error "$error_msg"
|
|
||||||
DEPLOYMENT_STATUS[$service_name]="FAILED"
|
|
||||||
DEPLOYMENT_ERRORS[$service_name]="$error_msg"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 创建Dockerfile
|
|
||||||
create_dockerfile $service_name $service_port
|
|
||||||
|
|
||||||
# 停止并删除旧容器
|
|
||||||
log_info "停止旧容器: $service_name"
|
|
||||||
ssh 'root@47.111.10.27' "
|
|
||||||
docker stop ${service_name} 2>/dev/null || true
|
|
||||||
docker rm ${service_name} 2>/dev/null || true
|
|
||||||
docker rmi ${PROJECT_NAME}/${service_name}:latest 2>/dev/null || true
|
|
||||||
"
|
|
||||||
|
|
||||||
# 构建Docker镜像
|
|
||||||
log_info "构建Docker镜像: $service_name"
|
|
||||||
ssh 'root@47.111.10.27' "
|
|
||||||
# 复制jar包到Docker构建目录
|
|
||||||
cp $REMOTE_BUILD_DIR/${service_name}-1.0.0.jar $REMOTE_DOCKER_COMPOSE_DIR/
|
|
||||||
|
|
||||||
# 构建镜像
|
|
||||||
cd $REMOTE_DOCKER_COMPOSE_DIR
|
|
||||||
docker build -t ${PROJECT_NAME}/${service_name}:latest -f Dockerfile.${service_name} .
|
|
||||||
|
|
||||||
# 清理临时文件
|
|
||||||
rm -f ${service_name}-1.0.0.jar
|
|
||||||
"
|
|
||||||
|
|
||||||
# 启动新容器
|
|
||||||
log_info "启动新容器: $service_name"
|
|
||||||
ssh 'root@47.111.10.27' "
|
|
||||||
docker run -d \\
|
|
||||||
--name ${service_name} \\
|
|
||||||
--network emotion-network \\
|
|
||||||
-p ${service_port}:${service_port} \\
|
|
||||||
-v /data/logs/emotion-museum:/app/logs \\
|
|
||||||
-e SPRING_PROFILES_ACTIVE=${PROFILE} \\
|
|
||||||
-e MYSQL_HOST=47.111.10.27 \\
|
|
||||||
-e MYSQL_PORT=3306 \\
|
|
||||||
-e MYSQL_DATABASE=emotion_museum \\
|
|
||||||
-e MYSQL_USERNAME=root \\
|
|
||||||
-e MYSQL_PASSWORD='EmotionMuseum2025*#' \\
|
|
||||||
-e REDIS_HOST=47.111.10.27 \\
|
|
||||||
-e REDIS_PORT=6379 \\
|
|
||||||
-e REDIS_PASSWORD= \\
|
|
||||||
-e REDIS_DATABASE=0 \\
|
|
||||||
-e NACOS_SERVER_ADDR=47.111.10.27:8848 \\
|
|
||||||
-e NACOS_USERNAME=nacos \\
|
|
||||||
-e NACOS_PASSWORD='Peanut2817*#' \\
|
|
||||||
--restart unless-stopped \\
|
|
||||||
${PROJECT_NAME}/${service_name}:latest
|
|
||||||
"
|
|
||||||
|
|
||||||
# 等待服务启动
|
|
||||||
log_info "等待服务启动: $service_name"
|
|
||||||
sleep 10
|
|
||||||
|
|
||||||
# 检查容器状态
|
|
||||||
if ssh 'root@47.111.10.27' "docker ps | grep ${service_name}" > /dev/null 2>&1; then
|
|
||||||
log_success "服务 $service_name 启动成功"
|
|
||||||
|
|
||||||
# 显示容器日志
|
|
||||||
log_info "显示服务日志 最后10行: $service_name"
|
|
||||||
ssh 'root@47.111.10.27' "docker logs --tail 10 ${service_name}" 2>/dev/null || true
|
|
||||||
|
|
||||||
# 记录成功状态
|
|
||||||
local end_time=$(date +%s)
|
|
||||||
local duration=$((end_time - start_time))
|
|
||||||
DEPLOYMENT_STATUS[$service_name]="SUCCESS"
|
|
||||||
DEPLOYMENT_TIMES[$service_name]="${duration}s"
|
|
||||||
return 0
|
|
||||||
else
|
|
||||||
local error_msg="服务启动失败"
|
|
||||||
log_error "服务 $service_name 启动失败"
|
|
||||||
log_error "错误日志:"
|
|
||||||
local error_logs=$(ssh 'root@47.111.10.27' "docker logs ${service_name}" 2>&1 || echo "无法获取日志")
|
|
||||||
echo "$error_logs"
|
|
||||||
|
|
||||||
# 记录失败状态
|
|
||||||
local end_time=$(date +%s)
|
|
||||||
local duration=$((end_time - start_time))
|
|
||||||
DEPLOYMENT_STATUS[$service_name]="FAILED"
|
|
||||||
DEPLOYMENT_ERRORS[$service_name]="$error_msg: $error_logs"
|
|
||||||
DEPLOYMENT_TIMES[$service_name]="${duration}s"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# 创建Dockerfile
|
|
||||||
create_dockerfile() {
|
|
||||||
local service_name=$1
|
|
||||||
local service_port=$2
|
|
||||||
|
|
||||||
log_info "创建Dockerfile: $service_name"
|
|
||||||
|
|
||||||
ssh 'root@47.111.10.27' "cat > $REMOTE_DOCKER_COMPOSE_DIR/Dockerfile.${service_name} << 'EOF'
|
|
||||||
FROM openjdk:17-jre-slim
|
|
||||||
|
|
||||||
# 设置工作目录
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
# 安装必要的工具
|
|
||||||
RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
# 复制jar包 (使用相对路径)
|
|
||||||
COPY ${service_name}-1.0.0.jar app.jar
|
|
||||||
|
|
||||||
# 创建日志目录
|
|
||||||
RUN mkdir -p /app/logs
|
|
||||||
|
|
||||||
# 设置时区
|
|
||||||
ENV TZ=Asia/Shanghai
|
|
||||||
RUN ln -snf /usr/share/zoneinfo/\$TZ /etc/localtime && echo \$TZ > /etc/timezone
|
|
||||||
|
|
||||||
# 暴露端口
|
|
||||||
EXPOSE ${service_port}
|
|
||||||
|
|
||||||
# 健康检查
|
|
||||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \\
|
|
||||||
CMD curl -f http://localhost:${service_port}/actuator/health || exit 1
|
|
||||||
|
|
||||||
# 启动应用
|
|
||||||
ENTRYPOINT [\"java\", \"-Djava.security.egd=file:/dev/./urandom\", \"-Xms512m\", \"-Xmx1024m\", \"-jar\", \"app.jar\"]
|
|
||||||
EOF"
|
|
||||||
}
|
|
||||||
|
|
||||||
# 创建Docker网络
|
|
||||||
create_docker_network() {
|
|
||||||
log_info "创建Docker网络..."
|
|
||||||
ssh 'root@47.111.10.27' "
|
|
||||||
docker network create emotion-network 2>/dev/null || true
|
|
||||||
"
|
|
||||||
log_success "Docker网络创建完成"
|
|
||||||
}
|
|
||||||
|
|
||||||
# 健康检查
|
|
||||||
health_check() {
|
|
||||||
log_info "执行服务健康检查..."
|
|
||||||
|
|
||||||
for service_info in "${SERVICES[@]}"; do
|
|
||||||
service_name=$(echo $service_info | cut -d':' -f1)
|
|
||||||
service_port=$(echo $service_info | cut -d':' -f2)
|
|
||||||
|
|
||||||
log_info "检查服务健康状态: $service_name"
|
|
||||||
|
|
||||||
# 等待服务完全启动
|
|
||||||
sleep 5
|
|
||||||
|
|
||||||
if ssh 'root@47.111.10.27' "curl -f -s http://localhost:${service_port}/actuator/health" > /dev/null 2>&1; then
|
|
||||||
log_success "服务 $service_name 健康检查通过"
|
|
||||||
else
|
|
||||||
log_warning "服务 $service_name 健康检查失败,可能仍在启动中"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
}
|
|
||||||
|
|
||||||
# 显示详细部署报告
|
|
||||||
show_deployment_report() {
|
|
||||||
local total_time=$1
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "========================================"
|
|
||||||
echo " 部署完成报告"
|
|
||||||
echo "========================================"
|
|
||||||
echo "项目名称: $PROJECT_NAME"
|
|
||||||
echo "部署环境: $PROFILE"
|
|
||||||
echo "目标服务器: $REMOTE_HOST"
|
|
||||||
echo "部署时间: $(date '+%Y-%m-%d %H:%M:%S')"
|
|
||||||
echo "总耗时: ${total_time}s"
|
|
||||||
if [ "$BUILD_NUMBER" != "manual" ]; then
|
|
||||||
echo "Jenkins构建: #$BUILD_NUMBER"
|
|
||||||
echo "Jenkins任务: $JOB_NAME"
|
|
||||||
[ -n "$BUILD_URL" ] && echo "构建链接: $BUILD_URL"
|
|
||||||
fi
|
|
||||||
echo "========================================"
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "📊 部署统计:"
|
|
||||||
echo " 总服务数: $TOTAL_SERVICES"
|
|
||||||
echo " 成功部署: $SUCCESSFUL_DEPLOYMENTS"
|
|
||||||
echo " 失败部署: $FAILED_DEPLOYMENTS"
|
|
||||||
echo " 成功率: $(( SUCCESSFUL_DEPLOYMENTS * 100 / TOTAL_SERVICES ))%"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
echo "📋 服务部署详情:"
|
|
||||||
printf "%-20s %-10s %-10s %s\n" "服务名称" "状态" "耗时" "备注"
|
|
||||||
echo "----------------------------------------"
|
|
||||||
|
|
||||||
for service_info in "${SERVICES[@]}"; do
|
|
||||||
service_name=$(echo $service_info | cut -d':' -f1)
|
|
||||||
service_port=$(echo $service_info | cut -d':' -f2)
|
|
||||||
status=${DEPLOYMENT_STATUS[$service_name]:-"UNKNOWN"}
|
|
||||||
time=${DEPLOYMENT_TIMES[$service_name]:-"N/A"}
|
|
||||||
|
|
||||||
case $status in
|
|
||||||
"SUCCESS")
|
|
||||||
printf "%-20s ${GREEN}%-10s${NC} %-10s %s\n" "$service_name" "✅ 成功" "$time" "http://47.111.10.27:$service_port"
|
|
||||||
;;
|
|
||||||
"FAILED")
|
|
||||||
printf "%-20s ${RED}%-10s${NC} %-10s %s\n" "$service_name" "❌ 失败" "$time" "查看错误日志"
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
printf "%-20s ${YELLOW}%-10s${NC} %-10s %s\n" "$service_name" "⚠️ 未知" "$time" "状态异常"
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# 显示失败服务的错误信息
|
|
||||||
if [ $FAILED_DEPLOYMENTS -gt 0 ]; then
|
|
||||||
echo "❌ 失败服务错误详情:"
|
|
||||||
echo "----------------------------------------"
|
|
||||||
for service_info in "${SERVICES[@]}"; do
|
|
||||||
service_name=$(echo $service_info | cut -d':' -f1)
|
|
||||||
if [ "${DEPLOYMENT_STATUS[$service_name]}" = "FAILED" ]; then
|
|
||||||
echo "🔸 $service_name:"
|
|
||||||
echo " ${DEPLOYMENT_ERRORS[$service_name]}" | head -3
|
|
||||||
echo ""
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 显示当前运行的容器状态
|
|
||||||
echo "🐳 当前容器运行状态:"
|
|
||||||
echo "----------------------------------------"
|
|
||||||
ssh 'root@47.111.10.27' "docker ps --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}' | grep emotion || echo '没有运行的emotion相关容器'"
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "========================================"
|
|
||||||
|
|
||||||
# 根据部署结果设置退出码
|
|
||||||
if [ $FAILED_DEPLOYMENTS -eq 0 ]; then
|
|
||||||
echo "🎉 所有服务部署成功!"
|
|
||||||
return 0
|
|
||||||
else
|
|
||||||
echo "⚠️ 部分服务部署失败,请检查错误日志"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# 主函数
|
|
||||||
main() {
|
|
||||||
local start_time=$(date +%s)
|
|
||||||
|
|
||||||
log_info "🚀 开始全服务容器化部署..."
|
|
||||||
log_info "目标服务器: $REMOTE_HOST"
|
|
||||||
log_info "部署环境: $PROFILE"
|
|
||||||
log_info "部署模式: $DEPLOY_MODE"
|
|
||||||
log_info "服务总数: $TOTAL_SERVICES"
|
|
||||||
|
|
||||||
# 根据部署模式执行不同的流程
|
|
||||||
case $DEPLOY_MODE in
|
|
||||||
"build")
|
|
||||||
log_info "🔨 执行构建模式 - 仅在Jenkins服务器构建jar包"
|
|
||||||
execute_build_only
|
|
||||||
;;
|
|
||||||
"deploy")
|
|
||||||
log_info "🚀 执行部署模式 - 仅部署到远程服务器"
|
|
||||||
execute_deploy_only
|
|
||||||
;;
|
|
||||||
"full"|*)
|
|
||||||
log_info "🔄 执行完整模式 - 构建+部署"
|
|
||||||
execute_full_deployment
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
|
|
||||||
# 仅构建模式
|
|
||||||
execute_build_only() {
|
|
||||||
local start_time=$(date +%s)
|
|
||||||
|
|
||||||
log_info "开始构建所有服务..."
|
|
||||||
|
|
||||||
# 构建服务
|
|
||||||
if ! build_all_services; then
|
|
||||||
log_error "服务构建失败"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 显示构建结果
|
|
||||||
log_info "📦 构建产物信息:"
|
|
||||||
for service_info in "${SERVICES[@]}"; do
|
|
||||||
service_name=$(echo $service_info | cut -d':' -f1)
|
|
||||||
jar_file="${service_name}/target/${service_name}-1.0.0.jar"
|
|
||||||
if [ -f "$jar_file" ]; then
|
|
||||||
jar_size=$(du -h "$jar_file" | cut -f1)
|
|
||||||
log_success "✅ $service_name: $jar_size"
|
|
||||||
else
|
|
||||||
log_error "❌ $service_name: jar包未生成"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
local end_time=$(date +%s)
|
|
||||||
local total_time=$((end_time - start_time))
|
|
||||||
log_success "🎉 构建完成!总耗时: ${total_time}s"
|
|
||||||
}
|
|
||||||
|
|
||||||
# 仅部署模式
|
|
||||||
execute_deploy_only() {
|
|
||||||
local start_time=$(date +%s)
|
|
||||||
|
|
||||||
log_info "开始部署到远程服务器..."
|
|
||||||
|
|
||||||
# 检查连接
|
|
||||||
if ! check_remote_connection; then
|
|
||||||
log_error "远程服务器连接失败,部署终止"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 创建目录和网络
|
|
||||||
create_remote_directories
|
|
||||||
create_docker_network
|
|
||||||
|
|
||||||
# 部署所有服务
|
|
||||||
deploy_all_services_to_remote
|
|
||||||
|
|
||||||
# 健康检查和报告
|
|
||||||
health_check
|
|
||||||
local end_time=$(date +%s)
|
|
||||||
local total_time=$((end_time - start_time))
|
|
||||||
show_deployment_report $total_time
|
|
||||||
}
|
|
||||||
|
|
||||||
# 完整部署模式
|
|
||||||
execute_full_deployment() {
|
|
||||||
local start_time=$(date +%s)
|
|
||||||
|
|
||||||
# 检查连接
|
|
||||||
if ! check_remote_connection; then
|
|
||||||
log_error "远程服务器连接失败,部署终止"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 创建目录
|
|
||||||
create_remote_directories
|
|
||||||
|
|
||||||
# 创建Docker网络
|
|
||||||
create_docker_network
|
|
||||||
|
|
||||||
# 构建服务
|
|
||||||
if ! build_all_services; then
|
|
||||||
log_error "服务构建失败,部署终止"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 部署所有服务
|
|
||||||
deploy_all_services_to_remote
|
|
||||||
|
|
||||||
# 健康检查
|
|
||||||
log_info "执行服务健康检查..."
|
|
||||||
health_check
|
|
||||||
|
|
||||||
# 计算总耗时
|
|
||||||
local end_time=$(date +%s)
|
|
||||||
local total_time=$((end_time - start_time))
|
|
||||||
|
|
||||||
# 显示详细报告
|
|
||||||
show_deployment_report $total_time
|
|
||||||
|
|
||||||
# 根据部署结果设置退出码
|
|
||||||
if [ $FAILED_DEPLOYMENTS -eq 0 ]; then
|
|
||||||
log_success "🎉 全服务容器化部署完成!"
|
|
||||||
exit 0
|
|
||||||
else
|
|
||||||
log_warning "⚠️ 部分服务部署失败,请查看详细报告"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# 执行主函数
|
|
||||||
main "$@"
|
|
||||||
@@ -1,209 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# 情绪博物馆自动化开发脚本
|
|
||||||
# 自动编译、启动、监控文件变化并重启服务
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
# 颜色定义
|
|
||||||
RED='\033[0;31m'
|
|
||||||
GREEN='\033[0;32m'
|
|
||||||
YELLOW='\033[1;33m'
|
|
||||||
BLUE='\033[0;34m'
|
|
||||||
NC='\033[0m' # No Color
|
|
||||||
|
|
||||||
# 配置
|
|
||||||
PROJECT_ROOT=$(pwd)
|
|
||||||
SERVICES=("emotion-gateway" "emotion-user" "emotion-ai" "emotion-record" "emotion-growth" "emotion-explore" "emotion-reward" "emotion-stats")
|
|
||||||
PID_FILE="$PROJECT_ROOT/.dev-pids"
|
|
||||||
LOG_DIR="$PROJECT_ROOT/dev-logs"
|
|
||||||
|
|
||||||
# 创建日志目录
|
|
||||||
mkdir -p "$LOG_DIR"
|
|
||||||
|
|
||||||
echo -e "${BLUE}===========================================${NC}"
|
|
||||||
echo -e "${BLUE}情绪博物馆自动化开发环境${NC}"
|
|
||||||
echo -e "${BLUE}===========================================${NC}"
|
|
||||||
|
|
||||||
# 清理函数
|
|
||||||
cleanup() {
|
|
||||||
echo -e "\n${YELLOW}正在停止所有服务...${NC}"
|
|
||||||
if [ -f "$PID_FILE" ]; then
|
|
||||||
while read line; do
|
|
||||||
if [ ! -z "$line" ]; then
|
|
||||||
echo "停止进程: $line"
|
|
||||||
kill -9 $line 2>/dev/null || true
|
|
||||||
fi
|
|
||||||
done < "$PID_FILE"
|
|
||||||
rm -f "$PID_FILE"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 清理Maven进程
|
|
||||||
pkill -f "mvn.*spring-boot:run" 2>/dev/null || true
|
|
||||||
|
|
||||||
echo -e "${GREEN}✅ 清理完成${NC}"
|
|
||||||
exit 0
|
|
||||||
}
|
|
||||||
|
|
||||||
# 捕获退出信号
|
|
||||||
trap cleanup SIGINT SIGTERM
|
|
||||||
|
|
||||||
# 编译项目
|
|
||||||
compile_project() {
|
|
||||||
echo -e "${YELLOW}🔄 编译项目...${NC}"
|
|
||||||
mvn clean compile -DskipTests -q
|
|
||||||
if [ $? -eq 0 ]; then
|
|
||||||
echo -e "${GREEN}✅ 编译成功${NC}"
|
|
||||||
return 0
|
|
||||||
else
|
|
||||||
echo -e "${RED}❌ 编译失败${NC}"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# 启动单个服务
|
|
||||||
start_service() {
|
|
||||||
local service=$1
|
|
||||||
local port=$2
|
|
||||||
|
|
||||||
echo -e "${BLUE}🚀 启动 $service (端口: $port)...${NC}"
|
|
||||||
|
|
||||||
cd "$PROJECT_ROOT/$service"
|
|
||||||
|
|
||||||
# 启动服务并获取PID
|
|
||||||
nohup mvn spring-boot:run -Dspring-boot.run.jvmArguments="-Dserver.port=$port" \
|
|
||||||
> "$LOG_DIR/$service.log" 2>&1 &
|
|
||||||
|
|
||||||
local pid=$!
|
|
||||||
echo "$pid" >> "$PID_FILE"
|
|
||||||
|
|
||||||
echo -e "${GREEN}✅ $service 已启动 (PID: $pid)${NC}"
|
|
||||||
|
|
||||||
cd "$PROJECT_ROOT"
|
|
||||||
}
|
|
||||||
|
|
||||||
# 检查服务健康状态
|
|
||||||
check_service_health() {
|
|
||||||
local service=$1
|
|
||||||
local port=$2
|
|
||||||
local max_attempts=30
|
|
||||||
local attempt=0
|
|
||||||
|
|
||||||
echo -e "${YELLOW}⏳ 等待 $service 启动...${NC}"
|
|
||||||
|
|
||||||
while [ $attempt -lt $max_attempts ]; do
|
|
||||||
if curl -s "http://localhost:$port/actuator/health" > /dev/null 2>&1; then
|
|
||||||
echo -e "${GREEN}✅ $service 健康检查通过${NC}"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
attempt=$((attempt + 1))
|
|
||||||
sleep 2
|
|
||||||
echo -n "."
|
|
||||||
done
|
|
||||||
|
|
||||||
echo -e "\n${RED}❌ $service 健康检查失败${NC}"
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# 监控文件变化
|
|
||||||
monitor_changes() {
|
|
||||||
echo -e "${BLUE}👀 开始监控文件变化...${NC}"
|
|
||||||
echo -e "${YELLOW}按 Ctrl+C 停止监控${NC}"
|
|
||||||
|
|
||||||
# 使用fswatch监控文件变化 (需要安装: brew install fswatch)
|
|
||||||
if command -v fswatch > /dev/null; then
|
|
||||||
fswatch -o -r --exclude="target" --exclude=".git" --exclude="node_modules" \
|
|
||||||
--include=".*\\.java$" --include=".*\\.yml$" --include=".*\\.xml$" \
|
|
||||||
"$PROJECT_ROOT" | while read f; do
|
|
||||||
echo -e "${YELLOW}🔄 检测到文件变化,重新编译...${NC}"
|
|
||||||
if compile_project; then
|
|
||||||
echo -e "${GREEN}📝 代码已更新,DevTools将自动重启服务${NC}"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
else
|
|
||||||
# 如果没有fswatch,使用简单的循环检查
|
|
||||||
echo -e "${YELLOW}⚠️ 建议安装 fswatch 以获得更好的文件监控体验:brew install fswatch${NC}"
|
|
||||||
while true; do
|
|
||||||
sleep 5
|
|
||||||
# 简单的时间戳检查(这里可以根据需要扩展)
|
|
||||||
echo -e "${BLUE}💓 服务运行中... ($(date))${NC}"
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# 显示服务状态
|
|
||||||
show_status() {
|
|
||||||
echo -e "${BLUE}===========================================${NC}"
|
|
||||||
echo -e "${BLUE}服务状态${NC}"
|
|
||||||
echo -e "${BLUE}===========================================${NC}"
|
|
||||||
|
|
||||||
local ports=(8080 8081 8082 8083 8084 8085 8086 8087)
|
|
||||||
local i=0
|
|
||||||
|
|
||||||
for service in "${SERVICES[@]}"; do
|
|
||||||
local port=${ports[$i]}
|
|
||||||
if curl -s "http://localhost:$port/actuator/health" > /dev/null 2>&1; then
|
|
||||||
echo -e "${GREEN}✅ $service (端口: $port) - 运行中${NC}"
|
|
||||||
else
|
|
||||||
echo -e "${RED}❌ $service (端口: $port) - 停止${NC}"
|
|
||||||
fi
|
|
||||||
i=$((i + 1))
|
|
||||||
done
|
|
||||||
|
|
||||||
echo -e "${BLUE}===========================================${NC}"
|
|
||||||
echo -e "${YELLOW}📋 日志文件位置: $LOG_DIR/${NC}"
|
|
||||||
echo -e "${YELLOW}🌐 API网关: http://localhost:9000${NC}"
|
|
||||||
echo -e "${YELLOW}📚 API文档: http://localhost:9000/doc.html${NC}"
|
|
||||||
echo -e "${BLUE}===========================================${NC}"
|
|
||||||
}
|
|
||||||
|
|
||||||
# 主函数
|
|
||||||
main() {
|
|
||||||
# 清理之前的进程
|
|
||||||
cleanup 2>/dev/null || true
|
|
||||||
|
|
||||||
# 编译项目
|
|
||||||
if ! compile_project; then
|
|
||||||
echo -e "${RED}❌ 编译失败,请检查代码错误${NC}"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 启动服务
|
|
||||||
echo -e "${BLUE}🚀 启动所有微服务...${NC}"
|
|
||||||
|
|
||||||
local ports=(9000 9001 9002 9003 9004 9005 9006 9007)
|
|
||||||
local i=0
|
|
||||||
|
|
||||||
for service in "${SERVICES[@]}"; do
|
|
||||||
start_service "$service" "${ports[$i]}"
|
|
||||||
sleep 3 # 给服务一些启动时间
|
|
||||||
i=$((i + 1))
|
|
||||||
done
|
|
||||||
|
|
||||||
# 等待所有服务启动
|
|
||||||
sleep 10
|
|
||||||
|
|
||||||
# 显示状态
|
|
||||||
show_status
|
|
||||||
|
|
||||||
# 开始监控
|
|
||||||
monitor_changes
|
|
||||||
}
|
|
||||||
|
|
||||||
# 检查参数
|
|
||||||
case "${1:-}" in
|
|
||||||
"status")
|
|
||||||
show_status
|
|
||||||
;;
|
|
||||||
"stop")
|
|
||||||
cleanup
|
|
||||||
;;
|
|
||||||
"logs")
|
|
||||||
echo -e "${BLUE}📋 实时日志 (按 Ctrl+C 退出):${NC}"
|
|
||||||
tail -f "$LOG_DIR"/*.log
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
main
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
@@ -1,129 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# 情绪博物馆微服务开发启动脚本
|
|
||||||
# 适用于本地开发环境,可以直接看到日志输出
|
|
||||||
# 作者: emotion-museum
|
|
||||||
# 日期: 2025-07-13
|
|
||||||
|
|
||||||
# 颜色定义
|
|
||||||
RED='\033[0;31m'
|
|
||||||
GREEN='\033[0;32m'
|
|
||||||
YELLOW='\033[1;33m'
|
|
||||||
BLUE='\033[0;34m'
|
|
||||||
PURPLE='\033[0;35m'
|
|
||||||
CYAN='\033[0;36m'
|
|
||||||
NC='\033[0m' # No Color
|
|
||||||
|
|
||||||
echo -e "${BLUE}=========================================="
|
|
||||||
echo -e "情绪博物馆微服务开发启动脚本"
|
|
||||||
echo -e "适用于本地开发环境 - 实时日志输出"
|
|
||||||
echo -e "==========================================${NC}"
|
|
||||||
|
|
||||||
# 检查Java环境
|
|
||||||
if ! command -v java &> /dev/null; then
|
|
||||||
echo -e "${RED}❌ 错误: 未找到Java环境,请安装JDK 17+${NC}"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 检查Maven环境
|
|
||||||
if ! command -v mvn &> /dev/null; then
|
|
||||||
echo -e "${RED}❌ 错误: 未找到Maven环境,请安装Maven 3.6+${NC}"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo -e "${GREEN}✅ Java环境检查通过${NC}"
|
|
||||||
echo -e "${GREEN}✅ Maven环境检查通过${NC}"
|
|
||||||
|
|
||||||
# 显示菜单
|
|
||||||
show_menu() {
|
|
||||||
echo ""
|
|
||||||
echo -e "${CYAN}请选择要启动的服务:${NC}"
|
|
||||||
echo -e "${YELLOW}1.${NC} 启动网关服务 (emotion-gateway:9000)"
|
|
||||||
echo -e "${YELLOW}2.${NC} 启动用户服务 (emotion-user:9001)"
|
|
||||||
echo -e "${YELLOW}3.${NC} 启动AI对话服务 (emotion-ai:9002)"
|
|
||||||
echo -e "${YELLOW}4.${NC} 启动情绪记录服务 (emotion-record:9003)"
|
|
||||||
echo -e "${YELLOW}5.${NC} 启动成长课题服务 (emotion-growth:9004)"
|
|
||||||
echo -e "${YELLOW}6.${NC} 启动地图探索服务 (emotion-explore:9005)"
|
|
||||||
echo -e "${YELLOW}7.${NC} 启动成就奖励服务 (emotion-reward:9006)"
|
|
||||||
echo -e "${YELLOW}8.${NC} 启动统计分析服务 (emotion-stats:9007)"
|
|
||||||
echo -e "${YELLOW}9.${NC} 编译所有项目"
|
|
||||||
echo -e "${YELLOW}0.${NC} 退出"
|
|
||||||
echo ""
|
|
||||||
}
|
|
||||||
|
|
||||||
# 编译项目
|
|
||||||
compile_project() {
|
|
||||||
echo -e "${BLUE}🔨 开始编译项目...${NC}"
|
|
||||||
mvn clean compile -DskipTests
|
|
||||||
|
|
||||||
if [ $? -eq 0 ]; then
|
|
||||||
echo -e "${GREEN}✅ 项目编译成功!${NC}"
|
|
||||||
return 0
|
|
||||||
else
|
|
||||||
echo -e "${RED}❌ 项目编译失败!${NC}"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# 启动单个服务
|
|
||||||
start_service() {
|
|
||||||
local service_name=$1
|
|
||||||
local service_port=$2
|
|
||||||
local service_desc=$3
|
|
||||||
|
|
||||||
echo -e "${BLUE}🚀 启动 ${service_desc} (${service_name}:${service_port})...${NC}"
|
|
||||||
echo -e "${YELLOW}💡 提示: 按 Ctrl+C 停止服务${NC}"
|
|
||||||
echo -e "${PURPLE}📋 日志输出开始:${NC}"
|
|
||||||
echo "----------------------------------------"
|
|
||||||
|
|
||||||
cd $service_name
|
|
||||||
mvn spring-boot:run -Dspring-boot.run.jvmArguments="-Dserver.port=$service_port"
|
|
||||||
cd ..
|
|
||||||
}
|
|
||||||
|
|
||||||
# 主循环
|
|
||||||
while true; do
|
|
||||||
show_menu
|
|
||||||
read -p "请输入选择 (0-9): " choice
|
|
||||||
|
|
||||||
case $choice in
|
|
||||||
1)
|
|
||||||
start_service "emotion-gateway" 9000 "网关服务"
|
|
||||||
;;
|
|
||||||
2)
|
|
||||||
start_service "emotion-user" 9001 "用户服务"
|
|
||||||
;;
|
|
||||||
3)
|
|
||||||
start_service "emotion-ai" 9002 "AI对话服务"
|
|
||||||
;;
|
|
||||||
4)
|
|
||||||
start_service "emotion-record" 9003 "情绪记录服务"
|
|
||||||
;;
|
|
||||||
5)
|
|
||||||
start_service "emotion-growth" 9004 "成长课题服务"
|
|
||||||
;;
|
|
||||||
6)
|
|
||||||
start_service "emotion-explore" 9005 "地图探索服务"
|
|
||||||
;;
|
|
||||||
7)
|
|
||||||
start_service "emotion-reward" 9006 "成就奖励服务"
|
|
||||||
;;
|
|
||||||
8)
|
|
||||||
start_service "emotion-stats" 9007 "统计分析服务"
|
|
||||||
;;
|
|
||||||
9)
|
|
||||||
compile_project
|
|
||||||
;;
|
|
||||||
0)
|
|
||||||
echo -e "${GREEN}👋 退出开发启动脚本${NC}"
|
|
||||||
exit 0
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
echo -e "${RED}❌ 无效选择,请输入 0-9${NC}"
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo -e "${YELLOW}按任意键继续...${NC}"
|
|
||||||
read -n 1
|
|
||||||
done
|
|
||||||
@@ -1,234 +0,0 @@
|
|||||||
# Emotion WebSocket 模块创建总结
|
|
||||||
|
|
||||||
## 概述
|
|
||||||
|
|
||||||
成功创建了独立的 `emotion-websocket` 微服务模块,用于实现WebSocket实时聊天功能,支持用户与AI的实时对话。
|
|
||||||
|
|
||||||
## 创建的文件结构
|
|
||||||
|
|
||||||
```
|
|
||||||
backend/emotion-websocket/
|
|
||||||
├── pom.xml # Maven配置文件
|
|
||||||
├── Dockerfile # Docker构建文件
|
|
||||||
├── README.md # 模块说明文档
|
|
||||||
├── src/
|
|
||||||
│ ├── main/
|
|
||||||
│ │ ├── java/com/emotionmuseum/websocket/
|
|
||||||
│ │ │ ├── WebsocketApplication.java # 主启动类
|
|
||||||
│ │ │ ├── config/
|
|
||||||
│ │ │ │ ├── WebSocketConfig.java # WebSocket配置
|
|
||||||
│ │ │ │ └── AsyncConfig.java # 异步配置
|
|
||||||
│ │ │ ├── controller/
|
|
||||||
│ │ │ │ ├── ChatWebSocketController.java # WebSocket控制器
|
|
||||||
│ │ │ │ └── WebSocketTestController.java # REST测试控制器
|
|
||||||
│ │ │ ├── dto/
|
|
||||||
│ │ │ │ ├── WebSocketMessage.java # WebSocket消息DTO
|
|
||||||
│ │ │ │ └── ChatRequest.java # 聊天请求DTO
|
|
||||||
│ │ │ ├── service/
|
|
||||||
│ │ │ │ ├── ChatWebSocketService.java # WebSocket服务接口
|
|
||||||
│ │ │ │ ├── AiChatService.java # AI聊天服务接口
|
|
||||||
│ │ │ │ └── impl/
|
|
||||||
│ │ │ │ ├── ChatWebSocketServiceImpl.java # WebSocket服务实现
|
|
||||||
│ │ │ │ └── AiChatServiceImpl.java # AI聊天服务实现
|
|
||||||
│ │ │ ├── manager/
|
|
||||||
│ │ │ │ └── WebSocketSessionManager.java # 会话管理器
|
|
||||||
│ │ │ ├── feign/
|
|
||||||
│ │ │ │ └── AiServiceClient.java # AI服务Feign客户端
|
|
||||||
│ │ │ └── listener/
|
|
||||||
│ │ │ └── WebSocketEventListener.java # WebSocket事件监听器
|
|
||||||
│ │ └── resources/
|
|
||||||
│ │ ├── application.yml # 主配置文件
|
|
||||||
│ │ ├── application-local.yml # 本地环境配置
|
|
||||||
│ │ ├── bootstrap.yml # 启动配置
|
|
||||||
│ │ └── static/
|
|
||||||
│ │ └── websocket-test.html # WebSocket测试页面
|
|
||||||
│ └── test/
|
|
||||||
│ ├── java/com/emotionmuseum/websocket/
|
|
||||||
│ │ └── WebSocketTestApplication.java # 测试类
|
|
||||||
│ └── resources/
|
|
||||||
│ └── application-test.yml # 测试环境配置
|
|
||||||
```
|
|
||||||
|
|
||||||
## 主要功能特性
|
|
||||||
|
|
||||||
### 1. WebSocket实时通信
|
|
||||||
- 基于STOMP协议的WebSocket通信
|
|
||||||
- 支持SockJS降级处理
|
|
||||||
- 实时双向消息传输
|
|
||||||
|
|
||||||
### 2. 消息类型支持
|
|
||||||
- **TEXT**: 文本消息
|
|
||||||
- **TYPING**: 正在输入状态
|
|
||||||
- **SYSTEM**: 系统消息
|
|
||||||
- **ERROR**: 错误消息
|
|
||||||
- **HEARTBEAT**: 心跳检测
|
|
||||||
- **CONNECTION**: 连接状态
|
|
||||||
- **AI_THINKING**: AI思考中状态
|
|
||||||
|
|
||||||
### 3. 发送者类型
|
|
||||||
- **USER**: 注册用户
|
|
||||||
- **GUEST**: 游客用户
|
|
||||||
- **AI**: AI系统
|
|
||||||
- **SYSTEM**: 系统
|
|
||||||
|
|
||||||
### 4. 会话管理
|
|
||||||
- 用户会话状态管理
|
|
||||||
- 在线用户统计
|
|
||||||
- 会话超时处理
|
|
||||||
|
|
||||||
### 5. AI集成
|
|
||||||
- 通过Feign调用emotion-ai服务
|
|
||||||
- 异步AI响应处理
|
|
||||||
- AI回复消息分割发送
|
|
||||||
|
|
||||||
## 核心组件说明
|
|
||||||
|
|
||||||
### 1. WebSocketConfig
|
|
||||||
- 配置STOMP消息代理
|
|
||||||
- 设置WebSocket端点
|
|
||||||
- 配置跨域访问策略
|
|
||||||
|
|
||||||
### 2. ChatWebSocketController
|
|
||||||
- 处理WebSocket消息映射
|
|
||||||
- 支持聊天消息发送
|
|
||||||
- 处理用户连接/断开连接
|
|
||||||
- 心跳检测处理
|
|
||||||
|
|
||||||
### 3. WebSocketSessionManager
|
|
||||||
- 管理用户会话映射
|
|
||||||
- 在线用户状态跟踪
|
|
||||||
- 会话信息存储
|
|
||||||
|
|
||||||
### 4. ChatWebSocketService
|
|
||||||
- WebSocket消息处理核心逻辑
|
|
||||||
- 消息路由和分发
|
|
||||||
- AI服务集成调用
|
|
||||||
|
|
||||||
### 5. AiServiceClient
|
|
||||||
- 通过Feign调用emotion-ai服务
|
|
||||||
- 支持用户聊天和游客聊天接口
|
|
||||||
|
|
||||||
## 配置说明
|
|
||||||
|
|
||||||
### 服务配置
|
|
||||||
- **端口**: 19007
|
|
||||||
- **服务名**: emotion-websocket
|
|
||||||
- **WebSocket端点**: `/ws/chat`
|
|
||||||
|
|
||||||
### 消息端点
|
|
||||||
- **发送消息**: `/app/chat.send`
|
|
||||||
- **用户连接**: `/app/chat.connect`
|
|
||||||
- **用户断开**: `/app/chat.disconnect`
|
|
||||||
- **心跳检测**: `/app/chat.heartbeat`
|
|
||||||
|
|
||||||
### 订阅端点
|
|
||||||
- **用户私有消息**: `/user/queue/messages`
|
|
||||||
- **会话消息**: `/topic/conversation/{conversationId}`
|
|
||||||
- **广播消息**: `/topic/broadcast`
|
|
||||||
|
|
||||||
## 依赖关系
|
|
||||||
|
|
||||||
### 内部依赖
|
|
||||||
- `emotion-common`: 公共组件
|
|
||||||
- `emotion-ai`: AI服务(通过Feign调用)
|
|
||||||
|
|
||||||
### 外部依赖
|
|
||||||
- Spring Boot WebSocket
|
|
||||||
- Spring Cloud Alibaba
|
|
||||||
- Nacos服务发现
|
|
||||||
- OpenFeign
|
|
||||||
- MyBatis Plus
|
|
||||||
- MySQL
|
|
||||||
- Redis
|
|
||||||
|
|
||||||
## 启动方式
|
|
||||||
|
|
||||||
### 1. 单独启动
|
|
||||||
```bash
|
|
||||||
cd backend/emotion-websocket
|
|
||||||
mvn spring-boot:run -Dspring-boot.run.profiles=local
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. 统一启动脚本
|
|
||||||
```bash
|
|
||||||
cd backend
|
|
||||||
./start-services.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Docker启动
|
|
||||||
```bash
|
|
||||||
cd backend/emotion-websocket
|
|
||||||
docker build -t emotion-websocket:1.0.0 .
|
|
||||||
docker run -d -p 19007:19007 emotion-websocket:1.0.0
|
|
||||||
```
|
|
||||||
|
|
||||||
## 测试方法
|
|
||||||
|
|
||||||
### 1. 内置测试页面
|
|
||||||
访问: http://localhost:19007/websocket-test.html
|
|
||||||
|
|
||||||
### 2. REST API测试
|
|
||||||
```bash
|
|
||||||
# 发送测试消息
|
|
||||||
curl -X POST "http://localhost:19007/websocket/send?userId=test-user&message=Hello"
|
|
||||||
|
|
||||||
# 查看在线用户
|
|
||||||
curl -X GET "http://localhost:19007/websocket/online-users"
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. JavaScript客户端测试
|
|
||||||
使用SockJS和STOMP.js连接WebSocket端点进行测试
|
|
||||||
|
|
||||||
## 集成说明
|
|
||||||
|
|
||||||
### 1. 与emotion-ai服务集成
|
|
||||||
- 通过Feign客户端调用AI服务
|
|
||||||
- 支持异步AI响应处理
|
|
||||||
- AI回复消息自动分割发送
|
|
||||||
|
|
||||||
### 2. 与前端集成
|
|
||||||
- 提供标准的WebSocket接口
|
|
||||||
- 支持SockJS降级处理
|
|
||||||
- 完整的消息格式定义
|
|
||||||
|
|
||||||
### 3. 与网关集成
|
|
||||||
- 通过emotion-gateway统一访问
|
|
||||||
- 支持负载均衡
|
|
||||||
- 统一的服务发现
|
|
||||||
|
|
||||||
## 监控和日志
|
|
||||||
|
|
||||||
### 健康检查
|
|
||||||
- http://localhost:19007/actuator/health
|
|
||||||
|
|
||||||
### 指标监控
|
|
||||||
- http://localhost:19007/actuator/metrics
|
|
||||||
- http://localhost:19007/actuator/prometheus
|
|
||||||
|
|
||||||
### 日志配置
|
|
||||||
- 日志文件: `logs/emotion-websocket.log`
|
|
||||||
- 支持DEBUG级别的WebSocket调试日志
|
|
||||||
|
|
||||||
## 后续扩展建议
|
|
||||||
|
|
||||||
1. **消息持久化**: 将聊天消息存储到数据库
|
|
||||||
2. **文件传输**: 支持图片、文件等多媒体消息
|
|
||||||
3. **群聊功能**: 支持多用户群组聊天
|
|
||||||
4. **消息加密**: 增加端到端消息加密
|
|
||||||
5. **消息撤回**: 支持消息撤回功能
|
|
||||||
6. **在线状态**: 更详细的用户在线状态管理
|
|
||||||
7. **消息推送**: 集成推送服务支持离线消息推送
|
|
||||||
|
|
||||||
## 总结
|
|
||||||
|
|
||||||
成功创建了功能完整的WebSocket聊天微服务模块,具备以下优势:
|
|
||||||
|
|
||||||
- ✅ 独立的微服务架构
|
|
||||||
- ✅ 完整的WebSocket实时通信功能
|
|
||||||
- ✅ 与AI服务的无缝集成
|
|
||||||
- ✅ 完善的会话管理机制
|
|
||||||
- ✅ 丰富的消息类型支持
|
|
||||||
- ✅ 良好的可扩展性和可维护性
|
|
||||||
- ✅ 完整的测试和文档支持
|
|
||||||
|
|
||||||
该模块可以直接用于生产环境,为用户提供流畅的实时聊天体验。
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
target/emotion-ai-1.0.0.jar中没有主清单属性
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
63083
|
|
||||||
@@ -1,102 +0,0 @@
|
|||||||
2025-07-16T09:03:31.799+08:00 WARN 20008 --- [ main] c.a.nacos.client.logging.NacosLogging : Load Logback Configuration of Nacos fail, message: Could not initialize Logback Nacos logging from classpath:nacos-logback.xml
|
|
||||||
2025-07-16T09:03:31.889+08:00 WARN 20008 --- [ main] c.a.nacos.client.logging.NacosLogging : Load Logback Configuration of Nacos fail, message: Could not initialize Logback Nacos logging from classpath:nacos-logback.xml
|
|
||||||
2025-07-16T09:03:32.194+08:00 WARN 20008 --- [ main] c.a.nacos.client.logging.NacosLogging : Load Logback Configuration of Nacos fail, message: Could not initialize Logback Nacos logging from classpath:nacos-logback.xml
|
|
||||||
2025-07-16T09:03:32.196+08:00 INFO 20008 --- [ main] c.alibaba.nacos.client.utils.ParamUtil : [settings] [req-serv] nacos-server port:8848
|
|
||||||
2025-07-16T09:03:32.196+08:00 INFO 20008 --- [ main] c.alibaba.nacos.client.utils.ParamUtil : [settings] [http-client] connect timeout:1000
|
|
||||||
2025-07-16T09:03:32.198+08:00 INFO 20008 --- [ main] c.alibaba.nacos.client.utils.ParamUtil : PER_TASK_CONFIG_SIZE: 3000.0
|
|
||||||
2025-07-16T09:03:32.254+08:00 INFO 20008 --- [ main] c.a.n.p.a.s.c.ClientAuthPluginManager : [ClientAuthPluginManager] Load ClientAuthService com.alibaba.nacos.client.auth.impl.NacosClientAuthServiceImpl success.
|
|
||||||
2025-07-16T09:03:32.254+08:00 INFO 20008 --- [ main] c.a.n.p.a.s.c.ClientAuthPluginManager : [ClientAuthPluginManager] Load ClientAuthService com.alibaba.nacos.client.auth.ram.RamClientAuthServiceImpl success.
|
|
||||||
2025-07-16T09:03:32.277+08:00 INFO 20008 --- [ main] c.a.n.c.a.r.identify.CredentialWatcher : null No credential found
|
|
||||||
2025-07-16 09:03:32 [main] WARN [com.alibaba.nacos.client.logging.NacosLogging] - Load Logback Configuration of Nacos fail, message: Could not initialize Logback Nacos logging from classpath:nacos-logback.xml
|
|
||||||
|
|
||||||
. ____ _ __ _ _
|
|
||||||
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
|
|
||||||
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
|
|
||||||
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
|
|
||||||
' |____| .__|_| |_|_| |_\__, | / / / /
|
|
||||||
=========|_|==============|___/=/_/_/_/
|
|
||||||
:: Spring Boot :: (v3.0.2)
|
|
||||||
|
|
||||||
2025-07-16 09:03:32 [main] INFO [c.a.n.client.config.impl.LocalConfigInfoProcessor] - LOCAL_SNAPSHOT_PATH:/Users/huazhongmin/nacos/config
|
|
||||||
2025-07-16 09:03:32 [main] INFO [com.alibaba.nacos.common.remote.client] - [RpcClientFactory] create a new rpc client of 90a9ad1e-7bf0-4f56-9d87-36c72dbc1d26_config-0
|
|
||||||
2025-07-16 09:03:32 [main] INFO [com.alibaba.nacos.common.remote.client] - [90a9ad1e-7bf0-4f56-9d87-36c72dbc1d26_config-0] Register server push request handler:com.alibaba.nacos.client.config.impl.ClientWorker$ConfigRpcTransportClient$$Lambda/0x00000001342dba98
|
|
||||||
2025-07-16 09:03:32 [main] INFO [com.alibaba.nacos.common.remote.client] - [90a9ad1e-7bf0-4f56-9d87-36c72dbc1d26_config-0] Register server push request handler:com.alibaba.nacos.client.config.impl.ClientWorker$ConfigRpcTransportClient$$Lambda/0x00000001342dbea8
|
|
||||||
2025-07-16 09:03:32 [main] INFO [com.alibaba.nacos.common.remote.client] - [90a9ad1e-7bf0-4f56-9d87-36c72dbc1d26_config-0] Registry connection listener to current client:com.alibaba.nacos.client.config.impl.ClientWorker$ConfigRpcTransportClient$1
|
|
||||||
2025-07-16 09:03:32 [main] INFO [com.alibaba.nacos.common.remote.client] - [90a9ad1e-7bf0-4f56-9d87-36c72dbc1d26_config-0] RpcClient init, ServerListFactory = com.alibaba.nacos.client.config.impl.ClientWorker$ConfigRpcTransportClient$2
|
|
||||||
2025-07-16 09:03:32 [main] INFO [com.alibaba.nacos.common.remote.client] - [90a9ad1e-7bf0-4f56-9d87-36c72dbc1d26_config-0] Try to connect to server on start up, server: {serverIp = '127.0.0.1', server main port = 8848}
|
|
||||||
2025-07-16 09:03:32 [main] INFO [c.a.nacos.common.remote.client.grpc.GrpcClient] - grpc client connection server:127.0.0.1 ip,serverPort:9848,grpcTslConfig:{"sslProvider":"OPENSSL","enableTls":false,"mutualAuthEnable":false,"trustAll":false}
|
|
||||||
2025-07-16 09:03:33 [main] INFO [com.alibaba.nacos.common.remote.client] - [90a9ad1e-7bf0-4f56-9d87-36c72dbc1d26_config-0] Success to connect to server [127.0.0.1:8848] on start up, connectionId = 1752627813368_127.0.0.1_62143
|
|
||||||
2025-07-16 09:03:33 [com.alibaba.nacos.client.remote.worker] INFO [com.alibaba.nacos.common.remote.client] - [90a9ad1e-7bf0-4f56-9d87-36c72dbc1d26_config-0] Notify connected event to listeners.
|
|
||||||
2025-07-16 09:03:33 [com.alibaba.nacos.client.remote.worker] INFO [com.alibaba.nacos.client.config.impl.ClientWorker] - [90a9ad1e-7bf0-4f56-9d87-36c72dbc1d26_config-0] Connected,notify listen context...
|
|
||||||
2025-07-16 09:03:33 [main] INFO [com.alibaba.nacos.common.remote.client] - [90a9ad1e-7bf0-4f56-9d87-36c72dbc1d26_config-0] Register server push request handler:com.alibaba.nacos.common.remote.client.RpcClient$ConnectResetRequestHandler
|
|
||||||
2025-07-16 09:03:33 [main] INFO [com.alibaba.nacos.common.remote.client] - [90a9ad1e-7bf0-4f56-9d87-36c72dbc1d26_config-0] Register server push request handler:com.alibaba.nacos.common.remote.client.RpcClient$$Lambda/0x000000013443efc8
|
|
||||||
2025-07-16 09:03:33 [main] INFO [com.alibaba.nacos.client.config.impl.Limiter] - limitTime:5.0
|
|
||||||
2025-07-16 09:03:33 [main] WARN [c.a.cloud.nacos.client.NacosPropertySourceBuilder] - Ignore the empty nacos configuration and get it based on dataId[emotion-ai] & group[DEFAULT_GROUP]
|
|
||||||
2025-07-16 09:03:33 [main] WARN [c.a.cloud.nacos.client.NacosPropertySourceBuilder] - Ignore the empty nacos configuration and get it based on dataId[emotion-ai.properties] & group[DEFAULT_GROUP]
|
|
||||||
2025-07-16 09:03:33 [main] WARN [c.a.cloud.nacos.client.NacosPropertySourceBuilder] - Ignore the empty nacos configuration and get it based on dataId[emotion-ai-local.properties] & group[DEFAULT_GROUP]
|
|
||||||
2025-07-16 09:03:33 [main] INFO [o.s.c.b.c.PropertySourceBootstrapConfiguration] - Located property source: [BootstrapPropertySource {name='bootstrapProperties-emotion-ai-local.properties,DEFAULT_GROUP'}, BootstrapPropertySource {name='bootstrapProperties-emotion-ai.properties,DEFAULT_GROUP'}, BootstrapPropertySource {name='bootstrapProperties-emotion-ai,DEFAULT_GROUP'}]
|
|
||||||
2025-07-16 09:03:33 [main] WARN [com.alibaba.nacos.client.logging.NacosLogging] - Load Logback Configuration of Nacos fail, message: Could not initialize Logback Nacos logging from classpath:nacos-logback.xml
|
|
||||||
2025-07-16 09:03:33 [main] INFO [com.emotionmuseum.ai.AiApplication] - The following 1 profile is active: "local"
|
|
||||||
2025-07-16 09:03:34 [main] INFO [o.s.d.r.config.RepositoryConfigurationDelegate] - Multiple Spring Data modules found, entering strict repository configuration mode
|
|
||||||
2025-07-16 09:03:34 [main] INFO [o.s.d.r.config.RepositoryConfigurationDelegate] - Bootstrapping Spring Data Redis repositories in DEFAULT mode.
|
|
||||||
2025-07-16 09:03:34 [main] INFO [o.s.d.r.config.RepositoryConfigurationDelegate] - Finished Spring Data repository scanning in 7 ms. Found 0 Redis repository interfaces.
|
|
||||||
2025-07-16 09:03:34 [main] INFO [o.springframework.cloud.context.scope.GenericScope] - BeanFactory id=75ad894c-a831-3fef-8183-57bf629af884
|
|
||||||
2025-07-16 09:03:35 [main] INFO [o.s.boot.web.embedded.tomcat.TomcatWebServer] - Tomcat initialized with port(s): 19002 (http)
|
|
||||||
2025-07-16 09:03:35 [main] INFO [org.apache.catalina.core.StandardService] - Starting service [Tomcat]
|
|
||||||
2025-07-16 09:03:35 [main] INFO [org.apache.catalina.core.StandardEngine] - Starting Servlet engine: [Apache Tomcat/10.1.5]
|
|
||||||
2025-07-16 09:03:35 [main] INFO [o.a.c.core.ContainerBase.[Tomcat].[localhost].[/]] - Initializing Spring embedded WebApplicationContext
|
|
||||||
2025-07-16 09:03:35 [main] INFO [o.s.b.w.s.c.ServletWebServerApplicationContext] - Root WebApplicationContext: initialization completed in 1976 ms
|
|
||||||
2025-07-16 09:03:35 [main] DEBUG [o.s.web.filter.ServerHttpObservationFilter] - Filter 'serverHttpObservationFilter' configured for use
|
|
||||||
2025-07-16 09:03:36 [main] INFO [com.emotionmuseum.common.config.SnowflakeConfig] - 使用MAC地址生成的机器ID: 669
|
|
||||||
2025-07-16 09:03:36 [main] INFO [com.emotionmuseum.common.config.SnowflakeConfig] - 雪花算法配置完成,使用机器ID: 669
|
|
||||||
2025-07-16 09:03:36 [main] INFO [c.emotionmuseum.common.util.SnowflakeIdGenerator] - 雪花算法ID生成器初始化完成,机器ID: 669
|
|
||||||
2025-07-16 09:03:36 [main] DEBUG [c.b.m.e.spring.MybatisSqlSessionFactoryBean] - Registered plugin: 'com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor@422b8438'
|
|
||||||
2025-07-16 09:03:36 [main] DEBUG [c.b.m.e.spring.MybatisSqlSessionFactoryBean] - Property 'mapperLocations' was not specified.
|
|
||||||
_ _ |_ _ _|_. ___ _ | _
|
|
||||||
| | |\/|_)(_| | |_\ |_)||_|_\
|
|
||||||
/ |
|
|
||||||
3.5.3.1
|
|
||||||
2025-07-16 09:03:37 [main] DEBUG [o.s.w.s.m.m.a.RequestMappingHandlerAdapter] - ControllerAdvice beans: 0 @ModelAttribute, 0 @InitBinder, 1 RequestBodyAdvice, 1 ResponseBodyAdvice
|
|
||||||
2025-07-16 09:03:37 [main] DEBUG [o.s.w.s.m.m.a.RequestMappingHandlerMapping] - 25 mappings in 'requestMappingHandlerMapping'
|
|
||||||
2025-07-16 09:03:37 [main] DEBUG [o.s.web.servlet.handler.SimpleUrlHandlerMapping] - Patterns [/webjars/**, /**] in 'resourceHandlerMapping'
|
|
||||||
2025-07-16 09:03:37 [main] DEBUG [o.s.w.s.m.m.a.ExceptionHandlerExceptionResolver] - ControllerAdvice beans: 0 @ExceptionHandler, 1 ResponseBodyAdvice
|
|
||||||
2025-07-16 09:03:38 [main] INFO [o.s.b.actuate.endpoint.web.EndpointLinksResolver] - Exposing 4 endpoint(s) beneath base path '/actuator'
|
|
||||||
2025-07-16 09:03:38 [main] INFO [o.s.boot.web.embedded.tomcat.TomcatWebServer] - Tomcat started on port(s): 19002 (http) with context path ''
|
|
||||||
2025-07-16 09:03:38 [main] INFO [com.emotionmuseum.ai.AiApplication] - Started AiApplication in 7.184 seconds (process running for 7.854)
|
|
||||||
2025-07-16 09:03:58 [http-nio-19002-exec-1] INFO [o.a.c.core.ContainerBase.[Tomcat].[localhost].[/]] - Initializing Spring DispatcherServlet 'dispatcherServlet'
|
|
||||||
2025-07-16 09:03:58 [http-nio-19002-exec-1] INFO [org.springframework.web.servlet.DispatcherServlet] - Initializing Servlet 'dispatcherServlet'
|
|
||||||
2025-07-16 09:03:58 [http-nio-19002-exec-1] DEBUG [org.springframework.web.servlet.DispatcherServlet] - Detected StandardServletMultipartResolver
|
|
||||||
2025-07-16 09:03:58 [http-nio-19002-exec-1] DEBUG [org.springframework.web.servlet.DispatcherServlet] - Detected AcceptHeaderLocaleResolver
|
|
||||||
2025-07-16 09:03:58 [http-nio-19002-exec-1] DEBUG [org.springframework.web.servlet.DispatcherServlet] - Detected FixedThemeResolver
|
|
||||||
2025-07-16 09:03:58 [http-nio-19002-exec-1] DEBUG [org.springframework.web.servlet.DispatcherServlet] - Detected org.springframework.web.servlet.view.DefaultRequestToViewNameTranslator@eb62a4b
|
|
||||||
2025-07-16 09:03:58 [http-nio-19002-exec-1] DEBUG [org.springframework.web.servlet.DispatcherServlet] - Detected org.springframework.web.servlet.support.SessionFlashMapManager@323186f9
|
|
||||||
2025-07-16 09:03:58 [http-nio-19002-exec-1] DEBUG [org.springframework.web.servlet.DispatcherServlet] - enableLoggingRequestDetails='false': request parameters and headers will be masked to prevent unsafe logging of potentially sensitive data
|
|
||||||
2025-07-16 09:03:58 [http-nio-19002-exec-1] INFO [org.springframework.web.servlet.DispatcherServlet] - Completed initialization in 3 ms
|
|
||||||
2025-07-16 09:03:58 [http-nio-19002-exec-1] DEBUG [org.springframework.web.servlet.DispatcherServlet] - GET "/actuator/health", parameters={}
|
|
||||||
2025-07-16 09:03:58 [http-nio-19002-exec-1] INFO [com.zaxxer.hikari.HikariDataSource] - HikariPool-1 - Starting...
|
|
||||||
2025-07-16 09:03:58 [http-nio-19002-exec-1] INFO [com.zaxxer.hikari.pool.HikariPool] - HikariPool-1 - Added connection com.mysql.cj.jdbc.ConnectionImpl@41120a95
|
|
||||||
2025-07-16 09:03:58 [http-nio-19002-exec-1] INFO [com.zaxxer.hikari.HikariDataSource] - HikariPool-1 - Start completed.
|
|
||||||
2025-07-16 09:03:59 [http-nio-19002-exec-1] DEBUG [o.s.w.s.m.m.annotation.HttpEntityMethodProcessor] - Using 'application/vnd.spring-boot.actuator.v3+json', given [*/*] and supported [application/vnd.spring-boot.actuator.v3+json, application/vnd.spring-boot.actuator.v2+json, application/json]
|
|
||||||
2025-07-16 09:03:59 [http-nio-19002-exec-1] DEBUG [o.s.w.s.m.m.annotation.HttpEntityMethodProcessor] - Writing [org.springframework.boot.actuate.health.SystemHealth@5be85732]
|
|
||||||
2025-07-16 09:03:59 [http-nio-19002-exec-1] DEBUG [org.springframework.web.servlet.DispatcherServlet] - Completed 200 OK
|
|
||||||
2025-07-16 09:04:12 [http-nio-19002-exec-2] DEBUG [org.springframework.web.servlet.DispatcherServlet] - GET "/actuator/health", parameters={}
|
|
||||||
2025-07-16 09:04:12 [http-nio-19002-exec-2] DEBUG [o.s.w.s.m.m.annotation.HttpEntityMethodProcessor] - Using 'application/vnd.spring-boot.actuator.v3+json', given [*/*] and supported [application/vnd.spring-boot.actuator.v3+json, application/vnd.spring-boot.actuator.v2+json, application/json]
|
|
||||||
2025-07-16 09:04:12 [http-nio-19002-exec-2] DEBUG [o.s.w.s.m.m.annotation.HttpEntityMethodProcessor] - Writing [org.springframework.boot.actuate.health.SystemHealth@2065d61c]
|
|
||||||
2025-07-16 09:04:12 [http-nio-19002-exec-2] DEBUG [org.springframework.web.servlet.DispatcherServlet] - Completed 200 OK
|
|
||||||
2025-07-16 09:04:31 [http-nio-19002-exec-4] DEBUG [org.springframework.web.servlet.DispatcherServlet] - GET "/ai/actuator/health", parameters={}
|
|
||||||
2025-07-16 09:04:31 [http-nio-19002-exec-4] DEBUG [o.s.web.servlet.handler.SimpleUrlHandlerMapping] - Mapped to ResourceHttpRequestHandler [classpath [META-INF/resources/], classpath [resources/], classpath [static/], classpath [public/], ServletContext [/]]
|
|
||||||
2025-07-16 09:04:32 [http-nio-19002-exec-4] DEBUG [c.e.common.interceptor.UserContextInterceptor] - 设置用户上下文: userId=guest_1901232820, requestUri=/ai/actuator/health
|
|
||||||
2025-07-16 09:04:32 [http-nio-19002-exec-4] DEBUG [o.s.w.servlet.resource.ResourceHttpRequestHandler] - Resource not found
|
|
||||||
2025-07-16 09:04:32 [http-nio-19002-exec-4] DEBUG [c.e.common.interceptor.UserContextInterceptor] - 清除用户上下文: requestUri=/ai/actuator/health
|
|
||||||
2025-07-16 09:04:32 [http-nio-19002-exec-4] DEBUG [org.springframework.web.servlet.DispatcherServlet] - Completed 404 NOT_FOUND
|
|
||||||
2025-07-16 09:04:32 [http-nio-19002-exec-4] DEBUG [org.springframework.web.servlet.DispatcherServlet] - "ERROR" dispatch for GET "/error", parameters={}
|
|
||||||
2025-07-16 09:04:32 [http-nio-19002-exec-4] DEBUG [o.s.w.s.m.m.a.RequestMappingHandlerMapping] - Mapped to org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController#error(HttpServletRequest)
|
|
||||||
2025-07-16 09:04:32 [http-nio-19002-exec-4] DEBUG [o.s.w.s.m.m.annotation.HttpEntityMethodProcessor] - Using 'application/json', given [*/*] and supported [application/json, application/*+json]
|
|
||||||
2025-07-16 09:04:32 [http-nio-19002-exec-4] DEBUG [o.s.w.s.m.m.annotation.HttpEntityMethodProcessor] - Writing [{timestamp=Wed Jul 16 09:04:32 CST 2025, status=404, error=Not Found, path=/ai/actuator/health}]
|
|
||||||
2025-07-16 09:04:32 [http-nio-19002-exec-4] DEBUG [org.springframework.web.servlet.DispatcherServlet] - Exiting from "ERROR" dispatch, status 404
|
|
||||||
2025-07-16 09:46:47 [Thread-1] WARN [c.alibaba.nacos.common.http.HttpClientBeanHolder] - [HttpClientBeanHolder] Start destroying common HttpClient
|
|
||||||
2025-07-16 09:46:47 [Thread-7] WARN [com.alibaba.nacos.common.notify.NotifyCenter] - [NotifyCenter] Start destroying Publisher
|
|
||||||
2025-07-16 09:46:47 [Thread-7] WARN [com.alibaba.nacos.common.notify.NotifyCenter] - [NotifyCenter] Destruction of the end
|
|
||||||
2025-07-16 09:46:47 [Thread-1] WARN [c.alibaba.nacos.common.http.HttpClientBeanHolder] - [HttpClientBeanHolder] Destruction of the end
|
|
||||||
2025-07-16 09:46:48 [SpringApplicationShutdownHook] INFO [com.zaxxer.hikari.HikariDataSource] - HikariPool-1 - Shutdown initiated...
|
|
||||||
2025-07-16 09:46:48 [SpringApplicationShutdownHook] INFO [com.zaxxer.hikari.HikariDataSource] - HikariPool-1 - Shutdown completed.
|
|
||||||
@@ -1,98 +0,0 @@
|
|||||||
[INFO] Scanning for projects...
|
|
||||||
[INFO]
|
|
||||||
[INFO] --------------------< com.emotionmuseum:emotion-ai >--------------------
|
|
||||||
[INFO] Building emotion-ai 1.0.0
|
|
||||||
[INFO] from pom.xml
|
|
||||||
[INFO] --------------------------------[ jar ]---------------------------------
|
|
||||||
[INFO]
|
|
||||||
[INFO] >>> spring-boot:3.0.2:run (default-cli) > test-compile @ emotion-ai >>>
|
|
||||||
[WARNING] The artifact mysql:mysql-connector-java:jar:8.0.33 has been relocated to com.mysql:mysql-connector-j:jar:8.0.33: MySQL Connector/J artifacts moved to reverse-DNS compliant Maven 2+ coordinates.
|
|
||||||
[INFO]
|
|
||||||
[INFO] --- resources:3.3.1:resources (default-resources) @ emotion-ai ---
|
|
||||||
[INFO] Copying 1 resource from src/main/resources to target/classes
|
|
||||||
[INFO]
|
|
||||||
[INFO] --- compiler:3.10.1:compile (default-compile) @ emotion-ai ---
|
|
||||||
[INFO] Nothing to compile - all classes are up to date
|
|
||||||
[INFO]
|
|
||||||
[INFO] --- resources:3.3.1:testResources (default-testResources) @ emotion-ai ---
|
|
||||||
[INFO] skip non existing resourceDirectory /Users/huazhongmin/peanut/AppleDevelop/EmotionMuseum/backend/emotion-ai/src/test/resources
|
|
||||||
[INFO]
|
|
||||||
[INFO] --- compiler:3.10.1:testCompile (default-testCompile) @ emotion-ai ---
|
|
||||||
[INFO] No sources to compile
|
|
||||||
[INFO]
|
|
||||||
[INFO] <<< spring-boot:3.0.2:run (default-cli) < test-compile @ emotion-ai <<<
|
|
||||||
[INFO]
|
|
||||||
[INFO]
|
|
||||||
[INFO] --- spring-boot:3.0.2:run (default-cli) @ emotion-ai ---
|
|
||||||
[INFO] Attaching agents: []
|
|
||||||
[2m2025-07-13T08:45:44.867+08:00[0;39m [33m WARN[0;39m [35m6918[0;39m [2m---[0;39m [2m[ restartedMain][0;39m [36mc.a.nacos.client.logging.NacosLogging [0;39m [2m:[0;39m Load Logback Configuration of Nacos fail, message: Could not initialize Logback Nacos logging from classpath:nacos-logback.xml
|
|
||||||
[2m2025-07-13T08:45:44.925+08:00[0;39m [33m WARN[0;39m [35m6918[0;39m [2m---[0;39m [2m[ restartedMain][0;39m [36mc.a.nacos.client.logging.NacosLogging [0;39m [2m:[0;39m Load Logback Configuration of Nacos fail, message: Could not initialize Logback Nacos logging from classpath:nacos-logback.xml
|
|
||||||
[2m2025-07-13T08:45:44.952+08:00[0;39m [32m INFO[0;39m [35m6918[0;39m [2m---[0;39m [2m[ restartedMain][0;39m [36m.e.DevToolsPropertyDefaultsPostProcessor[0;39m [2m:[0;39m Devtools property defaults active! Set 'spring.devtools.add-properties' to 'false' to disable
|
|
||||||
[2m2025-07-13T08:45:45.125+08:00[0;39m [33m WARN[0;39m [35m6918[0;39m [2m---[0;39m [2m[ restartedMain][0;39m [36mc.a.nacos.client.logging.NacosLogging [0;39m [2m:[0;39m Load Logback Configuration of Nacos fail, message: Could not initialize Logback Nacos logging from classpath:nacos-logback.xml
|
|
||||||
[2m2025-07-13T08:45:45.126+08:00[0;39m [32m INFO[0;39m [35m6918[0;39m [2m---[0;39m [2m[ restartedMain][0;39m [36mc.alibaba.nacos.client.utils.ParamUtil [0;39m [2m:[0;39m [settings] [req-serv] nacos-server port:8848
|
|
||||||
[2m2025-07-13T08:45:45.126+08:00[0;39m [32m INFO[0;39m [35m6918[0;39m [2m---[0;39m [2m[ restartedMain][0;39m [36mc.alibaba.nacos.client.utils.ParamUtil [0;39m [2m:[0;39m [settings] [http-client] connect timeout:1000
|
|
||||||
[2m2025-07-13T08:45:45.128+08:00[0;39m [32m INFO[0;39m [35m6918[0;39m [2m---[0;39m [2m[ restartedMain][0;39m [36mc.alibaba.nacos.client.utils.ParamUtil [0;39m [2m:[0;39m PER_TASK_CONFIG_SIZE: 3000.0
|
|
||||||
[2m2025-07-13T08:45:45.177+08:00[0;39m [32m INFO[0;39m [35m6918[0;39m [2m---[0;39m [2m[ restartedMain][0;39m [36mc.a.n.p.a.s.c.ClientAuthPluginManager [0;39m [2m:[0;39m [ClientAuthPluginManager] Load ClientAuthService com.alibaba.nacos.client.auth.impl.NacosClientAuthServiceImpl success.
|
|
||||||
[2m2025-07-13T08:45:45.177+08:00[0;39m [32m INFO[0;39m [35m6918[0;39m [2m---[0;39m [2m[ restartedMain][0;39m [36mc.a.n.p.a.s.c.ClientAuthPluginManager [0;39m [2m:[0;39m [ClientAuthPluginManager] Load ClientAuthService com.alibaba.nacos.client.auth.ram.RamClientAuthServiceImpl success.
|
|
||||||
[2m2025-07-13T08:45:45.196+08:00[0;39m [32m INFO[0;39m [35m6918[0;39m [2m---[0;39m [2m[ restartedMain][0;39m [36mc.a.n.c.a.r.identify.CredentialWatcher [0;39m [2m:[0;39m null No credential found
|
|
||||||
2025-07-13 08:45:45 [restartedMain] WARN [com.alibaba.nacos.client.logging.NacosLogging] - Load Logback Configuration of Nacos fail, message: Could not initialize Logback Nacos logging from classpath:nacos-logback.xml
|
|
||||||
|
|
||||||
. ____ _ __ _ _
|
|
||||||
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
|
|
||||||
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
|
|
||||||
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
|
|
||||||
' |____| .__|_| |_|_| |_\__, | / / / /
|
|
||||||
=========|_|==============|___/=/_/_/_/
|
|
||||||
[32m :: Spring Boot :: [39m [2m (v3.0.2)[0;39m
|
|
||||||
|
|
||||||
2025-07-13 08:45:45 [restartedMain] INFO [c.a.n.client.config.impl.LocalConfigInfoProcessor] - LOCAL_SNAPSHOT_PATH:/Users/huazhongmin/nacos/config
|
|
||||||
2025-07-13 08:45:45 [restartedMain] INFO [com.alibaba.nacos.common.remote.client] - [RpcClientFactory] create a new rpc client of 0ed45851-eb8a-42d7-b72e-31a045327a03_config-0
|
|
||||||
2025-07-13 08:45:45 [restartedMain] INFO [com.alibaba.nacos.common.remote.client] - [0ed45851-eb8a-42d7-b72e-31a045327a03_config-0] Register server push request handler:com.alibaba.nacos.client.config.impl.ClientWorker$ConfigRpcTransportClient$$Lambda/0x000000012531c670
|
|
||||||
2025-07-13 08:45:45 [restartedMain] INFO [com.alibaba.nacos.common.remote.client] - [0ed45851-eb8a-42d7-b72e-31a045327a03_config-0] Register server push request handler:com.alibaba.nacos.client.config.impl.ClientWorker$ConfigRpcTransportClient$$Lambda/0x000000012531caa0
|
|
||||||
2025-07-13 08:45:45 [restartedMain] INFO [com.alibaba.nacos.common.remote.client] - [0ed45851-eb8a-42d7-b72e-31a045327a03_config-0] Registry connection listener to current client:com.alibaba.nacos.client.config.impl.ClientWorker$ConfigRpcTransportClient$1
|
|
||||||
2025-07-13 08:45:45 [restartedMain] INFO [com.alibaba.nacos.common.remote.client] - [0ed45851-eb8a-42d7-b72e-31a045327a03_config-0] RpcClient init, ServerListFactory = com.alibaba.nacos.client.config.impl.ClientWorker$ConfigRpcTransportClient$2
|
|
||||||
2025-07-13 08:45:45 [restartedMain] INFO [com.alibaba.nacos.common.remote.client] - [0ed45851-eb8a-42d7-b72e-31a045327a03_config-0] Try to connect to server on start up, server: {serverIp = '127.0.0.1', server main port = 8848}
|
|
||||||
2025-07-13 08:45:45 [restartedMain] INFO [c.a.nacos.common.remote.client.grpc.GrpcClient] - grpc client connection server:127.0.0.1 ip,serverPort:9848,grpcTslConfig:{"sslProvider":"OPENSSL","enableTls":false,"mutualAuthEnable":false,"trustAll":false}
|
|
||||||
2025-07-13 08:45:45 [restartedMain] INFO [com.alibaba.nacos.common.remote.client] - [0ed45851-eb8a-42d7-b72e-31a045327a03_config-0] Success to connect to server [127.0.0.1:8848] on start up, connectionId = 1752367545782_127.0.0.1_51238
|
|
||||||
2025-07-13 08:45:45 [com.alibaba.nacos.client.remote.worker] INFO [com.alibaba.nacos.common.remote.client] - [0ed45851-eb8a-42d7-b72e-31a045327a03_config-0] Notify connected event to listeners.
|
|
||||||
2025-07-13 08:45:45 [com.alibaba.nacos.client.remote.worker] INFO [com.alibaba.nacos.client.config.impl.ClientWorker] - [0ed45851-eb8a-42d7-b72e-31a045327a03_config-0] Connected,notify listen context...
|
|
||||||
2025-07-13 08:45:45 [restartedMain] INFO [com.alibaba.nacos.common.remote.client] - [0ed45851-eb8a-42d7-b72e-31a045327a03_config-0] Register server push request handler:com.alibaba.nacos.common.remote.client.RpcClient$ConnectResetRequestHandler
|
|
||||||
2025-07-13 08:45:45 [restartedMain] INFO [com.alibaba.nacos.common.remote.client] - [0ed45851-eb8a-42d7-b72e-31a045327a03_config-0] Register server push request handler:com.alibaba.nacos.common.remote.client.RpcClient$$Lambda/0x00000001254a7778
|
|
||||||
2025-07-13 08:45:45 [restartedMain] INFO [com.alibaba.nacos.client.config.impl.Limiter] - limitTime:5.0
|
|
||||||
2025-07-13 08:45:46 [restartedMain] WARN [c.a.cloud.nacos.client.NacosPropertySourceBuilder] - Ignore the empty nacos configuration and get it based on dataId[emotion-ai] & group[DEFAULT_GROUP]
|
|
||||||
2025-07-13 08:45:46 [restartedMain] WARN [c.a.cloud.nacos.client.NacosPropertySourceBuilder] - Ignore the empty nacos configuration and get it based on dataId[emotion-ai.properties] & group[DEFAULT_GROUP]
|
|
||||||
2025-07-13 08:45:46 [restartedMain] WARN [c.a.cloud.nacos.client.NacosPropertySourceBuilder] - Ignore the empty nacos configuration and get it based on dataId[emotion-ai-dev.properties] & group[DEFAULT_GROUP]
|
|
||||||
2025-07-13 08:45:46 [restartedMain] INFO [o.s.c.b.c.PropertySourceBootstrapConfiguration] - Located property source: [BootstrapPropertySource {name='bootstrapProperties-emotion-ai-dev.properties,DEFAULT_GROUP'}, BootstrapPropertySource {name='bootstrapProperties-emotion-ai.properties,DEFAULT_GROUP'}, BootstrapPropertySource {name='bootstrapProperties-emotion-ai,DEFAULT_GROUP'}]
|
|
||||||
2025-07-13 08:45:46 [restartedMain] WARN [com.alibaba.nacos.client.logging.NacosLogging] - Load Logback Configuration of Nacos fail, message: Could not initialize Logback Nacos logging from classpath:nacos-logback.xml
|
|
||||||
2025-07-13 08:45:46 [restartedMain] INFO [com.emotionmuseum.ai.AiApplication] - The following 1 profile is active: "dev"
|
|
||||||
2025-07-13 08:45:46 [restartedMain] INFO [o.s.d.r.config.RepositoryConfigurationDelegate] - Multiple Spring Data modules found, entering strict repository configuration mode
|
|
||||||
2025-07-13 08:45:46 [restartedMain] INFO [o.s.d.r.config.RepositoryConfigurationDelegate] - Bootstrapping Spring Data Redis repositories in DEFAULT mode.
|
|
||||||
2025-07-13 08:45:46 [restartedMain] INFO [o.s.d.r.config.RepositoryConfigurationDelegate] - Finished Spring Data repository scanning in 6 ms. Found 0 Redis repository interfaces.
|
|
||||||
2025-07-13 08:45:46 [restartedMain] INFO [o.springframework.cloud.context.scope.GenericScope] - BeanFactory id=856a198a-0f81-3510-808c-6303647be993
|
|
||||||
2025-07-13 08:45:47 [restartedMain] INFO [o.s.boot.web.embedded.tomcat.TomcatWebServer] - Tomcat initialized with port(s): 9002 (http)
|
|
||||||
2025-07-13 08:45:47 [restartedMain] INFO [org.apache.catalina.core.StandardService] - Starting service [Tomcat]
|
|
||||||
2025-07-13 08:45:47 [restartedMain] INFO [org.apache.catalina.core.StandardEngine] - Starting Servlet engine: [Apache Tomcat/10.1.5]
|
|
||||||
2025-07-13 08:45:47 [restartedMain] INFO [o.a.c.core.ContainerBase.[Tomcat].[localhost].[/]] - Initializing Spring embedded WebApplicationContext
|
|
||||||
2025-07-13 08:45:47 [restartedMain] INFO [o.s.b.w.s.c.ServletWebServerApplicationContext] - Root WebApplicationContext: initialization completed in 1428 ms
|
|
||||||
2025-07-13 08:45:47 [restartedMain] DEBUG [c.b.m.e.spring.MybatisSqlSessionFactoryBean] - Registered plugin: 'com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor@35a5d991'
|
|
||||||
2025-07-13 08:45:47 [restartedMain] DEBUG [c.b.m.e.spring.MybatisSqlSessionFactoryBean] - Property 'mapperLocations' was not specified.
|
|
||||||
_ _ |_ _ _|_. ___ _ | _
|
|
||||||
| | |\/|_)(_| | |_\ |_)||_|_\
|
|
||||||
/ |
|
|
||||||
3.5.3.1
|
|
||||||
2025-07-13 08:45:49 [restartedMain] WARN [o.s.b.d.autoconfigure.OptionalLiveReloadServer] - Unable to start LiveReload server
|
|
||||||
2025-07-13 08:45:49 [restartedMain] INFO [o.s.b.actuate.endpoint.web.EndpointLinksResolver] - Exposing 4 endpoint(s) beneath base path '/actuator'
|
|
||||||
2025-07-13 08:45:49 [restartedMain] INFO [o.s.boot.web.embedded.tomcat.TomcatWebServer] - Tomcat started on port(s): 9002 (http) with context path ''
|
|
||||||
2025-07-13 08:45:49 [restartedMain] INFO [com.emotionmuseum.ai.AiApplication] - Started AiApplication in 4.676 seconds (process running for 5.147)
|
|
||||||
2025-07-13 08:45:49 [http-nio-9002-exec-1] INFO [o.a.c.core.ContainerBase.[Tomcat].[localhost].[/]] - Initializing Spring DispatcherServlet 'dispatcherServlet'
|
|
||||||
2025-07-13 08:45:49 [http-nio-9002-exec-1] INFO [org.springframework.web.servlet.DispatcherServlet] - Initializing Servlet 'dispatcherServlet'
|
|
||||||
2025-07-13 08:45:49 [http-nio-9002-exec-1] INFO [org.springframework.web.servlet.DispatcherServlet] - Completed initialization in 1 ms
|
|
||||||
2025-07-13 08:45:49 [http-nio-9002-exec-1] INFO [com.zaxxer.hikari.HikariDataSource] - EmotionAiHikariCP - Starting...
|
|
||||||
2025-07-13 08:45:49 [http-nio-9002-exec-1] INFO [com.zaxxer.hikari.pool.HikariPool] - EmotionAiHikariCP - Added connection com.mysql.cj.jdbc.ConnectionImpl@56486bc4
|
|
||||||
2025-07-13 08:45:49 [http-nio-9002-exec-1] INFO [com.zaxxer.hikari.HikariDataSource] - EmotionAiHikariCP - Start completed.
|
|
||||||
2025-07-13 08:49:24 [Thread-10] WARN [com.alibaba.nacos.common.notify.NotifyCenter] - [NotifyCenter] Start destroying Publisher
|
|
||||||
2025-07-13 08:49:24 [Thread-10] WARN [com.alibaba.nacos.common.notify.NotifyCenter] - [NotifyCenter] Destruction of the end
|
|
||||||
2025-07-13 08:49:24 [Thread-4] WARN [c.alibaba.nacos.common.http.HttpClientBeanHolder] - [HttpClientBeanHolder] Start destroying common HttpClient
|
|
||||||
2025-07-13 08:49:24 [Thread-4] WARN [c.alibaba.nacos.common.http.HttpClientBeanHolder] - [HttpClientBeanHolder] Destruction of the end
|
|
||||||
2025-07-13 08:49:24 [SpringApplicationShutdownHook] INFO [com.zaxxer.hikari.HikariDataSource] - EmotionAiHikariCP - Shutdown initiated...
|
|
||||||
2025-07-13 08:49:24 [SpringApplicationShutdownHook] INFO [com.zaxxer.hikari.HikariDataSource] - EmotionAiHikariCP - Shutdown completed.
|
|
||||||
@@ -1,120 +0,0 @@
|
|||||||
[INFO] Scanning for projects...
|
|
||||||
[INFO]
|
|
||||||
[INFO] -----------------< com.emotionmuseum:emotion-explore >------------------
|
|
||||||
[INFO] Building emotion-explore 1.0.0
|
|
||||||
[INFO] from pom.xml
|
|
||||||
[INFO] --------------------------------[ jar ]---------------------------------
|
|
||||||
[INFO]
|
|
||||||
[INFO] >>> spring-boot:3.0.2:run (default-cli) > test-compile @ emotion-explore >>>
|
|
||||||
[WARNING] The artifact mysql:mysql-connector-java:jar:8.0.33 has been relocated to com.mysql:mysql-connector-j:jar:8.0.33: MySQL Connector/J artifacts moved to reverse-DNS compliant Maven 2+ coordinates.
|
|
||||||
[INFO]
|
|
||||||
[INFO] --- resources:3.3.1:resources (default-resources) @ emotion-explore ---
|
|
||||||
[INFO] Copying 1 resource from src/main/resources to target/classes
|
|
||||||
[INFO]
|
|
||||||
[INFO] --- compiler:3.10.1:compile (default-compile) @ emotion-explore ---
|
|
||||||
[INFO] Nothing to compile - all classes are up to date
|
|
||||||
[INFO]
|
|
||||||
[INFO] --- resources:3.3.1:testResources (default-testResources) @ emotion-explore ---
|
|
||||||
[INFO] skip non existing resourceDirectory /Users/huazhongmin/peanut/AppleDevelop/EmotionMuseum/backend/emotion-explore/src/test/resources
|
|
||||||
[INFO]
|
|
||||||
[INFO] --- compiler:3.10.1:testCompile (default-testCompile) @ emotion-explore ---
|
|
||||||
[INFO] No sources to compile
|
|
||||||
[INFO]
|
|
||||||
[INFO] <<< spring-boot:3.0.2:run (default-cli) < test-compile @ emotion-explore <<<
|
|
||||||
[INFO]
|
|
||||||
[INFO]
|
|
||||||
[INFO] --- spring-boot:3.0.2:run (default-cli) @ emotion-explore ---
|
|
||||||
[INFO] Attaching agents: []
|
|
||||||
[2m2025-07-13T08:45:57.456+08:00[0;39m [33m WARN[0;39m [35m7147[0;39m [2m---[0;39m [2m[ restartedMain][0;39m [36mc.a.nacos.client.logging.NacosLogging [0;39m [2m:[0;39m Load Logback Configuration of Nacos fail, message: Could not initialize Logback Nacos logging from classpath:nacos-logback.xml
|
|
||||||
[2m2025-07-13T08:45:57.542+08:00[0;39m [33m WARN[0;39m [35m7147[0;39m [2m---[0;39m [2m[ restartedMain][0;39m [36mc.a.nacos.client.logging.NacosLogging [0;39m [2m:[0;39m Load Logback Configuration of Nacos fail, message: Could not initialize Logback Nacos logging from classpath:nacos-logback.xml
|
|
||||||
[2m2025-07-13T08:45:57.579+08:00[0;39m [32m INFO[0;39m [35m7147[0;39m [2m---[0;39m [2m[ restartedMain][0;39m [36m.e.DevToolsPropertyDefaultsPostProcessor[0;39m [2m:[0;39m Devtools property defaults active! Set 'spring.devtools.add-properties' to 'false' to disable
|
|
||||||
[2m2025-07-13T08:45:57.836+08:00[0;39m [33m WARN[0;39m [35m7147[0;39m [2m---[0;39m [2m[ restartedMain][0;39m [36mc.a.nacos.client.logging.NacosLogging [0;39m [2m:[0;39m Load Logback Configuration of Nacos fail, message: Could not initialize Logback Nacos logging from classpath:nacos-logback.xml
|
|
||||||
[2m2025-07-13T08:45:57.839+08:00[0;39m [32m INFO[0;39m [35m7147[0;39m [2m---[0;39m [2m[ restartedMain][0;39m [36mc.alibaba.nacos.client.utils.ParamUtil [0;39m [2m:[0;39m [settings] [req-serv] nacos-server port:8848
|
|
||||||
[2m2025-07-13T08:45:57.839+08:00[0;39m [32m INFO[0;39m [35m7147[0;39m [2m---[0;39m [2m[ restartedMain][0;39m [36mc.alibaba.nacos.client.utils.ParamUtil [0;39m [2m:[0;39m [settings] [http-client] connect timeout:1000
|
|
||||||
[2m2025-07-13T08:45:57.841+08:00[0;39m [32m INFO[0;39m [35m7147[0;39m [2m---[0;39m [2m[ restartedMain][0;39m [36mc.alibaba.nacos.client.utils.ParamUtil [0;39m [2m:[0;39m PER_TASK_CONFIG_SIZE: 3000.0
|
|
||||||
[2m2025-07-13T08:45:57.909+08:00[0;39m [32m INFO[0;39m [35m7147[0;39m [2m---[0;39m [2m[ restartedMain][0;39m [36mc.a.n.p.a.s.c.ClientAuthPluginManager [0;39m [2m:[0;39m [ClientAuthPluginManager] Load ClientAuthService com.alibaba.nacos.client.auth.impl.NacosClientAuthServiceImpl success.
|
|
||||||
[2m2025-07-13T08:45:57.909+08:00[0;39m [32m INFO[0;39m [35m7147[0;39m [2m---[0;39m [2m[ restartedMain][0;39m [36mc.a.n.p.a.s.c.ClientAuthPluginManager [0;39m [2m:[0;39m [ClientAuthPluginManager] Load ClientAuthService com.alibaba.nacos.client.auth.ram.RamClientAuthServiceImpl success.
|
|
||||||
[2m2025-07-13T08:45:57.936+08:00[0;39m [32m INFO[0;39m [35m7147[0;39m [2m---[0;39m [2m[ restartedMain][0;39m [36mc.a.n.c.a.r.identify.CredentialWatcher [0;39m [2m:[0;39m null No credential found
|
|
||||||
2025-07-13 08:45:58 [restartedMain] WARN [com.alibaba.nacos.client.logging.NacosLogging] - Load Logback Configuration of Nacos fail, message: Could not initialize Logback Nacos logging from classpath:nacos-logback.xml
|
|
||||||
|
|
||||||
. ____ _ __ _ _
|
|
||||||
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
|
|
||||||
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
|
|
||||||
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
|
|
||||||
' |____| .__|_| |_|_| |_\__, | / / / /
|
|
||||||
=========|_|==============|___/=/_/_/_/
|
|
||||||
[32m :: Spring Boot :: [39m [2m (v3.0.2)[0;39m
|
|
||||||
|
|
||||||
2025-07-13 08:45:58 [restartedMain] INFO [c.a.n.client.config.impl.LocalConfigInfoProcessor] - LOCAL_SNAPSHOT_PATH:/Users/huazhongmin/nacos/config
|
|
||||||
2025-07-13 08:45:58 [restartedMain] INFO [com.alibaba.nacos.common.remote.client] - [RpcClientFactory] create a new rpc client of 519c9471-ef87-4204-a56c-74ff37fe3e86_config-0
|
|
||||||
2025-07-13 08:45:58 [restartedMain] INFO [com.alibaba.nacos.common.remote.client] - [519c9471-ef87-4204-a56c-74ff37fe3e86_config-0] Register server push request handler:com.alibaba.nacos.client.config.impl.ClientWorker$ConfigRpcTransportClient$$Lambda/0x000000012b316a60
|
|
||||||
2025-07-13 08:45:58 [restartedMain] INFO [com.alibaba.nacos.common.remote.client] - [519c9471-ef87-4204-a56c-74ff37fe3e86_config-0] Register server push request handler:com.alibaba.nacos.client.config.impl.ClientWorker$ConfigRpcTransportClient$$Lambda/0x000000012b316e90
|
|
||||||
2025-07-13 08:45:58 [restartedMain] INFO [com.alibaba.nacos.common.remote.client] - [519c9471-ef87-4204-a56c-74ff37fe3e86_config-0] Registry connection listener to current client:com.alibaba.nacos.client.config.impl.ClientWorker$ConfigRpcTransportClient$1
|
|
||||||
2025-07-13 08:45:58 [restartedMain] INFO [com.alibaba.nacos.common.remote.client] - [519c9471-ef87-4204-a56c-74ff37fe3e86_config-0] RpcClient init, ServerListFactory = com.alibaba.nacos.client.config.impl.ClientWorker$ConfigRpcTransportClient$2
|
|
||||||
2025-07-13 08:45:58 [restartedMain] INFO [com.alibaba.nacos.common.remote.client] - [519c9471-ef87-4204-a56c-74ff37fe3e86_config-0] Try to connect to server on start up, server: {serverIp = '127.0.0.1', server main port = 8848}
|
|
||||||
2025-07-13 08:45:58 [restartedMain] INFO [c.a.nacos.common.remote.client.grpc.GrpcClient] - grpc client connection server:127.0.0.1 ip,serverPort:9848,grpcTslConfig:{"sslProvider":"OPENSSL","enableTls":false,"mutualAuthEnable":false,"trustAll":false}
|
|
||||||
2025-07-13 08:45:58 [restartedMain] INFO [com.alibaba.nacos.common.remote.client] - [519c9471-ef87-4204-a56c-74ff37fe3e86_config-0] Success to connect to server [127.0.0.1:8848] on start up, connectionId = 1752367558691_127.0.0.1_51338
|
|
||||||
2025-07-13 08:45:58 [com.alibaba.nacos.client.remote.worker] INFO [com.alibaba.nacos.common.remote.client] - [519c9471-ef87-4204-a56c-74ff37fe3e86_config-0] Notify connected event to listeners.
|
|
||||||
2025-07-13 08:45:58 [com.alibaba.nacos.client.remote.worker] INFO [com.alibaba.nacos.client.config.impl.ClientWorker] - [519c9471-ef87-4204-a56c-74ff37fe3e86_config-0] Connected,notify listen context...
|
|
||||||
2025-07-13 08:45:58 [restartedMain] INFO [com.alibaba.nacos.common.remote.client] - [519c9471-ef87-4204-a56c-74ff37fe3e86_config-0] Register server push request handler:com.alibaba.nacos.common.remote.client.RpcClient$ConnectResetRequestHandler
|
|
||||||
2025-07-13 08:45:58 [restartedMain] INFO [com.alibaba.nacos.common.remote.client] - [519c9471-ef87-4204-a56c-74ff37fe3e86_config-0] Register server push request handler:com.alibaba.nacos.common.remote.client.RpcClient$$Lambda/0x000000012b4a4d38
|
|
||||||
2025-07-13 08:45:58 [restartedMain] INFO [com.alibaba.nacos.client.config.impl.Limiter] - limitTime:5.0
|
|
||||||
2025-07-13 08:45:58 [restartedMain] WARN [c.a.cloud.nacos.client.NacosPropertySourceBuilder] - Ignore the empty nacos configuration and get it based on dataId[emotion-explore] & group[DEFAULT_GROUP]
|
|
||||||
2025-07-13 08:45:58 [restartedMain] WARN [c.a.cloud.nacos.client.NacosPropertySourceBuilder] - Ignore the empty nacos configuration and get it based on dataId[emotion-explore.properties] & group[DEFAULT_GROUP]
|
|
||||||
2025-07-13 08:45:58 [restartedMain] WARN [c.a.cloud.nacos.client.NacosPropertySourceBuilder] - Ignore the empty nacos configuration and get it based on dataId[emotion-explore-dev.properties] & group[DEFAULT_GROUP]
|
|
||||||
2025-07-13 08:45:58 [restartedMain] INFO [o.s.c.b.c.PropertySourceBootstrapConfiguration] - Located property source: [BootstrapPropertySource {name='bootstrapProperties-emotion-explore-dev.properties,DEFAULT_GROUP'}, BootstrapPropertySource {name='bootstrapProperties-emotion-explore.properties,DEFAULT_GROUP'}, BootstrapPropertySource {name='bootstrapProperties-emotion-explore,DEFAULT_GROUP'}]
|
|
||||||
2025-07-13 08:45:58 [restartedMain] WARN [com.alibaba.nacos.client.logging.NacosLogging] - Load Logback Configuration of Nacos fail, message: Could not initialize Logback Nacos logging from classpath:nacos-logback.xml
|
|
||||||
2025-07-13 08:45:58 [restartedMain] INFO [com.emotionmuseum.explore.ExploreApplication] - The following 1 profile is active: "dev"
|
|
||||||
2025-07-13 08:45:59 [restartedMain] INFO [o.s.d.r.config.RepositoryConfigurationDelegate] - Multiple Spring Data modules found, entering strict repository configuration mode
|
|
||||||
2025-07-13 08:45:59 [restartedMain] INFO [o.s.d.r.config.RepositoryConfigurationDelegate] - Bootstrapping Spring Data Redis repositories in DEFAULT mode.
|
|
||||||
2025-07-13 08:45:59 [restartedMain] INFO [o.s.d.r.config.RepositoryConfigurationDelegate] - Finished Spring Data repository scanning in 7 ms. Found 0 Redis repository interfaces.
|
|
||||||
2025-07-13 08:46:00 [restartedMain] WARN [org.mybatis.spring.mapper.ClassPathMapperScanner] - No MyBatis mapper was found in '[com.emotionmuseum.explore.mapper]' package. Please check your configuration.
|
|
||||||
2025-07-13 08:46:00 [restartedMain] INFO [o.springframework.cloud.context.scope.GenericScope] - BeanFactory id=e3b16e73-732b-3cd7-ba87-b0b05ca7eb72
|
|
||||||
2025-07-13 08:46:01 [restartedMain] INFO [o.s.boot.web.embedded.tomcat.TomcatWebServer] - Tomcat initialized with port(s): 9005 (http)
|
|
||||||
2025-07-13 08:46:01 [restartedMain] INFO [org.apache.catalina.core.StandardService] - Starting service [Tomcat]
|
|
||||||
2025-07-13 08:46:01 [restartedMain] INFO [org.apache.catalina.core.StandardEngine] - Starting Servlet engine: [Apache Tomcat/10.1.5]
|
|
||||||
2025-07-13 08:46:01 [restartedMain] INFO [o.a.c.core.ContainerBase.[Tomcat].[localhost].[/]] - Initializing Spring embedded WebApplicationContext
|
|
||||||
2025-07-13 08:46:01 [restartedMain] INFO [o.s.b.w.s.c.ServletWebServerApplicationContext] - Root WebApplicationContext: initialization completed in 2135 ms
|
|
||||||
2025-07-13 08:46:01 [restartedMain] DEBUG [c.b.m.e.spring.MybatisSqlSessionFactoryBean] - Property 'mapperLocations' was not specified.
|
|
||||||
_ _ |_ _ _|_. ___ _ | _
|
|
||||||
| | |\/|_)(_| | |_\ |_)||_|_\
|
|
||||||
/ |
|
|
||||||
3.5.3.1
|
|
||||||
2025-07-13 08:46:02 [restartedMain] WARN [o.s.b.d.autoconfigure.OptionalLiveReloadServer] - Unable to start LiveReload server
|
|
||||||
2025-07-13 08:46:02 [restartedMain] INFO [o.s.b.actuate.endpoint.web.EndpointLinksResolver] - Exposing 3 endpoint(s) beneath base path '/actuator'
|
|
||||||
2025-07-13 08:46:02 [restartedMain] WARN [o.s.b.w.s.c.AnnotationConfigServletWebServerApplicationContext] - Exception encountered during context initialization - cancelling refresh attempt: org.springframework.context.ApplicationContextException: Failed to start bean 'webServerStartStop'
|
|
||||||
2025-07-13 08:46:02 [restartedMain] INFO [com.alibaba.druid.pool.DruidDataSource] - {dataSource-0} closing ...
|
|
||||||
2025-07-13 08:46:02 [restartedMain] INFO [org.apache.catalina.core.StandardService] - Stopping service [Tomcat]
|
|
||||||
2025-07-13 08:46:02 [restartedMain] INFO [o.s.b.a.logging.ConditionEvaluationReportLogger] -
|
|
||||||
|
|
||||||
Error starting ApplicationContext. To display the condition evaluation report re-run your application with 'debug' enabled.
|
|
||||||
2025-07-13 08:46:02 [restartedMain] ERROR [o.s.b.diagnostics.LoggingFailureAnalysisReporter] -
|
|
||||||
|
|
||||||
***************************
|
|
||||||
APPLICATION FAILED TO START
|
|
||||||
***************************
|
|
||||||
|
|
||||||
Description:
|
|
||||||
|
|
||||||
Web server failed to start. Port 9005 was already in use.
|
|
||||||
|
|
||||||
Action:
|
|
||||||
|
|
||||||
Identify and stop the process that's listening on port 9005 or configure this application to listen on another port.
|
|
||||||
|
|
||||||
2025-07-13 08:46:02 [Thread-10] WARN [com.alibaba.nacos.common.notify.NotifyCenter] - [NotifyCenter] Start destroying Publisher
|
|
||||||
2025-07-13 08:46:02 [Thread-10] WARN [com.alibaba.nacos.common.notify.NotifyCenter] - [NotifyCenter] Destruction of the end
|
|
||||||
2025-07-13 08:46:02 [Thread-4] WARN [c.alibaba.nacos.common.http.HttpClientBeanHolder] - [HttpClientBeanHolder] Start destroying common HttpClient
|
|
||||||
[INFO] ------------------------------------------------------------------------
|
|
||||||
[INFO] BUILD FAILURE
|
|
||||||
[INFO] ------------------------------------------------------------------------
|
|
||||||
[INFO] Total time: 10.164 s
|
|
||||||
[INFO] Finished at: 2025-07-13T08:46:03+08:00
|
|
||||||
[INFO] ------------------------------------------------------------------------
|
|
||||||
[ERROR] Failed to execute goal org.springframework.boot:spring-boot-maven-plugin:3.0.2:run (default-cli) on project emotion-explore: Process terminated with exit code: 1 -> [Help 1]
|
|
||||||
[ERROR]
|
|
||||||
[ERROR] To see the full stack trace of the errors, re-run Maven with the -e switch.
|
|
||||||
[ERROR] Re-run Maven using the -X switch to enable full debug logging.
|
|
||||||
[ERROR]
|
|
||||||
[ERROR] For more information about the errors and possible solutions, please read the following articles:
|
|
||||||
[ERROR] [Help 1] http://cwiki.apache.org/confluence/display/MAVEN/MojoExecutionException
|
|
||||||
@@ -1,93 +0,0 @@
|
|||||||
2025-07-16 09:03:40 [main] WARN [com.alibaba.nacos.client.logging.NacosLogging] - Load Logback Configuration of Nacos fail, message: Could not initialize Logback Nacos logging from classpath:nacos-logback.xml
|
|
||||||
|
|
||||||
. ____ _ __ _ _
|
|
||||||
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
|
|
||||||
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
|
|
||||||
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
|
|
||||||
' |____| .__|_| |_|_| |_\__, | / / / /
|
|
||||||
=========|_|==============|___/=/_/_/_/
|
|
||||||
:: Spring Boot :: (v3.0.2)
|
|
||||||
|
|
||||||
2025-07-16 09:03:40 [main] WARN [com.alibaba.nacos.client.logging.NacosLogging] - Load Logback Configuration of Nacos fail, message: Could not initialize Logback Nacos logging from classpath:nacos-logback.xml
|
|
||||||
2025-07-16 09:03:40 [main] INFO [com.emotionmuseum.gateway.GatewayApplication] - Starting GatewayApplication using Java 21.0.7 with PID 20154 (/Users/huazhongmin/peanut/AppleDevelop/EmotionMuseum/backend/emotion-gateway/target/emotion-gateway-1.0.0.jar started by huazhongmin in /Users/huazhongmin/peanut/AppleDevelop/EmotionMuseum/backend/emotion-gateway)
|
|
||||||
2025-07-16 09:03:40 [main] DEBUG [com.emotionmuseum.gateway.GatewayApplication] - Running with Spring Boot v3.0.2, Spring v6.0.4
|
|
||||||
2025-07-16 09:03:40 [main] INFO [com.emotionmuseum.gateway.GatewayApplication] - The following 1 profile is active: "local"
|
|
||||||
2025-07-16 09:03:42 [main] INFO [o.s.d.r.config.RepositoryConfigurationDelegate] - Multiple Spring Data modules found, entering strict repository configuration mode
|
|
||||||
2025-07-16 09:03:42 [main] INFO [o.s.d.r.config.RepositoryConfigurationDelegate] - Bootstrapping Spring Data Redis repositories in DEFAULT mode.
|
|
||||||
2025-07-16 09:03:42 [main] INFO [o.s.d.r.config.RepositoryConfigurationDelegate] - Finished Spring Data repository scanning in 13 ms. Found 0 Redis repository interfaces.
|
|
||||||
2025-07-16 09:03:42 [main] INFO [o.springframework.cloud.context.scope.GenericScope] - BeanFactory id=4710d9c7-5e9d-353b-b960-5b878d180ffe
|
|
||||||
2025-07-16 09:03:42 [main] INFO [o.s.c.s.PostProcessorRegistrationDelegate$BeanPostProcessorChecker] - Bean 'org.springframework.cloud.client.loadbalancer.reactive.LoadBalancerBeanPostProcessorAutoConfiguration' of type [org.springframework.cloud.client.loadbalancer.reactive.LoadBalancerBeanPostProcessorAutoConfiguration] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)
|
|
||||||
2025-07-16 09:03:42 [main] INFO [o.s.c.s.PostProcessorRegistrationDelegate$BeanPostProcessorChecker] - Bean 'org.springframework.cloud.client.loadbalancer.reactive.LoadBalancerBeanPostProcessorAutoConfiguration$ReactorDeferringLoadBalancerFilterConfig' of type [org.springframework.cloud.client.loadbalancer.reactive.LoadBalancerBeanPostProcessorAutoConfiguration$ReactorDeferringLoadBalancerFilterConfig] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)
|
|
||||||
2025-07-16 09:03:42 [main] INFO [o.s.c.s.PostProcessorRegistrationDelegate$BeanPostProcessorChecker] - Bean 'reactorDeferringLoadBalancerExchangeFilterFunction' of type [org.springframework.cloud.client.loadbalancer.reactive.DeferringLoadBalancerExchangeFilterFunction] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)
|
|
||||||
2025-07-16 09:03:44 [main] DEBUG [o.s.cloud.gateway.config.GatewayProperties] - Routes supplied from Gateway Properties: [RouteDefinition{id='emotion-user-route', predicates=[PredicateDefinition{name='Path', args={_genkey_0=/user/**}}], filters=[FilterDefinition{name='StripPrefix', args={_genkey_0=0}}], uri=http://localhost:19001, order=0, metadata={}}, RouteDefinition{id='emotion-captcha-route', predicates=[PredicateDefinition{name='Path', args={_genkey_0=/captcha/**}}], filters=[FilterDefinition{name='StripPrefix', args={_genkey_0=0}}], uri=http://localhost:19001, order=0, metadata={}}, RouteDefinition{id='emotion-oauth-route', predicates=[PredicateDefinition{name='Path', args={_genkey_0=/oauth/**}}], filters=[FilterDefinition{name='StripPrefix', args={_genkey_0=0}}], uri=http://localhost:19001, order=0, metadata={}}, RouteDefinition{id='emotion-ai-route', predicates=[PredicateDefinition{name='Path', args={_genkey_0=/ai/**}}], filters=[FilterDefinition{name='StripPrefix', args={_genkey_0=0}}], uri=http://localhost:19002, order=0, metadata={}}]
|
|
||||||
2025-07-16 09:03:44 [main] INFO [o.s.c.gateway.route.RouteDefinitionRouteLocator] - Loaded RoutePredicateFactory [After]
|
|
||||||
2025-07-16 09:03:44 [main] INFO [o.s.c.gateway.route.RouteDefinitionRouteLocator] - Loaded RoutePredicateFactory [Before]
|
|
||||||
2025-07-16 09:03:44 [main] INFO [o.s.c.gateway.route.RouteDefinitionRouteLocator] - Loaded RoutePredicateFactory [Between]
|
|
||||||
2025-07-16 09:03:44 [main] INFO [o.s.c.gateway.route.RouteDefinitionRouteLocator] - Loaded RoutePredicateFactory [Cookie]
|
|
||||||
2025-07-16 09:03:44 [main] INFO [o.s.c.gateway.route.RouteDefinitionRouteLocator] - Loaded RoutePredicateFactory [Header]
|
|
||||||
2025-07-16 09:03:44 [main] INFO [o.s.c.gateway.route.RouteDefinitionRouteLocator] - Loaded RoutePredicateFactory [Host]
|
|
||||||
2025-07-16 09:03:44 [main] INFO [o.s.c.gateway.route.RouteDefinitionRouteLocator] - Loaded RoutePredicateFactory [Method]
|
|
||||||
2025-07-16 09:03:44 [main] INFO [o.s.c.gateway.route.RouteDefinitionRouteLocator] - Loaded RoutePredicateFactory [Path]
|
|
||||||
2025-07-16 09:03:44 [main] INFO [o.s.c.gateway.route.RouteDefinitionRouteLocator] - Loaded RoutePredicateFactory [Query]
|
|
||||||
2025-07-16 09:03:44 [main] INFO [o.s.c.gateway.route.RouteDefinitionRouteLocator] - Loaded RoutePredicateFactory [ReadBody]
|
|
||||||
2025-07-16 09:03:44 [main] INFO [o.s.c.gateway.route.RouteDefinitionRouteLocator] - Loaded RoutePredicateFactory [RemoteAddr]
|
|
||||||
2025-07-16 09:03:44 [main] INFO [o.s.c.gateway.route.RouteDefinitionRouteLocator] - Loaded RoutePredicateFactory [XForwardedRemoteAddr]
|
|
||||||
2025-07-16 09:03:44 [main] INFO [o.s.c.gateway.route.RouteDefinitionRouteLocator] - Loaded RoutePredicateFactory [Weight]
|
|
||||||
2025-07-16 09:03:44 [main] INFO [o.s.c.gateway.route.RouteDefinitionRouteLocator] - Loaded RoutePredicateFactory [CloudFoundryRouteService]
|
|
||||||
2025-07-16 09:03:44 [main] INFO [c.a.c.s.gateway.scg.SentinelSCGAutoConfiguration] - [Sentinel SpringCloudGateway] register SentinelGatewayFilter with order: -2147483648
|
|
||||||
2025-07-16 09:03:44 [main] DEBUG [o.s.web.reactive.handler.SimpleUrlHandlerMapping] - Patterns [/webjars/**, /**] in 'resourceHandlerMapping'
|
|
||||||
2025-07-16 09:03:44 [main] INFO [o.s.b.actuate.endpoint.web.EndpointLinksResolver] - Exposing 2 endpoint(s) beneath base path '/actuator'
|
|
||||||
2025-07-16 09:03:44 [main] DEBUG [o.s.w.r.r.m.annotation.ControllerMethodResolver] - ControllerAdvice beans: none
|
|
||||||
2025-07-16 09:03:44 [main] INFO [c.a.c.s.gateway.scg.SentinelSCGAutoConfiguration] - [Sentinel SpringCloudGateway] register SentinelGatewayBlockExceptionHandler
|
|
||||||
2025-07-16 09:03:44 [main] DEBUG [o.s.web.server.adapter.HttpWebHandlerAdapter] - enableLoggingRequestDetails='false': form data and headers will be masked to prevent unsafe logging of potentially sensitive data
|
|
||||||
2025-07-16 09:03:44 [main] WARN [o.s.c.l.c.LoadBalancerCacheAutoConfiguration$LoadBalancerCaffeineWarnLogger] - Spring Cloud LoadBalancer is currently working with the default cache. While this cache implementation is useful for development and tests, it's recommended to use Caffeine cache in production.You can switch to using Caffeine cache, by adding it and org.springframework.cache.caffeine.CaffeineCacheManager to the classpath.
|
|
||||||
2025-07-16 09:03:45 [main] INFO [o.s.boot.web.embedded.netty.NettyWebServer] - Netty started on port 19000
|
|
||||||
2025-07-16 09:03:45 [main] DEBUG [o.s.c.gateway.route.RouteDefinitionRouteLocator] - RouteDefinition emotion-user-route applying {_genkey_0=/user/**} to Path
|
|
||||||
2025-07-16 09:03:45 [main] DEBUG [o.s.c.gateway.route.RouteDefinitionRouteLocator] - RouteDefinition emotion-user-route applying filter {_genkey_0=0} to StripPrefix
|
|
||||||
2025-07-16 09:03:45 [main] DEBUG [o.s.c.gateway.route.RouteDefinitionRouteLocator] - RouteDefinition matched: emotion-user-route
|
|
||||||
2025-07-16 09:03:45 [main] DEBUG [o.s.c.gateway.route.RouteDefinitionRouteLocator] - RouteDefinition emotion-captcha-route applying {_genkey_0=/captcha/**} to Path
|
|
||||||
2025-07-16 09:03:45 [main] DEBUG [o.s.c.gateway.route.RouteDefinitionRouteLocator] - RouteDefinition emotion-captcha-route applying filter {_genkey_0=0} to StripPrefix
|
|
||||||
2025-07-16 09:03:45 [main] DEBUG [o.s.c.gateway.route.RouteDefinitionRouteLocator] - RouteDefinition matched: emotion-captcha-route
|
|
||||||
2025-07-16 09:03:45 [main] DEBUG [o.s.c.gateway.route.RouteDefinitionRouteLocator] - RouteDefinition emotion-oauth-route applying {_genkey_0=/oauth/**} to Path
|
|
||||||
2025-07-16 09:03:45 [main] DEBUG [o.s.c.gateway.route.RouteDefinitionRouteLocator] - RouteDefinition emotion-oauth-route applying filter {_genkey_0=0} to StripPrefix
|
|
||||||
2025-07-16 09:03:45 [main] DEBUG [o.s.c.gateway.route.RouteDefinitionRouteLocator] - RouteDefinition matched: emotion-oauth-route
|
|
||||||
2025-07-16 09:03:45 [main] DEBUG [o.s.c.gateway.route.RouteDefinitionRouteLocator] - RouteDefinition emotion-ai-route applying {_genkey_0=/ai/**} to Path
|
|
||||||
2025-07-16 09:03:45 [main] DEBUG [o.s.c.gateway.route.RouteDefinitionRouteLocator] - RouteDefinition emotion-ai-route applying filter {_genkey_0=0} to StripPrefix
|
|
||||||
2025-07-16 09:03:45 [main] DEBUG [o.s.c.gateway.route.RouteDefinitionRouteLocator] - RouteDefinition matched: emotion-ai-route
|
|
||||||
2025-07-16 09:03:45 [main] DEBUG [o.s.cloud.gateway.filter.GatewayMetricsFilter] - New routes count: 4
|
|
||||||
2025-07-16 09:03:45 [main] INFO [com.emotionmuseum.gateway.GatewayApplication] - Started GatewayApplication in 5.105 seconds (process running for 5.704)
|
|
||||||
2025-07-16 09:03:59 [reactor-http-nio-2] DEBUG [o.s.web.server.adapter.HttpWebHandlerAdapter] - [42be3651-1] HTTP GET "/actuator/health"
|
|
||||||
INFO: Sentinel log output type is: file
|
|
||||||
INFO: Sentinel log charset is: utf-8
|
|
||||||
INFO: Sentinel log base directory is: /Users/huazhongmin/logs/csp/
|
|
||||||
INFO: Sentinel log name use pid is: false
|
|
||||||
INFO: Sentinel log level is: INFO
|
|
||||||
2025-07-16 09:03:59 [lettuce-nioEventLoop-5-1] DEBUG [o.s.w.r.r.m.annotation.ResponseEntityResultHandler] - [42be3651-1] Using 'application/vnd.spring-boot.actuator.v3+json' given [*/*] and supported [application/vnd.spring-boot.actuator.v3+json, application/vnd.spring-boot.actuator.v2+json, application/json]
|
|
||||||
2025-07-16 09:03:59 [lettuce-nioEventLoop-5-1] DEBUG [o.s.w.r.r.m.annotation.ResponseEntityResultHandler] - [42be3651-1] 0..1 [org.springframework.boot.actuate.health.SystemHealth]
|
|
||||||
2025-07-16 09:03:59 [lettuce-nioEventLoop-5-1] DEBUG [org.springframework.web.HttpLogging] - [42be3651-1] Encoding [org.springframework.boot.actuate.health.SystemHealth@6fdce5a3]
|
|
||||||
2025-07-16 09:03:59 [reactor-http-nio-2] DEBUG [o.s.web.server.adapter.HttpWebHandlerAdapter] - [42be3651-1] Completed 200 OK
|
|
||||||
2025-07-16 09:04:12 [reactor-http-nio-3] DEBUG [o.s.web.server.adapter.HttpWebHandlerAdapter] - [127c1f50-2] HTTP GET "/actuator/health"
|
|
||||||
2025-07-16 09:04:12 [lettuce-nioEventLoop-5-1] DEBUG [o.s.w.r.r.m.annotation.ResponseEntityResultHandler] - [127c1f50-2] Using 'application/vnd.spring-boot.actuator.v3+json' given [*/*] and supported [application/vnd.spring-boot.actuator.v3+json, application/vnd.spring-boot.actuator.v2+json, application/json]
|
|
||||||
2025-07-16 09:04:12 [lettuce-nioEventLoop-5-1] DEBUG [o.s.w.r.r.m.annotation.ResponseEntityResultHandler] - [127c1f50-2] 0..1 [org.springframework.boot.actuate.health.SystemHealth]
|
|
||||||
2025-07-16 09:04:12 [lettuce-nioEventLoop-5-1] DEBUG [org.springframework.web.HttpLogging] - [127c1f50-2] Encoding [org.springframework.boot.actuate.health.SystemHealth@3fb8c67f]
|
|
||||||
2025-07-16 09:04:12 [reactor-http-nio-3] DEBUG [o.s.web.server.adapter.HttpWebHandlerAdapter] - [127c1f50-2] Completed 200 OK
|
|
||||||
2025-07-16 09:04:22 [reactor-http-nio-4] DEBUG [o.s.web.server.adapter.HttpWebHandlerAdapter] - [7b8bca8e-3] HTTP GET "/user/actuator/health"
|
|
||||||
2025-07-16 09:04:22 [reactor-http-nio-4] DEBUG [o.s.c.gateway.handler.RoutePredicateHandlerMapping] - Route matched: emotion-user-route
|
|
||||||
2025-07-16 09:04:22 [reactor-http-nio-4] DEBUG [o.s.c.gateway.handler.RoutePredicateHandlerMapping] - Mapping [Exchange: GET http://localhost:19000/user/actuator/health] to Route{id='emotion-user-route', uri=http://localhost:19001, order=0, predicate=Paths: [/user/**], match trailing slash: true, gatewayFilters=[[[StripPrefix parts = 0], order = 1]], metadata={}}
|
|
||||||
2025-07-16 09:04:22 [reactor-http-nio-4] DEBUG [o.s.c.gateway.handler.RoutePredicateHandlerMapping] - [7b8bca8e-3] Mapped to org.springframework.cloud.gateway.handler.FilteringWebHandler@373b5ee9
|
|
||||||
2025-07-16 09:04:22 [reactor-http-nio-4] DEBUG [o.s.cloud.gateway.handler.FilteringWebHandler] - Sorted gatewayFilterFactories: [[GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.RemoveCachedBodyFilter@451f87af}, order = -2147483648], [GatewayFilterAdapter{delegate=com.alibaba.csp.sentinel.adapter.gateway.sc.SentinelGatewayFilter@3051e0b2}, order = -2147483648], [GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.AdaptCachedBodyGlobalFilter@4dafba3e}, order = -2147482648], [GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.NettyWriteResponseFilter@287f7811}, order = -1], [GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.ForwardPathFilter@17271176}, order = 0], [GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.GatewayMetricsFilter@2e34384c}, order = 0], [[StripPrefix parts = 0], order = 1], [GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.RouteToRequestUrlFilter@2b556bb2}, order = 10000], [GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.ReactiveLoadBalancerClientFilter@57b75756}, order = 10150], [GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.LoadBalancerServiceInstanceCookieFilter@5327a06e}, order = 10151], [GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.WebsocketRoutingFilter@2e3cdec2}, order = 2147483646], [GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.NettyRoutingFilter@2679311f}, order = 2147483647], [GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.ForwardRoutingFilter@44cb460e}, order = 2147483647]]
|
|
||||||
2025-07-16 09:04:22 [reactor-http-nio-4] DEBUG [o.s.c.g.f.h.o.ObservedRequestHttpHeadersFilter] - Will instrument the HTTP request headers [Host:"localhost:19000", User-Agent:"curl/8.7.1", Accept:"*/*", Forwarded:"proto=http;host="localhost:19000";for="[0:0:0:0:0:0:0:1]:62291"", X-Forwarded-For:"0:0:0:0:0:0:0:1", X-Forwarded-Proto:"http", X-Forwarded-Port:"19000", X-Forwarded-Host:"localhost:19000"]
|
|
||||||
2025-07-16 09:04:22 [reactor-http-nio-4] DEBUG [o.s.c.g.f.h.o.ObservedRequestHttpHeadersFilter] - Client observation {name=http.client.requests(null), error=null, context=name='http.client.requests', contextualName='null', error='null', lowCardinalityKeyValues=[http.method='GET', http.status_code='UNKNOWN', spring.cloud.gateway.route.id='emotion-user-route', spring.cloud.gateway.route.uri='http://localhost:19001'], highCardinalityKeyValues=[http.uri='http://localhost:19000/user/actuator/health'], map=[class io.micrometer.core.instrument.Timer$Sample='io.micrometer.core.instrument.Timer$Sample@4443fec4', class io.micrometer.core.instrument.LongTaskTimer$Sample='SampleImpl{duration(seconds)=4.93244E-4, duration(nanos)=493244.0, startTimeNanos=302639138304957}'], parentObservation={name=http.server.requests(null), error=null, context=name='http.server.requests', contextualName='null', error='null', lowCardinalityKeyValues=[exception='none', method='GET', outcome='SUCCESS', status='200', uri='UNKNOWN'], highCardinalityKeyValues=[http.url='/user/actuator/health'], map=[class io.micrometer.core.instrument.Timer$Sample='io.micrometer.core.instrument.Timer$Sample@62ceb8d8', class io.micrometer.core.instrument.LongTaskTimer$Sample='SampleImpl{duration(seconds)=0.131310068, duration(nanos)=1.31310068E8, startTimeNanos=302639007609097}'], parentObservation=null}} created for the request. New headers are [Host:"localhost:19000", User-Agent:"curl/8.7.1", Accept:"*/*", Forwarded:"proto=http;host="localhost:19000";for="[0:0:0:0:0:0:0:1]:62291"", X-Forwarded-For:"0:0:0:0:0:0:0:1", X-Forwarded-Proto:"http", X-Forwarded-Port:"19000", X-Forwarded-Host:"localhost:19000"]
|
|
||||||
2025-07-16 09:04:22 [reactor-http-nio-4] DEBUG [o.s.c.g.f.h.o.ObservedResponseHttpHeadersFilter] - Will instrument the response
|
|
||||||
2025-07-16 09:04:22 [reactor-http-nio-4] DEBUG [o.s.c.g.f.h.o.ObservedResponseHttpHeadersFilter] - The response was handled for observation {name=http.client.requests(null), error=null, context=name='http.client.requests', contextualName='null', error='null', lowCardinalityKeyValues=[http.method='GET', http.status_code='UNKNOWN', spring.cloud.gateway.route.id='emotion-user-route', spring.cloud.gateway.route.uri='http://localhost:19001'], highCardinalityKeyValues=[http.uri='http://localhost:19000/user/actuator/health'], map=[class io.micrometer.core.instrument.Timer$Sample='io.micrometer.core.instrument.Timer$Sample@4443fec4', class io.micrometer.core.instrument.LongTaskTimer$Sample='SampleImpl{duration(seconds)=0.078344915, duration(nanos)=7.8344915E7, startTimeNanos=302639138304957}'], parentObservation={name=http.server.requests(null), error=null, context=name='http.server.requests', contextualName='null', error='null', lowCardinalityKeyValues=[exception='none', method='GET', outcome='SUCCESS', status='200', uri='UNKNOWN'], highCardinalityKeyValues=[http.url='/user/actuator/health'], map=[class io.micrometer.core.instrument.Timer$Sample='io.micrometer.core.instrument.Timer$Sample@62ceb8d8', class io.micrometer.core.instrument.LongTaskTimer$Sample='SampleImpl{duration(seconds)=0.209234105, duration(nanos)=2.09234105E8, startTimeNanos=302639007609097}'], parentObservation=null}}
|
|
||||||
2025-07-16 09:04:22 [reactor-http-nio-4] DEBUG [o.s.web.server.adapter.HttpWebHandlerAdapter] - [7b8bca8e-3] Completed 403 FORBIDDEN
|
|
||||||
2025-07-16 09:04:31 [reactor-http-nio-5] DEBUG [o.s.web.server.adapter.HttpWebHandlerAdapter] - [37305ece-4] HTTP GET "/ai/actuator/health"
|
|
||||||
2025-07-16 09:04:31 [reactor-http-nio-5] DEBUG [o.s.c.gateway.handler.RoutePredicateHandlerMapping] - Route matched: emotion-ai-route
|
|
||||||
2025-07-16 09:04:31 [reactor-http-nio-5] DEBUG [o.s.c.gateway.handler.RoutePredicateHandlerMapping] - Mapping [Exchange: GET http://localhost:19000/ai/actuator/health] to Route{id='emotion-ai-route', uri=http://localhost:19002, order=0, predicate=Paths: [/ai/**], match trailing slash: true, gatewayFilters=[[[StripPrefix parts = 0], order = 1]], metadata={}}
|
|
||||||
2025-07-16 09:04:31 [reactor-http-nio-5] DEBUG [o.s.c.gateway.handler.RoutePredicateHandlerMapping] - [37305ece-4] Mapped to org.springframework.cloud.gateway.handler.FilteringWebHandler@373b5ee9
|
|
||||||
2025-07-16 09:04:31 [reactor-http-nio-5] DEBUG [o.s.cloud.gateway.handler.FilteringWebHandler] - Sorted gatewayFilterFactories: [[GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.RemoveCachedBodyFilter@451f87af}, order = -2147483648], [GatewayFilterAdapter{delegate=com.alibaba.csp.sentinel.adapter.gateway.sc.SentinelGatewayFilter@3051e0b2}, order = -2147483648], [GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.AdaptCachedBodyGlobalFilter@4dafba3e}, order = -2147482648], [GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.NettyWriteResponseFilter@287f7811}, order = -1], [GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.ForwardPathFilter@17271176}, order = 0], [GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.GatewayMetricsFilter@2e34384c}, order = 0], [[StripPrefix parts = 0], order = 1], [GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.RouteToRequestUrlFilter@2b556bb2}, order = 10000], [GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.ReactiveLoadBalancerClientFilter@57b75756}, order = 10150], [GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.LoadBalancerServiceInstanceCookieFilter@5327a06e}, order = 10151], [GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.WebsocketRoutingFilter@2e3cdec2}, order = 2147483646], [GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.NettyRoutingFilter@2679311f}, order = 2147483647], [GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.ForwardRoutingFilter@44cb460e}, order = 2147483647]]
|
|
||||||
2025-07-16 09:04:31 [reactor-http-nio-5] DEBUG [o.s.c.g.f.h.o.ObservedRequestHttpHeadersFilter] - Will instrument the HTTP request headers [Host:"localhost:19000", User-Agent:"curl/8.7.1", Accept:"*/*", Forwarded:"proto=http;host="localhost:19000";for="[0:0:0:0:0:0:0:1]:62323"", X-Forwarded-For:"0:0:0:0:0:0:0:1", X-Forwarded-Proto:"http", X-Forwarded-Port:"19000", X-Forwarded-Host:"localhost:19000"]
|
|
||||||
2025-07-16 09:04:31 [reactor-http-nio-5] DEBUG [o.s.c.g.f.h.o.ObservedRequestHttpHeadersFilter] - Client observation {name=http.client.requests(null), error=null, context=name='http.client.requests', contextualName='null', error='null', lowCardinalityKeyValues=[http.method='GET', http.status_code='UNKNOWN', spring.cloud.gateway.route.id='emotion-ai-route', spring.cloud.gateway.route.uri='http://localhost:19002'], highCardinalityKeyValues=[http.uri='http://localhost:19000/ai/actuator/health'], map=[class io.micrometer.core.instrument.Timer$Sample='io.micrometer.core.instrument.Timer$Sample@14138b12', class io.micrometer.core.instrument.LongTaskTimer$Sample='SampleImpl{duration(seconds)=1.69279E-4, duration(nanos)=169279.0, startTimeNanos=302648957485505}'], parentObservation={name=http.server.requests(null), error=null, context=name='http.server.requests', contextualName='null', error='null', lowCardinalityKeyValues=[exception='none', method='GET', outcome='SUCCESS', status='200', uri='UNKNOWN'], highCardinalityKeyValues=[http.url='/ai/actuator/health'], map=[class io.micrometer.core.instrument.Timer$Sample='io.micrometer.core.instrument.Timer$Sample@3973cc2a', class io.micrometer.core.instrument.LongTaskTimer$Sample='SampleImpl{duration(seconds)=0.003046764, duration(nanos)=3046764.0, startTimeNanos=302648954707166}'], parentObservation=null}} created for the request. New headers are [Host:"localhost:19000", User-Agent:"curl/8.7.1", Accept:"*/*", Forwarded:"proto=http;host="localhost:19000";for="[0:0:0:0:0:0:0:1]:62323"", X-Forwarded-For:"0:0:0:0:0:0:0:1", X-Forwarded-Proto:"http", X-Forwarded-Port:"19000", X-Forwarded-Host:"localhost:19000"]
|
|
||||||
2025-07-16 09:04:32 [reactor-http-nio-5] DEBUG [o.s.c.g.f.h.o.ObservedResponseHttpHeadersFilter] - Will instrument the response
|
|
||||||
2025-07-16 09:04:32 [reactor-http-nio-5] DEBUG [o.s.c.g.f.h.o.ObservedResponseHttpHeadersFilter] - The response was handled for observation {name=http.client.requests(null), error=null, context=name='http.client.requests', contextualName='null', error='null', lowCardinalityKeyValues=[http.method='GET', http.status_code='UNKNOWN', spring.cloud.gateway.route.id='emotion-ai-route', spring.cloud.gateway.route.uri='http://localhost:19002'], highCardinalityKeyValues=[http.uri='http://localhost:19000/ai/actuator/health'], map=[class io.micrometer.core.instrument.Timer$Sample='io.micrometer.core.instrument.Timer$Sample@14138b12', class io.micrometer.core.instrument.LongTaskTimer$Sample='SampleImpl{duration(seconds)=0.078077624, duration(nanos)=7.8077624E7, startTimeNanos=302648957485505}'], parentObservation={name=http.server.requests(null), error=null, context=name='http.server.requests', contextualName='null', error='null', lowCardinalityKeyValues=[exception='none', method='GET', outcome='SUCCESS', status='200', uri='UNKNOWN'], highCardinalityKeyValues=[http.url='/ai/actuator/health'], map=[class io.micrometer.core.instrument.Timer$Sample='io.micrometer.core.instrument.Timer$Sample@3973cc2a', class io.micrometer.core.instrument.LongTaskTimer$Sample='SampleImpl{duration(seconds)=0.080973745, duration(nanos)=8.0973745E7, startTimeNanos=302648954707166}'], parentObservation=null}}
|
|
||||||
2025-07-16 09:04:32 [reactor-http-nio-5] DEBUG [o.s.web.server.adapter.HttpWebHandlerAdapter] - [37305ece-4] Completed 404 NOT_FOUND
|
|
||||||
@@ -1,98 +0,0 @@
|
|||||||
[INFO] Scanning for projects...
|
|
||||||
[INFO]
|
|
||||||
[INFO] -----------------< com.emotionmuseum:emotion-gateway >------------------
|
|
||||||
[INFO] Building emotion-gateway 1.0.0
|
|
||||||
[INFO] from pom.xml
|
|
||||||
[INFO] --------------------------------[ jar ]---------------------------------
|
|
||||||
[INFO]
|
|
||||||
[INFO] >>> spring-boot:3.0.2:run (default-cli) > test-compile @ emotion-gateway >>>
|
|
||||||
[INFO]
|
|
||||||
[INFO] --- resources:3.3.1:resources (default-resources) @ emotion-gateway ---
|
|
||||||
[INFO] Copying 1 resource from src/main/resources to target/classes
|
|
||||||
[INFO]
|
|
||||||
[INFO] --- compiler:3.10.1:compile (default-compile) @ emotion-gateway ---
|
|
||||||
[INFO] Nothing to compile - all classes are up to date
|
|
||||||
[INFO]
|
|
||||||
[INFO] --- resources:3.3.1:testResources (default-testResources) @ emotion-gateway ---
|
|
||||||
[INFO] skip non existing resourceDirectory /Users/huazhongmin/peanut/AppleDevelop/EmotionMuseum/backend/emotion-gateway/src/test/resources
|
|
||||||
[INFO]
|
|
||||||
[INFO] --- compiler:3.10.1:testCompile (default-testCompile) @ emotion-gateway ---
|
|
||||||
[INFO] No sources to compile
|
|
||||||
[INFO]
|
|
||||||
[INFO] <<< spring-boot:3.0.2:run (default-cli) < test-compile @ emotion-gateway <<<
|
|
||||||
[INFO]
|
|
||||||
[INFO]
|
|
||||||
[INFO] --- spring-boot:3.0.2:run (default-cli) @ emotion-gateway ---
|
|
||||||
[INFO] Attaching agents: []
|
|
||||||
2025-07-13 08:45:28 [restartedMain] WARN [com.alibaba.nacos.client.logging.NacosLogging] - Load Logback Configuration of Nacos fail, message: Could not initialize Logback Nacos logging from classpath:nacos-logback.xml
|
|
||||||
|
|
||||||
. ____ _ __ _ _
|
|
||||||
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
|
|
||||||
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
|
|
||||||
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
|
|
||||||
' |____| .__|_| |_|_| |_\__, | / / / /
|
|
||||||
=========|_|==============|___/=/_/_/_/
|
|
||||||
[32m :: Spring Boot :: [39m [2m (v3.0.2)[0;39m
|
|
||||||
|
|
||||||
2025-07-13 08:45:28 [restartedMain] WARN [com.alibaba.nacos.client.logging.NacosLogging] - Load Logback Configuration of Nacos fail, message: Could not initialize Logback Nacos logging from classpath:nacos-logback.xml
|
|
||||||
2025-07-13 08:45:28 [restartedMain] INFO [com.emotionmuseum.gateway.GatewayApplication] - Starting GatewayApplication using Java 23.0.2 with PID 6683 (/Users/huazhongmin/peanut/AppleDevelop/EmotionMuseum/backend/emotion-gateway/target/classes started by huazhongmin in /Users/huazhongmin/peanut/AppleDevelop/EmotionMuseum/backend/emotion-gateway)
|
|
||||||
2025-07-13 08:45:28 [restartedMain] DEBUG [com.emotionmuseum.gateway.GatewayApplication] - Running with Spring Boot v3.0.2, Spring v6.0.4
|
|
||||||
2025-07-13 08:45:28 [restartedMain] INFO [com.emotionmuseum.gateway.GatewayApplication] - The following 1 profile is active: "dev"
|
|
||||||
2025-07-13 08:45:28 [restartedMain] INFO [o.s.b.d.env.DevToolsPropertyDefaultsPostProcessor] - Devtools property defaults active! Set 'spring.devtools.add-properties' to 'false' to disable
|
|
||||||
2025-07-13 08:45:28 [restartedMain] INFO [o.s.b.d.env.DevToolsPropertyDefaultsPostProcessor] - For additional web related logging consider setting the 'logging.level.web' property to 'DEBUG'
|
|
||||||
2025-07-13 08:45:29 [restartedMain] INFO [o.s.d.r.config.RepositoryConfigurationDelegate] - Multiple Spring Data modules found, entering strict repository configuration mode
|
|
||||||
2025-07-13 08:45:29 [restartedMain] INFO [o.s.d.r.config.RepositoryConfigurationDelegate] - Bootstrapping Spring Data Redis repositories in DEFAULT mode.
|
|
||||||
2025-07-13 08:45:29 [restartedMain] INFO [o.s.d.r.config.RepositoryConfigurationDelegate] - Finished Spring Data repository scanning in 9 ms. Found 0 Redis repository interfaces.
|
|
||||||
2025-07-13 08:45:29 [restartedMain] INFO [o.springframework.cloud.context.scope.GenericScope] - BeanFactory id=52e78852-b349-32d3-9f4b-547073a85453
|
|
||||||
2025-07-13 08:45:29 [restartedMain] INFO [o.s.c.s.PostProcessorRegistrationDelegate$BeanPostProcessorChecker] - Bean 'org.springframework.cloud.client.loadbalancer.reactive.LoadBalancerBeanPostProcessorAutoConfiguration' of type [org.springframework.cloud.client.loadbalancer.reactive.LoadBalancerBeanPostProcessorAutoConfiguration] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)
|
|
||||||
2025-07-13 08:45:29 [restartedMain] INFO [o.s.c.s.PostProcessorRegistrationDelegate$BeanPostProcessorChecker] - Bean 'org.springframework.cloud.client.loadbalancer.reactive.LoadBalancerBeanPostProcessorAutoConfiguration$ReactorDeferringLoadBalancerFilterConfig' of type [org.springframework.cloud.client.loadbalancer.reactive.LoadBalancerBeanPostProcessorAutoConfiguration$ReactorDeferringLoadBalancerFilterConfig] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)
|
|
||||||
2025-07-13 08:45:29 [restartedMain] INFO [o.s.c.s.PostProcessorRegistrationDelegate$BeanPostProcessorChecker] - Bean 'reactorDeferringLoadBalancerExchangeFilterFunction' of type [org.springframework.cloud.client.loadbalancer.reactive.DeferringLoadBalancerExchangeFilterFunction] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)
|
|
||||||
2025-07-13 08:45:30 [restartedMain] DEBUG [o.s.cloud.gateway.config.GatewayProperties] - Routes supplied from Gateway Properties: [RouteDefinition{id='emotion-user', predicates=[PredicateDefinition{name='Path', args={_genkey_0=/api/user/**}}], filters=[FilterDefinition{name='StripPrefix', args={_genkey_0=2}}], uri=lb://emotion-user, order=0, metadata={}}, RouteDefinition{id='emotion-ai', predicates=[PredicateDefinition{name='Path', args={_genkey_0=/api/ai/**}}], filters=[FilterDefinition{name='StripPrefix', args={_genkey_0=2}}], uri=lb://emotion-ai, order=0, metadata={}}, RouteDefinition{id='emotion-record', predicates=[PredicateDefinition{name='Path', args={_genkey_0=/api/record/**}}], filters=[FilterDefinition{name='StripPrefix', args={_genkey_0=2}}], uri=lb://emotion-record, order=0, metadata={}}, RouteDefinition{id='emotion-growth', predicates=[PredicateDefinition{name='Path', args={_genkey_0=/api/growth/**}}], filters=[FilterDefinition{name='StripPrefix', args={_genkey_0=2}}], uri=lb://emotion-growth, order=0, metadata={}}, RouteDefinition{id='emotion-explore', predicates=[PredicateDefinition{name='Path', args={_genkey_0=/api/explore/**}}], filters=[FilterDefinition{name='StripPrefix', args={_genkey_0=2}}], uri=lb://emotion-explore, order=0, metadata={}}, RouteDefinition{id='emotion-reward', predicates=[PredicateDefinition{name='Path', args={_genkey_0=/api/reward/**}}], filters=[FilterDefinition{name='StripPrefix', args={_genkey_0=2}}], uri=lb://emotion-reward, order=0, metadata={}}, RouteDefinition{id='emotion-stats', predicates=[PredicateDefinition{name='Path', args={_genkey_0=/api/stats/**}}], filters=[FilterDefinition{name='StripPrefix', args={_genkey_0=2}}], uri=lb://emotion-stats, order=0, metadata={}}]
|
|
||||||
2025-07-13 08:45:30 [restartedMain] INFO [o.s.c.gateway.route.RouteDefinitionRouteLocator] - Loaded RoutePredicateFactory [After]
|
|
||||||
2025-07-13 08:45:30 [restartedMain] INFO [o.s.c.gateway.route.RouteDefinitionRouteLocator] - Loaded RoutePredicateFactory [Before]
|
|
||||||
2025-07-13 08:45:30 [restartedMain] INFO [o.s.c.gateway.route.RouteDefinitionRouteLocator] - Loaded RoutePredicateFactory [Between]
|
|
||||||
2025-07-13 08:45:30 [restartedMain] INFO [o.s.c.gateway.route.RouteDefinitionRouteLocator] - Loaded RoutePredicateFactory [Cookie]
|
|
||||||
2025-07-13 08:45:30 [restartedMain] INFO [o.s.c.gateway.route.RouteDefinitionRouteLocator] - Loaded RoutePredicateFactory [Header]
|
|
||||||
2025-07-13 08:45:30 [restartedMain] INFO [o.s.c.gateway.route.RouteDefinitionRouteLocator] - Loaded RoutePredicateFactory [Host]
|
|
||||||
2025-07-13 08:45:30 [restartedMain] INFO [o.s.c.gateway.route.RouteDefinitionRouteLocator] - Loaded RoutePredicateFactory [Method]
|
|
||||||
2025-07-13 08:45:30 [restartedMain] INFO [o.s.c.gateway.route.RouteDefinitionRouteLocator] - Loaded RoutePredicateFactory [Path]
|
|
||||||
2025-07-13 08:45:30 [restartedMain] INFO [o.s.c.gateway.route.RouteDefinitionRouteLocator] - Loaded RoutePredicateFactory [Query]
|
|
||||||
2025-07-13 08:45:30 [restartedMain] INFO [o.s.c.gateway.route.RouteDefinitionRouteLocator] - Loaded RoutePredicateFactory [ReadBody]
|
|
||||||
2025-07-13 08:45:30 [restartedMain] INFO [o.s.c.gateway.route.RouteDefinitionRouteLocator] - Loaded RoutePredicateFactory [RemoteAddr]
|
|
||||||
2025-07-13 08:45:30 [restartedMain] INFO [o.s.c.gateway.route.RouteDefinitionRouteLocator] - Loaded RoutePredicateFactory [XForwardedRemoteAddr]
|
|
||||||
2025-07-13 08:45:30 [restartedMain] INFO [o.s.c.gateway.route.RouteDefinitionRouteLocator] - Loaded RoutePredicateFactory [Weight]
|
|
||||||
2025-07-13 08:45:30 [restartedMain] INFO [o.s.c.gateway.route.RouteDefinitionRouteLocator] - Loaded RoutePredicateFactory [CloudFoundryRouteService]
|
|
||||||
2025-07-13 08:45:30 [restartedMain] INFO [c.a.c.s.gateway.scg.SentinelSCGAutoConfiguration] - [Sentinel SpringCloudGateway] register SentinelGatewayFilter with order: -2147483648
|
|
||||||
2025-07-13 08:45:31 [restartedMain] INFO [o.s.b.actuate.endpoint.web.EndpointLinksResolver] - Exposing 2 endpoint(s) beneath base path '/actuator'
|
|
||||||
2025-07-13 08:45:31 [restartedMain] INFO [c.a.c.s.gateway.scg.SentinelSCGAutoConfiguration] - [Sentinel SpringCloudGateway] register SentinelGatewayBlockExceptionHandler
|
|
||||||
2025-07-13 08:45:31 [restartedMain] WARN [o.s.b.d.autoconfigure.OptionalLiveReloadServer] - Unable to start LiveReload server
|
|
||||||
2025-07-13 08:45:31 [restartedMain] WARN [o.s.c.l.c.LoadBalancerCacheAutoConfiguration$LoadBalancerCaffeineWarnLogger] - Spring Cloud LoadBalancer is currently working with the default cache. While this cache implementation is useful for development and tests, it's recommended to use Caffeine cache in production.You can switch to using Caffeine cache, by adding it and org.springframework.cache.caffeine.CaffeineCacheManager to the classpath.
|
|
||||||
2025-07-13 08:45:31 [restartedMain] INFO [o.s.boot.web.embedded.netty.NettyWebServer] - Netty started on port 9000
|
|
||||||
2025-07-13 08:45:31 [restartedMain] DEBUG [o.s.c.gateway.route.RouteDefinitionRouteLocator] - RouteDefinition emotion-user applying {_genkey_0=/api/user/**} to Path
|
|
||||||
2025-07-13 08:45:31 [restartedMain] DEBUG [o.s.c.gateway.route.RouteDefinitionRouteLocator] - RouteDefinition emotion-user applying filter {_genkey_0=2} to StripPrefix
|
|
||||||
2025-07-13 08:45:31 [restartedMain] DEBUG [o.s.c.gateway.route.RouteDefinitionRouteLocator] - RouteDefinition matched: emotion-user
|
|
||||||
2025-07-13 08:45:31 [restartedMain] DEBUG [o.s.c.gateway.route.RouteDefinitionRouteLocator] - RouteDefinition emotion-ai applying {_genkey_0=/api/ai/**} to Path
|
|
||||||
2025-07-13 08:45:31 [restartedMain] DEBUG [o.s.c.gateway.route.RouteDefinitionRouteLocator] - RouteDefinition emotion-ai applying filter {_genkey_0=2} to StripPrefix
|
|
||||||
2025-07-13 08:45:31 [restartedMain] DEBUG [o.s.c.gateway.route.RouteDefinitionRouteLocator] - RouteDefinition matched: emotion-ai
|
|
||||||
2025-07-13 08:45:31 [restartedMain] DEBUG [o.s.c.gateway.route.RouteDefinitionRouteLocator] - RouteDefinition emotion-record applying {_genkey_0=/api/record/**} to Path
|
|
||||||
2025-07-13 08:45:31 [restartedMain] DEBUG [o.s.c.gateway.route.RouteDefinitionRouteLocator] - RouteDefinition emotion-record applying filter {_genkey_0=2} to StripPrefix
|
|
||||||
2025-07-13 08:45:31 [restartedMain] DEBUG [o.s.c.gateway.route.RouteDefinitionRouteLocator] - RouteDefinition matched: emotion-record
|
|
||||||
2025-07-13 08:45:31 [restartedMain] DEBUG [o.s.c.gateway.route.RouteDefinitionRouteLocator] - RouteDefinition emotion-growth applying {_genkey_0=/api/growth/**} to Path
|
|
||||||
2025-07-13 08:45:31 [restartedMain] DEBUG [o.s.c.gateway.route.RouteDefinitionRouteLocator] - RouteDefinition emotion-growth applying filter {_genkey_0=2} to StripPrefix
|
|
||||||
2025-07-13 08:45:31 [restartedMain] DEBUG [o.s.c.gateway.route.RouteDefinitionRouteLocator] - RouteDefinition matched: emotion-growth
|
|
||||||
2025-07-13 08:45:31 [restartedMain] DEBUG [o.s.c.gateway.route.RouteDefinitionRouteLocator] - RouteDefinition emotion-explore applying {_genkey_0=/api/explore/**} to Path
|
|
||||||
2025-07-13 08:45:31 [restartedMain] DEBUG [o.s.c.gateway.route.RouteDefinitionRouteLocator] - RouteDefinition emotion-explore applying filter {_genkey_0=2} to StripPrefix
|
|
||||||
2025-07-13 08:45:31 [restartedMain] DEBUG [o.s.c.gateway.route.RouteDefinitionRouteLocator] - RouteDefinition matched: emotion-explore
|
|
||||||
2025-07-13 08:45:31 [restartedMain] DEBUG [o.s.c.gateway.route.RouteDefinitionRouteLocator] - RouteDefinition emotion-reward applying {_genkey_0=/api/reward/**} to Path
|
|
||||||
2025-07-13 08:45:31 [restartedMain] DEBUG [o.s.c.gateway.route.RouteDefinitionRouteLocator] - RouteDefinition emotion-reward applying filter {_genkey_0=2} to StripPrefix
|
|
||||||
2025-07-13 08:45:31 [restartedMain] DEBUG [o.s.c.gateway.route.RouteDefinitionRouteLocator] - RouteDefinition matched: emotion-reward
|
|
||||||
2025-07-13 08:45:31 [restartedMain] DEBUG [o.s.c.gateway.route.RouteDefinitionRouteLocator] - RouteDefinition emotion-stats applying {_genkey_0=/api/stats/**} to Path
|
|
||||||
2025-07-13 08:45:31 [restartedMain] DEBUG [o.s.c.gateway.route.RouteDefinitionRouteLocator] - RouteDefinition emotion-stats applying filter {_genkey_0=2} to StripPrefix
|
|
||||||
2025-07-13 08:45:31 [restartedMain] DEBUG [o.s.c.gateway.route.RouteDefinitionRouteLocator] - RouteDefinition matched: emotion-stats
|
|
||||||
2025-07-13 08:45:31 [restartedMain] DEBUG [o.s.cloud.gateway.filter.GatewayMetricsFilter] - New routes count: 7
|
|
||||||
2025-07-13 08:45:31 [restartedMain] INFO [com.emotionmuseum.gateway.GatewayApplication] - Started GatewayApplication in 3.673 seconds (process running for 4.132)
|
|
||||||
INFO: Sentinel log output type is: file
|
|
||||||
INFO: Sentinel log charset is: utf-8
|
|
||||||
INFO: Sentinel log base directory is: /Users/huazhongmin/logs/csp/
|
|
||||||
INFO: Sentinel log name use pid is: false
|
|
||||||
INFO: Sentinel log level is: INFO
|
|
||||||
@@ -1,121 +0,0 @@
|
|||||||
[INFO] Scanning for projects...
|
|
||||||
[INFO]
|
|
||||||
[INFO] ------------------< com.emotionmuseum:emotion-growth >------------------
|
|
||||||
[INFO] Building emotion-growth 1.0.0
|
|
||||||
[INFO] from pom.xml
|
|
||||||
[INFO] --------------------------------[ jar ]---------------------------------
|
|
||||||
[INFO]
|
|
||||||
[INFO] >>> spring-boot:3.0.2:run (default-cli) > test-compile @ emotion-growth >>>
|
|
||||||
[WARNING] The artifact mysql:mysql-connector-java:jar:8.0.33 has been relocated to com.mysql:mysql-connector-j:jar:8.0.33: MySQL Connector/J artifacts moved to reverse-DNS compliant Maven 2+ coordinates.
|
|
||||||
[INFO]
|
|
||||||
[INFO] --- resources:3.3.1:resources (default-resources) @ emotion-growth ---
|
|
||||||
[INFO] Copying 1 resource from src/main/resources to target/classes
|
|
||||||
[INFO]
|
|
||||||
[INFO] --- compiler:3.10.1:compile (default-compile) @ emotion-growth ---
|
|
||||||
[INFO] Nothing to compile - all classes are up to date
|
|
||||||
[INFO]
|
|
||||||
[INFO] --- resources:3.3.1:testResources (default-testResources) @ emotion-growth ---
|
|
||||||
[INFO] skip non existing resourceDirectory /Users/huazhongmin/peanut/AppleDevelop/EmotionMuseum/backend/emotion-growth/src/test/resources
|
|
||||||
[INFO]
|
|
||||||
[INFO] --- compiler:3.10.1:testCompile (default-testCompile) @ emotion-growth ---
|
|
||||||
[INFO] No sources to compile
|
|
||||||
[INFO]
|
|
||||||
[INFO] <<< spring-boot:3.0.2:run (default-cli) < test-compile @ emotion-growth <<<
|
|
||||||
[INFO]
|
|
||||||
[INFO]
|
|
||||||
[INFO] --- spring-boot:3.0.2:run (default-cli) @ emotion-growth ---
|
|
||||||
[INFO] Attaching agents: []
|
|
||||||
[2m2025-07-13T08:45:56.958+08:00[0;39m [33m WARN[0;39m [35m7144[0;39m [2m---[0;39m [2m[ restartedMain][0;39m [36mc.a.nacos.client.logging.NacosLogging [0;39m [2m:[0;39m Load Logback Configuration of Nacos fail, message: Could not initialize Logback Nacos logging from classpath:nacos-logback.xml
|
|
||||||
[2m2025-07-13T08:45:57.046+08:00[0;39m [33m WARN[0;39m [35m7144[0;39m [2m---[0;39m [2m[ restartedMain][0;39m [36mc.a.nacos.client.logging.NacosLogging [0;39m [2m:[0;39m Load Logback Configuration of Nacos fail, message: Could not initialize Logback Nacos logging from classpath:nacos-logback.xml
|
|
||||||
[2m2025-07-13T08:45:57.085+08:00[0;39m [32m INFO[0;39m [35m7144[0;39m [2m---[0;39m [2m[ restartedMain][0;39m [36m.e.DevToolsPropertyDefaultsPostProcessor[0;39m [2m:[0;39m Devtools property defaults active! Set 'spring.devtools.add-properties' to 'false' to disable
|
|
||||||
[2m2025-07-13T08:45:57.400+08:00[0;39m [33m WARN[0;39m [35m7144[0;39m [2m---[0;39m [2m[ restartedMain][0;39m [36mc.a.nacos.client.logging.NacosLogging [0;39m [2m:[0;39m Load Logback Configuration of Nacos fail, message: Could not initialize Logback Nacos logging from classpath:nacos-logback.xml
|
|
||||||
[2m2025-07-13T08:45:57.403+08:00[0;39m [32m INFO[0;39m [35m7144[0;39m [2m---[0;39m [2m[ restartedMain][0;39m [36mc.alibaba.nacos.client.utils.ParamUtil [0;39m [2m:[0;39m [settings] [req-serv] nacos-server port:8848
|
|
||||||
[2m2025-07-13T08:45:57.403+08:00[0;39m [32m INFO[0;39m [35m7144[0;39m [2m---[0;39m [2m[ restartedMain][0;39m [36mc.alibaba.nacos.client.utils.ParamUtil [0;39m [2m:[0;39m [settings] [http-client] connect timeout:1000
|
|
||||||
[2m2025-07-13T08:45:57.406+08:00[0;39m [32m INFO[0;39m [35m7144[0;39m [2m---[0;39m [2m[ restartedMain][0;39m [36mc.alibaba.nacos.client.utils.ParamUtil [0;39m [2m:[0;39m PER_TASK_CONFIG_SIZE: 3000.0
|
|
||||||
[2m2025-07-13T08:45:57.468+08:00[0;39m [32m INFO[0;39m [35m7144[0;39m [2m---[0;39m [2m[ restartedMain][0;39m [36mc.a.n.p.a.s.c.ClientAuthPluginManager [0;39m [2m:[0;39m [ClientAuthPluginManager] Load ClientAuthService com.alibaba.nacos.client.auth.impl.NacosClientAuthServiceImpl success.
|
|
||||||
[2m2025-07-13T08:45:57.468+08:00[0;39m [32m INFO[0;39m [35m7144[0;39m [2m---[0;39m [2m[ restartedMain][0;39m [36mc.a.n.p.a.s.c.ClientAuthPluginManager [0;39m [2m:[0;39m [ClientAuthPluginManager] Load ClientAuthService com.alibaba.nacos.client.auth.ram.RamClientAuthServiceImpl success.
|
|
||||||
[2m2025-07-13T08:45:57.498+08:00[0;39m [32m INFO[0;39m [35m7144[0;39m [2m---[0;39m [2m[ restartedMain][0;39m [36mc.a.n.c.a.r.identify.CredentialWatcher [0;39m [2m:[0;39m null No credential found
|
|
||||||
2025-07-13 08:45:57 [restartedMain] WARN [com.alibaba.nacos.client.logging.NacosLogging] - Load Logback Configuration of Nacos fail, message: Could not initialize Logback Nacos logging from classpath:nacos-logback.xml
|
|
||||||
|
|
||||||
. ____ _ __ _ _
|
|
||||||
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
|
|
||||||
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
|
|
||||||
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
|
|
||||||
' |____| .__|_| |_|_| |_\__, | / / / /
|
|
||||||
=========|_|==============|___/=/_/_/_/
|
|
||||||
[32m :: Spring Boot :: [39m [2m (v3.0.2)[0;39m
|
|
||||||
|
|
||||||
2025-07-13 08:45:57 [restartedMain] INFO [c.a.n.client.config.impl.LocalConfigInfoProcessor] - LOCAL_SNAPSHOT_PATH:/Users/huazhongmin/nacos/config
|
|
||||||
2025-07-13 08:45:57 [restartedMain] INFO [com.alibaba.nacos.common.remote.client] - [RpcClientFactory] create a new rpc client of ae27a187-bd28-4e55-8e9f-a546efcdb1c0_config-0
|
|
||||||
2025-07-13 08:45:57 [restartedMain] INFO [com.alibaba.nacos.common.remote.client] - [ae27a187-bd28-4e55-8e9f-a546efcdb1c0_config-0] Register server push request handler:com.alibaba.nacos.client.config.impl.ClientWorker$ConfigRpcTransportClient$$Lambda/0x00000001263173d8
|
|
||||||
2025-07-13 08:45:57 [restartedMain] INFO [com.alibaba.nacos.common.remote.client] - [ae27a187-bd28-4e55-8e9f-a546efcdb1c0_config-0] Register server push request handler:com.alibaba.nacos.client.config.impl.ClientWorker$ConfigRpcTransportClient$$Lambda/0x0000000126317808
|
|
||||||
2025-07-13 08:45:57 [restartedMain] INFO [com.alibaba.nacos.common.remote.client] - [ae27a187-bd28-4e55-8e9f-a546efcdb1c0_config-0] Registry connection listener to current client:com.alibaba.nacos.client.config.impl.ClientWorker$ConfigRpcTransportClient$1
|
|
||||||
2025-07-13 08:45:57 [restartedMain] INFO [com.alibaba.nacos.common.remote.client] - [ae27a187-bd28-4e55-8e9f-a546efcdb1c0_config-0] RpcClient init, ServerListFactory = com.alibaba.nacos.client.config.impl.ClientWorker$ConfigRpcTransportClient$2
|
|
||||||
2025-07-13 08:45:57 [restartedMain] INFO [com.alibaba.nacos.common.remote.client] - [ae27a187-bd28-4e55-8e9f-a546efcdb1c0_config-0] Try to connect to server on start up, server: {serverIp = '127.0.0.1', server main port = 8848}
|
|
||||||
2025-07-13 08:45:57 [restartedMain] INFO [c.a.nacos.common.remote.client.grpc.GrpcClient] - grpc client connection server:127.0.0.1 ip,serverPort:9848,grpcTslConfig:{"sslProvider":"OPENSSL","enableTls":false,"mutualAuthEnable":false,"trustAll":false}
|
|
||||||
2025-07-13 08:45:58 [restartedMain] INFO [com.alibaba.nacos.common.remote.client] - [ae27a187-bd28-4e55-8e9f-a546efcdb1c0_config-0] Success to connect to server [127.0.0.1:8848] on start up, connectionId = 1752367558311_127.0.0.1_51335
|
|
||||||
2025-07-13 08:45:58 [com.alibaba.nacos.client.remote.worker] INFO [com.alibaba.nacos.common.remote.client] - [ae27a187-bd28-4e55-8e9f-a546efcdb1c0_config-0] Notify connected event to listeners.
|
|
||||||
2025-07-13 08:45:58 [com.alibaba.nacos.client.remote.worker] INFO [com.alibaba.nacos.client.config.impl.ClientWorker] - [ae27a187-bd28-4e55-8e9f-a546efcdb1c0_config-0] Connected,notify listen context...
|
|
||||||
2025-07-13 08:45:58 [restartedMain] INFO [com.alibaba.nacos.common.remote.client] - [ae27a187-bd28-4e55-8e9f-a546efcdb1c0_config-0] Register server push request handler:com.alibaba.nacos.common.remote.client.RpcClient$ConnectResetRequestHandler
|
|
||||||
2025-07-13 08:45:58 [restartedMain] INFO [com.alibaba.nacos.common.remote.client] - [ae27a187-bd28-4e55-8e9f-a546efcdb1c0_config-0] Register server push request handler:com.alibaba.nacos.common.remote.client.RpcClient$$Lambda/0x00000001264a5490
|
|
||||||
2025-07-13 08:45:58 [restartedMain] INFO [com.alibaba.nacos.client.config.impl.Limiter] - limitTime:5.0
|
|
||||||
2025-07-13 08:45:58 [restartedMain] WARN [c.a.cloud.nacos.client.NacosPropertySourceBuilder] - Ignore the empty nacos configuration and get it based on dataId[emotion-growth] & group[DEFAULT_GROUP]
|
|
||||||
2025-07-13 08:45:58 [restartedMain] WARN [c.a.cloud.nacos.client.NacosPropertySourceBuilder] - Ignore the empty nacos configuration and get it based on dataId[emotion-growth.properties] & group[DEFAULT_GROUP]
|
|
||||||
2025-07-13 08:45:58 [restartedMain] WARN [c.a.cloud.nacos.client.NacosPropertySourceBuilder] - Ignore the empty nacos configuration and get it based on dataId[emotion-growth-dev.properties] & group[DEFAULT_GROUP]
|
|
||||||
2025-07-13 08:45:58 [restartedMain] INFO [o.s.c.b.c.PropertySourceBootstrapConfiguration] - Located property source: [BootstrapPropertySource {name='bootstrapProperties-emotion-growth-dev.properties,DEFAULT_GROUP'}, BootstrapPropertySource {name='bootstrapProperties-emotion-growth.properties,DEFAULT_GROUP'}, BootstrapPropertySource {name='bootstrapProperties-emotion-growth,DEFAULT_GROUP'}]
|
|
||||||
2025-07-13 08:45:58 [restartedMain] WARN [com.alibaba.nacos.client.logging.NacosLogging] - Load Logback Configuration of Nacos fail, message: Could not initialize Logback Nacos logging from classpath:nacos-logback.xml
|
|
||||||
2025-07-13 08:45:58 [restartedMain] INFO [com.emotionmuseum.growth.GrowthApplication] - The following 1 profile is active: "dev"
|
|
||||||
2025-07-13 08:45:59 [restartedMain] INFO [o.s.d.r.config.RepositoryConfigurationDelegate] - Multiple Spring Data modules found, entering strict repository configuration mode
|
|
||||||
2025-07-13 08:45:59 [restartedMain] INFO [o.s.d.r.config.RepositoryConfigurationDelegate] - Bootstrapping Spring Data Redis repositories in DEFAULT mode.
|
|
||||||
2025-07-13 08:45:59 [restartedMain] INFO [o.s.d.r.config.RepositoryConfigurationDelegate] - Finished Spring Data repository scanning in 7 ms. Found 0 Redis repository interfaces.
|
|
||||||
2025-07-13 08:45:59 [restartedMain] WARN [org.mybatis.spring.mapper.ClassPathMapperScanner] - No MyBatis mapper was found in '[com.emotionmuseum.growth.mapper]' package. Please check your configuration.
|
|
||||||
2025-07-13 08:45:59 [restartedMain] INFO [o.springframework.cloud.context.scope.GenericScope] - BeanFactory id=4182ccb9-28af-3c79-b715-9374e44c89a7
|
|
||||||
2025-07-13 08:46:00 [restartedMain] INFO [o.s.boot.web.embedded.tomcat.TomcatWebServer] - Tomcat initialized with port(s): 9004 (http)
|
|
||||||
2025-07-13 08:46:00 [restartedMain] INFO [org.apache.catalina.core.StandardService] - Starting service [Tomcat]
|
|
||||||
2025-07-13 08:46:00 [restartedMain] INFO [org.apache.catalina.core.StandardEngine] - Starting Servlet engine: [Apache Tomcat/10.1.5]
|
|
||||||
2025-07-13 08:46:00 [restartedMain] INFO [o.a.c.core.ContainerBase.[Tomcat].[localhost].[/]] - Initializing Spring embedded WebApplicationContext
|
|
||||||
2025-07-13 08:46:00 [restartedMain] INFO [o.s.b.w.s.c.ServletWebServerApplicationContext] - Root WebApplicationContext: initialization completed in 2103 ms
|
|
||||||
2025-07-13 08:46:01 [restartedMain] DEBUG [c.b.m.e.spring.MybatisSqlSessionFactoryBean] - Property 'mapperLocations' was not specified.
|
|
||||||
_ _ |_ _ _|_. ___ _ | _
|
|
||||||
| | |\/|_)(_| | |_\ |_)||_|_\
|
|
||||||
/ |
|
|
||||||
3.5.3.1
|
|
||||||
2025-07-13 08:46:02 [restartedMain] WARN [o.s.b.d.autoconfigure.OptionalLiveReloadServer] - Unable to start LiveReload server
|
|
||||||
2025-07-13 08:46:02 [restartedMain] INFO [o.s.b.actuate.endpoint.web.EndpointLinksResolver] - Exposing 3 endpoint(s) beneath base path '/actuator'
|
|
||||||
2025-07-13 08:46:02 [restartedMain] WARN [o.s.b.w.s.c.AnnotationConfigServletWebServerApplicationContext] - Exception encountered during context initialization - cancelling refresh attempt: org.springframework.context.ApplicationContextException: Failed to start bean 'webServerStartStop'
|
|
||||||
2025-07-13 08:46:02 [restartedMain] INFO [com.alibaba.druid.pool.DruidDataSource] - {dataSource-0} closing ...
|
|
||||||
2025-07-13 08:46:02 [restartedMain] INFO [org.apache.catalina.core.StandardService] - Stopping service [Tomcat]
|
|
||||||
2025-07-13 08:46:02 [restartedMain] INFO [o.s.b.a.logging.ConditionEvaluationReportLogger] -
|
|
||||||
|
|
||||||
Error starting ApplicationContext. To display the condition evaluation report re-run your application with 'debug' enabled.
|
|
||||||
2025-07-13 08:46:02 [restartedMain] ERROR [o.s.b.diagnostics.LoggingFailureAnalysisReporter] -
|
|
||||||
|
|
||||||
***************************
|
|
||||||
APPLICATION FAILED TO START
|
|
||||||
***************************
|
|
||||||
|
|
||||||
Description:
|
|
||||||
|
|
||||||
Web server failed to start. Port 9004 was already in use.
|
|
||||||
|
|
||||||
Action:
|
|
||||||
|
|
||||||
Identify and stop the process that's listening on port 9004 or configure this application to listen on another port.
|
|
||||||
|
|
||||||
2025-07-13 08:46:02 [Thread-10] WARN [com.alibaba.nacos.common.notify.NotifyCenter] - [NotifyCenter] Start destroying Publisher
|
|
||||||
2025-07-13 08:46:02 [Thread-4] WARN [c.alibaba.nacos.common.http.HttpClientBeanHolder] - [HttpClientBeanHolder] Start destroying common HttpClient
|
|
||||||
2025-07-13 08:46:02 [Thread-10] WARN [com.alibaba.nacos.common.notify.NotifyCenter] - [NotifyCenter] Destruction of the end
|
|
||||||
2025-07-13 08:46:02 [Thread-4] WARN [c.alibaba.nacos.common.http.HttpClientBeanHolder] - [HttpClientBeanHolder] Destruction of the end
|
|
||||||
[INFO] ------------------------------------------------------------------------
|
|
||||||
[INFO] BUILD FAILURE
|
|
||||||
[INFO] ------------------------------------------------------------------------
|
|
||||||
[INFO] Total time: 10.247 s
|
|
||||||
[INFO] Finished at: 2025-07-13T08:46:02+08:00
|
|
||||||
[INFO] ------------------------------------------------------------------------
|
|
||||||
[ERROR] Failed to execute goal org.springframework.boot:spring-boot-maven-plugin:3.0.2:run (default-cli) on project emotion-growth: Process terminated with exit code: 1 -> [Help 1]
|
|
||||||
[ERROR]
|
|
||||||
[ERROR] To see the full stack trace of the errors, re-run Maven with the -e switch.
|
|
||||||
[ERROR] Re-run Maven using the -X switch to enable full debug logging.
|
|
||||||
[ERROR]
|
|
||||||
[ERROR] For more information about the errors and possible solutions, please read the following articles:
|
|
||||||
[ERROR] [Help 1] http://cwiki.apache.org/confluence/display/MAVEN/MojoExecutionException
|
|
||||||
@@ -1,121 +0,0 @@
|
|||||||
[INFO] Scanning for projects...
|
|
||||||
[INFO]
|
|
||||||
[INFO] ------------------< com.emotionmuseum:emotion-record >------------------
|
|
||||||
[INFO] Building emotion-record 1.0.0
|
|
||||||
[INFO] from pom.xml
|
|
||||||
[INFO] --------------------------------[ jar ]---------------------------------
|
|
||||||
[INFO]
|
|
||||||
[INFO] >>> spring-boot:3.0.2:run (default-cli) > test-compile @ emotion-record >>>
|
|
||||||
[WARNING] The artifact mysql:mysql-connector-java:jar:8.0.33 has been relocated to com.mysql:mysql-connector-j:jar:8.0.33: MySQL Connector/J artifacts moved to reverse-DNS compliant Maven 2+ coordinates.
|
|
||||||
[INFO]
|
|
||||||
[INFO] --- resources:3.3.1:resources (default-resources) @ emotion-record ---
|
|
||||||
[INFO] Copying 1 resource from src/main/resources to target/classes
|
|
||||||
[INFO]
|
|
||||||
[INFO] --- compiler:3.10.1:compile (default-compile) @ emotion-record ---
|
|
||||||
[INFO] Nothing to compile - all classes are up to date
|
|
||||||
[INFO]
|
|
||||||
[INFO] --- resources:3.3.1:testResources (default-testResources) @ emotion-record ---
|
|
||||||
[INFO] skip non existing resourceDirectory /Users/huazhongmin/peanut/AppleDevelop/EmotionMuseum/backend/emotion-record/src/test/resources
|
|
||||||
[INFO]
|
|
||||||
[INFO] --- compiler:3.10.1:testCompile (default-testCompile) @ emotion-record ---
|
|
||||||
[INFO] No sources to compile
|
|
||||||
[INFO]
|
|
||||||
[INFO] <<< spring-boot:3.0.2:run (default-cli) < test-compile @ emotion-record <<<
|
|
||||||
[INFO]
|
|
||||||
[INFO]
|
|
||||||
[INFO] --- spring-boot:3.0.2:run (default-cli) @ emotion-record ---
|
|
||||||
[INFO] Attaching agents: []
|
|
||||||
[2m2025-07-13T08:45:56.926+08:00[0;39m [33m WARN[0;39m [35m7142[0;39m [2m---[0;39m [2m[ restartedMain][0;39m [36mc.a.nacos.client.logging.NacosLogging [0;39m [2m:[0;39m Load Logback Configuration of Nacos fail, message: Could not initialize Logback Nacos logging from classpath:nacos-logback.xml
|
|
||||||
[2m2025-07-13T08:45:57.023+08:00[0;39m [33m WARN[0;39m [35m7142[0;39m [2m---[0;39m [2m[ restartedMain][0;39m [36mc.a.nacos.client.logging.NacosLogging [0;39m [2m:[0;39m Load Logback Configuration of Nacos fail, message: Could not initialize Logback Nacos logging from classpath:nacos-logback.xml
|
|
||||||
[2m2025-07-13T08:45:57.063+08:00[0;39m [32m INFO[0;39m [35m7142[0;39m [2m---[0;39m [2m[ restartedMain][0;39m [36m.e.DevToolsPropertyDefaultsPostProcessor[0;39m [2m:[0;39m Devtools property defaults active! Set 'spring.devtools.add-properties' to 'false' to disable
|
|
||||||
[2m2025-07-13T08:45:57.373+08:00[0;39m [33m WARN[0;39m [35m7142[0;39m [2m---[0;39m [2m[ restartedMain][0;39m [36mc.a.nacos.client.logging.NacosLogging [0;39m [2m:[0;39m Load Logback Configuration of Nacos fail, message: Could not initialize Logback Nacos logging from classpath:nacos-logback.xml
|
|
||||||
[2m2025-07-13T08:45:57.376+08:00[0;39m [32m INFO[0;39m [35m7142[0;39m [2m---[0;39m [2m[ restartedMain][0;39m [36mc.alibaba.nacos.client.utils.ParamUtil [0;39m [2m:[0;39m [settings] [req-serv] nacos-server port:8848
|
|
||||||
[2m2025-07-13T08:45:57.376+08:00[0;39m [32m INFO[0;39m [35m7142[0;39m [2m---[0;39m [2m[ restartedMain][0;39m [36mc.alibaba.nacos.client.utils.ParamUtil [0;39m [2m:[0;39m [settings] [http-client] connect timeout:1000
|
|
||||||
[2m2025-07-13T08:45:57.381+08:00[0;39m [32m INFO[0;39m [35m7142[0;39m [2m---[0;39m [2m[ restartedMain][0;39m [36mc.alibaba.nacos.client.utils.ParamUtil [0;39m [2m:[0;39m PER_TASK_CONFIG_SIZE: 3000.0
|
|
||||||
[2m2025-07-13T08:45:57.454+08:00[0;39m [32m INFO[0;39m [35m7142[0;39m [2m---[0;39m [2m[ restartedMain][0;39m [36mc.a.n.p.a.s.c.ClientAuthPluginManager [0;39m [2m:[0;39m [ClientAuthPluginManager] Load ClientAuthService com.alibaba.nacos.client.auth.impl.NacosClientAuthServiceImpl success.
|
|
||||||
[2m2025-07-13T08:45:57.454+08:00[0;39m [32m INFO[0;39m [35m7142[0;39m [2m---[0;39m [2m[ restartedMain][0;39m [36mc.a.n.p.a.s.c.ClientAuthPluginManager [0;39m [2m:[0;39m [ClientAuthPluginManager] Load ClientAuthService com.alibaba.nacos.client.auth.ram.RamClientAuthServiceImpl success.
|
|
||||||
[2m2025-07-13T08:45:57.484+08:00[0;39m [32m INFO[0;39m [35m7142[0;39m [2m---[0;39m [2m[ restartedMain][0;39m [36mc.a.n.c.a.r.identify.CredentialWatcher [0;39m [2m:[0;39m null No credential found
|
|
||||||
2025-07-13 08:45:57 [restartedMain] WARN [com.alibaba.nacos.client.logging.NacosLogging] - Load Logback Configuration of Nacos fail, message: Could not initialize Logback Nacos logging from classpath:nacos-logback.xml
|
|
||||||
|
|
||||||
. ____ _ __ _ _
|
|
||||||
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
|
|
||||||
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
|
|
||||||
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
|
|
||||||
' |____| .__|_| |_|_| |_\__, | / / / /
|
|
||||||
=========|_|==============|___/=/_/_/_/
|
|
||||||
[32m :: Spring Boot :: [39m [2m (v3.0.2)[0;39m
|
|
||||||
|
|
||||||
2025-07-13 08:45:57 [restartedMain] INFO [c.a.n.client.config.impl.LocalConfigInfoProcessor] - LOCAL_SNAPSHOT_PATH:/Users/huazhongmin/nacos/config
|
|
||||||
2025-07-13 08:45:57 [restartedMain] INFO [com.alibaba.nacos.common.remote.client] - [RpcClientFactory] create a new rpc client of b1838b09-f01b-45ad-907d-017e3278f9aa_config-0
|
|
||||||
2025-07-13 08:45:57 [restartedMain] INFO [com.alibaba.nacos.common.remote.client] - [b1838b09-f01b-45ad-907d-017e3278f9aa_config-0] Register server push request handler:com.alibaba.nacos.client.config.impl.ClientWorker$ConfigRpcTransportClient$$Lambda/0x0000000133317378
|
|
||||||
2025-07-13 08:45:57 [restartedMain] INFO [com.alibaba.nacos.common.remote.client] - [b1838b09-f01b-45ad-907d-017e3278f9aa_config-0] Register server push request handler:com.alibaba.nacos.client.config.impl.ClientWorker$ConfigRpcTransportClient$$Lambda/0x00000001333177a8
|
|
||||||
2025-07-13 08:45:57 [restartedMain] INFO [com.alibaba.nacos.common.remote.client] - [b1838b09-f01b-45ad-907d-017e3278f9aa_config-0] Registry connection listener to current client:com.alibaba.nacos.client.config.impl.ClientWorker$ConfigRpcTransportClient$1
|
|
||||||
2025-07-13 08:45:57 [restartedMain] INFO [com.alibaba.nacos.common.remote.client] - [b1838b09-f01b-45ad-907d-017e3278f9aa_config-0] RpcClient init, ServerListFactory = com.alibaba.nacos.client.config.impl.ClientWorker$ConfigRpcTransportClient$2
|
|
||||||
2025-07-13 08:45:57 [restartedMain] INFO [com.alibaba.nacos.common.remote.client] - [b1838b09-f01b-45ad-907d-017e3278f9aa_config-0] Try to connect to server on start up, server: {serverIp = '127.0.0.1', server main port = 8848}
|
|
||||||
2025-07-13 08:45:57 [restartedMain] INFO [c.a.nacos.common.remote.client.grpc.GrpcClient] - grpc client connection server:127.0.0.1 ip,serverPort:9848,grpcTslConfig:{"sslProvider":"OPENSSL","enableTls":false,"mutualAuthEnable":false,"trustAll":false}
|
|
||||||
2025-07-13 08:45:58 [restartedMain] INFO [com.alibaba.nacos.common.remote.client] - [b1838b09-f01b-45ad-907d-017e3278f9aa_config-0] Success to connect to server [127.0.0.1:8848] on start up, connectionId = 1752367558310_127.0.0.1_51334
|
|
||||||
2025-07-13 08:45:58 [com.alibaba.nacos.client.remote.worker] INFO [com.alibaba.nacos.common.remote.client] - [b1838b09-f01b-45ad-907d-017e3278f9aa_config-0] Notify connected event to listeners.
|
|
||||||
2025-07-13 08:45:58 [com.alibaba.nacos.client.remote.worker] INFO [com.alibaba.nacos.client.config.impl.ClientWorker] - [b1838b09-f01b-45ad-907d-017e3278f9aa_config-0] Connected,notify listen context...
|
|
||||||
2025-07-13 08:45:58 [restartedMain] INFO [com.alibaba.nacos.common.remote.client] - [b1838b09-f01b-45ad-907d-017e3278f9aa_config-0] Register server push request handler:com.alibaba.nacos.common.remote.client.RpcClient$ConnectResetRequestHandler
|
|
||||||
2025-07-13 08:45:58 [restartedMain] INFO [com.alibaba.nacos.common.remote.client] - [b1838b09-f01b-45ad-907d-017e3278f9aa_config-0] Register server push request handler:com.alibaba.nacos.common.remote.client.RpcClient$$Lambda/0x00000001334a4d38
|
|
||||||
2025-07-13 08:45:58 [restartedMain] INFO [com.alibaba.nacos.client.config.impl.Limiter] - limitTime:5.0
|
|
||||||
2025-07-13 08:45:58 [restartedMain] WARN [c.a.cloud.nacos.client.NacosPropertySourceBuilder] - Ignore the empty nacos configuration and get it based on dataId[emotion-record] & group[DEFAULT_GROUP]
|
|
||||||
2025-07-13 08:45:58 [restartedMain] WARN [c.a.cloud.nacos.client.NacosPropertySourceBuilder] - Ignore the empty nacos configuration and get it based on dataId[emotion-record.properties] & group[DEFAULT_GROUP]
|
|
||||||
2025-07-13 08:45:58 [restartedMain] WARN [c.a.cloud.nacos.client.NacosPropertySourceBuilder] - Ignore the empty nacos configuration and get it based on dataId[emotion-record-dev.properties] & group[DEFAULT_GROUP]
|
|
||||||
2025-07-13 08:45:58 [restartedMain] INFO [o.s.c.b.c.PropertySourceBootstrapConfiguration] - Located property source: [BootstrapPropertySource {name='bootstrapProperties-emotion-record-dev.properties,DEFAULT_GROUP'}, BootstrapPropertySource {name='bootstrapProperties-emotion-record.properties,DEFAULT_GROUP'}, BootstrapPropertySource {name='bootstrapProperties-emotion-record,DEFAULT_GROUP'}]
|
|
||||||
2025-07-13 08:45:58 [restartedMain] WARN [com.alibaba.nacos.client.logging.NacosLogging] - Load Logback Configuration of Nacos fail, message: Could not initialize Logback Nacos logging from classpath:nacos-logback.xml
|
|
||||||
2025-07-13 08:45:58 [restartedMain] INFO [com.emotionmuseum.record.RecordApplication] - The following 1 profile is active: "dev"
|
|
||||||
2025-07-13 08:45:59 [restartedMain] INFO [o.s.d.r.config.RepositoryConfigurationDelegate] - Multiple Spring Data modules found, entering strict repository configuration mode
|
|
||||||
2025-07-13 08:45:59 [restartedMain] INFO [o.s.d.r.config.RepositoryConfigurationDelegate] - Bootstrapping Spring Data Redis repositories in DEFAULT mode.
|
|
||||||
2025-07-13 08:45:59 [restartedMain] INFO [o.s.d.r.config.RepositoryConfigurationDelegate] - Finished Spring Data repository scanning in 9 ms. Found 0 Redis repository interfaces.
|
|
||||||
2025-07-13 08:45:59 [restartedMain] WARN [org.mybatis.spring.mapper.ClassPathMapperScanner] - No MyBatis mapper was found in '[com.emotionmuseum.record.mapper]' package. Please check your configuration.
|
|
||||||
2025-07-13 08:45:59 [restartedMain] INFO [o.springframework.cloud.context.scope.GenericScope] - BeanFactory id=5c2334fc-7543-34b7-9dd6-173e5c1ad22d
|
|
||||||
2025-07-13 08:46:00 [restartedMain] INFO [o.s.boot.web.embedded.tomcat.TomcatWebServer] - Tomcat initialized with port(s): 9003 (http)
|
|
||||||
2025-07-13 08:46:00 [restartedMain] INFO [org.apache.catalina.core.StandardService] - Starting service [Tomcat]
|
|
||||||
2025-07-13 08:46:00 [restartedMain] INFO [org.apache.catalina.core.StandardEngine] - Starting Servlet engine: [Apache Tomcat/10.1.5]
|
|
||||||
2025-07-13 08:46:00 [restartedMain] INFO [o.a.c.core.ContainerBase.[Tomcat].[localhost].[/]] - Initializing Spring embedded WebApplicationContext
|
|
||||||
2025-07-13 08:46:00 [restartedMain] INFO [o.s.b.w.s.c.ServletWebServerApplicationContext] - Root WebApplicationContext: initialization completed in 2100 ms
|
|
||||||
2025-07-13 08:46:01 [restartedMain] DEBUG [c.b.m.e.spring.MybatisSqlSessionFactoryBean] - Property 'mapperLocations' was not specified.
|
|
||||||
_ _ |_ _ _|_. ___ _ | _
|
|
||||||
| | |\/|_)(_| | |_\ |_)||_|_\
|
|
||||||
/ |
|
|
||||||
3.5.3.1
|
|
||||||
2025-07-13 08:46:02 [restartedMain] WARN [o.s.b.d.autoconfigure.OptionalLiveReloadServer] - Unable to start LiveReload server
|
|
||||||
2025-07-13 08:46:02 [restartedMain] INFO [o.s.b.actuate.endpoint.web.EndpointLinksResolver] - Exposing 3 endpoint(s) beneath base path '/actuator'
|
|
||||||
2025-07-13 08:46:02 [restartedMain] WARN [o.s.b.w.s.c.AnnotationConfigServletWebServerApplicationContext] - Exception encountered during context initialization - cancelling refresh attempt: org.springframework.context.ApplicationContextException: Failed to start bean 'webServerStartStop'
|
|
||||||
2025-07-13 08:46:02 [restartedMain] INFO [com.alibaba.druid.pool.DruidDataSource] - {dataSource-0} closing ...
|
|
||||||
2025-07-13 08:46:02 [restartedMain] INFO [org.apache.catalina.core.StandardService] - Stopping service [Tomcat]
|
|
||||||
2025-07-13 08:46:02 [restartedMain] INFO [o.s.b.a.logging.ConditionEvaluationReportLogger] -
|
|
||||||
|
|
||||||
Error starting ApplicationContext. To display the condition evaluation report re-run your application with 'debug' enabled.
|
|
||||||
2025-07-13 08:46:02 [restartedMain] ERROR [o.s.b.diagnostics.LoggingFailureAnalysisReporter] -
|
|
||||||
|
|
||||||
***************************
|
|
||||||
APPLICATION FAILED TO START
|
|
||||||
***************************
|
|
||||||
|
|
||||||
Description:
|
|
||||||
|
|
||||||
Web server failed to start. Port 9003 was already in use.
|
|
||||||
|
|
||||||
Action:
|
|
||||||
|
|
||||||
Identify and stop the process that's listening on port 9003 or configure this application to listen on another port.
|
|
||||||
|
|
||||||
2025-07-13 08:46:02 [Thread-4] WARN [c.alibaba.nacos.common.http.HttpClientBeanHolder] - [HttpClientBeanHolder] Start destroying common HttpClient
|
|
||||||
2025-07-13 08:46:02 [Thread-10] WARN [com.alibaba.nacos.common.notify.NotifyCenter] - [NotifyCenter] Start destroying Publisher
|
|
||||||
2025-07-13 08:46:02 [Thread-10] WARN [com.alibaba.nacos.common.notify.NotifyCenter] - [NotifyCenter] Destruction of the end
|
|
||||||
2025-07-13 08:46:02 [Thread-4] WARN [c.alibaba.nacos.common.http.HttpClientBeanHolder] - [HttpClientBeanHolder] Destruction of the end
|
|
||||||
[INFO] ------------------------------------------------------------------------
|
|
||||||
[INFO] BUILD FAILURE
|
|
||||||
[INFO] ------------------------------------------------------------------------
|
|
||||||
[INFO] Total time: 10.743 s
|
|
||||||
[INFO] Finished at: 2025-07-13T08:46:02+08:00
|
|
||||||
[INFO] ------------------------------------------------------------------------
|
|
||||||
[ERROR] Failed to execute goal org.springframework.boot:spring-boot-maven-plugin:3.0.2:run (default-cli) on project emotion-record: Process terminated with exit code: 1 -> [Help 1]
|
|
||||||
[ERROR]
|
|
||||||
[ERROR] To see the full stack trace of the errors, re-run Maven with the -e switch.
|
|
||||||
[ERROR] Re-run Maven using the -X switch to enable full debug logging.
|
|
||||||
[ERROR]
|
|
||||||
[ERROR] For more information about the errors and possible solutions, please read the following articles:
|
|
||||||
[ERROR] [Help 1] http://cwiki.apache.org/confluence/display/MAVEN/MojoExecutionException
|
|
||||||
@@ -1,121 +0,0 @@
|
|||||||
[INFO] Scanning for projects...
|
|
||||||
[INFO]
|
|
||||||
[INFO] ------------------< com.emotionmuseum:emotion-reward >------------------
|
|
||||||
[INFO] Building emotion-reward 1.0.0
|
|
||||||
[INFO] from pom.xml
|
|
||||||
[INFO] --------------------------------[ jar ]---------------------------------
|
|
||||||
[INFO]
|
|
||||||
[INFO] >>> spring-boot:3.0.2:run (default-cli) > test-compile @ emotion-reward >>>
|
|
||||||
[WARNING] The artifact mysql:mysql-connector-java:jar:8.0.33 has been relocated to com.mysql:mysql-connector-j:jar:8.0.33: MySQL Connector/J artifacts moved to reverse-DNS compliant Maven 2+ coordinates.
|
|
||||||
[INFO]
|
|
||||||
[INFO] --- resources:3.3.1:resources (default-resources) @ emotion-reward ---
|
|
||||||
[INFO] Copying 1 resource from src/main/resources to target/classes
|
|
||||||
[INFO]
|
|
||||||
[INFO] --- compiler:3.10.1:compile (default-compile) @ emotion-reward ---
|
|
||||||
[INFO] Nothing to compile - all classes are up to date
|
|
||||||
[INFO]
|
|
||||||
[INFO] --- resources:3.3.1:testResources (default-testResources) @ emotion-reward ---
|
|
||||||
[INFO] skip non existing resourceDirectory /Users/huazhongmin/peanut/AppleDevelop/EmotionMuseum/backend/emotion-reward/src/test/resources
|
|
||||||
[INFO]
|
|
||||||
[INFO] --- compiler:3.10.1:testCompile (default-testCompile) @ emotion-reward ---
|
|
||||||
[INFO] No sources to compile
|
|
||||||
[INFO]
|
|
||||||
[INFO] <<< spring-boot:3.0.2:run (default-cli) < test-compile @ emotion-reward <<<
|
|
||||||
[INFO]
|
|
||||||
[INFO]
|
|
||||||
[INFO] --- spring-boot:3.0.2:run (default-cli) @ emotion-reward ---
|
|
||||||
[INFO] Attaching agents: []
|
|
||||||
[2m2025-07-13T08:45:57.848+08:00[0;39m [33m WARN[0;39m [35m7150[0;39m [2m---[0;39m [2m[ restartedMain][0;39m [36mc.a.nacos.client.logging.NacosLogging [0;39m [2m:[0;39m Load Logback Configuration of Nacos fail, message: Could not initialize Logback Nacos logging from classpath:nacos-logback.xml
|
|
||||||
[2m2025-07-13T08:45:57.929+08:00[0;39m [33m WARN[0;39m [35m7150[0;39m [2m---[0;39m [2m[ restartedMain][0;39m [36mc.a.nacos.client.logging.NacosLogging [0;39m [2m:[0;39m Load Logback Configuration of Nacos fail, message: Could not initialize Logback Nacos logging from classpath:nacos-logback.xml
|
|
||||||
[2m2025-07-13T08:45:57.960+08:00[0;39m [32m INFO[0;39m [35m7150[0;39m [2m---[0;39m [2m[ restartedMain][0;39m [36m.e.DevToolsPropertyDefaultsPostProcessor[0;39m [2m:[0;39m Devtools property defaults active! Set 'spring.devtools.add-properties' to 'false' to disable
|
|
||||||
[2m2025-07-13T08:45:58.176+08:00[0;39m [33m WARN[0;39m [35m7150[0;39m [2m---[0;39m [2m[ restartedMain][0;39m [36mc.a.nacos.client.logging.NacosLogging [0;39m [2m:[0;39m Load Logback Configuration of Nacos fail, message: Could not initialize Logback Nacos logging from classpath:nacos-logback.xml
|
|
||||||
[2m2025-07-13T08:45:58.177+08:00[0;39m [32m INFO[0;39m [35m7150[0;39m [2m---[0;39m [2m[ restartedMain][0;39m [36mc.alibaba.nacos.client.utils.ParamUtil [0;39m [2m:[0;39m [settings] [req-serv] nacos-server port:8848
|
|
||||||
[2m2025-07-13T08:45:58.177+08:00[0;39m [32m INFO[0;39m [35m7150[0;39m [2m---[0;39m [2m[ restartedMain][0;39m [36mc.alibaba.nacos.client.utils.ParamUtil [0;39m [2m:[0;39m [settings] [http-client] connect timeout:1000
|
|
||||||
[2m2025-07-13T08:45:58.179+08:00[0;39m [32m INFO[0;39m [35m7150[0;39m [2m---[0;39m [2m[ restartedMain][0;39m [36mc.alibaba.nacos.client.utils.ParamUtil [0;39m [2m:[0;39m PER_TASK_CONFIG_SIZE: 3000.0
|
|
||||||
[2m2025-07-13T08:45:58.235+08:00[0;39m [32m INFO[0;39m [35m7150[0;39m [2m---[0;39m [2m[ restartedMain][0;39m [36mc.a.n.p.a.s.c.ClientAuthPluginManager [0;39m [2m:[0;39m [ClientAuthPluginManager] Load ClientAuthService com.alibaba.nacos.client.auth.impl.NacosClientAuthServiceImpl success.
|
|
||||||
[2m2025-07-13T08:45:58.235+08:00[0;39m [32m INFO[0;39m [35m7150[0;39m [2m---[0;39m [2m[ restartedMain][0;39m [36mc.a.n.p.a.s.c.ClientAuthPluginManager [0;39m [2m:[0;39m [ClientAuthPluginManager] Load ClientAuthService com.alibaba.nacos.client.auth.ram.RamClientAuthServiceImpl success.
|
|
||||||
[2m2025-07-13T08:45:58.262+08:00[0;39m [32m INFO[0;39m [35m7150[0;39m [2m---[0;39m [2m[ restartedMain][0;39m [36mc.a.n.c.a.r.identify.CredentialWatcher [0;39m [2m:[0;39m null No credential found
|
|
||||||
2025-07-13 08:45:58 [restartedMain] WARN [com.alibaba.nacos.client.logging.NacosLogging] - Load Logback Configuration of Nacos fail, message: Could not initialize Logback Nacos logging from classpath:nacos-logback.xml
|
|
||||||
|
|
||||||
. ____ _ __ _ _
|
|
||||||
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
|
|
||||||
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
|
|
||||||
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
|
|
||||||
' |____| .__|_| |_|_| |_\__, | / / / /
|
|
||||||
=========|_|==============|___/=/_/_/_/
|
|
||||||
[32m :: Spring Boot :: [39m [2m (v3.0.2)[0;39m
|
|
||||||
|
|
||||||
2025-07-13 08:45:58 [restartedMain] INFO [c.a.n.client.config.impl.LocalConfigInfoProcessor] - LOCAL_SNAPSHOT_PATH:/Users/huazhongmin/nacos/config
|
|
||||||
2025-07-13 08:45:58 [restartedMain] INFO [com.alibaba.nacos.common.remote.client] - [RpcClientFactory] create a new rpc client of e6c044c5-b7c5-4811-8b89-760db0f4f9ae_config-0
|
|
||||||
2025-07-13 08:45:58 [restartedMain] INFO [com.alibaba.nacos.common.remote.client] - [e6c044c5-b7c5-4811-8b89-760db0f4f9ae_config-0] Register server push request handler:com.alibaba.nacos.client.config.impl.ClientWorker$ConfigRpcTransportClient$$Lambda/0x0000000126316a60
|
|
||||||
2025-07-13 08:45:58 [restartedMain] INFO [com.alibaba.nacos.common.remote.client] - [e6c044c5-b7c5-4811-8b89-760db0f4f9ae_config-0] Register server push request handler:com.alibaba.nacos.client.config.impl.ClientWorker$ConfigRpcTransportClient$$Lambda/0x0000000126316e90
|
|
||||||
2025-07-13 08:45:58 [restartedMain] INFO [com.alibaba.nacos.common.remote.client] - [e6c044c5-b7c5-4811-8b89-760db0f4f9ae_config-0] Registry connection listener to current client:com.alibaba.nacos.client.config.impl.ClientWorker$ConfigRpcTransportClient$1
|
|
||||||
2025-07-13 08:45:58 [restartedMain] INFO [com.alibaba.nacos.common.remote.client] - [e6c044c5-b7c5-4811-8b89-760db0f4f9ae_config-0] RpcClient init, ServerListFactory = com.alibaba.nacos.client.config.impl.ClientWorker$ConfigRpcTransportClient$2
|
|
||||||
2025-07-13 08:45:58 [restartedMain] INFO [com.alibaba.nacos.common.remote.client] - [e6c044c5-b7c5-4811-8b89-760db0f4f9ae_config-0] Try to connect to server on start up, server: {serverIp = '127.0.0.1', server main port = 8848}
|
|
||||||
2025-07-13 08:45:58 [restartedMain] INFO [c.a.nacos.common.remote.client.grpc.GrpcClient] - grpc client connection server:127.0.0.1 ip,serverPort:9848,grpcTslConfig:{"sslProvider":"OPENSSL","enableTls":false,"mutualAuthEnable":false,"trustAll":false}
|
|
||||||
2025-07-13 08:45:59 [restartedMain] INFO [com.alibaba.nacos.common.remote.client] - [e6c044c5-b7c5-4811-8b89-760db0f4f9ae_config-0] Success to connect to server [127.0.0.1:8848] on start up, connectionId = 1752367559041_127.0.0.1_51343
|
|
||||||
2025-07-13 08:45:59 [com.alibaba.nacos.client.remote.worker] INFO [com.alibaba.nacos.common.remote.client] - [e6c044c5-b7c5-4811-8b89-760db0f4f9ae_config-0] Notify connected event to listeners.
|
|
||||||
2025-07-13 08:45:59 [com.alibaba.nacos.client.remote.worker] INFO [com.alibaba.nacos.client.config.impl.ClientWorker] - [e6c044c5-b7c5-4811-8b89-760db0f4f9ae_config-0] Connected,notify listen context...
|
|
||||||
2025-07-13 08:45:59 [restartedMain] INFO [com.alibaba.nacos.common.remote.client] - [e6c044c5-b7c5-4811-8b89-760db0f4f9ae_config-0] Register server push request handler:com.alibaba.nacos.common.remote.client.RpcClient$ConnectResetRequestHandler
|
|
||||||
2025-07-13 08:45:59 [restartedMain] INFO [com.alibaba.nacos.common.remote.client] - [e6c044c5-b7c5-4811-8b89-760db0f4f9ae_config-0] Register server push request handler:com.alibaba.nacos.common.remote.client.RpcClient$$Lambda/0x00000001264a4d38
|
|
||||||
2025-07-13 08:45:59 [restartedMain] INFO [com.alibaba.nacos.client.config.impl.Limiter] - limitTime:5.0
|
|
||||||
2025-07-13 08:45:59 [restartedMain] WARN [c.a.cloud.nacos.client.NacosPropertySourceBuilder] - Ignore the empty nacos configuration and get it based on dataId[emotion-reward] & group[DEFAULT_GROUP]
|
|
||||||
2025-07-13 08:45:59 [restartedMain] WARN [c.a.cloud.nacos.client.NacosPropertySourceBuilder] - Ignore the empty nacos configuration and get it based on dataId[emotion-reward.properties] & group[DEFAULT_GROUP]
|
|
||||||
2025-07-13 08:45:59 [restartedMain] WARN [c.a.cloud.nacos.client.NacosPropertySourceBuilder] - Ignore the empty nacos configuration and get it based on dataId[emotion-reward-dev.properties] & group[DEFAULT_GROUP]
|
|
||||||
2025-07-13 08:45:59 [restartedMain] INFO [o.s.c.b.c.PropertySourceBootstrapConfiguration] - Located property source: [BootstrapPropertySource {name='bootstrapProperties-emotion-reward-dev.properties,DEFAULT_GROUP'}, BootstrapPropertySource {name='bootstrapProperties-emotion-reward.properties,DEFAULT_GROUP'}, BootstrapPropertySource {name='bootstrapProperties-emotion-reward,DEFAULT_GROUP'}]
|
|
||||||
2025-07-13 08:45:59 [restartedMain] WARN [com.alibaba.nacos.client.logging.NacosLogging] - Load Logback Configuration of Nacos fail, message: Could not initialize Logback Nacos logging from classpath:nacos-logback.xml
|
|
||||||
2025-07-13 08:45:59 [restartedMain] INFO [com.emotionmuseum.reward.RewardApplication] - The following 1 profile is active: "dev"
|
|
||||||
2025-07-13 08:46:00 [restartedMain] INFO [o.s.d.r.config.RepositoryConfigurationDelegate] - Multiple Spring Data modules found, entering strict repository configuration mode
|
|
||||||
2025-07-13 08:46:00 [restartedMain] INFO [o.s.d.r.config.RepositoryConfigurationDelegate] - Bootstrapping Spring Data Redis repositories in DEFAULT mode.
|
|
||||||
2025-07-13 08:46:00 [restartedMain] INFO [o.s.d.r.config.RepositoryConfigurationDelegate] - Finished Spring Data repository scanning in 9 ms. Found 0 Redis repository interfaces.
|
|
||||||
2025-07-13 08:46:00 [restartedMain] WARN [org.mybatis.spring.mapper.ClassPathMapperScanner] - No MyBatis mapper was found in '[com.emotionmuseum.reward.mapper]' package. Please check your configuration.
|
|
||||||
2025-07-13 08:46:00 [restartedMain] INFO [o.springframework.cloud.context.scope.GenericScope] - BeanFactory id=2ebf993f-e9df-3362-92a0-57f648eeab93
|
|
||||||
2025-07-13 08:46:01 [restartedMain] INFO [o.s.boot.web.embedded.tomcat.TomcatWebServer] - Tomcat initialized with port(s): 9006 (http)
|
|
||||||
2025-07-13 08:46:01 [restartedMain] INFO [org.apache.catalina.core.StandardService] - Starting service [Tomcat]
|
|
||||||
2025-07-13 08:46:01 [restartedMain] INFO [org.apache.catalina.core.StandardEngine] - Starting Servlet engine: [Apache Tomcat/10.1.5]
|
|
||||||
2025-07-13 08:46:01 [restartedMain] INFO [o.a.c.core.ContainerBase.[Tomcat].[localhost].[/]] - Initializing Spring embedded WebApplicationContext
|
|
||||||
2025-07-13 08:46:01 [restartedMain] INFO [o.s.b.w.s.c.ServletWebServerApplicationContext] - Root WebApplicationContext: initialization completed in 2137 ms
|
|
||||||
2025-07-13 08:46:02 [restartedMain] DEBUG [c.b.m.e.spring.MybatisSqlSessionFactoryBean] - Property 'mapperLocations' was not specified.
|
|
||||||
_ _ |_ _ _|_. ___ _ | _
|
|
||||||
| | |\/|_)(_| | |_\ |_)||_|_\
|
|
||||||
/ |
|
|
||||||
3.5.3.1
|
|
||||||
2025-07-13 08:46:02 [restartedMain] WARN [o.s.b.d.autoconfigure.OptionalLiveReloadServer] - Unable to start LiveReload server
|
|
||||||
2025-07-13 08:46:02 [restartedMain] INFO [o.s.b.actuate.endpoint.web.EndpointLinksResolver] - Exposing 3 endpoint(s) beneath base path '/actuator'
|
|
||||||
2025-07-13 08:46:02 [restartedMain] WARN [o.s.b.w.s.c.AnnotationConfigServletWebServerApplicationContext] - Exception encountered during context initialization - cancelling refresh attempt: org.springframework.context.ApplicationContextException: Failed to start bean 'webServerStartStop'
|
|
||||||
2025-07-13 08:46:02 [restartedMain] INFO [com.alibaba.druid.pool.DruidDataSource] - {dataSource-0} closing ...
|
|
||||||
2025-07-13 08:46:02 [restartedMain] INFO [org.apache.catalina.core.StandardService] - Stopping service [Tomcat]
|
|
||||||
2025-07-13 08:46:02 [restartedMain] INFO [o.s.b.a.logging.ConditionEvaluationReportLogger] -
|
|
||||||
|
|
||||||
Error starting ApplicationContext. To display the condition evaluation report re-run your application with 'debug' enabled.
|
|
||||||
2025-07-13 08:46:02 [restartedMain] ERROR [o.s.b.diagnostics.LoggingFailureAnalysisReporter] -
|
|
||||||
|
|
||||||
***************************
|
|
||||||
APPLICATION FAILED TO START
|
|
||||||
***************************
|
|
||||||
|
|
||||||
Description:
|
|
||||||
|
|
||||||
Web server failed to start. Port 9006 was already in use.
|
|
||||||
|
|
||||||
Action:
|
|
||||||
|
|
||||||
Identify and stop the process that's listening on port 9006 or configure this application to listen on another port.
|
|
||||||
|
|
||||||
2025-07-13 08:46:03 [Thread-10] WARN [com.alibaba.nacos.common.notify.NotifyCenter] - [NotifyCenter] Start destroying Publisher
|
|
||||||
2025-07-13 08:46:03 [Thread-4] WARN [c.alibaba.nacos.common.http.HttpClientBeanHolder] - [HttpClientBeanHolder] Start destroying common HttpClient
|
|
||||||
2025-07-13 08:46:03 [Thread-10] WARN [com.alibaba.nacos.common.notify.NotifyCenter] - [NotifyCenter] Destruction of the end
|
|
||||||
2025-07-13 08:46:03 [Thread-4] WARN [c.alibaba.nacos.common.http.HttpClientBeanHolder] - [HttpClientBeanHolder] Destruction of the end
|
|
||||||
[INFO] ------------------------------------------------------------------------
|
|
||||||
[INFO] BUILD FAILURE
|
|
||||||
[INFO] ------------------------------------------------------------------------
|
|
||||||
[INFO] Total time: 9.965 s
|
|
||||||
[INFO] Finished at: 2025-07-13T08:46:03+08:00
|
|
||||||
[INFO] ------------------------------------------------------------------------
|
|
||||||
[ERROR] Failed to execute goal org.springframework.boot:spring-boot-maven-plugin:3.0.2:run (default-cli) on project emotion-reward: Process terminated with exit code: 1 -> [Help 1]
|
|
||||||
[ERROR]
|
|
||||||
[ERROR] To see the full stack trace of the errors, re-run Maven with the -e switch.
|
|
||||||
[ERROR] Re-run Maven using the -X switch to enable full debug logging.
|
|
||||||
[ERROR]
|
|
||||||
[ERROR] For more information about the errors and possible solutions, please read the following articles:
|
|
||||||
[ERROR] [Help 1] http://cwiki.apache.org/confluence/display/MAVEN/MojoExecutionException
|
|
||||||
@@ -1,121 +0,0 @@
|
|||||||
[INFO] Scanning for projects...
|
|
||||||
[INFO]
|
|
||||||
[INFO] ------------------< com.emotionmuseum:emotion-stats >-------------------
|
|
||||||
[INFO] Building emotion-stats 1.0.0
|
|
||||||
[INFO] from pom.xml
|
|
||||||
[INFO] --------------------------------[ jar ]---------------------------------
|
|
||||||
[INFO]
|
|
||||||
[INFO] >>> spring-boot:3.0.2:run (default-cli) > test-compile @ emotion-stats >>>
|
|
||||||
[WARNING] The artifact mysql:mysql-connector-java:jar:8.0.33 has been relocated to com.mysql:mysql-connector-j:jar:8.0.33: MySQL Connector/J artifacts moved to reverse-DNS compliant Maven 2+ coordinates.
|
|
||||||
[INFO]
|
|
||||||
[INFO] --- resources:3.3.1:resources (default-resources) @ emotion-stats ---
|
|
||||||
[INFO] Copying 1 resource from src/main/resources to target/classes
|
|
||||||
[INFO]
|
|
||||||
[INFO] --- compiler:3.10.1:compile (default-compile) @ emotion-stats ---
|
|
||||||
[INFO] Nothing to compile - all classes are up to date
|
|
||||||
[INFO]
|
|
||||||
[INFO] --- resources:3.3.1:testResources (default-testResources) @ emotion-stats ---
|
|
||||||
[INFO] skip non existing resourceDirectory /Users/huazhongmin/peanut/AppleDevelop/EmotionMuseum/backend/emotion-stats/src/test/resources
|
|
||||||
[INFO]
|
|
||||||
[INFO] --- compiler:3.10.1:testCompile (default-testCompile) @ emotion-stats ---
|
|
||||||
[INFO] No sources to compile
|
|
||||||
[INFO]
|
|
||||||
[INFO] <<< spring-boot:3.0.2:run (default-cli) < test-compile @ emotion-stats <<<
|
|
||||||
[INFO]
|
|
||||||
[INFO]
|
|
||||||
[INFO] --- spring-boot:3.0.2:run (default-cli) @ emotion-stats ---
|
|
||||||
[INFO] Attaching agents: []
|
|
||||||
[2m2025-07-13T08:45:58.270+08:00[0;39m [33m WARN[0;39m [35m7167[0;39m [2m---[0;39m [2m[ restartedMain][0;39m [36mc.a.nacos.client.logging.NacosLogging [0;39m [2m:[0;39m Load Logback Configuration of Nacos fail, message: Could not initialize Logback Nacos logging from classpath:nacos-logback.xml
|
|
||||||
[2m2025-07-13T08:45:58.356+08:00[0;39m [33m WARN[0;39m [35m7167[0;39m [2m---[0;39m [2m[ restartedMain][0;39m [36mc.a.nacos.client.logging.NacosLogging [0;39m [2m:[0;39m Load Logback Configuration of Nacos fail, message: Could not initialize Logback Nacos logging from classpath:nacos-logback.xml
|
|
||||||
[2m2025-07-13T08:45:58.403+08:00[0;39m [32m INFO[0;39m [35m7167[0;39m [2m---[0;39m [2m[ restartedMain][0;39m [36m.e.DevToolsPropertyDefaultsPostProcessor[0;39m [2m:[0;39m Devtools property defaults active! Set 'spring.devtools.add-properties' to 'false' to disable
|
|
||||||
[2m2025-07-13T08:45:58.635+08:00[0;39m [33m WARN[0;39m [35m7167[0;39m [2m---[0;39m [2m[ restartedMain][0;39m [36mc.a.nacos.client.logging.NacosLogging [0;39m [2m:[0;39m Load Logback Configuration of Nacos fail, message: Could not initialize Logback Nacos logging from classpath:nacos-logback.xml
|
|
||||||
[2m2025-07-13T08:45:58.637+08:00[0;39m [32m INFO[0;39m [35m7167[0;39m [2m---[0;39m [2m[ restartedMain][0;39m [36mc.alibaba.nacos.client.utils.ParamUtil [0;39m [2m:[0;39m [settings] [req-serv] nacos-server port:8848
|
|
||||||
[2m2025-07-13T08:45:58.637+08:00[0;39m [32m INFO[0;39m [35m7167[0;39m [2m---[0;39m [2m[ restartedMain][0;39m [36mc.alibaba.nacos.client.utils.ParamUtil [0;39m [2m:[0;39m [settings] [http-client] connect timeout:1000
|
|
||||||
[2m2025-07-13T08:45:58.638+08:00[0;39m [32m INFO[0;39m [35m7167[0;39m [2m---[0;39m [2m[ restartedMain][0;39m [36mc.alibaba.nacos.client.utils.ParamUtil [0;39m [2m:[0;39m PER_TASK_CONFIG_SIZE: 3000.0
|
|
||||||
[2m2025-07-13T08:45:58.691+08:00[0;39m [32m INFO[0;39m [35m7167[0;39m [2m---[0;39m [2m[ restartedMain][0;39m [36mc.a.n.p.a.s.c.ClientAuthPluginManager [0;39m [2m:[0;39m [ClientAuthPluginManager] Load ClientAuthService com.alibaba.nacos.client.auth.impl.NacosClientAuthServiceImpl success.
|
|
||||||
[2m2025-07-13T08:45:58.691+08:00[0;39m [32m INFO[0;39m [35m7167[0;39m [2m---[0;39m [2m[ restartedMain][0;39m [36mc.a.n.p.a.s.c.ClientAuthPluginManager [0;39m [2m:[0;39m [ClientAuthPluginManager] Load ClientAuthService com.alibaba.nacos.client.auth.ram.RamClientAuthServiceImpl success.
|
|
||||||
[2m2025-07-13T08:45:58.712+08:00[0;39m [32m INFO[0;39m [35m7167[0;39m [2m---[0;39m [2m[ restartedMain][0;39m [36mc.a.n.c.a.r.identify.CredentialWatcher [0;39m [2m:[0;39m null No credential found
|
|
||||||
2025-07-13 08:45:58 [restartedMain] WARN [com.alibaba.nacos.client.logging.NacosLogging] - Load Logback Configuration of Nacos fail, message: Could not initialize Logback Nacos logging from classpath:nacos-logback.xml
|
|
||||||
|
|
||||||
. ____ _ __ _ _
|
|
||||||
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
|
|
||||||
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
|
|
||||||
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
|
|
||||||
' |____| .__|_| |_|_| |_\__, | / / / /
|
|
||||||
=========|_|==============|___/=/_/_/_/
|
|
||||||
[32m :: Spring Boot :: [39m [2m (v3.0.2)[0;39m
|
|
||||||
|
|
||||||
2025-07-13 08:45:58 [restartedMain] INFO [c.a.n.client.config.impl.LocalConfigInfoProcessor] - LOCAL_SNAPSHOT_PATH:/Users/huazhongmin/nacos/config
|
|
||||||
2025-07-13 08:45:58 [restartedMain] INFO [com.alibaba.nacos.common.remote.client] - [RpcClientFactory] create a new rpc client of 51a04bce-977d-489e-ba5c-8147a68680fb_config-0
|
|
||||||
2025-07-13 08:45:58 [restartedMain] INFO [com.alibaba.nacos.common.remote.client] - [51a04bce-977d-489e-ba5c-8147a68680fb_config-0] Register server push request handler:com.alibaba.nacos.client.config.impl.ClientWorker$ConfigRpcTransportClient$$Lambda/0x000000012d31bc18
|
|
||||||
2025-07-13 08:45:58 [restartedMain] INFO [com.alibaba.nacos.common.remote.client] - [51a04bce-977d-489e-ba5c-8147a68680fb_config-0] Register server push request handler:com.alibaba.nacos.client.config.impl.ClientWorker$ConfigRpcTransportClient$$Lambda/0x000000012d31c048
|
|
||||||
2025-07-13 08:45:58 [restartedMain] INFO [com.alibaba.nacos.common.remote.client] - [51a04bce-977d-489e-ba5c-8147a68680fb_config-0] Registry connection listener to current client:com.alibaba.nacos.client.config.impl.ClientWorker$ConfigRpcTransportClient$1
|
|
||||||
2025-07-13 08:45:58 [restartedMain] INFO [com.alibaba.nacos.common.remote.client] - [51a04bce-977d-489e-ba5c-8147a68680fb_config-0] RpcClient init, ServerListFactory = com.alibaba.nacos.client.config.impl.ClientWorker$ConfigRpcTransportClient$2
|
|
||||||
2025-07-13 08:45:58 [restartedMain] INFO [com.alibaba.nacos.common.remote.client] - [51a04bce-977d-489e-ba5c-8147a68680fb_config-0] Try to connect to server on start up, server: {serverIp = '127.0.0.1', server main port = 8848}
|
|
||||||
2025-07-13 08:45:58 [restartedMain] INFO [c.a.nacos.common.remote.client.grpc.GrpcClient] - grpc client connection server:127.0.0.1 ip,serverPort:9848,grpcTslConfig:{"sslProvider":"OPENSSL","enableTls":false,"mutualAuthEnable":false,"trustAll":false}
|
|
||||||
2025-07-13 08:45:59 [restartedMain] INFO [com.alibaba.nacos.common.remote.client] - [51a04bce-977d-489e-ba5c-8147a68680fb_config-0] Success to connect to server [127.0.0.1:8848] on start up, connectionId = 1752367559501_127.0.0.1_51348
|
|
||||||
2025-07-13 08:45:59 [com.alibaba.nacos.client.remote.worker] INFO [com.alibaba.nacos.common.remote.client] - [51a04bce-977d-489e-ba5c-8147a68680fb_config-0] Notify connected event to listeners.
|
|
||||||
2025-07-13 08:45:59 [com.alibaba.nacos.client.remote.worker] INFO [com.alibaba.nacos.client.config.impl.ClientWorker] - [51a04bce-977d-489e-ba5c-8147a68680fb_config-0] Connected,notify listen context...
|
|
||||||
2025-07-13 08:45:59 [restartedMain] INFO [com.alibaba.nacos.common.remote.client] - [51a04bce-977d-489e-ba5c-8147a68680fb_config-0] Register server push request handler:com.alibaba.nacos.common.remote.client.RpcClient$ConnectResetRequestHandler
|
|
||||||
2025-07-13 08:45:59 [restartedMain] INFO [com.alibaba.nacos.common.remote.client] - [51a04bce-977d-489e-ba5c-8147a68680fb_config-0] Register server push request handler:com.alibaba.nacos.common.remote.client.RpcClient$$Lambda/0x000000012d4a7d30
|
|
||||||
2025-07-13 08:45:59 [restartedMain] INFO [com.alibaba.nacos.client.config.impl.Limiter] - limitTime:5.0
|
|
||||||
2025-07-13 08:45:59 [restartedMain] WARN [c.a.cloud.nacos.client.NacosPropertySourceBuilder] - Ignore the empty nacos configuration and get it based on dataId[emotion-stats] & group[DEFAULT_GROUP]
|
|
||||||
2025-07-13 08:45:59 [restartedMain] WARN [c.a.cloud.nacos.client.NacosPropertySourceBuilder] - Ignore the empty nacos configuration and get it based on dataId[emotion-stats.properties] & group[DEFAULT_GROUP]
|
|
||||||
2025-07-13 08:45:59 [restartedMain] WARN [c.a.cloud.nacos.client.NacosPropertySourceBuilder] - Ignore the empty nacos configuration and get it based on dataId[emotion-stats-dev.properties] & group[DEFAULT_GROUP]
|
|
||||||
2025-07-13 08:45:59 [restartedMain] INFO [o.s.c.b.c.PropertySourceBootstrapConfiguration] - Located property source: [BootstrapPropertySource {name='bootstrapProperties-emotion-stats-dev.properties,DEFAULT_GROUP'}, BootstrapPropertySource {name='bootstrapProperties-emotion-stats.properties,DEFAULT_GROUP'}, BootstrapPropertySource {name='bootstrapProperties-emotion-stats,DEFAULT_GROUP'}]
|
|
||||||
2025-07-13 08:45:59 [restartedMain] WARN [com.alibaba.nacos.client.logging.NacosLogging] - Load Logback Configuration of Nacos fail, message: Could not initialize Logback Nacos logging from classpath:nacos-logback.xml
|
|
||||||
2025-07-13 08:45:59 [restartedMain] INFO [com.emotionmuseum.stats.StatsApplication] - The following 1 profile is active: "dev"
|
|
||||||
2025-07-13 08:46:00 [restartedMain] INFO [o.s.d.r.config.RepositoryConfigurationDelegate] - Multiple Spring Data modules found, entering strict repository configuration mode
|
|
||||||
2025-07-13 08:46:00 [restartedMain] INFO [o.s.d.r.config.RepositoryConfigurationDelegate] - Bootstrapping Spring Data Redis repositories in DEFAULT mode.
|
|
||||||
2025-07-13 08:46:00 [restartedMain] INFO [o.s.d.r.config.RepositoryConfigurationDelegate] - Finished Spring Data repository scanning in 6 ms. Found 0 Redis repository interfaces.
|
|
||||||
2025-07-13 08:46:01 [restartedMain] WARN [org.mybatis.spring.mapper.ClassPathMapperScanner] - No MyBatis mapper was found in '[com.emotionmuseum.stats.mapper]' package. Please check your configuration.
|
|
||||||
2025-07-13 08:46:01 [restartedMain] INFO [o.springframework.cloud.context.scope.GenericScope] - BeanFactory id=af125b37-688d-3aab-bf37-e7a7060f4920
|
|
||||||
2025-07-13 08:46:01 [restartedMain] INFO [o.s.boot.web.embedded.tomcat.TomcatWebServer] - Tomcat initialized with port(s): 9007 (http)
|
|
||||||
2025-07-13 08:46:01 [restartedMain] INFO [org.apache.catalina.core.StandardService] - Starting service [Tomcat]
|
|
||||||
2025-07-13 08:46:01 [restartedMain] INFO [org.apache.catalina.core.StandardEngine] - Starting Servlet engine: [Apache Tomcat/10.1.5]
|
|
||||||
2025-07-13 08:46:01 [restartedMain] INFO [o.a.c.core.ContainerBase.[Tomcat].[localhost].[/]] - Initializing Spring embedded WebApplicationContext
|
|
||||||
2025-07-13 08:46:01 [restartedMain] INFO [o.s.b.w.s.c.ServletWebServerApplicationContext] - Root WebApplicationContext: initialization completed in 2077 ms
|
|
||||||
2025-07-13 08:46:02 [restartedMain] DEBUG [c.b.m.e.spring.MybatisSqlSessionFactoryBean] - Property 'mapperLocations' was not specified.
|
|
||||||
_ _ |_ _ _|_. ___ _ | _
|
|
||||||
| | |\/|_)(_| | |_\ |_)||_|_\
|
|
||||||
/ |
|
|
||||||
3.5.3.1
|
|
||||||
2025-07-13 08:46:02 [restartedMain] WARN [o.s.b.d.autoconfigure.OptionalLiveReloadServer] - Unable to start LiveReload server
|
|
||||||
2025-07-13 08:46:02 [restartedMain] INFO [o.s.b.actuate.endpoint.web.EndpointLinksResolver] - Exposing 3 endpoint(s) beneath base path '/actuator'
|
|
||||||
2025-07-13 08:46:03 [restartedMain] WARN [o.s.b.w.s.c.AnnotationConfigServletWebServerApplicationContext] - Exception encountered during context initialization - cancelling refresh attempt: org.springframework.context.ApplicationContextException: Failed to start bean 'webServerStartStop'
|
|
||||||
2025-07-13 08:46:03 [restartedMain] INFO [com.alibaba.druid.pool.DruidDataSource] - {dataSource-0} closing ...
|
|
||||||
2025-07-13 08:46:03 [restartedMain] INFO [org.apache.catalina.core.StandardService] - Stopping service [Tomcat]
|
|
||||||
2025-07-13 08:46:03 [restartedMain] INFO [o.s.b.a.logging.ConditionEvaluationReportLogger] -
|
|
||||||
|
|
||||||
Error starting ApplicationContext. To display the condition evaluation report re-run your application with 'debug' enabled.
|
|
||||||
2025-07-13 08:46:03 [restartedMain] ERROR [o.s.b.diagnostics.LoggingFailureAnalysisReporter] -
|
|
||||||
|
|
||||||
***************************
|
|
||||||
APPLICATION FAILED TO START
|
|
||||||
***************************
|
|
||||||
|
|
||||||
Description:
|
|
||||||
|
|
||||||
Web server failed to start. Port 9007 was already in use.
|
|
||||||
|
|
||||||
Action:
|
|
||||||
|
|
||||||
Identify and stop the process that's listening on port 9007 or configure this application to listen on another port.
|
|
||||||
|
|
||||||
2025-07-13 08:46:03 [Thread-4] WARN [c.alibaba.nacos.common.http.HttpClientBeanHolder] - [HttpClientBeanHolder] Start destroying common HttpClient
|
|
||||||
2025-07-13 08:46:03 [Thread-10] WARN [com.alibaba.nacos.common.notify.NotifyCenter] - [NotifyCenter] Start destroying Publisher
|
|
||||||
2025-07-13 08:46:03 [Thread-10] WARN [com.alibaba.nacos.common.notify.NotifyCenter] - [NotifyCenter] Destruction of the end
|
|
||||||
2025-07-13 08:46:03 [Thread-4] WARN [c.alibaba.nacos.common.http.HttpClientBeanHolder] - [HttpClientBeanHolder] Destruction of the end
|
|
||||||
[INFO] ------------------------------------------------------------------------
|
|
||||||
[INFO] BUILD FAILURE
|
|
||||||
[INFO] ------------------------------------------------------------------------
|
|
||||||
[INFO] Total time: 9.734 s
|
|
||||||
[INFO] Finished at: 2025-07-13T08:46:03+08:00
|
|
||||||
[INFO] ------------------------------------------------------------------------
|
|
||||||
[ERROR] Failed to execute goal org.springframework.boot:spring-boot-maven-plugin:3.0.2:run (default-cli) on project emotion-stats: Process terminated with exit code: 1 -> [Help 1]
|
|
||||||
[ERROR]
|
|
||||||
[ERROR] To see the full stack trace of the errors, re-run Maven with the -e switch.
|
|
||||||
[ERROR] Re-run Maven using the -X switch to enable full debug logging.
|
|
||||||
[ERROR]
|
|
||||||
[ERROR] For more information about the errors and possible solutions, please read the following articles:
|
|
||||||
[ERROR] [Help 1] http://cwiki.apache.org/confluence/display/MAVEN/MojoExecutionException
|
|
||||||
@@ -1,125 +0,0 @@
|
|||||||
2025-07-16T09:03:18.888+08:00 WARN 19784 --- [ main] c.a.nacos.client.logging.NacosLogging : Load Logback Configuration of Nacos fail, message: Could not initialize Logback Nacos logging from classpath:nacos-logback.xml
|
|
||||||
2025-07-16T09:03:18.983+08:00 WARN 19784 --- [ main] c.a.nacos.client.logging.NacosLogging : Load Logback Configuration of Nacos fail, message: Could not initialize Logback Nacos logging from classpath:nacos-logback.xml
|
|
||||||
2025-07-16T09:03:19.261+08:00 WARN 19784 --- [ main] c.a.nacos.client.logging.NacosLogging : Load Logback Configuration of Nacos fail, message: Could not initialize Logback Nacos logging from classpath:nacos-logback.xml
|
|
||||||
2025-07-16T09:03:19.262+08:00 INFO 19784 --- [ main] c.alibaba.nacos.client.utils.ParamUtil : [settings] [req-serv] nacos-server port:8848
|
|
||||||
2025-07-16T09:03:19.263+08:00 INFO 19784 --- [ main] c.alibaba.nacos.client.utils.ParamUtil : [settings] [http-client] connect timeout:1000
|
|
||||||
2025-07-16T09:03:19.265+08:00 INFO 19784 --- [ main] c.alibaba.nacos.client.utils.ParamUtil : PER_TASK_CONFIG_SIZE: 3000.0
|
|
||||||
2025-07-16T09:03:19.321+08:00 INFO 19784 --- [ main] c.a.n.p.a.s.c.ClientAuthPluginManager : [ClientAuthPluginManager] Load ClientAuthService com.alibaba.nacos.client.auth.impl.NacosClientAuthServiceImpl success.
|
|
||||||
2025-07-16T09:03:19.321+08:00 INFO 19784 --- [ main] c.a.n.p.a.s.c.ClientAuthPluginManager : [ClientAuthPluginManager] Load ClientAuthService com.alibaba.nacos.client.auth.ram.RamClientAuthServiceImpl success.
|
|
||||||
2025-07-16T09:03:19.345+08:00 INFO 19784 --- [ main] c.a.n.c.a.r.identify.CredentialWatcher : null No credential found
|
|
||||||
2025-07-16 09:03:19 [main] WARN [com.alibaba.nacos.client.logging.NacosLogging] - Load Logback Configuration of Nacos fail, message: Could not initialize Logback Nacos logging from classpath:nacos-logback.xml
|
|
||||||
|
|
||||||
. ____ _ __ _ _
|
|
||||||
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
|
|
||||||
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
|
|
||||||
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
|
|
||||||
' |____| .__|_| |_|_| |_\__, | / / / /
|
|
||||||
=========|_|==============|___/=/_/_/_/
|
|
||||||
:: Spring Boot :: (v3.0.2)
|
|
||||||
|
|
||||||
2025-07-16 09:03:19 [main] INFO [c.a.n.client.config.impl.LocalConfigInfoProcessor] - LOCAL_SNAPSHOT_PATH:/Users/huazhongmin/nacos/config
|
|
||||||
2025-07-16 09:03:19 [main] INFO [com.alibaba.nacos.common.remote.client] - [RpcClientFactory] create a new rpc client of 745c3cf4-7cb2-4ac6-a11a-86fd37de7293_config-0
|
|
||||||
2025-07-16 09:03:19 [main] INFO [com.alibaba.nacos.common.remote.client] - [745c3cf4-7cb2-4ac6-a11a-86fd37de7293_config-0] Register server push request handler:com.alibaba.nacos.client.config.impl.ClientWorker$ConfigRpcTransportClient$$Lambda/0x000000013d2dc530
|
|
||||||
2025-07-16 09:03:19 [main] INFO [com.alibaba.nacos.common.remote.client] - [745c3cf4-7cb2-4ac6-a11a-86fd37de7293_config-0] Register server push request handler:com.alibaba.nacos.client.config.impl.ClientWorker$ConfigRpcTransportClient$$Lambda/0x000000013d2dc940
|
|
||||||
2025-07-16 09:03:19 [main] INFO [com.alibaba.nacos.common.remote.client] - [745c3cf4-7cb2-4ac6-a11a-86fd37de7293_config-0] Registry connection listener to current client:com.alibaba.nacos.client.config.impl.ClientWorker$ConfigRpcTransportClient$1
|
|
||||||
2025-07-16 09:03:19 [main] INFO [com.alibaba.nacos.common.remote.client] - [745c3cf4-7cb2-4ac6-a11a-86fd37de7293_config-0] RpcClient init, ServerListFactory = com.alibaba.nacos.client.config.impl.ClientWorker$ConfigRpcTransportClient$2
|
|
||||||
2025-07-16 09:03:19 [main] INFO [com.alibaba.nacos.common.remote.client] - [745c3cf4-7cb2-4ac6-a11a-86fd37de7293_config-0] Try to connect to server on start up, server: {serverIp = '127.0.0.1', server main port = 8848}
|
|
||||||
2025-07-16 09:03:19 [main] INFO [c.a.nacos.common.remote.client.grpc.GrpcClient] - grpc client connection server:127.0.0.1 ip,serverPort:9848,grpcTslConfig:{"sslProvider":"OPENSSL","enableTls":false,"mutualAuthEnable":false,"trustAll":false}
|
|
||||||
2025-07-16 09:03:20 [main] INFO [com.alibaba.nacos.common.remote.client] - [745c3cf4-7cb2-4ac6-a11a-86fd37de7293_config-0] Success to connect to server [127.0.0.1:8848] on start up, connectionId = 1752627800162_127.0.0.1_62095
|
|
||||||
2025-07-16 09:03:20 [com.alibaba.nacos.client.remote.worker] INFO [com.alibaba.nacos.common.remote.client] - [745c3cf4-7cb2-4ac6-a11a-86fd37de7293_config-0] Notify connected event to listeners.
|
|
||||||
2025-07-16 09:03:20 [com.alibaba.nacos.client.remote.worker] INFO [com.alibaba.nacos.client.config.impl.ClientWorker] - [745c3cf4-7cb2-4ac6-a11a-86fd37de7293_config-0] Connected,notify listen context...
|
|
||||||
2025-07-16 09:03:20 [main] INFO [com.alibaba.nacos.common.remote.client] - [745c3cf4-7cb2-4ac6-a11a-86fd37de7293_config-0] Register server push request handler:com.alibaba.nacos.common.remote.client.RpcClient$ConnectResetRequestHandler
|
|
||||||
2025-07-16 09:03:20 [main] INFO [com.alibaba.nacos.common.remote.client] - [745c3cf4-7cb2-4ac6-a11a-86fd37de7293_config-0] Register server push request handler:com.alibaba.nacos.common.remote.client.RpcClient$$Lambda/0x000000013d43f118
|
|
||||||
2025-07-16 09:03:20 [main] INFO [com.alibaba.nacos.client.config.impl.Limiter] - limitTime:5.0
|
|
||||||
2025-07-16 09:03:20 [main] WARN [c.a.cloud.nacos.client.NacosPropertySourceBuilder] - Ignore the empty nacos configuration and get it based on dataId[emotion-user] & group[DEFAULT_GROUP]
|
|
||||||
2025-07-16 09:03:20 [main] WARN [c.a.cloud.nacos.client.NacosPropertySourceBuilder] - Ignore the empty nacos configuration and get it based on dataId[emotion-user.properties] & group[DEFAULT_GROUP]
|
|
||||||
2025-07-16 09:03:20 [main] WARN [c.a.cloud.nacos.client.NacosPropertySourceBuilder] - Ignore the empty nacos configuration and get it based on dataId[emotion-user-local.properties] & group[DEFAULT_GROUP]
|
|
||||||
2025-07-16 09:03:20 [main] INFO [o.s.c.b.c.PropertySourceBootstrapConfiguration] - Located property source: [BootstrapPropertySource {name='bootstrapProperties-emotion-user-local.properties,DEFAULT_GROUP'}, BootstrapPropertySource {name='bootstrapProperties-emotion-user.properties,DEFAULT_GROUP'}, BootstrapPropertySource {name='bootstrapProperties-emotion-user,DEFAULT_GROUP'}]
|
|
||||||
2025-07-16 09:03:20 [main] WARN [com.alibaba.nacos.client.logging.NacosLogging] - Load Logback Configuration of Nacos fail, message: Could not initialize Logback Nacos logging from classpath:nacos-logback.xml
|
|
||||||
2025-07-16 09:03:20 [main] INFO [com.emotionmuseum.user.UserApplication] - The following 1 profile is active: "local"
|
|
||||||
2025-07-16 09:03:21 [main] INFO [o.s.d.r.config.RepositoryConfigurationDelegate] - Multiple Spring Data modules found, entering strict repository configuration mode
|
|
||||||
2025-07-16 09:03:21 [main] INFO [o.s.d.r.config.RepositoryConfigurationDelegate] - Bootstrapping Spring Data Redis repositories in DEFAULT mode.
|
|
||||||
2025-07-16 09:03:21 [main] INFO [o.s.d.r.config.RepositoryConfigurationDelegate] - Finished Spring Data repository scanning in 12 ms. Found 0 Redis repository interfaces.
|
|
||||||
2025-07-16 09:03:21 [main] INFO [o.springframework.cloud.context.scope.GenericScope] - BeanFactory id=2c694ed0-114c-30e6-aed7-dcee6bca36f0
|
|
||||||
2025-07-16 09:03:22 [main] INFO [o.s.boot.web.embedded.tomcat.TomcatWebServer] - Tomcat initialized with port(s): 19001 (http)
|
|
||||||
2025-07-16 09:03:22 [main] INFO [org.apache.catalina.core.StandardService] - Starting service [Tomcat]
|
|
||||||
2025-07-16 09:03:22 [main] INFO [org.apache.catalina.core.StandardEngine] - Starting Servlet engine: [Apache Tomcat/10.1.5]
|
|
||||||
2025-07-16 09:03:22 [main] INFO [o.a.c.core.ContainerBase.[Tomcat].[localhost].[/]] - Initializing Spring embedded WebApplicationContext
|
|
||||||
2025-07-16 09:03:22 [main] INFO [o.s.b.w.s.c.ServletWebServerApplicationContext] - Root WebApplicationContext: initialization completed in 2214 ms
|
|
||||||
Logging initialized using 'class org.apache.ibatis.logging.stdout.StdOutImpl' adapter.
|
|
||||||
2025-07-16 09:03:23 [main] INFO [com.emotionmuseum.common.config.SnowflakeConfig] - 使用MAC地址生成的机器ID: 669
|
|
||||||
2025-07-16 09:03:23 [main] INFO [com.emotionmuseum.common.config.SnowflakeConfig] - 雪花算法配置完成,使用机器ID: 669
|
|
||||||
2025-07-16 09:03:23 [main] INFO [c.emotionmuseum.common.util.SnowflakeIdGenerator] - 雪花算法ID生成器初始化完成,机器ID: 669
|
|
||||||
Registered plugin: 'com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor@76889e60'
|
|
||||||
Parsed mapper file: 'URL [jar:file:/Users/huazhongmin/peanut/AppleDevelop/EmotionMuseum/backend/emotion-user/target/emotion-user-1.0.0.jar!/BOOT-INF/classes!/mapper/UserMapper.xml]'
|
|
||||||
_ _ |_ _ _|_. ___ _ | _
|
|
||||||
| | |\/|_)(_| | |_\ |_)||_|_\
|
|
||||||
/ |
|
|
||||||
3.5.3.1
|
|
||||||
2025-07-16 09:03:24 [main] DEBUG [c.e.user.security.JwtAuthenticationFilter] - Filter 'jwtAuthenticationFilter' configured for use
|
|
||||||
2025-07-16 09:03:24 [main] DEBUG [o.s.web.filter.ServerHttpObservationFilter] - Filter 'serverHttpObservationFilter' configured for use
|
|
||||||
2025-07-16 09:03:24 [main] DEBUG [o.s.w.s.m.m.a.RequestMappingHandlerMapping] - 19 mappings in 'requestMappingHandlerMapping'
|
|
||||||
2025-07-16 09:03:24 [main] DEBUG [o.s.web.servlet.handler.SimpleUrlHandlerMapping] - Patterns [/webjars/**, /**] in 'resourceHandlerMapping'
|
|
||||||
2025-07-16 09:03:25 [main] INFO [o.s.b.actuate.endpoint.web.EndpointLinksResolver] - Exposing 3 endpoint(s) beneath base path '/actuator'
|
|
||||||
2025-07-16 09:03:25 [main] INFO [o.s.security.web.DefaultSecurityFilterChain] - Will secure any request with [org.springframework.security.web.session.DisableEncodeUrlFilter@6993c8df, org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@57545c3f, org.springframework.security.web.context.SecurityContextHolderFilter@64920dc2, org.springframework.security.web.header.HeaderWriterFilter@794366a5, org.springframework.web.filter.CorsFilter@326e0b8e, org.springframework.security.web.authentication.logout.LogoutFilter@30839e44, com.emotionmuseum.user.security.JwtAuthenticationFilter@434514d8, org.springframework.security.web.savedrequest.RequestCacheAwareFilter@493ac8d3, org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@13dbed9e, org.springframework.security.web.authentication.AnonymousAuthenticationFilter@36baa049, org.springframework.security.web.session.SessionManagementFilter@1ee47d9e, org.springframework.security.web.access.ExceptionTranslationFilter@3f36e8d1, org.springframework.security.web.access.intercept.AuthorizationFilter@7978e022]
|
|
||||||
2025-07-16 09:03:25 [main] DEBUG [o.s.w.s.m.m.a.RequestMappingHandlerAdapter] - ControllerAdvice beans: 0 @ModelAttribute, 0 @InitBinder, 1 RequestBodyAdvice, 1 ResponseBodyAdvice
|
|
||||||
2025-07-16 09:03:25 [main] DEBUG [o.s.w.s.m.m.a.ExceptionHandlerExceptionResolver] - ControllerAdvice beans: 0 @ExceptionHandler, 1 ResponseBodyAdvice
|
|
||||||
2025-07-16 09:03:25 [main] INFO [o.s.boot.web.embedded.tomcat.TomcatWebServer] - Tomcat started on port(s): 19001 (http) with context path ''
|
|
||||||
2025-07-16 09:03:25 [main] INFO [com.emotionmuseum.user.UserApplication] - Started UserApplication in 7.27 seconds (process running for 7.838)
|
|
||||||
2025-07-16 09:03:57 [http-nio-19001-exec-1] INFO [o.a.c.core.ContainerBase.[Tomcat].[localhost].[/]] - Initializing Spring DispatcherServlet 'dispatcherServlet'
|
|
||||||
2025-07-16 09:03:57 [http-nio-19001-exec-1] INFO [org.springframework.web.servlet.DispatcherServlet] - Initializing Servlet 'dispatcherServlet'
|
|
||||||
2025-07-16 09:03:57 [http-nio-19001-exec-1] DEBUG [org.springframework.web.servlet.DispatcherServlet] - Detected StandardServletMultipartResolver
|
|
||||||
2025-07-16 09:03:57 [http-nio-19001-exec-1] DEBUG [org.springframework.web.servlet.DispatcherServlet] - Detected AcceptHeaderLocaleResolver
|
|
||||||
2025-07-16 09:03:57 [http-nio-19001-exec-1] DEBUG [org.springframework.web.servlet.DispatcherServlet] - Detected FixedThemeResolver
|
|
||||||
2025-07-16 09:03:57 [http-nio-19001-exec-1] DEBUG [org.springframework.web.servlet.DispatcherServlet] - Detected org.springframework.web.servlet.view.DefaultRequestToViewNameTranslator@5e240cb6
|
|
||||||
2025-07-16 09:03:57 [http-nio-19001-exec-1] DEBUG [org.springframework.web.servlet.DispatcherServlet] - Detected org.springframework.web.servlet.support.SessionFlashMapManager@2e21a5d9
|
|
||||||
2025-07-16 09:03:57 [http-nio-19001-exec-1] DEBUG [org.springframework.web.servlet.DispatcherServlet] - enableLoggingRequestDetails='false': request parameters and headers will be masked to prevent unsafe logging of potentially sensitive data
|
|
||||||
2025-07-16 09:03:57 [http-nio-19001-exec-1] INFO [org.springframework.web.servlet.DispatcherServlet] - Completed initialization in 2 ms
|
|
||||||
2025-07-16 09:03:57 [http-nio-19001-exec-1] DEBUG [org.springframework.web.servlet.DispatcherServlet] - GET "/actuator/health", parameters={}
|
|
||||||
2025-07-16 09:03:57 [http-nio-19001-exec-1] INFO [com.zaxxer.hikari.HikariDataSource] - HikariPool-1 - Starting...
|
|
||||||
2025-07-16 09:03:57 [http-nio-19001-exec-1] INFO [com.zaxxer.hikari.pool.HikariPool] - HikariPool-1 - Added connection com.mysql.cj.jdbc.ConnectionImpl@1703fbc9
|
|
||||||
2025-07-16 09:03:57 [http-nio-19001-exec-1] INFO [com.zaxxer.hikari.HikariDataSource] - HikariPool-1 - Start completed.
|
|
||||||
2025-07-16 09:03:58 [http-nio-19001-exec-1] DEBUG [o.s.w.s.m.m.annotation.HttpEntityMethodProcessor] - Using 'application/vnd.spring-boot.actuator.v3+json', given [*/*] and supported [application/vnd.spring-boot.actuator.v3+json, application/vnd.spring-boot.actuator.v2+json, application/json]
|
|
||||||
2025-07-16 09:03:58 [http-nio-19001-exec-1] DEBUG [o.s.w.s.m.m.annotation.HttpEntityMethodProcessor] - Writing [org.springframework.boot.actuate.health.SystemHealth@178369bf]
|
|
||||||
2025-07-16 09:03:58 [http-nio-19001-exec-1] DEBUG [org.springframework.web.servlet.DispatcherServlet] - Completed 200 OK
|
|
||||||
2025-07-16 09:04:12 [http-nio-19001-exec-3] DEBUG [org.springframework.web.servlet.DispatcherServlet] - GET "/actuator/health", parameters={}
|
|
||||||
2025-07-16 09:04:12 [http-nio-19001-exec-3] DEBUG [o.s.w.s.m.m.annotation.HttpEntityMethodProcessor] - Using 'application/vnd.spring-boot.actuator.v3+json', given [*/*] and supported [application/vnd.spring-boot.actuator.v3+json, application/vnd.spring-boot.actuator.v2+json, application/json]
|
|
||||||
2025-07-16 09:04:12 [http-nio-19001-exec-3] DEBUG [o.s.w.s.m.m.annotation.HttpEntityMethodProcessor] - Writing [org.springframework.boot.actuate.health.SystemHealth@159e4af4]
|
|
||||||
2025-07-16 09:04:12 [http-nio-19001-exec-3] DEBUG [org.springframework.web.servlet.DispatcherServlet] - Completed 200 OK
|
|
||||||
2025-07-16 09:04:22 [http-nio-19001-exec-5] DEBUG [o.s.web.servlet.handler.SimpleUrlHandlerMapping] - Mapped to ResourceHttpRequestHandler [classpath [META-INF/resources/], classpath [resources/], classpath [static/], classpath [public/], ServletContext [/]]
|
|
||||||
2025-07-16 09:04:22 [http-nio-19001-exec-5] DEBUG [o.s.web.servlet.handler.SimpleUrlHandlerMapping] - Mapped to ResourceHttpRequestHandler [classpath [META-INF/resources/], classpath [resources/], classpath [static/], classpath [public/], ServletContext [/]]
|
|
||||||
2025-07-16 09:04:22 [http-nio-19001-exec-5] DEBUG [o.s.web.servlet.handler.SimpleUrlHandlerMapping] - Mapped to ResourceHttpRequestHandler [classpath [META-INF/resources/], classpath [resources/], classpath [static/], classpath [public/], ServletContext [/]]
|
|
||||||
2025-07-16 09:04:22 [http-nio-19001-exec-5] DEBUG [o.s.web.servlet.handler.SimpleUrlHandlerMapping] - Mapped to ResourceHttpRequestHandler [classpath [META-INF/resources/], classpath [resources/], classpath [static/], classpath [public/], ServletContext [/]]
|
|
||||||
2025-07-16 09:04:22 [http-nio-19001-exec-5] DEBUG [o.s.web.servlet.handler.SimpleUrlHandlerMapping] - Mapped to ResourceHttpRequestHandler [classpath [META-INF/resources/], classpath [resources/], classpath [static/], classpath [public/], ServletContext [/]]
|
|
||||||
2025-07-16 09:04:22 [http-nio-19001-exec-5] DEBUG [o.s.web.servlet.handler.SimpleUrlHandlerMapping] - Mapped to ResourceHttpRequestHandler [classpath [META-INF/resources/], classpath [resources/], classpath [static/], classpath [public/], ServletContext [/]]
|
|
||||||
2025-07-16 09:04:22 [http-nio-19001-exec-5] DEBUG [o.s.web.servlet.handler.SimpleUrlHandlerMapping] - Mapped to ResourceHttpRequestHandler [classpath [META-INF/resources/], classpath [resources/], classpath [static/], classpath [public/], ServletContext [/]]
|
|
||||||
2025-07-16 09:04:22 [http-nio-19001-exec-5] DEBUG [o.s.web.servlet.handler.SimpleUrlHandlerMapping] - Mapped to ResourceHttpRequestHandler [classpath [META-INF/resources/], classpath [resources/], classpath [static/], classpath [public/], ServletContext [/]]
|
|
||||||
2025-07-16 09:04:22 [http-nio-19001-exec-5] DEBUG [o.s.web.servlet.handler.SimpleUrlHandlerMapping] - Mapped to ResourceHttpRequestHandler [classpath [META-INF/resources/], classpath [resources/], classpath [static/], classpath [public/], ServletContext [/]]
|
|
||||||
2025-07-16 09:04:22 [http-nio-19001-exec-5] DEBUG [o.s.web.servlet.handler.SimpleUrlHandlerMapping] - Mapped to ResourceHttpRequestHandler [classpath [META-INF/resources/], classpath [resources/], classpath [static/], classpath [public/], ServletContext [/]]
|
|
||||||
2025-07-16 09:04:22 [http-nio-19001-exec-5] DEBUG [o.s.web.servlet.handler.SimpleUrlHandlerMapping] - Mapped to ResourceHttpRequestHandler [classpath [META-INF/resources/], classpath [resources/], classpath [static/], classpath [public/], ServletContext [/]]
|
|
||||||
2025-07-16 09:04:22 [http-nio-19001-exec-5] DEBUG [o.s.web.servlet.handler.SimpleUrlHandlerMapping] - Mapped to ResourceHttpRequestHandler [classpath [META-INF/resources/], classpath [resources/], classpath [static/], classpath [public/], ServletContext [/]]
|
|
||||||
2025-07-16 09:04:22 [http-nio-19001-exec-5] DEBUG [o.s.web.servlet.handler.SimpleUrlHandlerMapping] - Mapped to ResourceHttpRequestHandler [classpath [META-INF/resources/], classpath [resources/], classpath [static/], classpath [public/], ServletContext [/]]
|
|
||||||
2025-07-16 09:04:22 [http-nio-19001-exec-5] DEBUG [o.s.w.s.m.m.a.RequestMappingHandlerMapping] - Mapped to org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController#error(HttpServletRequest)
|
|
||||||
2025-07-16 09:04:22 [http-nio-19001-exec-5] DEBUG [o.s.w.s.m.m.a.RequestMappingHandlerMapping] - Mapped to org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController#error(HttpServletRequest)
|
|
||||||
2025-07-16 09:04:22 [http-nio-19001-exec-5] DEBUG [o.s.w.s.m.m.a.RequestMappingHandlerMapping] - Mapped to org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController#error(HttpServletRequest)
|
|
||||||
2025-07-16 09:04:22 [http-nio-19001-exec-5] DEBUG [o.s.w.s.m.m.a.RequestMappingHandlerMapping] - Mapped to org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController#error(HttpServletRequest)
|
|
||||||
2025-07-16 09:04:22 [http-nio-19001-exec-5] DEBUG [o.s.w.s.m.m.a.RequestMappingHandlerMapping] - Mapped to org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController#error(HttpServletRequest)
|
|
||||||
2025-07-16 09:04:22 [http-nio-19001-exec-5] DEBUG [o.s.w.s.m.m.a.RequestMappingHandlerMapping] - Mapped to org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController#error(HttpServletRequest)
|
|
||||||
2025-07-16 09:04:22 [http-nio-19001-exec-5] DEBUG [o.s.w.s.m.m.a.RequestMappingHandlerMapping] - Mapped to org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController#error(HttpServletRequest)
|
|
||||||
2025-07-16 09:04:22 [http-nio-19001-exec-5] DEBUG [o.s.w.s.m.m.a.RequestMappingHandlerMapping] - Mapped to org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController#error(HttpServletRequest)
|
|
||||||
2025-07-16 09:04:22 [http-nio-19001-exec-5] DEBUG [o.s.w.s.m.m.a.RequestMappingHandlerMapping] - Mapped to org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController#error(HttpServletRequest)
|
|
||||||
2025-07-16 09:04:22 [http-nio-19001-exec-5] DEBUG [o.s.w.s.m.m.a.RequestMappingHandlerMapping] - Mapped to org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController#error(HttpServletRequest)
|
|
||||||
2025-07-16 09:04:22 [http-nio-19001-exec-5] DEBUG [o.s.w.s.m.m.a.RequestMappingHandlerMapping] - Mapped to org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController#error(HttpServletRequest)
|
|
||||||
2025-07-16 09:04:22 [http-nio-19001-exec-5] DEBUG [o.s.w.s.m.m.a.RequestMappingHandlerMapping] - Mapped to org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController#error(HttpServletRequest)
|
|
||||||
2025-07-16 09:04:22 [http-nio-19001-exec-5] DEBUG [o.s.w.s.m.m.a.RequestMappingHandlerMapping] - Mapped to org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController#error(HttpServletRequest)
|
|
||||||
2025-07-16 09:04:22 [http-nio-19001-exec-5] DEBUG [org.springframework.web.servlet.DispatcherServlet] - "ERROR" dispatch for GET "/error", parameters={}
|
|
||||||
2025-07-16 09:04:22 [http-nio-19001-exec-5] DEBUG [o.s.w.s.m.m.a.RequestMappingHandlerMapping] - Mapped to org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController#error(HttpServletRequest)
|
|
||||||
2025-07-16 09:04:22 [http-nio-19001-exec-5] DEBUG [o.s.w.s.m.m.annotation.HttpEntityMethodProcessor] - Using 'application/json', given [*/*] and supported [application/json, application/*+json]
|
|
||||||
2025-07-16 09:04:22 [http-nio-19001-exec-5] DEBUG [o.s.w.s.m.m.annotation.HttpEntityMethodProcessor] - Writing [{timestamp=Wed Jul 16 09:04:22 CST 2025, status=403, error=Forbidden, path=/user/actuator/health}]
|
|
||||||
2025-07-16 09:04:22 [http-nio-19001-exec-5] DEBUG [org.springframework.web.servlet.DispatcherServlet] - Exiting from "ERROR" dispatch, status 403
|
|
||||||
2025-07-16 09:46:48 [Thread-1] WARN [c.alibaba.nacos.common.http.HttpClientBeanHolder] - [HttpClientBeanHolder] Start destroying common HttpClient
|
|
||||||
2025-07-16 09:46:48 [Thread-7] WARN [com.alibaba.nacos.common.notify.NotifyCenter] - [NotifyCenter] Start destroying Publisher
|
|
||||||
2025-07-16 09:46:48 [Thread-7] WARN [com.alibaba.nacos.common.notify.NotifyCenter] - [NotifyCenter] Destruction of the end
|
|
||||||
2025-07-16 09:46:48 [Thread-1] WARN [c.alibaba.nacos.common.http.HttpClientBeanHolder] - [HttpClientBeanHolder] Destruction of the end
|
|
||||||
2025-07-16 09:46:49 [SpringApplicationShutdownHook] INFO [com.zaxxer.hikari.HikariDataSource] - HikariPool-1 - Shutdown initiated...
|
|
||||||
2025-07-16 09:46:49 [SpringApplicationShutdownHook] INFO [com.zaxxer.hikari.HikariDataSource] - HikariPool-1 - Shutdown completed.
|
|
||||||
@@ -1,99 +0,0 @@
|
|||||||
[INFO] Scanning for projects...
|
|
||||||
[INFO]
|
|
||||||
[INFO] -------------------< com.emotionmuseum:emotion-user >-------------------
|
|
||||||
[INFO] Building emotion-user 1.0.0
|
|
||||||
[INFO] from pom.xml
|
|
||||||
[INFO] --------------------------------[ jar ]---------------------------------
|
|
||||||
[INFO]
|
|
||||||
[INFO] >>> spring-boot:3.0.2:run (default-cli) > test-compile @ emotion-user >>>
|
|
||||||
[WARNING] The artifact mysql:mysql-connector-java:jar:8.0.33 has been relocated to com.mysql:mysql-connector-j:jar:8.0.33: MySQL Connector/J artifacts moved to reverse-DNS compliant Maven 2+ coordinates.
|
|
||||||
[INFO]
|
|
||||||
[INFO] --- resources:3.3.1:resources (default-resources) @ emotion-user ---
|
|
||||||
[INFO] Copying 2 resources from src/main/resources to target/classes
|
|
||||||
[INFO]
|
|
||||||
[INFO] --- compiler:3.10.1:compile (default-compile) @ emotion-user ---
|
|
||||||
[INFO] Nothing to compile - all classes are up to date
|
|
||||||
[INFO]
|
|
||||||
[INFO] --- resources:3.3.1:testResources (default-testResources) @ emotion-user ---
|
|
||||||
[INFO] skip non existing resourceDirectory /Users/huazhongmin/peanut/AppleDevelop/EmotionMuseum/backend/emotion-user/src/test/resources
|
|
||||||
[INFO]
|
|
||||||
[INFO] --- compiler:3.10.1:testCompile (default-testCompile) @ emotion-user ---
|
|
||||||
[INFO] No sources to compile
|
|
||||||
[INFO]
|
|
||||||
[INFO] <<< spring-boot:3.0.2:run (default-cli) < test-compile @ emotion-user <<<
|
|
||||||
[INFO]
|
|
||||||
[INFO]
|
|
||||||
[INFO] --- spring-boot:3.0.2:run (default-cli) @ emotion-user ---
|
|
||||||
[INFO] Attaching agents: []
|
|
||||||
[2m2025-07-13T08:45:36.142+08:00[0;39m [33m WARN[0;39m [35m6791[0;39m [2m---[0;39m [2m[ restartedMain][0;39m [36mc.a.nacos.client.logging.NacosLogging [0;39m [2m:[0;39m Load Logback Configuration of Nacos fail, message: Could not initialize Logback Nacos logging from classpath:nacos-logback.xml
|
|
||||||
[2m2025-07-13T08:45:36.195+08:00[0;39m [33m WARN[0;39m [35m6791[0;39m [2m---[0;39m [2m[ restartedMain][0;39m [36mc.a.nacos.client.logging.NacosLogging [0;39m [2m:[0;39m Load Logback Configuration of Nacos fail, message: Could not initialize Logback Nacos logging from classpath:nacos-logback.xml
|
|
||||||
[2m2025-07-13T08:45:36.221+08:00[0;39m [32m INFO[0;39m [35m6791[0;39m [2m---[0;39m [2m[ restartedMain][0;39m [36m.e.DevToolsPropertyDefaultsPostProcessor[0;39m [2m:[0;39m Devtools property defaults active! Set 'spring.devtools.add-properties' to 'false' to disable
|
|
||||||
[2m2025-07-13T08:45:36.387+08:00[0;39m [33m WARN[0;39m [35m6791[0;39m [2m---[0;39m [2m[ restartedMain][0;39m [36mc.a.nacos.client.logging.NacosLogging [0;39m [2m:[0;39m Load Logback Configuration of Nacos fail, message: Could not initialize Logback Nacos logging from classpath:nacos-logback.xml
|
|
||||||
[2m2025-07-13T08:45:36.388+08:00[0;39m [32m INFO[0;39m [35m6791[0;39m [2m---[0;39m [2m[ restartedMain][0;39m [36mc.alibaba.nacos.client.utils.ParamUtil [0;39m [2m:[0;39m [settings] [req-serv] nacos-server port:8848
|
|
||||||
[2m2025-07-13T08:45:36.389+08:00[0;39m [32m INFO[0;39m [35m6791[0;39m [2m---[0;39m [2m[ restartedMain][0;39m [36mc.alibaba.nacos.client.utils.ParamUtil [0;39m [2m:[0;39m [settings] [http-client] connect timeout:1000
|
|
||||||
[2m2025-07-13T08:45:36.391+08:00[0;39m [32m INFO[0;39m [35m6791[0;39m [2m---[0;39m [2m[ restartedMain][0;39m [36mc.alibaba.nacos.client.utils.ParamUtil [0;39m [2m:[0;39m PER_TASK_CONFIG_SIZE: 3000.0
|
|
||||||
[2m2025-07-13T08:45:36.437+08:00[0;39m [32m INFO[0;39m [35m6791[0;39m [2m---[0;39m [2m[ restartedMain][0;39m [36mc.a.n.p.a.s.c.ClientAuthPluginManager [0;39m [2m:[0;39m [ClientAuthPluginManager] Load ClientAuthService com.alibaba.nacos.client.auth.impl.NacosClientAuthServiceImpl success.
|
|
||||||
[2m2025-07-13T08:45:36.437+08:00[0;39m [32m INFO[0;39m [35m6791[0;39m [2m---[0;39m [2m[ restartedMain][0;39m [36mc.a.n.p.a.s.c.ClientAuthPluginManager [0;39m [2m:[0;39m [ClientAuthPluginManager] Load ClientAuthService com.alibaba.nacos.client.auth.ram.RamClientAuthServiceImpl success.
|
|
||||||
[2m2025-07-13T08:45:36.455+08:00[0;39m [32m INFO[0;39m [35m6791[0;39m [2m---[0;39m [2m[ restartedMain][0;39m [36mc.a.n.c.a.r.identify.CredentialWatcher [0;39m [2m:[0;39m null No credential found
|
|
||||||
2025-07-13 08:45:36 [restartedMain] WARN [com.alibaba.nacos.client.logging.NacosLogging] - Load Logback Configuration of Nacos fail, message: Could not initialize Logback Nacos logging from classpath:nacos-logback.xml
|
|
||||||
|
|
||||||
. ____ _ __ _ _
|
|
||||||
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
|
|
||||||
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
|
|
||||||
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
|
|
||||||
' |____| .__|_| |_|_| |_\__, | / / / /
|
|
||||||
=========|_|==============|___/=/_/_/_/
|
|
||||||
[32m :: Spring Boot :: [39m [2m (v3.0.2)[0;39m
|
|
||||||
|
|
||||||
2025-07-13 08:45:36 [restartedMain] INFO [c.a.n.client.config.impl.LocalConfigInfoProcessor] - LOCAL_SNAPSHOT_PATH:/Users/huazhongmin/nacos/config
|
|
||||||
2025-07-13 08:45:36 [restartedMain] INFO [com.alibaba.nacos.common.remote.client] - [RpcClientFactory] create a new rpc client of d7ae7ba9-4fec-4362-a0ff-0d9a56ce7c0d_config-0
|
|
||||||
2025-07-13 08:45:36 [restartedMain] INFO [com.alibaba.nacos.common.remote.client] - [d7ae7ba9-4fec-4362-a0ff-0d9a56ce7c0d_config-0] Register server push request handler:com.alibaba.nacos.client.config.impl.ClientWorker$ConfigRpcTransportClient$$Lambda/0x000000012e318670
|
|
||||||
2025-07-13 08:45:36 [restartedMain] INFO [com.alibaba.nacos.common.remote.client] - [d7ae7ba9-4fec-4362-a0ff-0d9a56ce7c0d_config-0] Register server push request handler:com.alibaba.nacos.client.config.impl.ClientWorker$ConfigRpcTransportClient$$Lambda/0x000000012e318aa0
|
|
||||||
2025-07-13 08:45:36 [restartedMain] INFO [com.alibaba.nacos.common.remote.client] - [d7ae7ba9-4fec-4362-a0ff-0d9a56ce7c0d_config-0] Registry connection listener to current client:com.alibaba.nacos.client.config.impl.ClientWorker$ConfigRpcTransportClient$1
|
|
||||||
2025-07-13 08:45:36 [restartedMain] INFO [com.alibaba.nacos.common.remote.client] - [d7ae7ba9-4fec-4362-a0ff-0d9a56ce7c0d_config-0] RpcClient init, ServerListFactory = com.alibaba.nacos.client.config.impl.ClientWorker$ConfigRpcTransportClient$2
|
|
||||||
2025-07-13 08:45:36 [restartedMain] INFO [com.alibaba.nacos.common.remote.client] - [d7ae7ba9-4fec-4362-a0ff-0d9a56ce7c0d_config-0] Try to connect to server on start up, server: {serverIp = '127.0.0.1', server main port = 8848}
|
|
||||||
2025-07-13 08:45:36 [restartedMain] INFO [c.a.nacos.common.remote.client.grpc.GrpcClient] - grpc client connection server:127.0.0.1 ip,serverPort:9848,grpcTslConfig:{"sslProvider":"OPENSSL","enableTls":false,"mutualAuthEnable":false,"trustAll":false}
|
|
||||||
2025-07-13 08:45:37 [restartedMain] INFO [com.alibaba.nacos.common.remote.client] - [d7ae7ba9-4fec-4362-a0ff-0d9a56ce7c0d_config-0] Success to connect to server [127.0.0.1:8848] on start up, connectionId = 1752367537016_127.0.0.1_51165
|
|
||||||
2025-07-13 08:45:37 [com.alibaba.nacos.client.remote.worker] INFO [com.alibaba.nacos.common.remote.client] - [d7ae7ba9-4fec-4362-a0ff-0d9a56ce7c0d_config-0] Notify connected event to listeners.
|
|
||||||
2025-07-13 08:45:37 [com.alibaba.nacos.client.remote.worker] INFO [com.alibaba.nacos.client.config.impl.ClientWorker] - [d7ae7ba9-4fec-4362-a0ff-0d9a56ce7c0d_config-0] Connected,notify listen context...
|
|
||||||
2025-07-13 08:45:37 [restartedMain] INFO [com.alibaba.nacos.common.remote.client] - [d7ae7ba9-4fec-4362-a0ff-0d9a56ce7c0d_config-0] Register server push request handler:com.alibaba.nacos.common.remote.client.RpcClient$ConnectResetRequestHandler
|
|
||||||
2025-07-13 08:45:37 [restartedMain] INFO [com.alibaba.nacos.common.remote.client] - [d7ae7ba9-4fec-4362-a0ff-0d9a56ce7c0d_config-0] Register server push request handler:com.alibaba.nacos.common.remote.client.RpcClient$$Lambda/0x000000012e4a2a18
|
|
||||||
2025-07-13 08:45:37 [restartedMain] INFO [com.alibaba.nacos.client.config.impl.Limiter] - limitTime:5.0
|
|
||||||
2025-07-13 08:45:37 [restartedMain] WARN [c.a.cloud.nacos.client.NacosPropertySourceBuilder] - Ignore the empty nacos configuration and get it based on dataId[emotion-user] & group[DEFAULT_GROUP]
|
|
||||||
2025-07-13 08:45:37 [restartedMain] WARN [c.a.cloud.nacos.client.NacosPropertySourceBuilder] - Ignore the empty nacos configuration and get it based on dataId[emotion-user.properties] & group[DEFAULT_GROUP]
|
|
||||||
2025-07-13 08:45:37 [restartedMain] WARN [c.a.cloud.nacos.client.NacosPropertySourceBuilder] - Ignore the empty nacos configuration and get it based on dataId[emotion-user-dev.properties] & group[DEFAULT_GROUP]
|
|
||||||
2025-07-13 08:45:37 [restartedMain] INFO [o.s.c.b.c.PropertySourceBootstrapConfiguration] - Located property source: [BootstrapPropertySource {name='bootstrapProperties-emotion-user-dev.properties,DEFAULT_GROUP'}, BootstrapPropertySource {name='bootstrapProperties-emotion-user.properties,DEFAULT_GROUP'}, BootstrapPropertySource {name='bootstrapProperties-emotion-user,DEFAULT_GROUP'}]
|
|
||||||
2025-07-13 08:45:37 [restartedMain] WARN [com.alibaba.nacos.client.logging.NacosLogging] - Load Logback Configuration of Nacos fail, message: Could not initialize Logback Nacos logging from classpath:nacos-logback.xml
|
|
||||||
2025-07-13 08:45:37 [restartedMain] INFO [com.emotionmuseum.user.UserApplication] - The following 1 profile is active: "dev"
|
|
||||||
2025-07-13 08:45:37 [restartedMain] INFO [o.s.d.r.config.RepositoryConfigurationDelegate] - Multiple Spring Data modules found, entering strict repository configuration mode
|
|
||||||
2025-07-13 08:45:37 [restartedMain] INFO [o.s.d.r.config.RepositoryConfigurationDelegate] - Bootstrapping Spring Data Redis repositories in DEFAULT mode.
|
|
||||||
2025-07-13 08:45:37 [restartedMain] INFO [o.s.d.r.config.RepositoryConfigurationDelegate] - Finished Spring Data repository scanning in 7 ms. Found 0 Redis repository interfaces.
|
|
||||||
2025-07-13 08:45:38 [restartedMain] INFO [o.springframework.cloud.context.scope.GenericScope] - BeanFactory id=7e3acccb-1d48-37df-bf0c-1c15c4e1d510
|
|
||||||
2025-07-13 08:45:38 [restartedMain] INFO [o.s.boot.web.embedded.tomcat.TomcatWebServer] - Tomcat initialized with port(s): 9001 (http)
|
|
||||||
2025-07-13 08:45:38 [restartedMain] INFO [org.apache.catalina.core.StandardService] - Starting service [Tomcat]
|
|
||||||
2025-07-13 08:45:38 [restartedMain] INFO [org.apache.catalina.core.StandardEngine] - Starting Servlet engine: [Apache Tomcat/10.1.5]
|
|
||||||
2025-07-13 08:45:38 [restartedMain] INFO [o.a.c.core.ContainerBase.[Tomcat].[localhost].[/]] - Initializing Spring embedded WebApplicationContext
|
|
||||||
2025-07-13 08:45:38 [restartedMain] INFO [o.s.b.w.s.c.ServletWebServerApplicationContext] - Root WebApplicationContext: initialization completed in 1416 ms
|
|
||||||
Logging initialized using 'class org.apache.ibatis.logging.stdout.StdOutImpl' adapter.
|
|
||||||
Registered plugin: 'com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor@7d6aa779'
|
|
||||||
Parsed mapper file: 'file [/Users/huazhongmin/peanut/AppleDevelop/EmotionMuseum/backend/emotion-user/target/classes/mapper/UserMapper.xml]'
|
|
||||||
_ _ |_ _ _|_. ___ _ | _
|
|
||||||
| | |\/|_)(_| | |_\ |_)||_|_\
|
|
||||||
/ |
|
|
||||||
3.5.3.1
|
|
||||||
2025-07-13 08:45:39 [restartedMain] WARN [o.s.b.d.autoconfigure.OptionalLiveReloadServer] - Unable to start LiveReload server
|
|
||||||
2025-07-13 08:45:39 [restartedMain] INFO [o.s.b.actuate.endpoint.web.EndpointLinksResolver] - Exposing 3 endpoint(s) beneath base path '/actuator'
|
|
||||||
2025-07-13 08:45:39 [restartedMain] INFO [o.s.boot.web.embedded.tomcat.TomcatWebServer] - Tomcat started on port(s): 9001 (http) with context path ''
|
|
||||||
2025-07-13 08:45:39 [restartedMain] INFO [com.emotionmuseum.user.UserApplication] - Started UserApplication in 3.951 seconds (process running for 4.401)
|
|
||||||
2025-07-13 08:45:40 [http-nio-9001-exec-1] INFO [o.a.c.core.ContainerBase.[Tomcat].[localhost].[/]] - Initializing Spring DispatcherServlet 'dispatcherServlet'
|
|
||||||
2025-07-13 08:45:40 [http-nio-9001-exec-1] INFO [org.springframework.web.servlet.DispatcherServlet] - Initializing Servlet 'dispatcherServlet'
|
|
||||||
2025-07-13 08:45:40 [http-nio-9001-exec-1] INFO [org.springframework.web.servlet.DispatcherServlet] - Completed initialization in 1 ms
|
|
||||||
2025-07-13 08:45:40 [http-nio-9001-exec-1] INFO [com.zaxxer.hikari.HikariDataSource] - HikariPool-1 - Starting...
|
|
||||||
2025-07-13 08:45:40 [http-nio-9001-exec-1] INFO [com.zaxxer.hikari.pool.HikariPool] - HikariPool-1 - Added connection com.mysql.cj.jdbc.ConnectionImpl@3eaf6ca4
|
|
||||||
2025-07-13 08:45:40 [http-nio-9001-exec-1] INFO [com.zaxxer.hikari.HikariDataSource] - HikariPool-1 - Start completed.
|
|
||||||
2025-07-13 08:49:25 [Thread-4] WARN [c.alibaba.nacos.common.http.HttpClientBeanHolder] - [HttpClientBeanHolder] Start destroying common HttpClient
|
|
||||||
2025-07-13 08:49:25 [Thread-10] WARN [com.alibaba.nacos.common.notify.NotifyCenter] - [NotifyCenter] Start destroying Publisher
|
|
||||||
2025-07-13 08:49:25 [Thread-10] WARN [com.alibaba.nacos.common.notify.NotifyCenter] - [NotifyCenter] Destruction of the end
|
|
||||||
2025-07-13 08:49:25 [Thread-4] WARN [c.alibaba.nacos.common.http.HttpClientBeanHolder] - [HttpClientBeanHolder] Destruction of the end
|
|
||||||
2025-07-13 08:49:25 [SpringApplicationShutdownHook] INFO [com.zaxxer.hikari.HikariDataSource] - HikariPool-1 - Shutdown initiated...
|
|
||||||
2025-07-13 08:49:25 [SpringApplicationShutdownHook] INFO [com.zaxxer.hikari.HikariDataSource] - HikariPool-1 - Shutdown completed.
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
target/emotion-gateway-1.0.0.jar中没有主清单属性
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
63251
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
2025-07-15T17:59:48.763+08:00 WARN 62881 --- [ main] c.a.nacos.client.logging.NacosLogging : Load Logback Configuration of Nacos fail, message: Could not initialize Logback Nacos logging from classpath:nacos-logback.xml
|
|
||||||
2025-07-15T17:59:48.862+08:00 WARN 62881 --- [ main] c.a.nacos.client.logging.NacosLogging : Load Logback Configuration of Nacos fail, message: Could not initialize Logback Nacos logging from classpath:nacos-logback.xml
|
|
||||||
2025-07-15 17:59:49 [main] WARN com.alibaba.nacos.client.logging.NacosLogging - Load Logback Configuration of Nacos fail, message: Could not initialize Logback Nacos logging from classpath:nacos-logback.xml
|
|
||||||
|
|
||||||
. ____ _ __ _ _
|
|
||||||
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
|
|
||||||
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
|
|
||||||
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
|
|
||||||
' |____| .__|_| |_|_| |_\__, | / / / /
|
|
||||||
=========|_|==============|___/=/_/_/_/
|
|
||||||
:: Spring Boot :: (v3.0.2)
|
|
||||||
|
|
||||||
2025-07-15 17:59:49 [main] WARN com.alibaba.nacos.client.logging.NacosLogging - Load Logback Configuration of Nacos fail, message: Could not initialize Logback Nacos logging from classpath:nacos-logback.xml
|
|
||||||
2025-07-15 17:59:49 [main] INFO com.emotionmuseum.user.UserApplication - The following 1 profile is active: "local"
|
|
||||||
2025-07-15 17:59:50 [main] WARN o.s.b.w.s.c.AnnotationConfigServletWebServerApplicationContext - Exception encountered during context initialization - cancelling refresh attempt: org.springframework.beans.factory.support.BeanDefinitionOverrideException: Invalid bean definition with name 'snowflakeIdGenerator' defined in class path resource [com/emotionmuseum/common/config/SnowflakeConfig.class]: Cannot register bean definition [Root bean: class [null]; scope=; abstract=false; lazyInit=null; autowireMode=3; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=snowflakeConfig; factoryMethodName=snowflakeIdGenerator; initMethodNames=null; destroyMethodNames=[(inferred)]; defined in class path resource [com/emotionmuseum/common/config/SnowflakeConfig.class]] for bean 'snowflakeIdGenerator' since there is already [Generic bean: class [com.emotionmuseum.common.util.SnowflakeIdGenerator]; scope=singleton; abstract=false; lazyInit=null; autowireMode=0; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=null; factoryMethodName=null; initMethodNames=null; destroyMethodNames=null; defined in URL [jar:file:/Users/huazhongmin/peanut/AppleDevelop/EmotionMuseum/backend/emotion-user/target/emotion-user-1.0.0.jar!/BOOT-INF/lib/emotion-common-1.0.0.jar!/com/emotionmuseum/common/util/SnowflakeIdGenerator.class]] bound.
|
|
||||||
2025-07-15 17:59:50 [main] INFO o.s.b.a.logging.ConditionEvaluationReportLogger -
|
|
||||||
|
|
||||||
Error starting ApplicationContext. To display the condition evaluation report re-run your application with 'debug' enabled.
|
|
||||||
2025-07-15 17:59:50 [main] ERROR o.s.b.diagnostics.LoggingFailureAnalysisReporter -
|
|
||||||
|
|
||||||
***************************
|
|
||||||
APPLICATION FAILED TO START
|
|
||||||
***************************
|
|
||||||
|
|
||||||
Description:
|
|
||||||
|
|
||||||
The bean 'snowflakeIdGenerator', defined in class path resource [com/emotionmuseum/common/config/SnowflakeConfig.class], could not be registered. A bean with that name has already been defined in URL [jar:file:/Users/huazhongmin/peanut/AppleDevelop/EmotionMuseum/backend/emotion-user/target/emotion-user-1.0.0.jar!/BOOT-INF/lib/emotion-common-1.0.0.jar!/com/emotionmuseum/common/util/SnowflakeIdGenerator.class] and overriding is disabled.
|
|
||||||
|
|
||||||
Action:
|
|
||||||
|
|
||||||
Consider renaming one of the beans or enabling overriding by setting spring.main.allow-bean-definition-overriding=true
|
|
||||||
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
62881
|
|
||||||
@@ -1,134 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# 情感博物馆微服务启动脚本
|
|
||||||
# 支持环境参数控制
|
|
||||||
|
|
||||||
# 默认环境
|
|
||||||
DEFAULT_ENV="local"
|
|
||||||
ENV=${1:-$DEFAULT_ENV}
|
|
||||||
|
|
||||||
echo "=========================================="
|
|
||||||
echo "情感博物馆微服务启动脚本"
|
|
||||||
echo "启动环境: $ENV"
|
|
||||||
echo "=========================================="
|
|
||||||
|
|
||||||
# 检查基础服务
|
|
||||||
check_services() {
|
|
||||||
echo "📊 检查基础服务..."
|
|
||||||
|
|
||||||
if ! nc -z localhost 3306; then
|
|
||||||
echo "❌ MySQL服务未启动,请先启动MySQL服务"
|
|
||||||
echo "可以使用: brew services start mysql"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if ! nc -z localhost 6379; then
|
|
||||||
echo "❌ Redis服务未启动,请先启动Redis服务"
|
|
||||||
echo "可以使用: brew services start redis"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "✅ 基础服务检查通过"
|
|
||||||
}
|
|
||||||
|
|
||||||
# 启动服务函数
|
|
||||||
start_service() {
|
|
||||||
local service_name=$1
|
|
||||||
local port=$2
|
|
||||||
local description=$3
|
|
||||||
|
|
||||||
echo "🚀 启动 $description ($service_name:$port)..."
|
|
||||||
|
|
||||||
cd $service_name
|
|
||||||
|
|
||||||
# 检查端口是否被占用
|
|
||||||
if lsof -Pi :$port -sTCP:LISTEN -t >/dev/null ; then
|
|
||||||
echo "⚠️ 端口 $port 已被占用,跳过 $service_name"
|
|
||||||
cd ..
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 编译项目
|
|
||||||
echo "📦 编译 $service_name..."
|
|
||||||
mvn clean package -DskipTests -q
|
|
||||||
|
|
||||||
if [ $? -ne 0 ]; then
|
|
||||||
echo "❌ $service_name 编译失败"
|
|
||||||
cd ..
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 启动服务
|
|
||||||
echo "🔄 启动 $service_name (环境: $ENV)..."
|
|
||||||
nohup java -jar -Dspring.profiles.active=$ENV target/$service_name-1.0.0.jar > ../logs/$service_name-$ENV.log 2>&1 &
|
|
||||||
|
|
||||||
# 记录PID
|
|
||||||
echo $! > ../logs/$service_name.pid
|
|
||||||
|
|
||||||
echo "✅ $service_name 启动完成,PID: $!"
|
|
||||||
echo "📋 日志文件: logs/$service_name-$ENV.log"
|
|
||||||
|
|
||||||
cd ..
|
|
||||||
|
|
||||||
# 等待服务启动
|
|
||||||
sleep 3
|
|
||||||
}
|
|
||||||
|
|
||||||
# 创建日志目录
|
|
||||||
mkdir -p logs
|
|
||||||
|
|
||||||
# 检查基础服务
|
|
||||||
check_services
|
|
||||||
|
|
||||||
# 服务列表
|
|
||||||
services=(
|
|
||||||
"emotion-user:19001:用户服务"
|
|
||||||
"emotion-ai:19002:AI服务"
|
|
||||||
"emotion-websocket:19007:WebSocket聊天服务"
|
|
||||||
"emotion-gateway:19000:网关服务"
|
|
||||||
)
|
|
||||||
|
|
||||||
echo "🚀 开始启动核心服务 (环境: $ENV)..."
|
|
||||||
|
|
||||||
# 按顺序启动服务
|
|
||||||
for service_info in "${services[@]}"; do
|
|
||||||
IFS=':' read -r service_name port description <<< "$service_info"
|
|
||||||
start_service "$service_name" "$port" "$description"
|
|
||||||
done
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "🎉 核心服务启动完成!"
|
|
||||||
echo ""
|
|
||||||
echo "📋 服务列表:"
|
|
||||||
for service_info in "${services[@]}"; do
|
|
||||||
IFS=':' read -r service_name port description <<< "$service_info"
|
|
||||||
echo " $description: http://localhost:$port"
|
|
||||||
done
|
|
||||||
echo ""
|
|
||||||
echo "📝 使用 './stop-services.sh' 停止所有服务"
|
|
||||||
echo "📝 查看日志: tail -f logs/服务名-$ENV.log"
|
|
||||||
|
|
||||||
# 等待服务完全启动
|
|
||||||
echo ""
|
|
||||||
echo "📊 等待服务完全启动..."
|
|
||||||
sleep 15
|
|
||||||
|
|
||||||
# 检查服务状态
|
|
||||||
echo "📊 检查服务状态..."
|
|
||||||
for service_info in "${services[@]}"; do
|
|
||||||
IFS=':' read -r service_name port description <<< "$service_info"
|
|
||||||
|
|
||||||
if curl -s http://localhost:$port/actuator/health >/dev/null 2>&1; then
|
|
||||||
echo "✅ $description 运行正常"
|
|
||||||
else
|
|
||||||
echo "⚠️ $description 可能未完全启动,请查看日志: tail -f logs/$service_name-$ENV.log"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "🎉 启动完成!环境: $ENV"
|
|
||||||
echo ""
|
|
||||||
echo "使用方法:"
|
|
||||||
echo " ./start-services.sh # 使用默认local环境启动"
|
|
||||||
echo " ./start-services.sh dev # 使用dev环境启动"
|
|
||||||
echo " ./start-services.sh prod # 使用prod环境启动"
|
|
||||||
@@ -1,92 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# 情绪博物馆微服务停止脚本
|
|
||||||
# 作者: emotion-museum
|
|
||||||
# 日期: 2025-07-12
|
|
||||||
|
|
||||||
echo "=========================================="
|
|
||||||
echo "情绪博物馆微服务停止脚本"
|
|
||||||
echo "=========================================="
|
|
||||||
|
|
||||||
# 停止服务函数
|
|
||||||
stop_service() {
|
|
||||||
local service_name=$1
|
|
||||||
local pid_file="logs/${service_name}.pid"
|
|
||||||
|
|
||||||
if [ -f "$pid_file" ]; then
|
|
||||||
local pid=$(cat $pid_file)
|
|
||||||
if ps -p $pid > /dev/null 2>&1; then
|
|
||||||
echo "停止 $service_name 服务 (PID: $pid)..."
|
|
||||||
kill $pid
|
|
||||||
|
|
||||||
# 等待进程结束
|
|
||||||
local count=0
|
|
||||||
while [ $count -lt 10 ]; do
|
|
||||||
if ! ps -p $pid > /dev/null 2>&1; then
|
|
||||||
echo "$service_name 服务已停止"
|
|
||||||
rm -f $pid_file
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
sleep 1
|
|
||||||
count=$((count + 1))
|
|
||||||
done
|
|
||||||
|
|
||||||
# 强制杀死进程
|
|
||||||
echo "强制停止 $service_name 服务..."
|
|
||||||
kill -9 $pid 2>/dev/null
|
|
||||||
rm -f $pid_file
|
|
||||||
else
|
|
||||||
echo "$service_name 服务未运行"
|
|
||||||
rm -f $pid_file
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
echo "$service_name 服务PID文件不存在"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# 停止所有微服务
|
|
||||||
echo "开始停止微服务..."
|
|
||||||
|
|
||||||
# 停止统计分析服务
|
|
||||||
stop_service "emotion-stats"
|
|
||||||
|
|
||||||
# 停止成就奖励服务
|
|
||||||
stop_service "emotion-reward"
|
|
||||||
|
|
||||||
# 停止地图探索服务
|
|
||||||
stop_service "emotion-explore"
|
|
||||||
|
|
||||||
# 停止成长课题服务
|
|
||||||
stop_service "emotion-growth"
|
|
||||||
|
|
||||||
# 停止情绪记录服务
|
|
||||||
stop_service "emotion-record"
|
|
||||||
|
|
||||||
# 停止AI对话服务
|
|
||||||
stop_service "emotion-ai"
|
|
||||||
|
|
||||||
# 停止WebSocket聊天服务
|
|
||||||
stop_service "emotion-websocket"
|
|
||||||
|
|
||||||
# 停止用户服务
|
|
||||||
stop_service "emotion-user"
|
|
||||||
|
|
||||||
# 停止网关服务
|
|
||||||
stop_service "emotion-gateway"
|
|
||||||
|
|
||||||
# 清理可能残留的Java进程
|
|
||||||
echo "清理残留进程..."
|
|
||||||
pkill -f "emotion-gateway"
|
|
||||||
pkill -f "emotion-user"
|
|
||||||
pkill -f "emotion-ai"
|
|
||||||
pkill -f "emotion-websocket"
|
|
||||||
pkill -f "emotion-record"
|
|
||||||
pkill -f "emotion-growth"
|
|
||||||
pkill -f "emotion-explore"
|
|
||||||
pkill -f "emotion-reward"
|
|
||||||
pkill -f "emotion-stats"
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "=========================================="
|
|
||||||
echo "所有微服务已停止!"
|
|
||||||
echo "=========================================="
|
|
||||||
@@ -1,90 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# 认证功能测试脚本
|
|
||||||
# 用于测试emotion-auth模块的认证功能
|
|
||||||
|
|
||||||
BASE_URL="http://localhost:19000/api/auth"
|
|
||||||
CONTENT_TYPE="Content-Type: application/json"
|
|
||||||
|
|
||||||
echo "========================================="
|
|
||||||
echo "开始测试emotion-auth模块认证功能"
|
|
||||||
echo "========================================="
|
|
||||||
|
|
||||||
# 1. 测试获取验证码
|
|
||||||
echo "1. 测试获取验证码..."
|
|
||||||
CAPTCHA_RESPONSE=$(curl -s -X GET "${BASE_URL}/../captcha/generate")
|
|
||||||
echo "验证码响应: $CAPTCHA_RESPONSE"
|
|
||||||
|
|
||||||
# 提取验证码ID(假设返回JSON格式)
|
|
||||||
CAPTCHA_ID=$(echo $CAPTCHA_RESPONSE | grep -o '"key":"[^"]*"' | cut -d'"' -f4)
|
|
||||||
echo "验证码ID: $CAPTCHA_ID"
|
|
||||||
|
|
||||||
# 2. 测试用户注册
|
|
||||||
echo -e "\n2. 测试用户注册..."
|
|
||||||
REGISTER_DATA='{
|
|
||||||
"account": "testuser001",
|
|
||||||
"password": "123456",
|
|
||||||
"confirmPassword": "123456",
|
|
||||||
"email": "test@example.com",
|
|
||||||
"captcha": "1234",
|
|
||||||
"captchaId": "'$CAPTCHA_ID'"
|
|
||||||
}'
|
|
||||||
|
|
||||||
REGISTER_RESPONSE=$(curl -s -X POST "${BASE_URL}/register" \
|
|
||||||
-H "$CONTENT_TYPE" \
|
|
||||||
-d "$REGISTER_DATA")
|
|
||||||
echo "注册响应: $REGISTER_RESPONSE"
|
|
||||||
|
|
||||||
# 3. 测试用户登录
|
|
||||||
echo -e "\n3. 测试用户登录..."
|
|
||||||
LOGIN_DATA='{
|
|
||||||
"account": "testuser001",
|
|
||||||
"password": "123456",
|
|
||||||
"captcha": "1234",
|
|
||||||
"captchaId": "'$CAPTCHA_ID'"
|
|
||||||
}'
|
|
||||||
|
|
||||||
LOGIN_RESPONSE=$(curl -s -X POST "${BASE_URL}/login" \
|
|
||||||
-H "$CONTENT_TYPE" \
|
|
||||||
-d "$LOGIN_DATA")
|
|
||||||
echo "登录响应: $LOGIN_RESPONSE"
|
|
||||||
|
|
||||||
# 提取访问Token
|
|
||||||
ACCESS_TOKEN=$(echo $LOGIN_RESPONSE | grep -o '"accessToken":"[^"]*"' | cut -d'"' -f4)
|
|
||||||
echo "访问Token: $ACCESS_TOKEN"
|
|
||||||
|
|
||||||
# 4. 测试获取用户信息
|
|
||||||
if [ ! -z "$ACCESS_TOKEN" ]; then
|
|
||||||
echo -e "\n4. 测试获取用户信息..."
|
|
||||||
USER_INFO_RESPONSE=$(curl -s -X GET "${BASE_URL}/user-info" \
|
|
||||||
-H "$CONTENT_TYPE" \
|
|
||||||
-H "Authorization: Bearer $ACCESS_TOKEN")
|
|
||||||
echo "用户信息响应: $USER_INFO_RESPONSE"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 5. 测试验证Token
|
|
||||||
if [ ! -z "$ACCESS_TOKEN" ]; then
|
|
||||||
echo -e "\n5. 测试验证Token..."
|
|
||||||
VALIDATE_RESPONSE=$(curl -s -X GET "${BASE_URL}/validate-token" \
|
|
||||||
-H "$CONTENT_TYPE" \
|
|
||||||
-H "Authorization: Bearer $ACCESS_TOKEN")
|
|
||||||
echo "Token验证响应: $VALIDATE_RESPONSE"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 6. 测试检查账号是否存在
|
|
||||||
echo -e "\n6. 测试检查账号是否存在..."
|
|
||||||
CHECK_ACCOUNT_RESPONSE=$(curl -s -X GET "${BASE_URL}/check-account?account=testuser001")
|
|
||||||
echo "检查账号响应: $CHECK_ACCOUNT_RESPONSE"
|
|
||||||
|
|
||||||
# 7. 测试用户登出
|
|
||||||
if [ ! -z "$ACCESS_TOKEN" ]; then
|
|
||||||
echo -e "\n7. 测试用户登出..."
|
|
||||||
LOGOUT_RESPONSE=$(curl -s -X POST "${BASE_URL}/logout?userId=test-user-id" \
|
|
||||||
-H "$CONTENT_TYPE" \
|
|
||||||
-H "Authorization: Bearer $ACCESS_TOKEN")
|
|
||||||
echo "登出响应: $LOGOUT_RESPONSE"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo -e "\n========================================="
|
|
||||||
echo "认证功能测试完成"
|
|
||||||
echo "========================================="
|
|
||||||
@@ -1,255 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# 批量更新所有微服务的Nacos配置
|
|
||||||
# 为每个服务创建本地、测试、生产环境的配置文件
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
# 颜色定义
|
|
||||||
RED='\033[0;31m'
|
|
||||||
GREEN='\033[0;32m'
|
|
||||||
YELLOW='\033[1;33m'
|
|
||||||
BLUE='\033[0;34m'
|
|
||||||
NC='\033[0m' # No Color
|
|
||||||
|
|
||||||
echo -e "${BLUE}===========================================${NC}"
|
|
||||||
echo -e "${BLUE}批量更新微服务Nacos配置${NC}"
|
|
||||||
echo -e "${BLUE}===========================================${NC}"
|
|
||||||
|
|
||||||
# 服务配置
|
|
||||||
SERVICES="emotion-user:19001 emotion-ai:19002 emotion-record:19003 emotion-growth:19004 emotion-explore:19005 emotion-reward:19006 emotion-websocket:19007 emotion-stats:19008"
|
|
||||||
|
|
||||||
# 生成本地环境配置
|
|
||||||
generate_local_config() {
|
|
||||||
local service_name=$1
|
|
||||||
local port=$2
|
|
||||||
|
|
||||||
cat > "backend/${service_name}/src/main/resources/application-local.yml" << EOF
|
|
||||||
# 本地开发环境配置
|
|
||||||
|
|
||||||
spring:
|
|
||||||
cloud:
|
|
||||||
nacos:
|
|
||||||
discovery:
|
|
||||||
server-addr: localhost:8848
|
|
||||||
namespace:
|
|
||||||
group: DEFAULT_GROUP
|
|
||||||
enabled: true
|
|
||||||
username: nacos
|
|
||||||
password: nacos
|
|
||||||
metadata:
|
|
||||||
version: 1.0.0
|
|
||||||
zone: local
|
|
||||||
register-enabled: true
|
|
||||||
ephemeral: true
|
|
||||||
cluster-name: DEFAULT
|
|
||||||
service: \${spring.application.name}
|
|
||||||
weight: 1
|
|
||||||
heart-beat-interval: 5000
|
|
||||||
heart-beat-timeout: 15000
|
|
||||||
ip-delete-timeout: 30000
|
|
||||||
config:
|
|
||||||
server-addr: localhost:8848
|
|
||||||
namespace:
|
|
||||||
group: DEFAULT_GROUP
|
|
||||||
file-extension: yml
|
|
||||||
enabled: false
|
|
||||||
username: nacos
|
|
||||||
password: nacos
|
|
||||||
|
|
||||||
# 数据源配置
|
|
||||||
datasource:
|
|
||||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
|
||||||
url: jdbc:mysql://localhost:3306/emotion_museum?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&serverTimezone=GMT%2B8&allowPublicKeyRetrieval=true
|
|
||||||
username: root
|
|
||||||
password: 123456
|
|
||||||
|
|
||||||
# Redis配置
|
|
||||||
data:
|
|
||||||
redis:
|
|
||||||
host: localhost
|
|
||||||
port: 6379
|
|
||||||
password:
|
|
||||||
database: 0
|
|
||||||
|
|
||||||
# 日志配置
|
|
||||||
logging:
|
|
||||||
level:
|
|
||||||
com.emotionmuseum: debug
|
|
||||||
com.baomidou.mybatisplus: debug
|
|
||||||
com.alibaba.nacos: info
|
|
||||||
file:
|
|
||||||
name: logs/${service_name}-local.log
|
|
||||||
EOF
|
|
||||||
}
|
|
||||||
|
|
||||||
# 生成测试环境配置
|
|
||||||
generate_test_config() {
|
|
||||||
local service_name=$1
|
|
||||||
local port=$2
|
|
||||||
|
|
||||||
cat > "backend/${service_name}/src/main/resources/application-test.yml" << EOF
|
|
||||||
# 测试环境配置
|
|
||||||
|
|
||||||
spring:
|
|
||||||
cloud:
|
|
||||||
nacos:
|
|
||||||
discovery:
|
|
||||||
server-addr: 47.111.10.27:8848
|
|
||||||
namespace: test
|
|
||||||
group: DEFAULT_GROUP
|
|
||||||
enabled: true
|
|
||||||
username: nacos
|
|
||||||
password: nacos
|
|
||||||
metadata:
|
|
||||||
version: 1.0.0
|
|
||||||
zone: test
|
|
||||||
register-enabled: true
|
|
||||||
ephemeral: true
|
|
||||||
cluster-name: DEFAULT
|
|
||||||
service: \${spring.application.name}
|
|
||||||
weight: 1
|
|
||||||
heart-beat-interval: 5000
|
|
||||||
heart-beat-timeout: 15000
|
|
||||||
ip-delete-timeout: 30000
|
|
||||||
config:
|
|
||||||
server-addr: 47.111.10.27:8848
|
|
||||||
namespace: test
|
|
||||||
group: DEFAULT_GROUP
|
|
||||||
file-extension: yml
|
|
||||||
enabled: false
|
|
||||||
username: nacos
|
|
||||||
password: nacos
|
|
||||||
|
|
||||||
# 数据源配置
|
|
||||||
datasource:
|
|
||||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
|
||||||
url: jdbc:mysql://47.111.10.27:3306/emotion_museum?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&serverTimezone=GMT%2B8&allowPublicKeyRetrieval=true
|
|
||||||
username: root
|
|
||||||
password: EmotionMuseum2025*#
|
|
||||||
|
|
||||||
# Redis配置
|
|
||||||
data:
|
|
||||||
redis:
|
|
||||||
host: 47.111.10.27
|
|
||||||
port: 6379
|
|
||||||
password: EmotionMuseum2025*#
|
|
||||||
database: 0
|
|
||||||
|
|
||||||
# 日志配置
|
|
||||||
logging:
|
|
||||||
level:
|
|
||||||
com.emotionmuseum: info
|
|
||||||
com.baomidou.mybatisplus: info
|
|
||||||
com.alibaba.nacos: warn
|
|
||||||
file:
|
|
||||||
name: logs/${service_name}-test.log
|
|
||||||
EOF
|
|
||||||
}
|
|
||||||
|
|
||||||
# 生成生产环境配置
|
|
||||||
generate_prod_config() {
|
|
||||||
local service_name=$1
|
|
||||||
local port=$2
|
|
||||||
|
|
||||||
cat > "backend/${service_name}/src/main/resources/application-prod.yml" << EOF
|
|
||||||
# 生产环境配置
|
|
||||||
|
|
||||||
spring:
|
|
||||||
cloud:
|
|
||||||
nacos:
|
|
||||||
discovery:
|
|
||||||
server-addr: 47.111.10.27:8848
|
|
||||||
namespace: prod
|
|
||||||
group: DEFAULT_GROUP
|
|
||||||
enabled: true
|
|
||||||
username: nacos
|
|
||||||
password: nacos
|
|
||||||
metadata:
|
|
||||||
version: 1.0.0
|
|
||||||
zone: prod
|
|
||||||
register-enabled: true
|
|
||||||
ephemeral: true
|
|
||||||
cluster-name: DEFAULT
|
|
||||||
service: \${spring.application.name}
|
|
||||||
weight: 1
|
|
||||||
heart-beat-interval: 5000
|
|
||||||
heart-beat-timeout: 15000
|
|
||||||
ip-delete-timeout: 30000
|
|
||||||
config:
|
|
||||||
server-addr: 47.111.10.27:8848
|
|
||||||
namespace: prod
|
|
||||||
group: DEFAULT_GROUP
|
|
||||||
file-extension: yml
|
|
||||||
enabled: false
|
|
||||||
username: nacos
|
|
||||||
password: nacos
|
|
||||||
|
|
||||||
# 数据源配置
|
|
||||||
datasource:
|
|
||||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
|
||||||
url: jdbc:mysql://47.111.10.27:3306/emotion_museum?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&serverTimezone=GMT%2B8&allowPublicKeyRetrieval=true
|
|
||||||
username: root
|
|
||||||
password: EmotionMuseum2025*#
|
|
||||||
|
|
||||||
# Redis配置
|
|
||||||
data:
|
|
||||||
redis:
|
|
||||||
host: 47.111.10.27
|
|
||||||
port: 6379
|
|
||||||
password: EmotionMuseum2025*#
|
|
||||||
database: 0
|
|
||||||
|
|
||||||
# 日志配置
|
|
||||||
logging:
|
|
||||||
level:
|
|
||||||
com.emotionmuseum: warn
|
|
||||||
com.baomidou.mybatisplus: warn
|
|
||||||
com.alibaba.nacos: error
|
|
||||||
file:
|
|
||||||
name: logs/${service_name}-prod.log
|
|
||||||
EOF
|
|
||||||
}
|
|
||||||
|
|
||||||
# 处理每个服务
|
|
||||||
for service_info in $SERVICES; do
|
|
||||||
service_name=$(echo $service_info | cut -d: -f1)
|
|
||||||
port=$(echo $service_info | cut -d: -f2)
|
|
||||||
|
|
||||||
echo -e "${YELLOW}处理服务: $service_name (端口: $port)${NC}"
|
|
||||||
|
|
||||||
# 检查服务目录是否存在
|
|
||||||
if [ ! -d "backend/$service_name" ]; then
|
|
||||||
echo -e "${RED}❌ 服务目录不存在: backend/$service_name${NC}"
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 创建resources目录(如果不存在)
|
|
||||||
mkdir -p "backend/$service_name/src/main/resources"
|
|
||||||
|
|
||||||
# 生成配置文件
|
|
||||||
echo " 生成本地环境配置..."
|
|
||||||
generate_local_config "$service_name" "$port"
|
|
||||||
|
|
||||||
echo " 生成测试环境配置..."
|
|
||||||
generate_test_config "$service_name" "$port"
|
|
||||||
|
|
||||||
echo " 生成生产环境配置..."
|
|
||||||
generate_prod_config "$service_name" "$port"
|
|
||||||
|
|
||||||
echo -e "${GREEN}✅ $service_name 配置文件生成完成${NC}"
|
|
||||||
done
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo -e "${BLUE}===========================================${NC}"
|
|
||||||
echo -e "${GREEN}✅ 所有服务的Nacos配置更新完成!${NC}"
|
|
||||||
echo -e "${BLUE}===========================================${NC}"
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo -e "${YELLOW}配置说明:${NC}"
|
|
||||||
echo -e "1. 本地环境: localhost:8848, 无命名空间"
|
|
||||||
echo -e "2. 测试环境: 47.111.10.27:8848, test命名空间"
|
|
||||||
echo -e "3. 生产环境: 47.111.10.27:8848, prod命名空间"
|
|
||||||
echo ""
|
|
||||||
echo -e "${YELLOW}使用方法:${NC}"
|
|
||||||
echo -e "启动时指定环境: ${GREEN}--spring.profiles.active=local|test|prod${NC}"
|
|
||||||
@@ -1,157 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# 批量更新所有微服务的Nacos密码配置
|
|
||||||
# 本地环境: Peanut2817*#
|
|
||||||
# 测试和生产环境: EmotionMuseum2025
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
# 颜色定义
|
|
||||||
RED='\033[0;31m'
|
|
||||||
GREEN='\033[0;32m'
|
|
||||||
YELLOW='\033[1;33m'
|
|
||||||
BLUE='\033[0;34m'
|
|
||||||
NC='\033[0m' # No Color
|
|
||||||
|
|
||||||
echo -e "${BLUE}===========================================${NC}"
|
|
||||||
echo -e "${BLUE}批量更新微服务Nacos密码配置${NC}"
|
|
||||||
echo -e "${BLUE}===========================================${NC}"
|
|
||||||
|
|
||||||
# 服务列表
|
|
||||||
SERVICES="emotion-user emotion-ai emotion-record emotion-growth emotion-explore emotion-reward emotion-websocket emotion-stats"
|
|
||||||
|
|
||||||
# 更新本地环境密码
|
|
||||||
update_local_password() {
|
|
||||||
local service_name=$1
|
|
||||||
local config_file="backend/${service_name}/src/main/resources/application-local.yml"
|
|
||||||
|
|
||||||
if [ ! -f "$config_file" ]; then
|
|
||||||
echo -e "${RED} ❌ 配置文件不存在: $config_file${NC}"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 使用sed替换密码
|
|
||||||
sed -i.bak 's/password: nacos$/password: Peanut2817*#/g' "$config_file"
|
|
||||||
|
|
||||||
# 检查是否替换成功
|
|
||||||
if grep -q "password: Peanut2817*#" "$config_file"; then
|
|
||||||
echo -e "${GREEN} ✅ 本地环境密码更新成功${NC}"
|
|
||||||
rm -f "${config_file}.bak"
|
|
||||||
return 0
|
|
||||||
else
|
|
||||||
echo -e "${RED} ❌ 本地环境密码更新失败${NC}"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# 更新测试环境密码
|
|
||||||
update_test_password() {
|
|
||||||
local service_name=$1
|
|
||||||
local config_file="backend/${service_name}/src/main/resources/application-test.yml"
|
|
||||||
|
|
||||||
if [ ! -f "$config_file" ]; then
|
|
||||||
echo -e "${RED} ❌ 配置文件不存在: $config_file${NC}"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 使用sed替换密码
|
|
||||||
sed -i.bak 's/password: nacos$/password: EmotionMuseum2025/g' "$config_file"
|
|
||||||
|
|
||||||
# 检查是否替换成功
|
|
||||||
if grep -q "password: EmotionMuseum2025" "$config_file"; then
|
|
||||||
echo -e "${GREEN} ✅ 测试环境密码更新成功${NC}"
|
|
||||||
rm -f "${config_file}.bak"
|
|
||||||
return 0
|
|
||||||
else
|
|
||||||
echo -e "${RED} ❌ 测试环境密码更新失败${NC}"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# 更新生产环境密码
|
|
||||||
update_prod_password() {
|
|
||||||
local service_name=$1
|
|
||||||
local config_file="backend/${service_name}/src/main/resources/application-prod.yml"
|
|
||||||
|
|
||||||
if [ ! -f "$config_file" ]; then
|
|
||||||
echo -e "${RED} ❌ 配置文件不存在: $config_file${NC}"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 使用sed替换密码
|
|
||||||
sed -i.bak 's/password: nacos$/password: EmotionMuseum2025/g' "$config_file"
|
|
||||||
|
|
||||||
# 检查是否替换成功
|
|
||||||
if grep -q "password: EmotionMuseum2025" "$config_file"; then
|
|
||||||
echo -e "${GREEN} ✅ 生产环境密码更新成功${NC}"
|
|
||||||
rm -f "${config_file}.bak"
|
|
||||||
return 0
|
|
||||||
else
|
|
||||||
echo -e "${RED} ❌ 生产环境密码更新失败${NC}"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# 统计结果
|
|
||||||
total_services=0
|
|
||||||
success_services=0
|
|
||||||
failed_services=0
|
|
||||||
|
|
||||||
# 处理每个服务
|
|
||||||
for service_name in $SERVICES; do
|
|
||||||
echo -e "${YELLOW}更新服务: $service_name${NC}"
|
|
||||||
total_services=$((total_services + 1))
|
|
||||||
|
|
||||||
# 检查服务目录是否存在
|
|
||||||
if [ ! -d "backend/$service_name" ]; then
|
|
||||||
echo -e "${RED} ❌ 服务目录不存在: backend/$service_name${NC}"
|
|
||||||
failed_services=$((failed_services + 1))
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 更新各环境密码
|
|
||||||
service_success=true
|
|
||||||
|
|
||||||
if ! update_local_password "$service_name"; then
|
|
||||||
service_success=false
|
|
||||||
fi
|
|
||||||
|
|
||||||
if ! update_test_password "$service_name"; then
|
|
||||||
service_success=false
|
|
||||||
fi
|
|
||||||
|
|
||||||
if ! update_prod_password "$service_name"; then
|
|
||||||
service_success=false
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ "$service_success" = true ]; then
|
|
||||||
echo -e "${GREEN} ✅ $service_name 密码更新完成${NC}"
|
|
||||||
success_services=$((success_services + 1))
|
|
||||||
else
|
|
||||||
echo -e "${RED} ❌ $service_name 密码更新失败${NC}"
|
|
||||||
failed_services=$((failed_services + 1))
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
done
|
|
||||||
|
|
||||||
# 显示统计结果
|
|
||||||
echo -e "${BLUE}===========================================${NC}"
|
|
||||||
echo -e "${BLUE}密码更新结果统计${NC}"
|
|
||||||
echo -e "${BLUE}===========================================${NC}"
|
|
||||||
echo -e "总服务数: ${BLUE}$total_services${NC}"
|
|
||||||
echo -e "更新成功: ${GREEN}$success_services${NC}"
|
|
||||||
echo -e "更新失败: ${RED}$failed_services${NC}"
|
|
||||||
|
|
||||||
if [ $failed_services -eq 0 ]; then
|
|
||||||
echo -e "${GREEN}🎉 所有服务密码更新完成!${NC}"
|
|
||||||
echo ""
|
|
||||||
echo -e "${YELLOW}密码配置:${NC}"
|
|
||||||
echo -e "本地环境: ${GREEN}Peanut2817*#${NC}"
|
|
||||||
echo -e "测试环境: ${GREEN}EmotionMuseum2025${NC}"
|
|
||||||
echo -e "生产环境: ${GREEN}EmotionMuseum2025${NC}"
|
|
||||||
exit 0
|
|
||||||
else
|
|
||||||
echo -e "${RED}❌ 有 $failed_services 个服务密码更新失败${NC}"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
-- ============================================================================
|
|
||||||
-- 数据库脚本验证查询
|
|
||||||
-- 用于验证 mysql_emotion_museum_final.sql 执行后的表结构
|
|
||||||
-- ============================================================================
|
|
||||||
|
|
||||||
-- 验证数据库是否存在
|
|
||||||
SELECT SCHEMA_NAME FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME = 'emotion_museum';
|
|
||||||
|
|
||||||
-- 验证所有表是否创建成功
|
|
||||||
SELECT TABLE_NAME, TABLE_COMMENT
|
|
||||||
FROM INFORMATION_SCHEMA.TABLES
|
|
||||||
WHERE TABLE_SCHEMA = 'emotion_museum'
|
|
||||||
ORDER BY TABLE_NAME;
|
|
||||||
|
|
||||||
-- 验证conversation表的字段结构(重点验证新增字段)
|
|
||||||
SELECT
|
|
||||||
COLUMN_NAME,
|
|
||||||
DATA_TYPE,
|
|
||||||
IS_NULLABLE,
|
|
||||||
COLUMN_DEFAULT,
|
|
||||||
COLUMN_COMMENT
|
|
||||||
FROM INFORMATION_SCHEMA.COLUMNS
|
|
||||||
WHERE TABLE_SCHEMA = 'emotion_museum'
|
|
||||||
AND TABLE_NAME = 'conversation'
|
|
||||||
ORDER BY ORDINAL_POSITION;
|
|
||||||
|
|
||||||
-- 验证conversation表的索引
|
|
||||||
SELECT
|
|
||||||
INDEX_NAME,
|
|
||||||
COLUMN_NAME,
|
|
||||||
NON_UNIQUE
|
|
||||||
FROM INFORMATION_SCHEMA.STATISTICS
|
|
||||||
WHERE TABLE_SCHEMA = 'emotion_museum'
|
|
||||||
AND TABLE_NAME = 'conversation'
|
|
||||||
ORDER BY INDEX_NAME, SEQ_IN_INDEX;
|
|
||||||
|
|
||||||
-- 验证新增字段是否存在
|
|
||||||
SELECT
|
|
||||||
CASE
|
|
||||||
WHEN COUNT(*) = 9 THEN '✅ 所有新增字段都存在'
|
|
||||||
ELSE CONCAT('❌ 缺少字段,只找到 ', COUNT(*), ' 个,应该是 9 个')
|
|
||||||
END AS validation_result
|
|
||||||
FROM INFORMATION_SCHEMA.COLUMNS
|
|
||||||
WHERE TABLE_SCHEMA = 'emotion_museum'
|
|
||||||
AND TABLE_NAME = 'conversation'
|
|
||||||
AND COLUMN_NAME IN (
|
|
||||||
'user_type', 'emotion_trend', 'keywords', 'ai_insights',
|
|
||||||
'confidence', 'client_ip', 'user_agent', 'summary', 'tags'
|
|
||||||
);
|
|
||||||
|
|
||||||
-- 验证新增索引是否存在
|
|
||||||
SELECT
|
|
||||||
CASE
|
|
||||||
WHEN COUNT(*) = 4 THEN '✅ 所有新增索引都存在'
|
|
||||||
ELSE CONCAT('❌ 缺少索引,只找到 ', COUNT(*), ' 个,应该是 4 个')
|
|
||||||
END AS index_validation_result
|
|
||||||
FROM INFORMATION_SCHEMA.STATISTICS
|
|
||||||
WHERE TABLE_SCHEMA = 'emotion_museum'
|
|
||||||
AND TABLE_NAME = 'conversation'
|
|
||||||
AND INDEX_NAME IN (
|
|
||||||
'idx_conversation_user_type',
|
|
||||||
'idx_conversation_emotion_trend',
|
|
||||||
'idx_conversation_confidence',
|
|
||||||
'idx_conversation_client_ip'
|
|
||||||
);
|
|
||||||
|
|
||||||
-- 统计总表数
|
|
||||||
SELECT
|
|
||||||
CASE
|
|
||||||
WHEN COUNT(*) = 15 THEN '✅ 所有15个表都创建成功'
|
|
||||||
ELSE CONCAT('❌ 表数量不正确,只有 ', COUNT(*), ' 个表,应该是 15 个')
|
|
||||||
END AS table_count_result
|
|
||||||
FROM INFORMATION_SCHEMA.TABLES
|
|
||||||
WHERE TABLE_SCHEMA = 'emotion_museum';
|
|
||||||
|
|
||||||
-- 统计总索引数(conversation表)
|
|
||||||
SELECT
|
|
||||||
CONCAT('conversation表共有 ', COUNT(DISTINCT INDEX_NAME), ' 个索引') AS conversation_index_count
|
|
||||||
FROM INFORMATION_SCHEMA.STATISTICS
|
|
||||||
WHERE TABLE_SCHEMA = 'emotion_museum'
|
|
||||||
AND TABLE_NAME = 'conversation';
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user