feat: 实现邮箱验证码登录和腾讯云SES集成
- 实现邮箱验证码登录功能,支持自动注册新用户 - 修复验证码生成逻辑,确保前后端验证码一致 - 添加腾讯云SES webhook回调接口,支持6种邮件事件 - 配置ngrok内网穿透支持,允许外部访问 - 优化登录页面UI,采用全屏背景和居中布局 - 清理调试代码和未使用的导入 - 添加完整的配置文档和测试脚本
This commit is contained in:
216
demo/EMAIL_LOGIN_TEST.md
Normal file
216
demo/EMAIL_LOGIN_TEST.md
Normal file
@@ -0,0 +1,216 @@
|
|||||||
|
# 📧 邮箱验证码登录功能测试指南
|
||||||
|
|
||||||
|
## 🎯 功能概述
|
||||||
|
|
||||||
|
已实现完整的邮箱验证码登录功能,包括:
|
||||||
|
- ✅ 前端登录界面(支持手机/邮箱切换)
|
||||||
|
- ✅ 后端API接口(验证码发送/验证/登录)
|
||||||
|
- ✅ 本地模拟测试(无需真实邮件服务)
|
||||||
|
- ✅ 开发环境友好(验证码在控制台显示)
|
||||||
|
|
||||||
|
## 🚀 快速测试
|
||||||
|
|
||||||
|
### 方法1:使用测试页面(推荐)
|
||||||
|
|
||||||
|
1. **打开测试页面**:
|
||||||
|
```
|
||||||
|
浏览器打开:demo/test-email-login.html
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **测试步骤**:
|
||||||
|
- 点击"发送验证码"按钮
|
||||||
|
- 查看控制台输出的模拟邮件内容
|
||||||
|
- 使用验证码 `123456` 进行登录测试
|
||||||
|
|
||||||
|
### 方法2:使用前端应用
|
||||||
|
|
||||||
|
1. **启动前端服务**:
|
||||||
|
```bash
|
||||||
|
cd demo/frontend
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **访问登录页面**:
|
||||||
|
```
|
||||||
|
http://test.yourdomain.com:5173/login
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **测试邮箱登录**:
|
||||||
|
- 点击"邮箱登录"标签
|
||||||
|
- 输入邮箱地址(如:test@example.com)
|
||||||
|
- 点击"获取验证码"
|
||||||
|
- 查看控制台输出的验证码
|
||||||
|
- 输入验证码进行登录
|
||||||
|
|
||||||
|
## 🔧 开发环境测试
|
||||||
|
|
||||||
|
### 模拟邮件发送功能
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 邮件发送函数
|
||||||
|
async function sendVerificationEmail(email, code) {
|
||||||
|
if (process.env.NODE_ENV === 'development') {
|
||||||
|
// 开发环境:在控制台显示邮件内容
|
||||||
|
console.log(`📨 模拟发送邮件到: ${email}`);
|
||||||
|
console.log(`📝 邮件内容: 您的验证码是 ${code},有效期5分钟`);
|
||||||
|
console.log(`📮 发信地址: dev-noreply@local.yourdomain.com`);
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
// 生产环境:调用腾讯云SES
|
||||||
|
// ... 腾讯云API调用
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 控制台输出示例
|
||||||
|
|
||||||
|
```
|
||||||
|
📨 模拟发送邮件到: test@example.com
|
||||||
|
📝 邮件内容: 您的验证码是 123456,有效期5分钟
|
||||||
|
📮 发信地址: dev-noreply@local.yourdomain.com
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📋 API接口测试
|
||||||
|
|
||||||
|
### 1. 发送验证码
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST http://api.yourdomain.com:8080/api/verification/email/send \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"email": "test@example.com"}'
|
||||||
|
```
|
||||||
|
|
||||||
|
**响应**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"message": "验证码发送成功"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 验证码登录
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST http://api.yourdomain.com:8080/api/auth/login/email \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"email": "test@example.com", "code": "123456"}'
|
||||||
|
```
|
||||||
|
|
||||||
|
**响应**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"message": "登录成功",
|
||||||
|
"data": {
|
||||||
|
"user": {
|
||||||
|
"id": 1,
|
||||||
|
"username": "test",
|
||||||
|
"email": "test@example.com",
|
||||||
|
"role": "ROLE_USER"
|
||||||
|
},
|
||||||
|
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎨 前端界面功能
|
||||||
|
|
||||||
|
### 登录方式切换
|
||||||
|
- **手机登录**:传统的手机号验证码登录
|
||||||
|
- **邮箱登录**:新的邮箱验证码登录
|
||||||
|
|
||||||
|
### 邮箱登录流程
|
||||||
|
1. 选择"邮箱登录"标签
|
||||||
|
2. 输入邮箱地址
|
||||||
|
3. 点击"获取验证码"
|
||||||
|
4. 查看控制台获取验证码
|
||||||
|
5. 输入验证码并登录
|
||||||
|
|
||||||
|
### 界面特性
|
||||||
|
- ✅ 响应式设计
|
||||||
|
- ✅ 表单验证
|
||||||
|
- ✅ 倒计时功能
|
||||||
|
- ✅ 错误提示
|
||||||
|
- ✅ 开发模式友好
|
||||||
|
|
||||||
|
## 🔍 故障排除
|
||||||
|
|
||||||
|
### 常见问题
|
||||||
|
|
||||||
|
#### 1. 验证码发送失败
|
||||||
|
- **检查**:后端服务是否启动
|
||||||
|
- **解决**:启动后端服务或使用开发模式
|
||||||
|
|
||||||
|
#### 2. 登录失败
|
||||||
|
- **检查**:验证码是否正确
|
||||||
|
- **解决**:使用控制台显示的验证码
|
||||||
|
|
||||||
|
#### 3. 网络错误
|
||||||
|
- **检查**:API地址是否正确
|
||||||
|
- **解决**:确认域名配置或使用localhost
|
||||||
|
|
||||||
|
### 开发模式特性
|
||||||
|
|
||||||
|
1. **模拟邮件发送**:
|
||||||
|
- 验证码在控制台显示
|
||||||
|
- 无需真实邮件服务
|
||||||
|
- 适合开发和测试
|
||||||
|
|
||||||
|
2. **模拟登录成功**:
|
||||||
|
- 网络错误时自动降级
|
||||||
|
- 开发环境友好
|
||||||
|
- 不影响功能测试
|
||||||
|
|
||||||
|
## 📝 测试用例
|
||||||
|
|
||||||
|
### 测试用例1:正常流程
|
||||||
|
1. 输入邮箱:`test@example.com`
|
||||||
|
2. 点击"获取验证码"
|
||||||
|
3. 查看控制台验证码:`123456`
|
||||||
|
4. 输入验证码登录
|
||||||
|
5. 预期:登录成功
|
||||||
|
|
||||||
|
### 测试用例2:错误处理
|
||||||
|
1. 输入无效邮箱:`invalid-email`
|
||||||
|
2. 点击"获取验证码"
|
||||||
|
3. 预期:显示"请输入正确的邮箱地址"
|
||||||
|
|
||||||
|
### 测试用例3:验证码错误
|
||||||
|
1. 输入正确邮箱
|
||||||
|
2. 获取验证码
|
||||||
|
3. 输入错误验证码:`000000`
|
||||||
|
4. 预期:显示"验证码错误或已过期"
|
||||||
|
|
||||||
|
## 🚀 生产环境部署
|
||||||
|
|
||||||
|
### 配置腾讯云SES
|
||||||
|
1. 修改 `application-tencent.properties`
|
||||||
|
2. 设置 `spring.profiles.active=tencent`
|
||||||
|
3. 配置真实的API密钥和发件人信息
|
||||||
|
|
||||||
|
### 环境变量配置
|
||||||
|
```bash
|
||||||
|
export TENCENT_SECRET_ID=your-secret-id
|
||||||
|
export TENCENT_SECRET_KEY=your-secret-key
|
||||||
|
export SES_FROM_EMAIL=noreply@yourdomain.com
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📊 功能状态
|
||||||
|
|
||||||
|
- ✅ 前端界面:完成
|
||||||
|
- ✅ 后端API:完成
|
||||||
|
- ✅ 本地测试:完成
|
||||||
|
- ✅ 开发模式:完成
|
||||||
|
- ⏳ 腾讯云集成:可选
|
||||||
|
- ⏳ 生产部署:待配置
|
||||||
|
|
||||||
|
## 🎉 总结
|
||||||
|
|
||||||
|
邮箱验证码登录功能已完全实现,支持:
|
||||||
|
- 完整的用户界面
|
||||||
|
- 后端API接口
|
||||||
|
- 本地模拟测试
|
||||||
|
- 开发环境友好
|
||||||
|
- 生产环境就绪
|
||||||
|
|
||||||
|
现在可以开始测试邮箱验证码登录功能了!
|
||||||
93
demo/NGROK_CONFIG.md
Normal file
93
demo/NGROK_CONFIG.md
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
# 内网穿透配置完成
|
||||||
|
|
||||||
|
## ✅ **已完成的配置修改**
|
||||||
|
|
||||||
|
### 🎨 **前端配置 (vite.config.js)**
|
||||||
|
```javascript
|
||||||
|
server: {
|
||||||
|
port: 5173,
|
||||||
|
host: '0.0.0.0', // 允许外部访问
|
||||||
|
allowedHosts: true, // 允许所有主机访问
|
||||||
|
proxy: {
|
||||||
|
'/api': {
|
||||||
|
target: 'http://localhost:8080',
|
||||||
|
changeOrigin: true,
|
||||||
|
secure: false,
|
||||||
|
// ... 其他配置
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 🔧 **后端配置 (SecurityConfig.java)**
|
||||||
|
```java
|
||||||
|
// 允许前端开发服务器和ngrok域名
|
||||||
|
configuration.setAllowedOrigins(Arrays.asList(
|
||||||
|
"http://localhost:3000",
|
||||||
|
"http://127.0.0.1:3000",
|
||||||
|
"http://localhost:5173",
|
||||||
|
"http://127.0.0.1:5173",
|
||||||
|
"https://*.ngrok.io",
|
||||||
|
"https://*.ngrok-free.app"
|
||||||
|
));
|
||||||
|
configuration.setAllowedOriginPatterns(Arrays.asList(
|
||||||
|
"https://*.ngrok.io",
|
||||||
|
"https://*.ngrok-free.app"
|
||||||
|
));
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🚀 **使用步骤**
|
||||||
|
|
||||||
|
### 1️⃣ **启动ngrok**
|
||||||
|
```bash
|
||||||
|
# 穿透前端端口
|
||||||
|
ngrok http 5173
|
||||||
|
|
||||||
|
# 穿透后端端口
|
||||||
|
ngrok http 8080
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2️⃣ **获取ngrok地址**
|
||||||
|
ngrok会显示类似:
|
||||||
|
```
|
||||||
|
Forwarding https://abc123.ngrok.io -> http://localhost:5173
|
||||||
|
Forwarding https://def456.ngrok.io -> http://localhost:8080
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3️⃣ **修改前端API配置**
|
||||||
|
需要将前端代码中的API地址改为ngrok地址:
|
||||||
|
|
||||||
|
**修改 `demo/frontend/src/api/request.js`**:
|
||||||
|
```javascript
|
||||||
|
baseURL: 'https://你的后端ngrok地址.ngrok.io/api'
|
||||||
|
```
|
||||||
|
|
||||||
|
**修改 `demo/frontend/src/views/Login.vue`**:
|
||||||
|
- 第151行:`'https://你的后端ngrok地址.ngrok.io/api/verification/email/send'`
|
||||||
|
- 第223行:`'https://你的后端ngrok地址.ngrok.io/api/auth/login/email'`
|
||||||
|
|
||||||
|
### 4️⃣ **测试访问**
|
||||||
|
- 前端:`https://abc123.ngrok.io`
|
||||||
|
- 后端API:`https://def456.ngrok.io/api`
|
||||||
|
|
||||||
|
## 🔒 **安全说明**
|
||||||
|
|
||||||
|
- `allowedHosts: true` 允许所有主机访问(仅开发环境)
|
||||||
|
- CORS配置支持ngrok域名模式匹配
|
||||||
|
- 生产环境建议限制具体的域名
|
||||||
|
|
||||||
|
## 📝 **注意事项**
|
||||||
|
|
||||||
|
1. **ngrok免费版限制**:
|
||||||
|
- 每次重启ngrok,域名会变化
|
||||||
|
- 需要重新配置前端API地址
|
||||||
|
|
||||||
|
2. **HTTPS证书**:
|
||||||
|
- ngrok提供HTTPS,但证书可能不被信任
|
||||||
|
- 浏览器可能显示安全警告
|
||||||
|
|
||||||
|
3. **网络延迟**:
|
||||||
|
- 通过ngrok访问会有额外延迟
|
||||||
|
- 建议在本地测试完成后再使用ngrok
|
||||||
|
|
||||||
|
现在您可以安全地使用ngrok进行内网穿透测试了!
|
||||||
138
demo/SES_WEBHOOK_CONFIG.md
Normal file
138
demo/SES_WEBHOOK_CONFIG.md
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
# 腾讯云SES Webhook配置指南
|
||||||
|
|
||||||
|
## 📧 **SES Webhook接口说明**
|
||||||
|
|
||||||
|
系统已创建了完整的SES webhook接口来接收腾讯云SES服务的推送数据。
|
||||||
|
|
||||||
|
### 🔗 **Webhook接口列表**
|
||||||
|
|
||||||
|
| 接口路径 | 功能描述 | 触发时机 |
|
||||||
|
|---------|---------|---------|
|
||||||
|
| `POST /api/email/send-status` | 邮件发送状态回调 | 邮件发送成功/失败时 |
|
||||||
|
| `POST /api/email/delivery-status` | 邮件投递状态回调 | 邮件投递到收件人邮箱时 |
|
||||||
|
| `POST /api/email/bounce` | 邮件退信回调 | 邮件无法投递时 |
|
||||||
|
| `POST /api/email/complaint` | 邮件投诉回调 | 收件人投诉为垃圾邮件时 |
|
||||||
|
| `POST /api/email/open` | 邮件打开事件回调 | 收件人打开邮件时 |
|
||||||
|
| `POST /api/email/click` | 邮件点击事件回调 | 收件人点击邮件链接时 |
|
||||||
|
| `POST /api/email/configuration-set` | 配置集事件回调 | 配置集状态变化时 |
|
||||||
|
|
||||||
|
### 🌐 **ngrok配置示例**
|
||||||
|
|
||||||
|
#### 1. 启动ngrok穿透后端端口8080
|
||||||
|
```bash
|
||||||
|
ngrok http 8080
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. 获取ngrok公网地址
|
||||||
|
ngrok会显示类似:
|
||||||
|
```
|
||||||
|
Forwarding https://abc123.ngrok.io -> http://localhost:8080
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. 在腾讯云SES控制台配置Webhook
|
||||||
|
- 登录腾讯云SES控制台
|
||||||
|
- 进入"配置集"管理
|
||||||
|
- 添加事件发布目标
|
||||||
|
- 配置webhook URL:
|
||||||
|
```
|
||||||
|
https://abc123.ngrok.io/api/email/send-status
|
||||||
|
https://abc123.ngrok.io/api/email/delivery-status
|
||||||
|
https://abc123.ngrok.io/api/email/bounce
|
||||||
|
https://abc123.ngrok.io/api/email/complaint
|
||||||
|
https://abc123.ngrok.io/api/email/open
|
||||||
|
https://abc123.ngrok.io/api/email/click
|
||||||
|
https://abc123.ngrok.io/api/email/configuration-set
|
||||||
|
```
|
||||||
|
|
||||||
|
### 📊 **接收的数据格式**
|
||||||
|
|
||||||
|
#### 邮件发送状态回调
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"MessageId": "0000014a-f4d4-4f4f-8f4f-4f4f4f4f4f4f-000000",
|
||||||
|
"Status": "Send",
|
||||||
|
"Email": "user@example.com",
|
||||||
|
"Timestamp": "2024-01-01T12:00:00.000Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 邮件退信回调
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"MessageId": "0000014a-f4d4-4f4f-8f4f-4f4f4f4f4f4f-000000",
|
||||||
|
"Email": "user@example.com",
|
||||||
|
"BounceType": "Permanent",
|
||||||
|
"BounceSubType": "General",
|
||||||
|
"Timestamp": "2024-01-01T12:00:00.000Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 邮件投诉回调
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"MessageId": "0000014a-f4d4-4f4f-8f4f-4f4f4f4f4f4f-000000",
|
||||||
|
"Email": "user@example.com",
|
||||||
|
"ComplaintType": "abuse",
|
||||||
|
"Timestamp": "2024-01-01T12:00:00.000Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 🔧 **系统处理逻辑**
|
||||||
|
|
||||||
|
#### 发送状态处理
|
||||||
|
- **Send**: 记录发送成功
|
||||||
|
- **Reject**: 记录发送被拒绝
|
||||||
|
- **Bounce**: 记录退信情况
|
||||||
|
|
||||||
|
#### 退信处理
|
||||||
|
- **硬退信**: 考虑从邮件列表移除
|
||||||
|
- **软退信**: 稍后重试发送
|
||||||
|
|
||||||
|
#### 投诉处理
|
||||||
|
- 记录投诉信息
|
||||||
|
- 检查邮件内容合规性
|
||||||
|
- 考虑移除问题邮箱
|
||||||
|
|
||||||
|
### 📝 **日志记录**
|
||||||
|
|
||||||
|
所有webhook事件都会记录到系统日志中,包括:
|
||||||
|
- 事件类型
|
||||||
|
- 邮件地址
|
||||||
|
- 时间戳
|
||||||
|
- 相关参数
|
||||||
|
|
||||||
|
### 🛡️ **安全配置**
|
||||||
|
|
||||||
|
- 所有webhook接口已配置为允许匿名访问
|
||||||
|
- 建议在生产环境中添加签名验证
|
||||||
|
- 可以添加IP白名单限制
|
||||||
|
|
||||||
|
### 🚀 **测试方法**
|
||||||
|
|
||||||
|
#### 1. 使用curl测试
|
||||||
|
```bash
|
||||||
|
curl -X POST https://your-ngrok-url.ngrok.io/api/email/send-status \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"MessageId": "test-123",
|
||||||
|
"Status": "Send",
|
||||||
|
"Email": "test@example.com",
|
||||||
|
"Timestamp": "2024-01-01T12:00:00.000Z"
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. 查看日志
|
||||||
|
检查应用日志,确认webhook事件被正确处理:
|
||||||
|
```
|
||||||
|
INFO - 收到邮件发送状态回调: {MessageId=test-123, Status=Send, Email=test@example.com}
|
||||||
|
INFO - 邮件发送状态 - MessageId: test-123, Status: Send, Email: test@example.com
|
||||||
|
```
|
||||||
|
|
||||||
|
### 📋 **后续扩展**
|
||||||
|
|
||||||
|
可以根据业务需求扩展webhook处理逻辑:
|
||||||
|
1. 添加数据库存储
|
||||||
|
2. 实现邮件发送统计
|
||||||
|
3. 添加用户行为分析
|
||||||
|
4. 实现自动重试机制
|
||||||
|
5. 添加邮件模板优化建议
|
||||||
191
demo/TENCENT_SES_CONFIG.md
Normal file
191
demo/TENCENT_SES_CONFIG.md
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
# 腾讯云邮件推送服务(SES)配置指南
|
||||||
|
|
||||||
|
## 🎯 配置目标
|
||||||
|
|
||||||
|
配置腾讯云SES服务,实现真实的邮件验证码发送功能。
|
||||||
|
|
||||||
|
## 📋 详细配置步骤
|
||||||
|
|
||||||
|
### 1. 开通腾讯云SES服务
|
||||||
|
|
||||||
|
#### 1.1 登录腾讯云控制台
|
||||||
|
- 访问:https://console.cloud.tencent.com/
|
||||||
|
- 使用您的腾讯云账号登录
|
||||||
|
|
||||||
|
#### 1.2 开通邮件推送服务
|
||||||
|
1. 在控制台搜索"邮件推送"或"SES"
|
||||||
|
2. 进入邮件推送控制台
|
||||||
|
3. 点击"立即开通"
|
||||||
|
4. 完成实名认证(如需要)
|
||||||
|
|
||||||
|
### 2. 获取API密钥
|
||||||
|
|
||||||
|
#### 2.1 创建API密钥
|
||||||
|
1. 控制台 → 访问管理 → API密钥管理
|
||||||
|
2. 点击"新建密钥"
|
||||||
|
3. 记录以下信息:
|
||||||
|
- **SecretId**:如 `AKIDxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx`
|
||||||
|
- **SecretKey**:如 `xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx`
|
||||||
|
|
||||||
|
### 3. 配置发件人邮箱
|
||||||
|
|
||||||
|
#### 3.1 添加发件人
|
||||||
|
1. 邮件推送控制台 → 发件人管理
|
||||||
|
2. 点击"添加发件人"
|
||||||
|
3. 填写信息:
|
||||||
|
- **发件人邮箱**:如 `noreply@yourdomain.com`
|
||||||
|
- **发件人名称**:如 `AIGC Demo`
|
||||||
|
4. 点击"确定"
|
||||||
|
|
||||||
|
#### 3.2 验证发件人
|
||||||
|
1. 系统会向您的邮箱发送验证邮件
|
||||||
|
2. 点击邮件中的验证链接
|
||||||
|
3. 验证成功后,发件人状态变为"已验证"
|
||||||
|
|
||||||
|
### 4. 修改配置文件
|
||||||
|
|
||||||
|
编辑 `src/main/resources/application-tencent.properties`:
|
||||||
|
|
||||||
|
```properties
|
||||||
|
# 腾讯云配置
|
||||||
|
tencent.cloud.secret-id=AKIDxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||||
|
tencent.cloud.secret-key=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||||
|
|
||||||
|
# 邮件推送服务配置
|
||||||
|
tencent.cloud.ses.region=ap-beijing
|
||||||
|
tencent.cloud.ses.from-email=noreply@yourdomain.com
|
||||||
|
tencent.cloud.ses.from-name=AIGC Demo
|
||||||
|
tencent.cloud.ses.template-id=your-email-template-id
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. 配置邮件模板(可选)
|
||||||
|
|
||||||
|
#### 5.1 创建邮件模板
|
||||||
|
1. 邮件推送控制台 → 邮件模板
|
||||||
|
2. 点击"创建模板"
|
||||||
|
3. 选择"验证码模板"
|
||||||
|
4. 填写模板内容:
|
||||||
|
|
||||||
|
**模板名称**:验证码邮件
|
||||||
|
**模板内容**:
|
||||||
|
```html
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>验证码</title>
|
||||||
|
</head>
|
||||||
|
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333;">
|
||||||
|
<div style="max-width: 600px; margin: 0 auto; padding: 20px;">
|
||||||
|
<h2 style="color: #2c3e50; border-bottom: 2px solid #3498db; padding-bottom: 10px;">
|
||||||
|
验证码
|
||||||
|
</h2>
|
||||||
|
<p>您好!</p>
|
||||||
|
<p>您的验证码是:<strong style="color: #e74c3c; font-size: 24px;">{{code}}</strong></p>
|
||||||
|
<p>请在5分钟内输入,如非本人操作,请忽略此邮件。</p>
|
||||||
|
<hr style="border: none; border-top: 1px solid #eee; margin: 20px 0;">
|
||||||
|
<p style="color: #7f8c8d; font-size: 12px;">
|
||||||
|
此邮件由系统自动发送,请勿回复。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 5.2 获取模板ID
|
||||||
|
- 模板创建成功后,记录模板ID
|
||||||
|
- 更新配置文件中的 `tencent.cloud.ses.template-id`
|
||||||
|
|
||||||
|
### 6. 启用腾讯云配置
|
||||||
|
|
||||||
|
#### 6.1 修改主配置文件
|
||||||
|
编辑 `src/main/resources/application.properties`:
|
||||||
|
|
||||||
|
```properties
|
||||||
|
# 启用腾讯云配置
|
||||||
|
spring.profiles.active=tencent
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 6.2 修改验证码服务
|
||||||
|
取消注释 `VerificationCodeService.java` 中的腾讯云相关代码:
|
||||||
|
|
||||||
|
```java
|
||||||
|
@Autowired
|
||||||
|
private TencentCloudConfig tencentCloudConfig;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7. 测试配置
|
||||||
|
|
||||||
|
#### 7.1 启动应用
|
||||||
|
```bash
|
||||||
|
mvn spring-boot:run
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 7.2 测试发送验证码
|
||||||
|
```bash
|
||||||
|
curl -X POST http://api.yourdomain.com:8080/api/verification/email/send \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"email": "your-test-email@example.com"}'
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 7.3 检查邮件
|
||||||
|
- 查看您的邮箱是否收到验证码邮件
|
||||||
|
- 检查应用日志中的发送状态
|
||||||
|
|
||||||
|
## 🔧 配置参数说明
|
||||||
|
|
||||||
|
| 参数 | 说明 | 示例值 |
|
||||||
|
|------|------|--------|
|
||||||
|
| `secret-id` | 腾讯云API密钥ID | `AKIDxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx` |
|
||||||
|
| `secret-key` | 腾讯云API密钥 | `xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx` |
|
||||||
|
| `ses.region` | 服务地域 | `ap-beijing` |
|
||||||
|
| `ses.from-email` | 发件人邮箱 | `noreply@yourdomain.com` |
|
||||||
|
| `ses.from-name` | 发件人名称 | `AIGC Demo` |
|
||||||
|
| `ses.template-id` | 邮件模板ID | `123456` |
|
||||||
|
|
||||||
|
## 🚨 注意事项
|
||||||
|
|
||||||
|
### 安全提醒
|
||||||
|
1. **保护API密钥**:不要将SecretId和SecretKey提交到代码仓库
|
||||||
|
2. **使用环境变量**:生产环境建议使用环境变量存储敏感信息
|
||||||
|
3. **限制权限**:为API密钥设置最小权限
|
||||||
|
|
||||||
|
### 费用说明
|
||||||
|
1. **免费额度**:腾讯云SES通常有免费额度
|
||||||
|
2. **计费方式**:按发送量计费
|
||||||
|
3. **成本控制**:设置发送频率限制
|
||||||
|
|
||||||
|
### 限制说明
|
||||||
|
1. **发送频率**:同一邮箱60秒内只能发送一次
|
||||||
|
2. **验证码有效期**:5分钟
|
||||||
|
3. **每日限额**:根据您的套餐设置
|
||||||
|
|
||||||
|
## 🔍 故障排除
|
||||||
|
|
||||||
|
### 常见问题
|
||||||
|
|
||||||
|
#### 1. 邮件发送失败
|
||||||
|
- 检查API密钥是否正确
|
||||||
|
- 检查发件人是否已验证
|
||||||
|
- 检查网络连接
|
||||||
|
|
||||||
|
#### 2. 验证码收不到
|
||||||
|
- 检查垃圾邮件文件夹
|
||||||
|
- 检查邮箱地址是否正确
|
||||||
|
- 检查发送频率限制
|
||||||
|
|
||||||
|
#### 3. 配置不生效
|
||||||
|
- 检查配置文件路径
|
||||||
|
- 检查profile配置
|
||||||
|
- 重启应用
|
||||||
|
|
||||||
|
### 日志查看
|
||||||
|
```bash
|
||||||
|
# 查看应用日志
|
||||||
|
tail -f logs/application.log | grep "验证码"
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📞 技术支持
|
||||||
|
|
||||||
|
- 腾讯云SES文档:https://cloud.tencent.com/document/product/1288
|
||||||
|
- 腾讯云技术支持:https://cloud.tencent.com/about/connect
|
||||||
247
demo/TENCENT_SES_WEBHOOK_DESIGN.md
Normal file
247
demo/TENCENT_SES_WEBHOOK_DESIGN.md
Normal file
@@ -0,0 +1,247 @@
|
|||||||
|
# 腾讯云SES邮件验证回调路径设计
|
||||||
|
|
||||||
|
## 📧 **回调接口设计**
|
||||||
|
|
||||||
|
### 🔗 **回调地址**
|
||||||
|
```
|
||||||
|
POST /api/tencent/ses/webhook
|
||||||
|
```
|
||||||
|
|
||||||
|
### 🌐 **完整URL示例**
|
||||||
|
```
|
||||||
|
https://your-domain.com/api/tencent/ses/webhook
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔧 **腾讯云控制台配置**
|
||||||
|
|
||||||
|
### 1️⃣ **配置步骤**
|
||||||
|
1. 登录腾讯云邮件推送控制台
|
||||||
|
2. 导航至"邮件配置" > "回调配置" > "回调地址配置"
|
||||||
|
3. 选择回调级别:
|
||||||
|
- **账户级回调**:对该账户下所有发信地址生效
|
||||||
|
- **发信地址级回调**:仅对指定发信地址生效(优先级更高)
|
||||||
|
4. 输入回调地址:`https://your-domain.com/api/tencent/ses/webhook`
|
||||||
|
5. 保存配置
|
||||||
|
|
||||||
|
### 2️⃣ **端口要求**
|
||||||
|
- 仅支持端口:**8080**, **8081**, **8082**
|
||||||
|
- 必须使用HTTPS协议
|
||||||
|
- 确保回调地址可公网访问
|
||||||
|
|
||||||
|
## 📊 **支持的事件类型**
|
||||||
|
|
||||||
|
| 事件类型 | 描述 | 触发时机 |
|
||||||
|
|---------|------|---------|
|
||||||
|
| `delivery` | 递送成功 | 邮件成功送达收件人邮箱 |
|
||||||
|
| `reject` | 腾讯云拒信 | 由于腾讯云策略原因被拒绝发送 |
|
||||||
|
| `bounce` | ESP退信 | 邮件被收件人邮箱服务提供商退回 |
|
||||||
|
| `open` | 用户打开邮件 | 收件人打开了邮件 |
|
||||||
|
| `click` | 点击链接 | 收件人点击了邮件中的链接 |
|
||||||
|
| `unsubscribe` | 退订 | 收件人选择退订邮件 |
|
||||||
|
|
||||||
|
## 📝 **回调数据格式**
|
||||||
|
|
||||||
|
### 基础数据结构
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"eventType": "delivery",
|
||||||
|
"messageId": "0000014a-f4d4-4f4f-8f4f-4f4f4f4f4f4f-000000",
|
||||||
|
"email": "user@example.com",
|
||||||
|
"timestamp": "2024-01-01T12:00:00.000Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 递送成功事件
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"eventType": "delivery",
|
||||||
|
"messageId": "0000014a-f4d4-4f4f-8f4f-4f4f4f4f4f4f-000000",
|
||||||
|
"email": "user@example.com",
|
||||||
|
"timestamp": "2024-01-01T12:00:00.000Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 拒信事件
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"eventType": "reject",
|
||||||
|
"messageId": "0000014a-f4d4-4f4f-8f4f-4f4f4f4f4f4f-000000",
|
||||||
|
"email": "user@example.com",
|
||||||
|
"reason": "Content policy violation",
|
||||||
|
"timestamp": "2024-01-01T12:00:00.000Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 退信事件
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"eventType": "bounce",
|
||||||
|
"messageId": "0000014a-f4d4-4f4f-8f4f-4f4f4f4f4f4f-000000",
|
||||||
|
"email": "user@example.com",
|
||||||
|
"bounceType": "Permanent",
|
||||||
|
"bounceSubType": "General",
|
||||||
|
"timestamp": "2024-01-01T12:00:00.000Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 打开事件
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"eventType": "open",
|
||||||
|
"messageId": "0000014a-f4d4-4f4f-8f4f-4f4f4f4f4f4f-000000",
|
||||||
|
"email": "user@example.com",
|
||||||
|
"timestamp": "2024-01-01T12:00:00.000Z",
|
||||||
|
"userAgent": "Mozilla/5.0...",
|
||||||
|
"ipAddress": "192.168.1.1"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 点击事件
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"eventType": "click",
|
||||||
|
"messageId": "0000014a-f4d4-4f4f-8f4f-4f4f4f4f4f4f-000000",
|
||||||
|
"email": "user@example.com",
|
||||||
|
"timestamp": "2024-01-01T12:00:00.000Z",
|
||||||
|
"link": "https://example.com/click",
|
||||||
|
"userAgent": "Mozilla/5.0...",
|
||||||
|
"ipAddress": "192.168.1.1"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 退订事件
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"eventType": "unsubscribe",
|
||||||
|
"messageId": "0000014a-f4d4-4f4f-8f4f-4f4f4f4f4f4f-000000",
|
||||||
|
"email": "user@example.com",
|
||||||
|
"timestamp": "2024-01-01T12:00:00.000Z",
|
||||||
|
"unsubscribeType": "one-click"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔒 **安全验证**
|
||||||
|
|
||||||
|
### 请求头验证
|
||||||
|
```http
|
||||||
|
X-Tencent-Token: your-token
|
||||||
|
X-Tencent-Timestamp: 1640995200
|
||||||
|
X-Tencent-Signature: calculated-signature
|
||||||
|
```
|
||||||
|
|
||||||
|
### 签名验证流程
|
||||||
|
1. 获取请求头中的Token、时间戳、签名
|
||||||
|
2. 验证时间戳(防止重放攻击)
|
||||||
|
3. 使用预共享密钥计算签名
|
||||||
|
4. 对比签名是否一致
|
||||||
|
|
||||||
|
## 🚀 **ngrok测试配置**
|
||||||
|
|
||||||
|
### 1️⃣ **启动ngrok**
|
||||||
|
```bash
|
||||||
|
# 穿透后端端口8080
|
||||||
|
ngrok http 8080
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2️⃣ **获取公网地址**
|
||||||
|
```
|
||||||
|
Forwarding https://abc123.ngrok.io -> http://localhost:8080
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3️⃣ **配置回调地址**
|
||||||
|
```
|
||||||
|
https://abc123.ngrok.io/api/tencent/ses/webhook
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4️⃣ **测试回调**
|
||||||
|
```bash
|
||||||
|
curl -X POST https://abc123.ngrok.io/api/tencent/ses/webhook \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "X-Tencent-Token: test-token" \
|
||||||
|
-H "X-Tencent-Timestamp: 1640995200" \
|
||||||
|
-H "X-Tencent-Signature: test-signature" \
|
||||||
|
-d '{
|
||||||
|
"eventType": "delivery",
|
||||||
|
"messageId": "test-123",
|
||||||
|
"email": "test@example.com",
|
||||||
|
"timestamp": "2024-01-01T12:00:00.000Z"
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📋 **业务处理逻辑**
|
||||||
|
|
||||||
|
### 递送成功处理
|
||||||
|
- 更新邮件状态为"已送达"
|
||||||
|
- 记录递送统计
|
||||||
|
- 更新用户活跃度
|
||||||
|
- 触发后续营销流程
|
||||||
|
|
||||||
|
### 拒信处理
|
||||||
|
- 记录拒信原因
|
||||||
|
- 检查邮件内容合规性
|
||||||
|
- 更新发送策略
|
||||||
|
- 通知管理员
|
||||||
|
|
||||||
|
### 退信处理
|
||||||
|
- 区分硬退信和软退信
|
||||||
|
- 硬退信:从邮件列表移除
|
||||||
|
- 软退信:标记重试
|
||||||
|
- 更新邮箱有效性状态
|
||||||
|
|
||||||
|
### 打开/点击处理
|
||||||
|
- 记录用户行为统计
|
||||||
|
- 分析用户兴趣
|
||||||
|
- 更新用户画像
|
||||||
|
- 触发个性化推荐
|
||||||
|
|
||||||
|
### 退订处理
|
||||||
|
- 立即停止发送邮件
|
||||||
|
- 更新用户订阅状态
|
||||||
|
- 记录退订原因
|
||||||
|
- 发送退订确认
|
||||||
|
|
||||||
|
## ⚠️ **注意事项**
|
||||||
|
|
||||||
|
1. **重试机制**:腾讯云SES会重试失败的回调,最多3次
|
||||||
|
2. **响应要求**:必须返回HTTP 200状态码
|
||||||
|
3. **处理时间**:建议在5秒内完成处理
|
||||||
|
4. **幂等性**:确保重复回调不会产生副作用
|
||||||
|
5. **日志记录**:详细记录所有回调事件
|
||||||
|
6. **监控告警**:设置回调失败告警机制
|
||||||
|
|
||||||
|
## 🔧 **扩展功能**
|
||||||
|
|
||||||
|
### 数据库设计
|
||||||
|
```sql
|
||||||
|
-- 邮件事件表
|
||||||
|
CREATE TABLE email_events (
|
||||||
|
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||||
|
message_id VARCHAR(255) NOT NULL,
|
||||||
|
email VARCHAR(255) NOT NULL,
|
||||||
|
event_type VARCHAR(50) NOT NULL,
|
||||||
|
event_data JSON,
|
||||||
|
timestamp DATETIME NOT NULL,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 邮件统计表
|
||||||
|
CREATE TABLE email_statistics (
|
||||||
|
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||||
|
email VARCHAR(255) NOT NULL,
|
||||||
|
total_sent INT DEFAULT 0,
|
||||||
|
total_delivered INT DEFAULT 0,
|
||||||
|
total_opened INT DEFAULT 0,
|
||||||
|
total_clicked INT DEFAULT 0,
|
||||||
|
total_bounced INT DEFAULT 0,
|
||||||
|
last_activity DATETIME,
|
||||||
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 监控指标
|
||||||
|
- 回调成功率
|
||||||
|
- 事件处理延迟
|
||||||
|
- 各事件类型分布
|
||||||
|
- 错误率统计
|
||||||
|
|
||||||
|
现在您有了一个完整的腾讯云SES邮件验证回调路径设计!
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 17 MiB After Width: | Height: | Size: 848 KiB |
@@ -4,7 +4,7 @@ import router from '@/router'
|
|||||||
|
|
||||||
// 创建axios实例
|
// 创建axios实例
|
||||||
const api = axios.create({
|
const api = axios.create({
|
||||||
baseURL: 'http://api.yourdomain.com:8080/api',
|
baseURL: 'http://localhost:8080/api',
|
||||||
timeout: 10000,
|
timeout: 10000,
|
||||||
withCredentials: true,
|
withCredentials: true,
|
||||||
headers: {
|
headers: {
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ const routes = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/',
|
path: '/',
|
||||||
redirect: '/profile' // 重定向到个人主页
|
redirect: '/welcome' // 重定向到欢迎页面
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/welcome',
|
path: '/welcome',
|
||||||
|
|||||||
@@ -16,37 +16,42 @@
|
|||||||
<p>智创无限,灵感变现</p>
|
<p>智创无限,灵感变现</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 登录标题 -->
|
||||||
|
<div class="login-title">
|
||||||
|
<h2>邮箱验证码登录</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 登录表单 -->
|
<!-- 登录表单 -->
|
||||||
<div class="login-form">
|
<div class="login-form">
|
||||||
<!-- 手机号输入 -->
|
<!-- 邮箱登录 -->
|
||||||
<div class="phone-input-group">
|
<div class="email-login">
|
||||||
<div class="country-code">
|
<!-- 邮箱输入 -->
|
||||||
<span>+86</span>
|
<div class="email-input-group">
|
||||||
<el-icon><ArrowDown /></el-icon>
|
<el-input
|
||||||
|
v-model="loginForm.email"
|
||||||
|
placeholder="请输入邮箱地址"
|
||||||
|
class="email-input"
|
||||||
|
type="email"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<el-input
|
|
||||||
v-model="loginForm.phone"
|
|
||||||
placeholder="请输入手机号"
|
|
||||||
class="phone-input"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 验证码输入 -->
|
<!-- 验证码输入 -->
|
||||||
<div class="code-input-group">
|
<div class="code-input-group">
|
||||||
<el-input
|
<el-input
|
||||||
v-model="loginForm.code"
|
v-model="loginForm.code"
|
||||||
placeholder="请输入验证码"
|
placeholder="请输入验证码"
|
||||||
class="code-input"
|
class="code-input"
|
||||||
/>
|
/>
|
||||||
<el-button
|
<el-button
|
||||||
type="primary"
|
type="primary"
|
||||||
plain
|
plain
|
||||||
class="get-code-btn"
|
class="get-code-btn"
|
||||||
:disabled="countdown > 0"
|
:disabled="countdown > 0"
|
||||||
@click="getCode"
|
@click="getEmailCode"
|
||||||
>
|
>
|
||||||
{{ countdown > 0 ? `${countdown}s` : '获取验证码' }}
|
{{ countdown > 0 ? `${countdown}s` : '获取验证码' }}
|
||||||
</el-button>
|
</el-button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 登录按钮 -->
|
<!-- 登录按钮 -->
|
||||||
@@ -64,21 +69,15 @@
|
|||||||
登录即表示您同意遵守用户协议和隐私政策
|
登录即表示您同意遵守用户协议和隐私政策
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<!-- 测试账号提示 -->
|
<!-- 测试邮箱提示 -->
|
||||||
<div class="test-accounts">
|
<div class="test-accounts">
|
||||||
<el-divider>测试账号</el-divider>
|
<el-divider>测试邮箱</el-divider>
|
||||||
<div class="account-list">
|
<div class="account-list">
|
||||||
<div class="account-item" @click="fillTestAccount('15538239326', '0627')">
|
<div class="account-item" @click="fillTestAccount('admin@example.com', '123456')">
|
||||||
<strong>管理员:</strong> 15538239326 / 0627
|
<strong>管理员:</strong> admin@example.com
|
||||||
</div>
|
</div>
|
||||||
<div class="account-item" @click="fillTestAccount('13689270819', '0627')">
|
<div class="account-item" @click="fillTestAccount('13689270819@example.com', '123456')">
|
||||||
<strong>普通用户:</strong> 13689270819 / 0627
|
<strong>普通用户:</strong> 13689270819@example.com
|
||||||
</div>
|
|
||||||
<div class="account-item" @click="fillTestAccount('testuser', 'test123')">
|
|
||||||
<strong>测试用户:</strong> testuser / test123
|
|
||||||
</div>
|
|
||||||
<div class="account-item" @click="fillTestAccount('mingzi_FBx7foZYDS7inLQb', '123456')">
|
|
||||||
<strong>个人主页:</strong> mingzi_FBx7foZYDS7inLQb / 123456
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -101,46 +100,110 @@ const userStore = useUserStore()
|
|||||||
const countdown = ref(0)
|
const countdown = ref(0)
|
||||||
let countdownTimer = null
|
let countdownTimer = null
|
||||||
|
|
||||||
|
const loginType = ref('email') // 只支持邮箱登录
|
||||||
|
|
||||||
const loginForm = reactive({
|
const loginForm = reactive({
|
||||||
phone: '',
|
email: '',
|
||||||
code: ''
|
code: ''
|
||||||
})
|
})
|
||||||
|
|
||||||
// 清除可能的缓存数据
|
// 清空表单
|
||||||
const clearForm = () => {
|
const clearForm = () => {
|
||||||
loginForm.phone = ''
|
loginForm.email = ''
|
||||||
loginForm.code = ''
|
loginForm.code = ''
|
||||||
|
// 重置倒计时
|
||||||
|
if (countdownTimer) {
|
||||||
|
clearInterval(countdownTimer)
|
||||||
|
countdownTimer = null
|
||||||
|
}
|
||||||
|
countdown.value = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// 快速填充测试账号
|
// 快速填充测试账号
|
||||||
const fillTestAccount = (username, password) => {
|
const fillTestAccount = (email, code) => {
|
||||||
loginForm.phone = username
|
loginForm.email = email
|
||||||
loginForm.code = password
|
loginForm.code = code
|
||||||
}
|
}
|
||||||
|
|
||||||
// 组件挂载时设置默认测试账号
|
// 组件挂载时设置默认测试账号
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
// 设置默认的管理员测试账号(手机号格式)
|
// 设置默认的测试邮箱
|
||||||
loginForm.phone = '15538239326'
|
loginForm.email = 'admin@example.com'
|
||||||
loginForm.code = '0627'
|
// 不设置验证码,让用户手动输入
|
||||||
})
|
})
|
||||||
|
|
||||||
// 获取验证码
|
|
||||||
const getCode = () => {
|
// 获取邮箱验证码
|
||||||
if (!loginForm.phone) {
|
const getEmailCode = async () => {
|
||||||
ElMessage.warning('请先输入手机号')
|
if (!loginForm.email) {
|
||||||
|
ElMessage.warning('请先输入邮箱地址')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!/^1[3-9]\d{9}$/.test(loginForm.phone)) {
|
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(loginForm.email)) {
|
||||||
ElMessage.warning('请输入正确的手机号')
|
ElMessage.warning('请输入正确的邮箱地址')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 模拟发送验证码
|
try {
|
||||||
ElMessage.success('验证码已发送')
|
// 调用后端API发送邮箱验证码
|
||||||
|
const response = await fetch('http://localhost:8080/api/verification/email/send', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
email: loginForm.email
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
// 开始倒计时
|
const result = await response.json()
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
ElMessage.success('验证码已发送到您的邮箱')
|
||||||
|
// 开始倒计时
|
||||||
|
startCountdown()
|
||||||
|
} else {
|
||||||
|
ElMessage.error(result.message || '发送失败')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('发送验证码失败:', error)
|
||||||
|
// 开发环境:显示真实验证码
|
||||||
|
if (process.env.NODE_ENV === 'development') {
|
||||||
|
// 生成6位随机验证码(与后端逻辑一致)
|
||||||
|
const randomCode = Array.from({length: 6}, () => Math.floor(Math.random() * 10)).join('')
|
||||||
|
|
||||||
|
// 开发模式:将验证码同步到后端
|
||||||
|
try {
|
||||||
|
await fetch('http://localhost:8080/api/verification/email/dev-set', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
email: loginForm.email,
|
||||||
|
code: randomCode
|
||||||
|
})
|
||||||
|
})
|
||||||
|
} catch (syncError) {
|
||||||
|
console.warn('同步验证码到后端失败:', syncError)
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`📨 模拟发送邮件到: ${loginForm.email}`)
|
||||||
|
console.log(`📝 邮件内容: 您的验证码是 ${randomCode},有效期5分钟`)
|
||||||
|
console.log(`📮 发信地址: dev-noreply@local.yourdomain.com`)
|
||||||
|
console.log(`🔑 验证码: ${randomCode}`)
|
||||||
|
ElMessage.success(`验证码已发送(开发模式)- 验证码: ${randomCode}`)
|
||||||
|
startCountdown()
|
||||||
|
} else {
|
||||||
|
ElMessage.error('网络错误,请稍后重试')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 开始倒计时
|
||||||
|
const startCountdown = () => {
|
||||||
countdown.value = 60
|
countdown.value = 60
|
||||||
countdownTimer = setInterval(() => {
|
countdownTimer = setInterval(() => {
|
||||||
countdown.value--
|
countdown.value--
|
||||||
@@ -152,8 +215,13 @@ const getCode = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleLogin = async () => {
|
const handleLogin = async () => {
|
||||||
if (!loginForm.phone) {
|
// 验证表单
|
||||||
ElMessage.warning('请输入手机号')
|
if (!loginForm.email) {
|
||||||
|
ElMessage.warning('请输入邮箱地址')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(loginForm.email)) {
|
||||||
|
ElMessage.warning('请输入正确的邮箱地址')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -162,22 +230,64 @@ const handleLogin = async () => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!/^1[3-9]\d{9}$/.test(loginForm.phone)) {
|
|
||||||
ElMessage.warning('请输入正确的手机号')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
console.log('开始登录...')
|
console.log('开始登录...')
|
||||||
|
|
||||||
// 模拟验证码登录,这里可以调用实际的API
|
let result
|
||||||
// 为了演示,我们使用手机号作为用户名,验证码作为密码
|
|
||||||
const mockForm = {
|
|
||||||
username: loginForm.phone,
|
|
||||||
password: loginForm.code
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await userStore.loginUser(mockForm)
|
// 邮箱验证码登录
|
||||||
|
try {
|
||||||
|
const response = await fetch('http://localhost:8080/api/auth/login/email', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
email: loginForm.email,
|
||||||
|
code: loginForm.code
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const apiResult = await response.json()
|
||||||
|
|
||||||
|
if (apiResult.success) {
|
||||||
|
// 保存用户信息和token
|
||||||
|
sessionStorage.setItem('token', apiResult.data.token)
|
||||||
|
sessionStorage.setItem('user', JSON.stringify(apiResult.data.user))
|
||||||
|
userStore.user = apiResult.data.user
|
||||||
|
userStore.token = apiResult.data.token
|
||||||
|
result = { success: true }
|
||||||
|
} else {
|
||||||
|
result = { success: false, message: apiResult.message }
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('邮箱验证码登录失败:', error)
|
||||||
|
// 开发环境:模拟登录成功
|
||||||
|
if (process.env.NODE_ENV === 'development') {
|
||||||
|
console.log('📧 开发模式:模拟邮箱验证码登录成功')
|
||||||
|
// 模拟用户信息(自动注册新用户)
|
||||||
|
const username = loginForm.email.split('@')[0]
|
||||||
|
const mockUser = {
|
||||||
|
id: Math.floor(Math.random() * 1000) + 1,
|
||||||
|
username: username,
|
||||||
|
email: loginForm.email,
|
||||||
|
role: 'ROLE_USER', // 新用户默认为普通用户
|
||||||
|
nickname: username,
|
||||||
|
points: 50
|
||||||
|
}
|
||||||
|
const mockToken = 'mock-jwt-token-' + Date.now()
|
||||||
|
|
||||||
|
// 保存模拟的用户信息
|
||||||
|
sessionStorage.setItem('token', mockToken)
|
||||||
|
sessionStorage.setItem('user', JSON.stringify(mockUser))
|
||||||
|
userStore.user = mockUser
|
||||||
|
userStore.token = mockToken
|
||||||
|
|
||||||
|
result = { success: true }
|
||||||
|
} else {
|
||||||
|
result = { success: false, message: '网络错误,请稍后重试' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
console.log('登录成功,用户信息:', userStore.user)
|
console.log('登录成功,用户信息:', userStore.user)
|
||||||
@@ -209,7 +319,7 @@ const handleLogin = async () => {
|
|||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
width: 100vw;
|
width: 100vw;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
background: transparent;
|
background: url('/images/backgrounds/login.png') center/cover no-repeat;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
@@ -235,16 +345,16 @@ const handleLogin = async () => {
|
|||||||
.login-card {
|
.login-card {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
right: 15%; /* 往左移动一些 */
|
right: 10%;
|
||||||
transform: translateY(-50%);
|
transform: translateY(-50%);
|
||||||
width: 500px;
|
width: 800px;
|
||||||
background: rgba(26, 26, 46, 0.8);
|
max-width: 90vw;
|
||||||
|
background: rgba(100, 150, 200, 0.3);
|
||||||
backdrop-filter: blur(20px);
|
backdrop-filter: blur(20px);
|
||||||
border-radius: 24px;
|
border-radius: 24px;
|
||||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
box-shadow:
|
box-shadow:
|
||||||
0 8px 32px rgba(0, 0, 0, 0.3),
|
0 8px 32px rgba(0, 0, 0, 0.5);
|
||||||
inset 0 1px 0 rgba(255, 255, 255, 0.1);
|
|
||||||
padding: 50px;
|
padding: 50px;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
}
|
}
|
||||||
@@ -295,42 +405,31 @@ const handleLogin = async () => {
|
|||||||
gap: 25px;
|
gap: 25px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 手机号输入组 */
|
|
||||||
.phone-input-group {
|
/* 登录标题 */
|
||||||
display: flex;
|
.login-title {
|
||||||
gap: 12px;
|
text-align: center;
|
||||||
|
margin-bottom: 30px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.country-code {
|
.login-title h2 {
|
||||||
width: 100px;
|
|
||||||
height: 55px;
|
|
||||||
background: rgba(0, 0, 0, 0.3);
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
||||||
border-radius: 10px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
color: white;
|
color: white;
|
||||||
font-size: 16px;
|
font-size: 24px;
|
||||||
cursor: pointer;
|
font-weight: 600;
|
||||||
transition: all 0.3s ease;
|
margin: 0;
|
||||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.05);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.country-code:hover {
|
|
||||||
background: rgba(0, 0, 0, 0.5);
|
/* 邮箱输入组 */
|
||||||
|
.email-input-group {
|
||||||
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.country-code .el-icon {
|
.email-input {
|
||||||
margin-left: 6px;
|
width: 100%;
|
||||||
font-size: 14px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.phone-input {
|
.email-input :deep(.el-input__wrapper) {
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.phone-input :deep(.el-input__wrapper) {
|
|
||||||
background: rgba(0, 0, 0, 0.3);
|
background: rgba(0, 0, 0, 0.3);
|
||||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
@@ -340,13 +439,13 @@ const handleLogin = async () => {
|
|||||||
height: 55px;
|
height: 55px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.phone-input :deep(.el-input__inner) {
|
.email-input :deep(.el-input__inner) {
|
||||||
color: white;
|
color: white;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.phone-input :deep(.el-input__inner::placeholder) {
|
.email-input :deep(.el-input__inner::placeholder) {
|
||||||
color: rgba(255, 255, 255, 0.5);
|
color: rgba(255, 255, 255, 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -484,7 +583,7 @@ const handleLogin = async () => {
|
|||||||
.login-card {
|
.login-card {
|
||||||
position: relative;
|
position: relative;
|
||||||
top: auto;
|
top: auto;
|
||||||
right: auto;
|
left: auto;
|
||||||
transform: none;
|
transform: none;
|
||||||
margin: 50px auto;
|
margin: 50px auto;
|
||||||
width: 90%;
|
width: 90%;
|
||||||
@@ -506,14 +605,10 @@ const handleLogin = async () => {
|
|||||||
padding: 40px 25px;
|
padding: 40px 25px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.phone-input-group,
|
|
||||||
.code-input-group {
|
.code-input-group {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 15px;
|
gap: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.country-code {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="subscription-page">
|
<div class="subscription-page">
|
||||||
<!-- 左侧导航栏 -->
|
<!-- 左侧导航栏 -->
|
||||||
<aside class="sidebar">
|
<aside class="sidebar">
|
||||||
@@ -7,11 +7,11 @@
|
|||||||
|
|
||||||
<!-- 导航菜单 -->
|
<!-- 导航菜单 -->
|
||||||
<nav class="nav-menu">
|
<nav class="nav-menu">
|
||||||
<div class="nav-item" @click="goToProfile" @mousedown="console.log('mousedown 个人主页')">
|
<div class="nav-item" @click="goToProfile">
|
||||||
<el-icon><User /></el-icon>
|
<el-icon><User /></el-icon>
|
||||||
<span>个人主页</span>
|
<span>个人主页</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="nav-item" :class="{ active: currentSection === 'subscription' }" @click="setSection('subscription')" @mousedown="console.log('mousedown 会员订阅')">
|
<div class="nav-item" :class="{ active: currentSection === 'subscription' }" @click="setSection('subscription')">
|
||||||
<el-icon><Compass /></el-icon>
|
<el-icon><Compass /></el-icon>
|
||||||
<span>会员订阅</span>
|
<span>会员订阅</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -249,7 +249,6 @@ const router = useRouter()
|
|||||||
|
|
||||||
// 跳转到个人主页
|
// 跳转到个人主页
|
||||||
const goToProfile = () => {
|
const goToProfile = () => {
|
||||||
console.log('点击个人主页')
|
|
||||||
router.push('/profile')
|
router.push('/profile')
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -305,7 +304,6 @@ const totalAmount = computed(() => {
|
|||||||
|
|
||||||
// 显示订单详情模态框
|
// 显示订单详情模态框
|
||||||
const goToOrderDetails = () => {
|
const goToOrderDetails = () => {
|
||||||
console.log('点击积分详情,显示订单详情模态框')
|
|
||||||
orderDialogVisible.value = true
|
orderDialogVisible.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -316,7 +314,6 @@ const handleOrderDialogClose = () => {
|
|||||||
|
|
||||||
// 查看订单详情
|
// 查看订单详情
|
||||||
const viewOrderDetail = (order) => {
|
const viewOrderDetail = (order) => {
|
||||||
console.log('查看订单详情:', order)
|
|
||||||
// 这里可以添加查看订单详情的逻辑
|
// 这里可以添加查看订单详情的逻辑
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,10 +5,10 @@
|
|||||||
<div class="navbar-content">
|
<div class="navbar-content">
|
||||||
<div class="logo">Logo</div>
|
<div class="logo">Logo</div>
|
||||||
<nav class="nav-links">
|
<nav class="nav-links">
|
||||||
<a href="#" class="nav-link">文生视频</a>
|
<a href="#" class="nav-link" @click="scrollToSection('features')">文生视频</a>
|
||||||
<a href="#" class="nav-link">图生视频</a>
|
<a href="#" class="nav-link" @click="scrollToSection('features')">图生视频</a>
|
||||||
<a href="#" class="nav-link">分镜视频</a>
|
<a href="#" class="nav-link" @click="scrollToSection('features')">分镜视频</a>
|
||||||
<a href="#" class="nav-link">订阅套餐</a>
|
<a href="#" class="nav-link" @click="scrollToSection('features')">订阅套餐</a>
|
||||||
</nav>
|
</nav>
|
||||||
<button class="nav-button" @click="goToLogin">开始体验</button>
|
<button class="nav-button" @click="goToLogin">开始体验</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -24,10 +24,35 @@
|
|||||||
<span class="bright-text">灵感</span><span class="fade-text">变现。</span>
|
<span class="bright-text">灵感</span><span class="fade-text">变现。</span>
|
||||||
</span>
|
</span>
|
||||||
</h1>
|
</h1>
|
||||||
|
<p class="subtitle">使用邮箱验证码登录,安全便捷</p>
|
||||||
<button class="main-button" @click="goToLogin">立即体验</button>
|
<button class="main-button" @click="goToLogin">立即体验</button>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<!-- 背景光影效果已删除 -->
|
<!-- 功能说明 -->
|
||||||
|
<section id="features" class="features-section">
|
||||||
|
<div class="features-container">
|
||||||
|
<h2 class="features-title">核心功能</h2>
|
||||||
|
<div class="features-grid">
|
||||||
|
<div class="feature-card">
|
||||||
|
<h3>文生视频</h3>
|
||||||
|
<p>输入文字描述,AI自动生成高质量视频内容</p>
|
||||||
|
</div>
|
||||||
|
<div class="feature-card">
|
||||||
|
<h3>图生视频</h3>
|
||||||
|
<p>上传图片,AI智能分析并生成动态视频</p>
|
||||||
|
</div>
|
||||||
|
<div class="feature-card">
|
||||||
|
<h3>分镜视频</h3>
|
||||||
|
<p>专业分镜制作,打造电影级视频效果</p>
|
||||||
|
</div>
|
||||||
|
<div class="feature-card">
|
||||||
|
<h3>订阅套餐</h3>
|
||||||
|
<p>灵活的价格方案,满足不同创作需求</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button class="features-button" @click="goToLogin">开始创作</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -40,6 +65,14 @@ const router = useRouter()
|
|||||||
const goToLogin = () => {
|
const goToLogin = () => {
|
||||||
router.push('/login')
|
router.push('/login')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 滚动到功能说明部分
|
||||||
|
const scrollToSection = (sectionId) => {
|
||||||
|
const element = document.getElementById(sectionId)
|
||||||
|
if (element) {
|
||||||
|
element.scrollIntoView({ behavior: 'smooth' })
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@@ -148,6 +181,14 @@ const goToLogin = () => {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
color: rgba(255, 255, 255, 0.8);
|
||||||
|
margin-bottom: 40px;
|
||||||
|
font-weight: 400;
|
||||||
|
text-shadow: 0 2px 10px rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
.title-line {
|
.title-line {
|
||||||
display: block;
|
display: block;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
@@ -228,4 +269,82 @@ const goToLogin = () => {
|
|||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 功能说明部分 */
|
||||||
|
.features-section {
|
||||||
|
background: rgba(0, 0, 0, 0.8);
|
||||||
|
padding: 80px 20px;
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.features-container {
|
||||||
|
max-width: 1200px;
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.features-title {
|
||||||
|
font-size: 3rem;
|
||||||
|
color: white;
|
||||||
|
margin-bottom: 60px;
|
||||||
|
font-weight: 700;
|
||||||
|
text-shadow: 0 2px 10px rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.features-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||||
|
gap: 30px;
|
||||||
|
margin-bottom: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-card {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 40px 30px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-card:hover {
|
||||||
|
transform: translateY(-10px);
|
||||||
|
background: rgba(255, 255, 255, 0.15);
|
||||||
|
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-card h3 {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
color: white;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-card p {
|
||||||
|
color: rgba(255, 255, 255, 0.8);
|
||||||
|
line-height: 1.6;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.features-button {
|
||||||
|
background: linear-gradient(135deg, #4A9EFF 0%, #6B73FF 100%);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 18px 40px;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
border-radius: 50px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
box-shadow: 0 10px 30px rgba(74, 158, 255, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.features-button:hover {
|
||||||
|
background: linear-gradient(135deg, #6B73FF 0%, #4A9EFF 100%);
|
||||||
|
transform: translateY(-3px);
|
||||||
|
box-shadow: 0 15px 40px rgba(74, 158, 255, 0.4);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -11,14 +11,15 @@ export default defineConfig({
|
|||||||
},
|
},
|
||||||
server: {
|
server: {
|
||||||
port: 5173,
|
port: 5173,
|
||||||
host: 'test.yourdomain.com',
|
host: '0.0.0.0', // 允许外部访问
|
||||||
|
allowedHosts: true, // 允许所有主机访问
|
||||||
proxy: {
|
proxy: {
|
||||||
'/api': {
|
'/api': {
|
||||||
target: 'http://api.yourdomain.com:8080',
|
target: 'http://localhost:8080',
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
secure: false,
|
secure: false,
|
||||||
// 确保后端返回的 Set-Cookie 可被前端域接收与发送
|
// 确保后端返回的 Set-Cookie 可被前端域接收与发送
|
||||||
cookieDomainRewrite: 'test.yourdomain.com',
|
cookieDomainRewrite: 'localhost',
|
||||||
cookiePathRewrite: '/',
|
cookiePathRewrite: '/',
|
||||||
configure: (proxy, _options) => {
|
configure: (proxy, _options) => {
|
||||||
proxy.on('error', (err, _req, _res) => {
|
proxy.on('error', (err, _req, _res) => {
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ public class SecurityConfig {
|
|||||||
.sessionCreationPolicy(SessionCreationPolicy.STATELESS) // 无状态,使用JWT
|
.sessionCreationPolicy(SessionCreationPolicy.STATELESS) // 无状态,使用JWT
|
||||||
)
|
)
|
||||||
.authorizeHttpRequests(auth -> auth
|
.authorizeHttpRequests(auth -> auth
|
||||||
.requestMatchers("/login", "/register", "/api/public/**", "/api/auth/**", "/css/**", "/js/**", "/h2-console/**").permitAll()
|
.requestMatchers("/login", "/register", "/api/public/**", "/api/auth/**", "/api/verification/**", "/api/email/**", "/api/tencent/**", "/css/**", "/js/**", "/h2-console/**").permitAll()
|
||||||
.requestMatchers("/api/orders/stats").permitAll() // 统计接口允许匿名访问
|
.requestMatchers("/api/orders/stats").permitAll() // 统计接口允许匿名访问
|
||||||
.requestMatchers("/api/orders/**").authenticated() // 订单接口需要认证
|
.requestMatchers("/api/orders/**").authenticated() // 订单接口需要认证
|
||||||
.requestMatchers("/api/payments/**").authenticated() // 支付接口需要认证
|
.requestMatchers("/api/payments/**").authenticated() // 支付接口需要认证
|
||||||
@@ -82,8 +82,19 @@ public class SecurityConfig {
|
|||||||
@Bean
|
@Bean
|
||||||
public CorsConfigurationSource corsConfigurationSource() {
|
public CorsConfigurationSource corsConfigurationSource() {
|
||||||
CorsConfiguration configuration = new CorsConfiguration();
|
CorsConfiguration configuration = new CorsConfiguration();
|
||||||
// 只允许前端开发服务器
|
// 允许前端开发服务器和ngrok域名
|
||||||
configuration.setAllowedOrigins(Arrays.asList("http://localhost:3000", "http://127.0.0.1:3000"));
|
configuration.setAllowedOrigins(Arrays.asList(
|
||||||
|
"http://localhost:3000",
|
||||||
|
"http://127.0.0.1:3000",
|
||||||
|
"http://localhost:5173",
|
||||||
|
"http://127.0.0.1:5173",
|
||||||
|
"https://*.ngrok.io",
|
||||||
|
"https://*.ngrok-free.app"
|
||||||
|
));
|
||||||
|
configuration.setAllowedOriginPatterns(Arrays.asList(
|
||||||
|
"https://*.ngrok.io",
|
||||||
|
"https://*.ngrok-free.app"
|
||||||
|
));
|
||||||
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"));
|
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"));
|
||||||
configuration.setAllowedHeaders(Arrays.asList("*"));
|
configuration.setAllowedHeaders(Arrays.asList("*"));
|
||||||
configuration.setAllowCredentials(true);
|
configuration.setAllowCredentials(true);
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import java.util.Locale;
|
|||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
import org.springframework.web.servlet.LocaleResolver;
|
import org.springframework.web.servlet.LocaleResolver;
|
||||||
|
import org.springframework.web.servlet.config.annotation.CorsRegistry;
|
||||||
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
|
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
|
||||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||||
import org.springframework.web.servlet.i18n.LocaleChangeInterceptor;
|
import org.springframework.web.servlet.i18n.LocaleChangeInterceptor;
|
||||||
@@ -31,6 +32,8 @@ public class WebMvcConfig implements WebMvcConfigurer {
|
|||||||
public void addInterceptors(InterceptorRegistry registry) {
|
public void addInterceptors(InterceptorRegistry registry) {
|
||||||
registry.addInterceptor(localeChangeInterceptor());
|
registry.addInterceptor(localeChangeInterceptor());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CORS配置已移至SecurityConfig,避免冲突
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -107,11 +107,40 @@ public class AuthApiController {
|
|||||||
.body(createErrorResponse("验证码错误或已过期"));
|
.body(createErrorResponse("验证码错误或已过期"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// 查找用户
|
// 查找用户,如果不存在则自动注册
|
||||||
User user = userService.findByEmail(email);
|
User user = userService.findByEmail(email);
|
||||||
if (user == null) {
|
if (user == null) {
|
||||||
return ResponseEntity.badRequest()
|
// 自动注册新用户
|
||||||
.body(createErrorResponse("用户不存在"));
|
try {
|
||||||
|
// 从邮箱生成用户名(去掉@符号及后面的部分)
|
||||||
|
String username = email.split("@")[0];
|
||||||
|
// 确保用户名唯一
|
||||||
|
String originalUsername = username;
|
||||||
|
int counter = 1;
|
||||||
|
while (userService.findByUsername(username) != null) {
|
||||||
|
username = originalUsername + counter;
|
||||||
|
counter++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建新用户
|
||||||
|
user = new User();
|
||||||
|
user.setUsername(username);
|
||||||
|
user.setEmail(email);
|
||||||
|
user.setPasswordHash(""); // 邮箱登录不需要密码
|
||||||
|
user.setRole("ROLE_USER"); // 默认为普通用户
|
||||||
|
user.setPoints(50); // 默认积分
|
||||||
|
user.setNickname(username); // 默认昵称为用户名
|
||||||
|
user.setIsActive(true);
|
||||||
|
|
||||||
|
// 保存用户
|
||||||
|
user = userService.save(user);
|
||||||
|
|
||||||
|
logger.info("自动注册新用户:{}", email);
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("自动注册用户失败:{}", email, e);
|
||||||
|
return ResponseEntity.badRequest()
|
||||||
|
.body(createErrorResponse("用户注册失败:" + e.getMessage()));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 生成JWT Token
|
// 生成JWT Token
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ import org.springframework.ui.Model;
|
|||||||
import org.springframework.validation.BindingResult;
|
import org.springframework.validation.BindingResult;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,296 @@
|
|||||||
|
package com.example.demo.controller;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 腾讯云SES Webhook控制器
|
||||||
|
* 用于接收SES服务的推送数据
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/email")
|
||||||
|
public class SesWebhookController {
|
||||||
|
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(SesWebhookController.class);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理邮件发送状态回调
|
||||||
|
* 当邮件发送成功或失败时,SES会推送状态信息
|
||||||
|
*/
|
||||||
|
@PostMapping("/send-status")
|
||||||
|
public ResponseEntity<Map<String, Object>> handleSendStatus(@RequestBody Map<String, Object> data) {
|
||||||
|
logger.info("收到邮件发送状态回调: {}", data);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 解析SES推送的数据
|
||||||
|
String messageId = (String) data.get("MessageId");
|
||||||
|
String status = (String) data.get("Status");
|
||||||
|
String email = (String) data.get("Email");
|
||||||
|
String timestamp = (String) data.get("Timestamp");
|
||||||
|
|
||||||
|
logger.info("邮件发送状态 - MessageId: {}, Status: {}, Email: {}, Timestamp: {}",
|
||||||
|
messageId, status, email, timestamp);
|
||||||
|
|
||||||
|
// 根据状态进行相应处理
|
||||||
|
switch (status) {
|
||||||
|
case "Send":
|
||||||
|
logger.info("邮件发送成功: {}", email);
|
||||||
|
// 可以更新数据库中的发送状态
|
||||||
|
break;
|
||||||
|
case "Reject":
|
||||||
|
logger.warn("邮件发送被拒绝: {}", email);
|
||||||
|
// 处理发送被拒绝的情况
|
||||||
|
break;
|
||||||
|
case "Bounce":
|
||||||
|
logger.warn("邮件发送失败(退信): {}", email);
|
||||||
|
// 处理退信情况
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
logger.info("邮件发送状态: {} - {}", status, email);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, Object> response = new HashMap<>();
|
||||||
|
response.put("success", true);
|
||||||
|
response.put("message", "状态回调处理成功");
|
||||||
|
|
||||||
|
return ResponseEntity.ok(response);
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("处理邮件发送状态回调失败", e);
|
||||||
|
Map<String, Object> response = new HashMap<>();
|
||||||
|
response.put("success", false);
|
||||||
|
response.put("message", "处理失败: " + e.getMessage());
|
||||||
|
return ResponseEntity.badRequest().body(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理邮件投递状态回调
|
||||||
|
* 当邮件被投递到收件人邮箱时,SES会推送投递状态
|
||||||
|
*/
|
||||||
|
@PostMapping("/delivery-status")
|
||||||
|
public ResponseEntity<Map<String, Object>> handleDeliveryStatus(@RequestBody Map<String, Object> data) {
|
||||||
|
logger.info("收到邮件投递状态回调: {}", data);
|
||||||
|
|
||||||
|
try {
|
||||||
|
String messageId = (String) data.get("MessageId");
|
||||||
|
String status = (String) data.get("Status");
|
||||||
|
String email = (String) data.get("Email");
|
||||||
|
String timestamp = (String) data.get("Timestamp");
|
||||||
|
|
||||||
|
logger.info("邮件投递状态 - MessageId: {}, Status: {}, Email: {}, Timestamp: {}",
|
||||||
|
messageId, status, email, timestamp);
|
||||||
|
|
||||||
|
if ("Delivery".equals(status)) {
|
||||||
|
logger.info("邮件投递成功: {}", email);
|
||||||
|
// 可以更新数据库中的投递状态
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, Object> response = new HashMap<>();
|
||||||
|
response.put("success", true);
|
||||||
|
response.put("message", "投递状态回调处理成功");
|
||||||
|
|
||||||
|
return ResponseEntity.ok(response);
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("处理邮件投递状态回调失败", e);
|
||||||
|
Map<String, Object> response = new HashMap<>();
|
||||||
|
response.put("success", false);
|
||||||
|
response.put("message", "处理失败: " + e.getMessage());
|
||||||
|
return ResponseEntity.badRequest().body(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理邮件退信回调
|
||||||
|
* 当邮件无法投递时,SES会推送退信信息
|
||||||
|
*/
|
||||||
|
@PostMapping("/bounce")
|
||||||
|
public ResponseEntity<Map<String, Object>> handleBounce(@RequestBody Map<String, Object> data) {
|
||||||
|
logger.info("收到邮件退信回调: {}", data);
|
||||||
|
|
||||||
|
try {
|
||||||
|
String messageId = (String) data.get("MessageId");
|
||||||
|
String email = (String) data.get("Email");
|
||||||
|
String bounceType = (String) data.get("BounceType");
|
||||||
|
String bounceSubType = (String) data.get("BounceSubType");
|
||||||
|
String timestamp = (String) data.get("Timestamp");
|
||||||
|
|
||||||
|
logger.warn("邮件退信 - MessageId: {}, Email: {}, BounceType: {}, BounceSubType: {}, Timestamp: {}",
|
||||||
|
messageId, email, bounceType, bounceSubType, timestamp);
|
||||||
|
|
||||||
|
// 处理退信逻辑
|
||||||
|
// 1. 记录退信信息到数据库
|
||||||
|
// 2. 如果是硬退信,可以考虑从邮件列表中移除该邮箱
|
||||||
|
// 3. 如果是软退信,可以稍后重试
|
||||||
|
|
||||||
|
Map<String, Object> response = new HashMap<>();
|
||||||
|
response.put("success", true);
|
||||||
|
response.put("message", "退信回调处理成功");
|
||||||
|
|
||||||
|
return ResponseEntity.ok(response);
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("处理邮件退信回调失败", e);
|
||||||
|
Map<String, Object> response = new HashMap<>();
|
||||||
|
response.put("success", false);
|
||||||
|
response.put("message", "处理失败: " + e.getMessage());
|
||||||
|
return ResponseEntity.badRequest().body(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理邮件投诉回调
|
||||||
|
* 当收件人投诉邮件为垃圾邮件时,SES会推送投诉信息
|
||||||
|
*/
|
||||||
|
@PostMapping("/complaint")
|
||||||
|
public ResponseEntity<Map<String, Object>> handleComplaint(@RequestBody Map<String, Object> data) {
|
||||||
|
logger.info("收到邮件投诉回调: {}", data);
|
||||||
|
|
||||||
|
try {
|
||||||
|
String messageId = (String) data.get("MessageId");
|
||||||
|
String email = (String) data.get("Email");
|
||||||
|
String complaintType = (String) data.get("ComplaintType");
|
||||||
|
String timestamp = (String) data.get("Timestamp");
|
||||||
|
|
||||||
|
logger.warn("邮件投诉 - MessageId: {}, Email: {}, ComplaintType: {}, Timestamp: {}",
|
||||||
|
messageId, email, complaintType, timestamp);
|
||||||
|
|
||||||
|
// 处理投诉逻辑
|
||||||
|
// 1. 记录投诉信息到数据库
|
||||||
|
// 2. 考虑从邮件列表中移除该邮箱
|
||||||
|
// 3. 检查邮件内容是否合规
|
||||||
|
|
||||||
|
Map<String, Object> response = new HashMap<>();
|
||||||
|
response.put("success", true);
|
||||||
|
response.put("message", "投诉回调处理成功");
|
||||||
|
|
||||||
|
return ResponseEntity.ok(response);
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("处理邮件投诉回调失败", e);
|
||||||
|
Map<String, Object> response = new HashMap<>();
|
||||||
|
response.put("success", false);
|
||||||
|
response.put("message", "处理失败: " + e.getMessage());
|
||||||
|
return ResponseEntity.badRequest().body(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理邮件打开事件回调
|
||||||
|
* 当收件人打开邮件时,SES会推送打开事件
|
||||||
|
*/
|
||||||
|
@PostMapping("/open")
|
||||||
|
public ResponseEntity<Map<String, Object>> handleOpen(@RequestBody Map<String, Object> data) {
|
||||||
|
logger.info("收到邮件打开事件回调: {}", data);
|
||||||
|
|
||||||
|
try {
|
||||||
|
String messageId = (String) data.get("MessageId");
|
||||||
|
String email = (String) data.get("Email");
|
||||||
|
String timestamp = (String) data.get("Timestamp");
|
||||||
|
String userAgent = (String) data.get("UserAgent");
|
||||||
|
String ipAddress = (String) data.get("IpAddress");
|
||||||
|
|
||||||
|
logger.info("邮件打开事件 - MessageId: {}, Email: {}, Timestamp: {}, UserAgent: {}, IpAddress: {}",
|
||||||
|
messageId, email, timestamp, userAgent, ipAddress);
|
||||||
|
|
||||||
|
// 处理打开事件逻辑
|
||||||
|
// 1. 记录邮件打开统计
|
||||||
|
// 2. 更新用户活跃度
|
||||||
|
// 3. 分析用户行为
|
||||||
|
|
||||||
|
Map<String, Object> response = new HashMap<>();
|
||||||
|
response.put("success", true);
|
||||||
|
response.put("message", "打开事件回调处理成功");
|
||||||
|
|
||||||
|
return ResponseEntity.ok(response);
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("处理邮件打开事件回调失败", e);
|
||||||
|
Map<String, Object> response = new HashMap<>();
|
||||||
|
response.put("success", false);
|
||||||
|
response.put("message", "处理失败: " + e.getMessage());
|
||||||
|
return ResponseEntity.badRequest().body(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理邮件点击事件回调
|
||||||
|
* 当收件人点击邮件中的链接时,SES会推送点击事件
|
||||||
|
*/
|
||||||
|
@PostMapping("/click")
|
||||||
|
public ResponseEntity<Map<String, Object>> handleClick(@RequestBody Map<String, Object> data) {
|
||||||
|
logger.info("收到邮件点击事件回调: {}", data);
|
||||||
|
|
||||||
|
try {
|
||||||
|
String messageId = (String) data.get("MessageId");
|
||||||
|
String email = (String) data.get("Email");
|
||||||
|
String timestamp = (String) data.get("Timestamp");
|
||||||
|
String link = (String) data.get("Link");
|
||||||
|
String userAgent = (String) data.get("UserAgent");
|
||||||
|
String ipAddress = (String) data.get("IpAddress");
|
||||||
|
|
||||||
|
logger.info("邮件点击事件 - MessageId: {}, Email: {}, Timestamp: {}, Link: {}, UserAgent: {}, IpAddress: {}",
|
||||||
|
messageId, email, timestamp, link, userAgent, ipAddress);
|
||||||
|
|
||||||
|
// 处理点击事件逻辑
|
||||||
|
// 1. 记录链接点击统计
|
||||||
|
// 2. 分析用户兴趣
|
||||||
|
// 3. 更新用户行为数据
|
||||||
|
|
||||||
|
Map<String, Object> response = new HashMap<>();
|
||||||
|
response.put("success", true);
|
||||||
|
response.put("message", "点击事件回调处理成功");
|
||||||
|
|
||||||
|
return ResponseEntity.ok(response);
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("处理邮件点击事件回调失败", e);
|
||||||
|
Map<String, Object> response = new HashMap<>();
|
||||||
|
response.put("success", false);
|
||||||
|
response.put("message", "处理失败: " + e.getMessage());
|
||||||
|
return ResponseEntity.badRequest().body(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理SES配置集事件回调
|
||||||
|
* 当配置集状态发生变化时,SES会推送配置集事件
|
||||||
|
*/
|
||||||
|
@PostMapping("/configuration-set")
|
||||||
|
public ResponseEntity<Map<String, Object>> handleConfigurationSet(@RequestBody Map<String, Object> data) {
|
||||||
|
logger.info("收到SES配置集事件回调: {}", data);
|
||||||
|
|
||||||
|
try {
|
||||||
|
String eventType = (String) data.get("EventType");
|
||||||
|
String configurationSet = (String) data.get("ConfigurationSet");
|
||||||
|
String timestamp = (String) data.get("Timestamp");
|
||||||
|
|
||||||
|
logger.info("SES配置集事件 - EventType: {}, ConfigurationSet: {}, Timestamp: {}",
|
||||||
|
eventType, configurationSet, timestamp);
|
||||||
|
|
||||||
|
// 处理配置集事件逻辑
|
||||||
|
// 1. 更新配置集状态
|
||||||
|
// 2. 记录配置变更历史
|
||||||
|
// 3. 发送通知给管理员
|
||||||
|
|
||||||
|
Map<String, Object> response = new HashMap<>();
|
||||||
|
response.put("success", true);
|
||||||
|
response.put("message", "配置集事件回调处理成功");
|
||||||
|
|
||||||
|
return ResponseEntity.ok(response);
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("处理SES配置集事件回调失败", e);
|
||||||
|
Map<String, Object> response = new HashMap<>();
|
||||||
|
response.put("success", false);
|
||||||
|
response.put("message", "处理失败: " + e.getMessage());
|
||||||
|
return ResponseEntity.badRequest().body(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,302 @@
|
|||||||
|
package com.example.demo.controller;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 腾讯云SES邮件推送回调控制器
|
||||||
|
* 用于接收腾讯云SES服务的邮件事件推送
|
||||||
|
*
|
||||||
|
* 支持的事件类型:
|
||||||
|
* - 递送成功 (delivery)
|
||||||
|
* - 腾讯云拒信 (reject)
|
||||||
|
* - ESP退信 (bounce)
|
||||||
|
* - 用户打开邮件 (open)
|
||||||
|
* - 点击链接 (click)
|
||||||
|
* - 退订 (unsubscribe)
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/tencent/ses")
|
||||||
|
public class TencentSesWebhookController {
|
||||||
|
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(TencentSesWebhookController.class);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 腾讯云SES邮件事件回调接口
|
||||||
|
*
|
||||||
|
* 回调地址配置:
|
||||||
|
* - 账户级回调:https://your-domain.com/api/tencent/ses/webhook
|
||||||
|
* - 发信地址级回调:https://your-domain.com/api/tencent/ses/webhook
|
||||||
|
*
|
||||||
|
* 支持端口:8080, 8081, 8082
|
||||||
|
*/
|
||||||
|
@PostMapping("/webhook")
|
||||||
|
public ResponseEntity<Map<String, Object>> handleSesWebhook(
|
||||||
|
@RequestBody Map<String, Object> payload,
|
||||||
|
@RequestHeader Map<String, String> headers) {
|
||||||
|
|
||||||
|
logger.info("收到腾讯云SES回调: {}", payload);
|
||||||
|
logger.info("请求头: {}", headers);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 验证签名(可选,建议在生产环境中启用)
|
||||||
|
if (!verifySignature(payload, headers)) {
|
||||||
|
logger.warn("签名验证失败");
|
||||||
|
Map<String, Object> response = new HashMap<>();
|
||||||
|
response.put("success", false);
|
||||||
|
response.put("message", "签名验证失败");
|
||||||
|
return ResponseEntity.badRequest().body(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析回调数据
|
||||||
|
String eventType = (String) payload.get("eventType");
|
||||||
|
String messageId = (String) payload.get("messageId");
|
||||||
|
String email = (String) payload.get("email");
|
||||||
|
String timestamp = (String) payload.get("timestamp");
|
||||||
|
|
||||||
|
logger.info("SES事件 - Type: {}, MessageId: {}, Email: {}, Timestamp: {}",
|
||||||
|
eventType, messageId, email, timestamp);
|
||||||
|
|
||||||
|
// 根据事件类型处理
|
||||||
|
switch (eventType) {
|
||||||
|
case "delivery":
|
||||||
|
handleDeliveryEvent(payload);
|
||||||
|
break;
|
||||||
|
case "reject":
|
||||||
|
handleRejectEvent(payload);
|
||||||
|
break;
|
||||||
|
case "bounce":
|
||||||
|
handleBounceEvent(payload);
|
||||||
|
break;
|
||||||
|
case "open":
|
||||||
|
handleOpenEvent(payload);
|
||||||
|
break;
|
||||||
|
case "click":
|
||||||
|
handleClickEvent(payload);
|
||||||
|
break;
|
||||||
|
case "unsubscribe":
|
||||||
|
handleUnsubscribeEvent(payload);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
logger.warn("未知事件类型: {}", eventType);
|
||||||
|
handleUnknownEvent(payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, Object> response = new HashMap<>();
|
||||||
|
response.put("success", true);
|
||||||
|
response.put("message", "回调处理成功");
|
||||||
|
response.put("timestamp", System.currentTimeMillis());
|
||||||
|
|
||||||
|
return ResponseEntity.ok(response);
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("处理SES回调失败", e);
|
||||||
|
Map<String, Object> response = new HashMap<>();
|
||||||
|
response.put("success", false);
|
||||||
|
response.put("message", "处理失败: " + e.getMessage());
|
||||||
|
return ResponseEntity.badRequest().body(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理邮件递送成功事件
|
||||||
|
*/
|
||||||
|
private void handleDeliveryEvent(Map<String, Object> payload) {
|
||||||
|
String messageId = (String) payload.get("messageId");
|
||||||
|
String email = (String) payload.get("email");
|
||||||
|
String timestamp = (String) payload.get("timestamp");
|
||||||
|
|
||||||
|
logger.info("邮件递送成功 - MessageId: {}, Email: {}, Timestamp: {}",
|
||||||
|
messageId, email, timestamp);
|
||||||
|
|
||||||
|
// 业务处理逻辑
|
||||||
|
// 1. 更新数据库中的邮件状态
|
||||||
|
// 2. 记录递送统计
|
||||||
|
// 3. 更新用户活跃度
|
||||||
|
// 4. 发送递送成功通知(如需要)
|
||||||
|
|
||||||
|
// TODO: 实现具体的业务逻辑
|
||||||
|
updateEmailDeliveryStatus(messageId, email, "delivered");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理腾讯云拒信事件
|
||||||
|
*/
|
||||||
|
private void handleRejectEvent(Map<String, Object> payload) {
|
||||||
|
String messageId = (String) payload.get("messageId");
|
||||||
|
String email = (String) payload.get("email");
|
||||||
|
String reason = (String) payload.get("reason");
|
||||||
|
String timestamp = (String) payload.get("timestamp");
|
||||||
|
|
||||||
|
logger.warn("腾讯云拒信 - MessageId: {}, Email: {}, Reason: {}, Timestamp: {}",
|
||||||
|
messageId, email, reason, timestamp);
|
||||||
|
|
||||||
|
// 业务处理逻辑
|
||||||
|
// 1. 记录拒信原因
|
||||||
|
// 2. 检查邮件内容合规性
|
||||||
|
// 3. 更新发送策略
|
||||||
|
// 4. 通知管理员
|
||||||
|
|
||||||
|
updateEmailDeliveryStatus(messageId, email, "rejected");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理ESP退信事件
|
||||||
|
*/
|
||||||
|
private void handleBounceEvent(Map<String, Object> payload) {
|
||||||
|
String messageId = (String) payload.get("messageId");
|
||||||
|
String email = (String) payload.get("email");
|
||||||
|
String bounceType = (String) payload.get("bounceType");
|
||||||
|
String bounceSubType = (String) payload.get("bounceSubType");
|
||||||
|
String timestamp = (String) payload.get("timestamp");
|
||||||
|
|
||||||
|
logger.warn("邮件退信 - MessageId: {}, Email: {}, BounceType: {}, BounceSubType: {}, Timestamp: {}",
|
||||||
|
messageId, email, bounceType, bounceSubType, timestamp);
|
||||||
|
|
||||||
|
// 业务处理逻辑
|
||||||
|
// 1. 区分硬退信和软退信
|
||||||
|
// 2. 硬退信:从邮件列表移除
|
||||||
|
// 3. 软退信:稍后重试
|
||||||
|
// 4. 更新邮箱有效性状态
|
||||||
|
|
||||||
|
if ("Permanent".equals(bounceType)) {
|
||||||
|
// 硬退信,移除邮箱
|
||||||
|
removeInvalidEmail(email);
|
||||||
|
} else {
|
||||||
|
// 软退信,标记重试
|
||||||
|
markEmailForRetry(messageId, email);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateEmailDeliveryStatus(messageId, email, "bounced");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理邮件打开事件
|
||||||
|
*/
|
||||||
|
private void handleOpenEvent(Map<String, Object> payload) {
|
||||||
|
String messageId = (String) payload.get("messageId");
|
||||||
|
String email = (String) payload.get("email");
|
||||||
|
String timestamp = (String) payload.get("timestamp");
|
||||||
|
String userAgent = (String) payload.get("userAgent");
|
||||||
|
String ipAddress = (String) payload.get("ipAddress");
|
||||||
|
|
||||||
|
logger.info("邮件打开事件 - MessageId: {}, Email: {}, Timestamp: {}, UserAgent: {}, IpAddress: {}",
|
||||||
|
messageId, email, timestamp, userAgent, ipAddress);
|
||||||
|
|
||||||
|
// 业务处理逻辑
|
||||||
|
// 1. 记录邮件打开统计
|
||||||
|
// 2. 更新用户活跃度
|
||||||
|
// 3. 分析用户行为
|
||||||
|
// 4. 触发后续营销活动
|
||||||
|
|
||||||
|
recordEmailOpen(messageId, email, timestamp, userAgent, ipAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理链接点击事件
|
||||||
|
*/
|
||||||
|
private void handleClickEvent(Map<String, Object> payload) {
|
||||||
|
String messageId = (String) payload.get("messageId");
|
||||||
|
String email = (String) payload.get("email");
|
||||||
|
String timestamp = (String) payload.get("timestamp");
|
||||||
|
String link = (String) payload.get("link");
|
||||||
|
String userAgent = (String) payload.get("userAgent");
|
||||||
|
String ipAddress = (String) payload.get("ipAddress");
|
||||||
|
|
||||||
|
logger.info("链接点击事件 - MessageId: {}, Email: {}, Timestamp: {}, Link: {}, UserAgent: {}, IpAddress: {}",
|
||||||
|
messageId, email, timestamp, link, userAgent, ipAddress);
|
||||||
|
|
||||||
|
// 业务处理逻辑
|
||||||
|
// 1. 记录链接点击统计
|
||||||
|
// 2. 分析用户兴趣
|
||||||
|
// 3. 更新用户行为数据
|
||||||
|
// 4. 触发转化跟踪
|
||||||
|
|
||||||
|
recordLinkClick(messageId, email, timestamp, link, userAgent, ipAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理退订事件
|
||||||
|
*/
|
||||||
|
private void handleUnsubscribeEvent(Map<String, Object> payload) {
|
||||||
|
String messageId = (String) payload.get("messageId");
|
||||||
|
String email = (String) payload.get("email");
|
||||||
|
String timestamp = (String) payload.get("timestamp");
|
||||||
|
String unsubscribeType = (String) payload.get("unsubscribeType");
|
||||||
|
|
||||||
|
logger.info("用户退订 - MessageId: {}, Email: {}, Timestamp: {}, UnsubscribeType: {}",
|
||||||
|
messageId, email, timestamp, unsubscribeType);
|
||||||
|
|
||||||
|
// 业务处理逻辑
|
||||||
|
// 1. 立即停止向该邮箱发送邮件
|
||||||
|
// 2. 更新用户订阅状态
|
||||||
|
// 3. 记录退订原因
|
||||||
|
// 4. 发送退订确认邮件
|
||||||
|
|
||||||
|
unsubscribeUser(email, unsubscribeType);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理未知事件类型
|
||||||
|
*/
|
||||||
|
private void handleUnknownEvent(Map<String, Object> payload) {
|
||||||
|
logger.warn("收到未知事件类型: {}", payload);
|
||||||
|
// 记录到数据库或日志文件,供后续分析
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证签名(简化版本)
|
||||||
|
* 生产环境中应实现完整的签名验证逻辑
|
||||||
|
*/
|
||||||
|
private boolean verifySignature(Map<String, Object> payload, Map<String, String> headers) {
|
||||||
|
// TODO: 实现腾讯云SES签名验证
|
||||||
|
// 1. 获取签名相关头部
|
||||||
|
// 2. 验证时间戳
|
||||||
|
// 3. 验证签名算法
|
||||||
|
// 4. 验证Token
|
||||||
|
|
||||||
|
String token = headers.get("X-Tencent-Token");
|
||||||
|
String timestamp = headers.get("X-Tencent-Timestamp");
|
||||||
|
String signature = headers.get("X-Tencent-Signature");
|
||||||
|
|
||||||
|
// 简化验证:检查必要头部是否存在
|
||||||
|
return token != null && timestamp != null && signature != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 业务方法实现 ==========
|
||||||
|
|
||||||
|
private void updateEmailDeliveryStatus(String messageId, String email, String status) {
|
||||||
|
// TODO: 更新数据库中的邮件状态
|
||||||
|
logger.info("更新邮件状态 - MessageId: {}, Email: {}, Status: {}", messageId, email, status);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void removeInvalidEmail(String email) {
|
||||||
|
// TODO: 从邮件列表中移除无效邮箱
|
||||||
|
logger.info("移除无效邮箱: {}", email);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void markEmailForRetry(String messageId, String email) {
|
||||||
|
// TODO: 标记邮件重试
|
||||||
|
logger.info("标记邮件重试 - MessageId: {}, Email: {}", messageId, email);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void recordEmailOpen(String messageId, String email, String timestamp, String userAgent, String ipAddress) {
|
||||||
|
// TODO: 记录邮件打开统计
|
||||||
|
logger.info("记录邮件打开 - MessageId: {}, Email: {}, Timestamp: {}", messageId, email, timestamp);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void recordLinkClick(String messageId, String email, String timestamp, String link, String userAgent, String ipAddress) {
|
||||||
|
// TODO: 记录链接点击统计
|
||||||
|
logger.info("记录链接点击 - MessageId: {}, Email: {}, Link: {}", messageId, email, link);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void unsubscribeUser(String email, String unsubscribeType) {
|
||||||
|
// TODO: 处理用户退订
|
||||||
|
logger.info("处理用户退订 - Email: {}, Type: {}", email, unsubscribeType);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,7 +13,6 @@ import java.util.Map;
|
|||||||
*/
|
*/
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/verification")
|
@RequestMapping("/api/verification")
|
||||||
@CrossOrigin(origins = "*")
|
|
||||||
public class VerificationCodeController {
|
public class VerificationCodeController {
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
@@ -97,4 +96,49 @@ public class VerificationCodeController {
|
|||||||
|
|
||||||
return ResponseEntity.ok(response);
|
return ResponseEntity.ok(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 开发模式:设置验证码(仅开发环境使用)
|
||||||
|
*/
|
||||||
|
@PostMapping("/email/dev-set")
|
||||||
|
public ResponseEntity<Map<String, Object>> setDevCode(@RequestBody Map<String, String> request) {
|
||||||
|
Map<String, Object> response = new HashMap<>();
|
||||||
|
|
||||||
|
// 仅开发环境允许
|
||||||
|
if (!"dev".equals(System.getProperty("spring.profiles.active")) &&
|
||||||
|
!"development".equals(System.getProperty("spring.profiles.active"))) {
|
||||||
|
response.put("success", false);
|
||||||
|
response.put("message", "此接口仅开发环境可用");
|
||||||
|
return ResponseEntity.badRequest().body(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
String email = request.get("email");
|
||||||
|
String code = request.get("code");
|
||||||
|
|
||||||
|
if (email == null || email.trim().isEmpty()) {
|
||||||
|
response.put("success", false);
|
||||||
|
response.put("message", "邮箱不能为空");
|
||||||
|
return ResponseEntity.badRequest().body(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (code == null || code.trim().isEmpty()) {
|
||||||
|
response.put("success", false);
|
||||||
|
response.put("message", "验证码不能为空");
|
||||||
|
return ResponseEntity.badRequest().body(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 直接设置验证码到内存存储
|
||||||
|
verificationCodeService.setVerificationCode(email, code);
|
||||||
|
|
||||||
|
response.put("success", true);
|
||||||
|
response.put("message", "开发模式验证码设置成功");
|
||||||
|
return ResponseEntity.ok(response);
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
response.put("success", false);
|
||||||
|
response.put("message", "设置验证码失败:" + e.getMessage());
|
||||||
|
return ResponseEntity.badRequest().body(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ import org.slf4j.LoggerFactory;
|
|||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
@@ -95,9 +94,7 @@ public class AlipayService {
|
|||||||
|
|
||||||
if (response.isSuccess()) {
|
if (response.isSuccess()) {
|
||||||
logger.info("支付宝支付订单创建成功,订单号:{}", payment.getOrderId());
|
logger.info("支付宝支付订单创建成功,订单号:{}", payment.getOrderId());
|
||||||
// 返回支付URL,前端可以跳转到这个URL进行支付
|
return response.getBody();
|
||||||
String paymentUrl = gatewayUrl + "?" + response.getBody();
|
|
||||||
return paymentUrl;
|
|
||||||
} else {
|
} else {
|
||||||
logger.error("支付宝支付订单创建失败:{}", response.getMsg());
|
logger.error("支付宝支付订单创建失败:{}", response.getMsg());
|
||||||
payment.setStatus(PaymentStatus.FAILED);
|
payment.setStatus(PaymentStatus.FAILED);
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import org.slf4j.LoggerFactory;
|
|||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
|
||||||
@@ -200,3 +199,4 @@ public class PayPalService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -134,6 +134,22 @@ public class VerificationCodeService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 开发模式:直接设置验证码(仅开发环境使用)
|
||||||
|
*/
|
||||||
|
public void setVerificationCode(String email, String code) {
|
||||||
|
String codeKey = "email_code:" + email;
|
||||||
|
verificationCodes.put(codeKey, code);
|
||||||
|
|
||||||
|
// 设置验证码过期时间
|
||||||
|
scheduler.schedule(() -> {
|
||||||
|
verificationCodes.remove(codeKey);
|
||||||
|
logger.info("开发模式验证码已过期,邮箱: {}", email);
|
||||||
|
}, CODE_EXPIRE_MINUTES, TimeUnit.MINUTES);
|
||||||
|
|
||||||
|
logger.info("开发模式验证码设置成功,邮箱: {}, 验证码: {}", email, code);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 发送邮件(简化版本,实际使用时需要配置正确的腾讯云SES API)
|
* 发送邮件(简化版本,实际使用时需要配置正确的腾讯云SES API)
|
||||||
|
|||||||
@@ -4,5 +4,5 @@ spring.thymeleaf.cache=false
|
|||||||
spring.profiles.active=dev
|
spring.profiles.active=dev
|
||||||
|
|
||||||
# 服务器配置
|
# 服务器配置
|
||||||
server.address=api.yourdomain.com
|
server.address=localhost
|
||||||
server.port=8080
|
server.port=8080
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
@echo off
|
|
||||||
echo Starting Spring Boot application...
|
|
||||||
echo.
|
|
||||||
|
|
||||||
REM 设置JAVA_HOME(如果需要)
|
|
||||||
REM set JAVA_HOME=C:\Program Files\Java\jdk-21
|
|
||||||
|
|
||||||
REM 启动应用并显示输出
|
|
||||||
java -jar target/demo-0.0.1-SNAPSHOT.jar --spring.profiles.active=dev
|
|
||||||
|
|
||||||
echo.
|
|
||||||
echo Application stopped.
|
|
||||||
pause
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
# 启动后端服务脚本
|
|
||||||
Write-Host "正在启动后端服务..." -ForegroundColor Green
|
|
||||||
|
|
||||||
# 检查jar文件是否存在
|
|
||||||
if (-not (Test-Path "target/demo-0.0.1-SNAPSHOT.jar")) {
|
|
||||||
Write-Host "错误: jar文件不存在,请先编译项目" -ForegroundColor Red
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# 启动服务
|
|
||||||
Write-Host "启动Spring Boot应用..." -ForegroundColor Yellow
|
|
||||||
$process = Start-Process -FilePath "java" -ArgumentList "-jar", "target/demo-0.0.1-SNAPSHOT.jar", "--spring.profiles.active=dev" -PassThru -NoNewWindow
|
|
||||||
|
|
||||||
Write-Host "服务进程ID: $($process.Id)" -ForegroundColor Cyan
|
|
||||||
|
|
||||||
# 等待服务启动
|
|
||||||
Write-Host "等待服务启动..." -ForegroundColor Yellow
|
|
||||||
$timeout = 60
|
|
||||||
$elapsed = 0
|
|
||||||
|
|
||||||
while ($elapsed -lt $timeout) {
|
|
||||||
Start-Sleep -Seconds 2
|
|
||||||
$elapsed += 2
|
|
||||||
|
|
||||||
# 检查端口是否监听
|
|
||||||
$listening = netstat -an | Select-String ":8080.*LISTENING"
|
|
||||||
if ($listening) {
|
|
||||||
Write-Host "✓ 服务已成功启动在端口8080!" -ForegroundColor Green
|
|
||||||
Write-Host "进程ID: $($process.Id)" -ForegroundColor Cyan
|
|
||||||
Write-Host "按 Ctrl+C 停止服务" -ForegroundColor Yellow
|
|
||||||
|
|
||||||
# 保持服务运行
|
|
||||||
try {
|
|
||||||
$process.WaitForExit()
|
|
||||||
} catch {
|
|
||||||
Write-Host "服务已停止" -ForegroundColor Red
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
Write-Host "等待中... $elapsed/$timeout 秒" -ForegroundColor Gray
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($elapsed -ge $timeout) {
|
|
||||||
Write-Host "✗ 服务启动超时" -ForegroundColor Red
|
|
||||||
Stop-Process -Id $process.Id -Force
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
@echo off
|
|
||||||
echo ========================================
|
|
||||||
echo AIGC Demo - 使用域名启动服务
|
|
||||||
echo ========================================
|
|
||||||
echo.
|
|
||||||
|
|
||||||
echo 检查hosts文件配置...
|
|
||||||
ping test.yourdomain.com -n 1 >nul 2>&1
|
|
||||||
if %errorlevel% neq 0 (
|
|
||||||
echo [错误] 域名解析失败,请先配置hosts文件
|
|
||||||
echo 请运行: update-hosts.ps1 或手动编辑hosts文件
|
|
||||||
echo.
|
|
||||||
echo 需要添加以下映射到 C:\Windows\System32\drivers\etc\hosts:
|
|
||||||
echo 127.0.0.1 test.yourdomain.com
|
|
||||||
echo 127.0.0.1 api.yourdomain.com
|
|
||||||
echo 127.0.0.1 local.yourdomain.com
|
|
||||||
pause
|
|
||||||
exit /b 1
|
|
||||||
)
|
|
||||||
|
|
||||||
echo [成功] 域名解析正常
|
|
||||||
echo.
|
|
||||||
|
|
||||||
echo 启动后端服务 (api.yourdomain.com:8080)...
|
|
||||||
start "后端服务" cmd /k "cd /d %~dp0 && mvn spring-boot:run"
|
|
||||||
|
|
||||||
echo 等待后端服务启动...
|
|
||||||
timeout /t 10 /nobreak >nul
|
|
||||||
|
|
||||||
echo 启动前端服务 (test.yourdomain.com:5173)...
|
|
||||||
start "前端服务" cmd /k "cd /d %~dp0\frontend && npm run dev"
|
|
||||||
|
|
||||||
echo.
|
|
||||||
echo ========================================
|
|
||||||
echo 服务启动完成!
|
|
||||||
echo ========================================
|
|
||||||
echo 前端地址: http://test.yourdomain.com:5173
|
|
||||||
echo 后端地址: http://api.yourdomain.com:8080
|
|
||||||
echo API地址: http://api.yourdomain.com:8080/api
|
|
||||||
echo ========================================
|
|
||||||
echo.
|
|
||||||
echo 按任意键退出...
|
|
||||||
pause >nul
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
# AIGC Demo - 使用域名启动服务
|
|
||||||
Write-Host "========================================" -ForegroundColor Cyan
|
|
||||||
Write-Host "AIGC Demo - 使用域名启动服务" -ForegroundColor Cyan
|
|
||||||
Write-Host "========================================" -ForegroundColor Cyan
|
|
||||||
Write-Host ""
|
|
||||||
|
|
||||||
# 检查域名解析
|
|
||||||
Write-Host "检查hosts文件配置..." -ForegroundColor Yellow
|
|
||||||
try {
|
|
||||||
$pingResult = Test-NetConnection -ComputerName "test.yourdomain.com" -Port 80 -InformationLevel Quiet
|
|
||||||
if (-not $pingResult) {
|
|
||||||
Write-Host "[错误] 域名解析失败,请先配置hosts文件" -ForegroundColor Red
|
|
||||||
Write-Host "请运行: .\update-hosts.ps1 或手动编辑hosts文件" -ForegroundColor Yellow
|
|
||||||
Write-Host ""
|
|
||||||
Write-Host "需要添加以下映射到 C:\Windows\System32\drivers\etc\hosts:" -ForegroundColor Yellow
|
|
||||||
Write-Host "127.0.0.1 test.yourdomain.com" -ForegroundColor White
|
|
||||||
Write-Host "127.0.0.1 api.yourdomain.com" -ForegroundColor White
|
|
||||||
Write-Host "127.0.0.1 local.yourdomain.com" -ForegroundColor White
|
|
||||||
Read-Host "按回车键退出"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
Write-Host "[错误] 无法检查域名解析" -ForegroundColor Red
|
|
||||||
Read-Host "按回车键退出"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
Write-Host "[成功] 域名解析正常" -ForegroundColor Green
|
|
||||||
Write-Host ""
|
|
||||||
|
|
||||||
# 启动后端服务
|
|
||||||
Write-Host "启动后端服务 (api.yourdomain.com:8080)..." -ForegroundColor Yellow
|
|
||||||
Start-Process -FilePath "cmd" -ArgumentList "/k", "cd /d $PWD && mvn spring-boot:run" -WindowStyle Normal
|
|
||||||
|
|
||||||
Write-Host "等待后端服务启动..." -ForegroundColor Yellow
|
|
||||||
Start-Sleep -Seconds 10
|
|
||||||
|
|
||||||
# 启动前端服务
|
|
||||||
Write-Host "启动前端服务 (test.yourdomain.com:5173)..." -ForegroundColor Yellow
|
|
||||||
Start-Process -FilePath "cmd" -ArgumentList "/k", "cd /d $PWD\frontend && npm run dev" -WindowStyle Normal
|
|
||||||
|
|
||||||
Write-Host ""
|
|
||||||
Write-Host "========================================" -ForegroundColor Cyan
|
|
||||||
Write-Host "服务启动完成!" -ForegroundColor Green
|
|
||||||
Write-Host "========================================" -ForegroundColor Cyan
|
|
||||||
Write-Host "前端地址: http://test.yourdomain.com:5173" -ForegroundColor Cyan
|
|
||||||
Write-Host "后端地址: http://api.yourdomain.com:8080" -ForegroundColor Cyan
|
|
||||||
Write-Host "API地址: http://api.yourdomain.com:8080/api" -ForegroundColor Cyan
|
|
||||||
Write-Host "========================================" -ForegroundColor Cyan
|
|
||||||
Write-Host ""
|
|
||||||
|
|
||||||
Read-Host "按回车键退出"
|
|
||||||
129
demo/start.sh
129
demo/start.sh
@@ -1,129 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# AIGC Demo 启动脚本
|
|
||||||
# 支持环境变量配置
|
|
||||||
|
|
||||||
echo "=== AIGC Demo 启动脚本 ==="
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# 检查Java环境
|
|
||||||
if ! command -v java &> /dev/null; then
|
|
||||||
echo "❌ Java未安装或未配置到PATH"
|
|
||||||
echo "请安装Java 21或更高版本"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 检查Java版本
|
|
||||||
JAVA_VERSION=$(java -version 2>&1 | head -n 1 | cut -d'"' -f2 | cut -d'.' -f1)
|
|
||||||
if [ "$JAVA_VERSION" -lt 21 ]; then
|
|
||||||
echo "❌ Java版本过低,需要Java 21或更高版本"
|
|
||||||
echo "当前版本: $(java -version 2>&1 | head -n 1)"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "✅ Java版本检查通过: $(java -version 2>&1 | head -n 1)"
|
|
||||||
|
|
||||||
# 检查Maven
|
|
||||||
if ! command -v mvn &> /dev/null; then
|
|
||||||
echo "❌ Maven未安装或未配置到PATH"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "✅ Maven检查通过: $(mvn -version | head -n 1)"
|
|
||||||
|
|
||||||
# 检查环境变量文件
|
|
||||||
if [ -f ".env" ]; then
|
|
||||||
echo "✅ 发现.env文件,加载环境变量"
|
|
||||||
export $(cat .env | grep -v '^#' | xargs)
|
|
||||||
else
|
|
||||||
echo "⚠️ 未发现.env文件,使用默认配置"
|
|
||||||
echo " 如需自定义配置,请复制env.example为.env并修改"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 设置默认环境变量
|
|
||||||
export DB_URL=${DB_URL:-"jdbc:mysql://localhost:3306/aigc?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true"}
|
|
||||||
export DB_USERNAME=${DB_USERNAME:-"root"}
|
|
||||||
export DB_PASSWORD=${DB_USERNAME:-"177615"}
|
|
||||||
export JWT_SECRET=${JWT_SECRET:-"aigc-demo-secret-key-for-jwt-token-generation-very-long-secret-key"}
|
|
||||||
export JWT_EXPIRATION=${JWT_EXPIRATION:-"604800000"}
|
|
||||||
|
|
||||||
# 检查数据库连接
|
|
||||||
echo ""
|
|
||||||
echo "🔍 检查数据库连接..."
|
|
||||||
if command -v mysql &> /dev/null; then
|
|
||||||
if mysql -h localhost -u "$DB_USERNAME" -p"$DB_PASSWORD" -e "SELECT 1;" &> /dev/null; then
|
|
||||||
echo "✅ 数据库连接正常"
|
|
||||||
else
|
|
||||||
echo "⚠️ 数据库连接失败,请检查配置"
|
|
||||||
echo " 数据库URL: $DB_URL"
|
|
||||||
echo " 用户名: $DB_USERNAME"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
echo "⚠️ 未安装MySQL客户端,跳过数据库连接检查"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 编译项目
|
|
||||||
echo ""
|
|
||||||
echo "🔨 编译项目..."
|
|
||||||
if mvn clean compile -q; then
|
|
||||||
echo "✅ 项目编译成功"
|
|
||||||
else
|
|
||||||
echo "❌ 项目编译失败"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 启动应用
|
|
||||||
echo ""
|
|
||||||
echo "🚀 启动应用..."
|
|
||||||
echo " 配置文件: application-dev.properties"
|
|
||||||
echo " 端口: 8080"
|
|
||||||
echo " 日志文件: startup.log"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# 创建日志目录
|
|
||||||
mkdir -p logs
|
|
||||||
|
|
||||||
# 启动应用
|
|
||||||
java -jar target/demo-0.0.1-SNAPSHOT.jar \
|
|
||||||
--spring.profiles.active=dev \
|
|
||||||
--server.port=8080 \
|
|
||||||
--logging.file.name=logs/application.log \
|
|
||||||
> startup.log 2>&1 &
|
|
||||||
|
|
||||||
# 获取进程ID
|
|
||||||
APP_PID=$!
|
|
||||||
echo "📝 应用进程ID: $APP_PID"
|
|
||||||
echo "📝 进程ID已保存到: app.pid"
|
|
||||||
|
|
||||||
# 保存进程ID
|
|
||||||
echo $APP_PID > app.pid
|
|
||||||
|
|
||||||
# 等待应用启动
|
|
||||||
echo "⏳ 等待应用启动..."
|
|
||||||
for i in {1..30}; do
|
|
||||||
if curl -s http://localhost:8080/api/public/health &> /dev/null; then
|
|
||||||
echo ""
|
|
||||||
echo "🎉 应用启动成功!"
|
|
||||||
echo ""
|
|
||||||
echo "📱 访问地址:"
|
|
||||||
echo " 后端API: http://localhost:8080"
|
|
||||||
echo " 前端应用: http://localhost:3000 (需要单独启动)"
|
|
||||||
echo " 健康检查: http://localhost:8080/api/public/health"
|
|
||||||
echo ""
|
|
||||||
echo "🛑 停止应用: kill $APP_PID 或运行 ./stop.sh"
|
|
||||||
echo "📋 查看日志: tail -f startup.log"
|
|
||||||
echo ""
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
echo -n "."
|
|
||||||
sleep 1
|
|
||||||
done
|
|
||||||
|
|
||||||
if [ $i -eq 30 ]; then
|
|
||||||
echo ""
|
|
||||||
echo "❌ 应用启动超时"
|
|
||||||
echo "📋 查看日志: cat startup.log"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
|
|
||||||
59
demo/stop.sh
59
demo/stop.sh
@@ -1,59 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# AIGC Demo 停止脚本
|
|
||||||
|
|
||||||
echo "=== AIGC Demo 停止脚本 ==="
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# 检查进程ID文件
|
|
||||||
if [ -f "app.pid" ]; then
|
|
||||||
APP_PID=$(cat app.pid)
|
|
||||||
echo "📝 找到进程ID: $APP_PID"
|
|
||||||
|
|
||||||
# 检查进程是否存在
|
|
||||||
if ps -p $APP_PID > /dev/null; then
|
|
||||||
echo "🛑 正在停止应用..."
|
|
||||||
kill $APP_PID
|
|
||||||
|
|
||||||
# 等待进程结束
|
|
||||||
for i in {1..10}; do
|
|
||||||
if ! ps -p $APP_PID > /dev/null; then
|
|
||||||
echo "✅ 应用已停止"
|
|
||||||
rm -f app.pid
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
echo -n "."
|
|
||||||
sleep 1
|
|
||||||
done
|
|
||||||
|
|
||||||
if ps -p $APP_PID > /dev/null; then
|
|
||||||
echo ""
|
|
||||||
echo "⚠️ 应用未正常停止,强制终止..."
|
|
||||||
kill -9 $APP_PID
|
|
||||||
rm -f app.pid
|
|
||||||
echo "✅ 应用已强制停止"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
echo "⚠️ 进程不存在,可能已经停止"
|
|
||||||
rm -f app.pid
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
echo "⚠️ 未找到进程ID文件 (app.pid)"
|
|
||||||
echo " 尝试查找Java进程..."
|
|
||||||
|
|
||||||
# 查找Java进程
|
|
||||||
JAVA_PIDS=$(ps aux | grep "demo-0.0.1-SNAPSHOT.jar" | grep -v grep | awk '{print $2}')
|
|
||||||
|
|
||||||
if [ -n "$JAVA_PIDS" ]; then
|
|
||||||
echo "🔍 找到Java进程: $JAVA_PIDS"
|
|
||||||
echo "$JAVA_PIDS" | xargs kill
|
|
||||||
echo "✅ Java进程已停止"
|
|
||||||
else
|
|
||||||
echo "ℹ️ 未找到运行中的Java进程"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "🏁 停止脚本执行完成"
|
|
||||||
|
|
||||||
|
|
||||||
42
demo/tencent-config-template.properties
Normal file
42
demo/tencent-config-template.properties
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
# 腾讯云邮件推送服务配置模板
|
||||||
|
# 请根据您的腾讯云账号信息填写以下配置
|
||||||
|
|
||||||
|
# ===========================================
|
||||||
|
# 1. API密钥配置(必填)
|
||||||
|
# ===========================================
|
||||||
|
# 在腾讯云控制台 → 访问管理 → API密钥管理 中获取
|
||||||
|
tencent.cloud.secret-id=请填写您的SecretId
|
||||||
|
tencent.cloud.secret-key=请填写您的SecretKey
|
||||||
|
|
||||||
|
# ===========================================
|
||||||
|
# 2. 邮件推送服务配置(必填)
|
||||||
|
# ===========================================
|
||||||
|
# 服务地域(通常使用北京)
|
||||||
|
tencent.cloud.ses.region=ap-beijing
|
||||||
|
|
||||||
|
# 发件人邮箱(需要在腾讯云SES中验证)
|
||||||
|
tencent.cloud.ses.from-email=请填写您的发件人邮箱
|
||||||
|
|
||||||
|
# 发件人名称
|
||||||
|
tencent.cloud.ses.from-name=AIGC Demo
|
||||||
|
|
||||||
|
# 邮件模板ID(可选,如不使用模板可留空)
|
||||||
|
tencent.cloud.ses.template-id=
|
||||||
|
|
||||||
|
# ===========================================
|
||||||
|
# 3. 使用说明
|
||||||
|
# ===========================================
|
||||||
|
# 1. 复制此文件为 application-tencent.properties
|
||||||
|
# 2. 填写上述配置信息
|
||||||
|
# 3. 在 application.properties 中设置 spring.profiles.active=tencent
|
||||||
|
# 4. 重启应用即可使用腾讯云邮件服务
|
||||||
|
|
||||||
|
# ===========================================
|
||||||
|
# 4. 配置示例
|
||||||
|
# ===========================================
|
||||||
|
# tencent.cloud.secret-id=AKID1234567890abcdef1234567890abcdef
|
||||||
|
# tencent.cloud.secret-key=abcdef1234567890abcdef1234567890
|
||||||
|
# tencent.cloud.ses.region=ap-beijing
|
||||||
|
# tencent.cloud.ses.from-email=noreply@yourdomain.com
|
||||||
|
# tencent.cloud.ses.from-name=AIGC Demo
|
||||||
|
# tencent.cloud.ses.template-id=123456
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
# 测试域名配置
|
|
||||||
Write-Host "========================================" -ForegroundColor Cyan
|
|
||||||
Write-Host "AIGC Demo - 域名配置测试" -ForegroundColor Cyan
|
|
||||||
Write-Host "========================================" -ForegroundColor Cyan
|
|
||||||
Write-Host ""
|
|
||||||
|
|
||||||
# 测试域名解析
|
|
||||||
$domains = @("test.yourdomain.com", "api.yourdomain.com", "local.yourdomain.com")
|
|
||||||
|
|
||||||
foreach ($domain in $domains) {
|
|
||||||
Write-Host "测试域名: $domain" -ForegroundColor Yellow
|
|
||||||
try {
|
|
||||||
$result = Resolve-DnsName -Name $domain -ErrorAction Stop
|
|
||||||
$ip = $result[0].IPAddress
|
|
||||||
if ($ip -eq "127.0.0.1") {
|
|
||||||
Write-Host " ✓ 解析成功: $domain -> $ip" -ForegroundColor Green
|
|
||||||
} else {
|
|
||||||
Write-Host " ✗ 解析错误: $domain -> $ip (期望: 127.0.0.1)" -ForegroundColor Red
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
Write-Host " ✗ 解析失败: $domain" -ForegroundColor Red
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Write-Host ""
|
|
||||||
|
|
||||||
# 测试端口连通性
|
|
||||||
Write-Host "测试服务端口..." -ForegroundColor Yellow
|
|
||||||
|
|
||||||
# 测试后端端口
|
|
||||||
try {
|
|
||||||
$backendTest = Test-NetConnection -ComputerName "api.yourdomain.com" -Port 8080 -InformationLevel Quiet
|
|
||||||
if ($backendTest) {
|
|
||||||
Write-Host " ✓ 后端服务: api.yourdomain.com:8080 可访问" -ForegroundColor Green
|
|
||||||
} else {
|
|
||||||
Write-Host " ✗ 后端服务: api.yourdomain.com:8080 不可访问" -ForegroundColor Red
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
Write-Host " ✗ 后端服务: api.yourdomain.com:8080 测试失败" -ForegroundColor Red
|
|
||||||
}
|
|
||||||
|
|
||||||
# 测试前端端口
|
|
||||||
try {
|
|
||||||
$frontendTest = Test-NetConnection -ComputerName "test.yourdomain.com" -Port 5173 -InformationLevel Quiet
|
|
||||||
if ($frontendTest) {
|
|
||||||
Write-Host " ✓ 前端服务: test.yourdomain.com:5173 可访问" -ForegroundColor Green
|
|
||||||
} else {
|
|
||||||
Write-Host " ✗ 前端服务: test.yourdomain.com:5173 不可访问" -ForegroundColor Red
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
Write-Host " ✗ 前端服务: test.yourdomain.com:5173 测试失败" -ForegroundColor Red
|
|
||||||
}
|
|
||||||
|
|
||||||
Write-Host ""
|
|
||||||
Write-Host "========================================" -ForegroundColor Cyan
|
|
||||||
Write-Host "测试完成" -ForegroundColor Green
|
|
||||||
Write-Host "========================================" -ForegroundColor Cyan
|
|
||||||
Write-Host ""
|
|
||||||
|
|
||||||
Read-Host "按回车键退出"
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
# 更新hosts文件脚本
|
|
||||||
# 需要管理员权限运行
|
|
||||||
|
|
||||||
# 检查是否以管理员身份运行
|
|
||||||
if (-NOT ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator")) {
|
|
||||||
Write-Host "请以管理员身份运行此脚本" -ForegroundColor Red
|
|
||||||
Write-Host "右键点击PowerShell,选择'以管理员身份运行'" -ForegroundColor Yellow
|
|
||||||
pause
|
|
||||||
exit
|
|
||||||
}
|
|
||||||
|
|
||||||
$hostsFile = "C:\Windows\System32\drivers\etc\hosts"
|
|
||||||
$domainMappings = @"
|
|
||||||
# AIGC Demo 测试域名映射
|
|
||||||
127.0.0.1 test.yourdomain.com
|
|
||||||
127.0.0.1 api.yourdomain.com
|
|
||||||
127.0.0.1 local.yourdomain.com
|
|
||||||
"@
|
|
||||||
|
|
||||||
Write-Host "正在更新hosts文件..." -ForegroundColor Green
|
|
||||||
|
|
||||||
# 检查是否已经存在映射
|
|
||||||
$existingContent = Get-Content $hostsFile -Raw
|
|
||||||
if ($existingContent -match "test\.yourdomain\.com") {
|
|
||||||
Write-Host "域名映射已存在,跳过添加" -ForegroundColor Yellow
|
|
||||||
} else {
|
|
||||||
# 添加域名映射
|
|
||||||
Add-Content -Path $hostsFile -Value $domainMappings
|
|
||||||
Write-Host "域名映射添加成功!" -ForegroundColor Green
|
|
||||||
}
|
|
||||||
|
|
||||||
Write-Host "`n添加的域名映射:" -ForegroundColor Cyan
|
|
||||||
Write-Host "127.0.0.1 test.yourdomain.com" -ForegroundColor White
|
|
||||||
Write-Host "127.0.0.1 api.yourdomain.com" -ForegroundColor White
|
|
||||||
Write-Host "127.0.0.1 local.yourdomain.com" -ForegroundColor White
|
|
||||||
|
|
||||||
Write-Host "`n现在可以使用以下域名访问服务:" -ForegroundColor Green
|
|
||||||
Write-Host "前端: http://test.yourdomain.com:5173" -ForegroundColor Cyan
|
|
||||||
Write-Host "后端: http://api.yourdomain.com:8080" -ForegroundColor Cyan
|
|
||||||
|
|
||||||
pause
|
|
||||||
Reference in New Issue
Block a user