初始化

This commit is contained in:
2025-10-18 16:18:20 +08:00
parent cf2273e6da
commit e287d7bbde
33 changed files with 3575 additions and 303 deletions

372
.trae/rules/save.md Normal file
View File

@@ -0,0 +1,372 @@
# 存入胶囊功能 API 文档
## 概述
存入胶囊功能允许用户将邮件保存为时光胶囊状态,邮件将以草稿形式保存,用户可以随时编辑或发送。
## API 接口
### 创建胶囊邮件
**接口地址:** `POST /api/v1/mails`
**接口描述:** 创建一个新邮件并将其保存为时光胶囊状态(草稿)
#### 请求参数
| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| title | string | 是 | 邮件标题 |
| content | string | 是 | 邮件内容 |
| recipientType | string | 是 | 收件人类型SELF自己、SPECIFIC指定收件人、PUBLIC公开信 |
| recipientEmail | string | 否 | 指定收件人邮箱当recipientType为SPECIFIC时必填 |
| sendTime | string | 否 | 发送时间ISO格式时间字符串2025-12-31T23:59:59Z |
| triggerType | string | 否 | 触发类型TIME时间、LOCATION地点、EVENT事件 |
| triggerCondition | object | 否 | 触发条件 |
| triggerCondition.location | object | 否 | 地点触发条件 |
| triggerCondition.location.latitude | number | 否 | 纬度 |
| triggerCondition.location.longitude | number | 否 | 经度 |
| triggerCondition.location.city | string | 否 | 城市 |
| triggerCondition.event | object | 否 | 事件触发条件 |
| triggerCondition.event.keywords | array | 否 | 关键词列表 |
| triggerCondition.event.type | string | 否 | 事件类型 |
| attachments | array | 否 | 附件列表 |
| attachments[].type | string | 否 | 附件类型IMAGE、VOICE、VIDEO |
| attachments[].url | string | 否 | 附件URL |
| attachments[].thumbnail | string | 否 | 缩略图URL |
| isEncrypted | boolean | 否 | 是否加密 |
| capsuleStyle | string | 否 | 胶囊样式 |
| status | string | 是 | 邮件状态存入胶囊时固定为DRAFT |
#### 请求示例
```json
{
"title": "写给未来的自己",
"content": "亲爱的未来的我,当你读到这封信时,希望你已经实现了现在的梦想...",
"recipientType": "SELF",
"sendTime": "2025-12-31T23:59:59Z",
"triggerType": "TIME",
"attachments": [
{
"type": "IMAGE",
"url": "https://example.com/image.jpg",
"thumbnail": "https://example.com/thumb.jpg"
}
],
"isEncrypted": false,
"capsuleStyle": "default",
"status": "DRAFT"
}
```
#### 响应参数
| 参数名 | 类型 | 说明 |
|--------|------|------|
| code | number | 响应状态码200表示成功 |
| message | string | 响应消息 |
| data | object | 响应数据 |
| data.mailId | string | 邮件ID |
| data.capsuleId | string | 胶囊ID |
| data.status | string | 邮件状态DRAFT、PENDING、DELIVERING、DELIVERED |
| data.createdAt | string | 创建时间ISO格式时间字符串 |
#### 响应示例
```json
{
"code": 200,
"message": "success",
"data": {
"mailId": "mail_1234567890",
"capsuleId": "capsule_1234567890",
"status": "DRAFT",
"createdAt": "2023-07-20T10:30:00Z"
}
}
```
### 获取胶囊列表
**接口地址:** `GET /api/v1/mails`
**接口描述:** 获取用户的胶囊邮件列表
#### 请求参数
| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| type | string | 否 | 邮件类型INBOX、SENT、DRAFT获取胶囊时使用DRAFT |
| status | string | 否 | 状态筛选PENDING、DELIVERING、DELIVERED、DRAFT |
| page | number | 否 | 页码默认为1 |
| size | number | 否 | 每页数量默认为10 |
#### 请求示例
```
GET /api/v1/mails?type=DRAFT&page=1&size=10
```
#### 响应参数
| 参数名 | 类型 | 说明 |
|--------|------|------|
| code | number | 响应状态码200表示成功 |
| message | string | 响应消息 |
| data | object | 响应数据 |
| data.list | array | 邮件列表 |
| data.list[].mailId | string | 邮件ID |
| data.list[].title | string | 邮件标题 |
| data.list[].sender | object | 发件人信息 |
| data.list[].recipient | object | 收件人信息 |
| data.list[].sendTime | string | 发送时间 |
| data.list[].deliveryTime | string | 送达时间 |
| data.list[].status | string | 邮件状态 |
| data.list[].hasAttachments | boolean | 是否有附件 |
| data.list[].isEncrypted | boolean | 是否加密 |
| data.list[].capsuleStyle | string | 胶囊样式 |
| data.total | number | 总数量 |
| data.page | number | 当前页码 |
| data.size | number | 每页数量 |
#### 响应示例
```json
{
"code": 200,
"message": "success",
"data": {
"list": [
{
"mailId": "mail_1234567890",
"title": "写给未来的自己",
"sender": {
"userId": "user_123",
"username": "张三",
"avatar": "https://example.com/avatar.jpg"
},
"recipient": {
"userId": "user_123",
"username": "张三",
"avatar": "https://example.com/avatar.jpg"
},
"sendTime": "2025-12-31T23:59:59Z",
"deliveryTime": null,
"status": "DRAFT",
"hasAttachments": true,
"isEncrypted": false,
"capsuleStyle": "default"
}
],
"total": 1,
"page": 1,
"size": 10
}
}
```
### 获取胶囊详情
**接口地址:** `GET /api/v1/mails/{mailId}`
**接口描述:** 获取指定胶囊邮件的详细信息
#### 请求参数
| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| mailId | string | 是 | 邮件ID |
#### 请求示例
```
GET /api/v1/mails/mail_1234567890
```
#### 响应参数
| 参数名 | 类型 | 说明 |
|--------|------|------|
| code | number | 响应状态码200表示成功 |
| message | string | 响应消息 |
| data | object | 响应数据 |
| data.mailId | string | 邮件ID |
| data.title | string | 邮件标题 |
| data.content | string | 邮件内容 |
| data.sender | object | 发件人信息 |
| data.recipient | object | 收件人信息 |
| data.sendTime | string | 发送时间 |
| data.createdAt | string | 创建时间 |
| data.deliveryTime | string | 送达时间 |
| data.status | string | 邮件状态 |
| data.triggerType | string | 触发类型 |
| data.triggerCondition | object | 触发条件 |
| data.attachments | array | 附件列表 |
| data.isEncrypted | boolean | 是否加密 |
| data.capsuleStyle | string | 胶囊样式 |
| data.canEdit | boolean | 是否可编辑草稿状态为true |
| data.canRevoke | boolean | 是否可撤销待投递状态为true |
#### 响应示例
```json
{
"code": 200,
"message": "success",
"data": {
"mailId": "mail_1234567890",
"title": "写给未来的自己",
"content": "亲爱的未来的我,当你读到这封信时,希望你已经实现了现在的梦想...",
"sender": {
"userId": "user_123",
"username": "张三",
"avatar": "https://example.com/avatar.jpg",
"email": "zhangsan@example.com"
},
"recipient": {
"userId": "user_123",
"username": "张三",
"avatar": "https://example.com/avatar.jpg",
"email": "zhangsan@example.com"
},
"sendTime": "2025-12-31T23:59:59Z",
"createdAt": "2023-07-20T10:30:00Z",
"deliveryTime": null,
"status": "DRAFT",
"triggerType": "TIME",
"triggerCondition": {},
"attachments": [
{
"id": "attach_123",
"type": "IMAGE",
"url": "https://example.com/image.jpg",
"thumbnail": "https://example.com/thumb.jpg",
"size": 1024000
}
],
"isEncrypted": false,
"capsuleStyle": "default",
"canEdit": true,
"canRevoke": false
}
}
```
### 更新胶囊邮件
**接口地址:** `PUT /api/v1/mails/{mailId}`
**接口描述:** 更新胶囊邮件内容(仅草稿状态可更新)
#### 请求参数
| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| mailId | string | 是 | 邮件ID路径参数 |
| title | string | 否 | 邮件标题 |
| content | string | 否 | 邮件内容 |
| recipientType | string | 否 | 收件人类型SELF、SPECIFIC、PUBLIC |
| recipientEmail | string | 否 | 指定收件人邮箱当recipientType为SPECIFIC时必填 |
| sendTime | string | 否 | 发送时间ISO格式时间字符串 |
| triggerType | string | 否 | 触发类型TIME、LOCATION、EVENT |
| triggerCondition | object | 否 | 触发条件 |
| attachments | array | 否 | 附件列表 |
| isEncrypted | boolean | 否 | 是否加密 |
| capsuleStyle | string | 否 | 胶囊样式 |
#### 请求示例
```json
{
"title": "更新后的标题",
"content": "更新后的内容...",
"sendTime": "2026-12-31T23:59:59Z"
}
```
#### 响应参数
| 参数名 | 类型 | 说明 |
|--------|------|------|
| code | number | 响应状态码200表示成功 |
| message | string | 响应消息 |
| data | object | 响应数据 |
| data.mailId | string | 邮件ID |
| data.capsuleId | string | 胶囊ID |
| data.status | string | 邮件状态 |
| data.updatedAt | string | 更新时间ISO格式时间字符串 |
#### 响应示例
```json
{
"code": 200,
"message": "success",
"data": {
"mailId": "mail_1234567890",
"capsuleId": "capsule_1234567890",
"status": "DRAFT",
"updatedAt": "2023-07-21T14:30:00Z"
}
}
```
### 删除胶囊邮件
**接口地址:** `DELETE /api/v1/mails/{mailId}`
**接口描述:** 删除指定的胶囊邮件
#### 请求参数
| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| mailId | string | 是 | 邮件ID路径参数 |
#### 请求示例
```
DELETE /api/v1/mails/mail_1234567890
```
#### 响应参数
| 参数名 | 类型 | 说明 |
|--------|------|------|
| code | number | 响应状态码200表示成功 |
| message | string | 响应消息 |
| data | object | 响应数据 |
| data.mailId | string | 已删除的邮件ID |
#### 响应示例
```json
{
"code": 200,
"message": "success",
"data": {
"mailId": "mail_1234567890"
}
}
```
## 错误码
| 错误码 | 说明 |
|--------|------|
| 200 | 成功 |
| 400 | 请求参数错误 |
| 401 | 未授权,需要登录 |
| 403 | 权限不足 |
| 404 | 资源不存在 |
| 422 | 验证失败 |
| 500 | 服务器内部错误 |
## 注意事项
1. 存入胶囊的邮件状态为DRAFT可以在任何时候编辑或发送
2. 只有草稿状态的邮件可以编辑或删除
3. 发送时间必须晚于当前时间
4. 附件大小限制为10MB
5. 免费用户每月最多可创建10个胶囊邮件
6. 加密邮件需要额外验证才能查看内容

307
.trae/rules/seed.md Normal file
View File

@@ -0,0 +1,307 @@
# 发送至未来功能 API 文档
## 概述
发送至未来功能允许用户将邮件设置为在未来特定时间自动发送,邮件状态将变为待投递(PENDING),系统会在指定时间自动处理发送。
## API 接口
### 发送至未来
**接口地址:** `POST /api/v1/mails/send-to-future`
**接口描述:** 将草稿状态的邮件设置为在未来特定时间自动发送
#### 请求参数
| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| mailId | string | 是 | 邮件ID |
| sendTime | string | 是 | 发送时间ISO格式时间字符串2025-12-31T23:59:59Z |
| triggerType | string | 否 | 触发类型TIME时间、LOCATION地点、EVENT事件默认为TIME |
| triggerCondition | object | 否 | 触发条件 |
| triggerCondition.location | object | 否 | 地点触发条件 |
| triggerCondition.location.latitude | number | 否 | 纬度 |
| triggerCondition.location.longitude | number | 否 | 经度 |
| triggerCondition.location.city | string | 否 | 城市 |
| triggerCondition.event | object | 否 | 事件触发条件 |
| triggerCondition.event.keywords | array | 否 | 关键词列表 |
| triggerCondition.event.type | string | 否 | 事件类型 |
#### 请求示例
```json
{
"mailId": "mail_1234567890",
"sendTime": "2025-12-31T23:59:59Z",
"triggerType": "TIME",
"triggerCondition": {}
}
```
#### 响应参数
| 参数名 | 类型 | 说明 |
|--------|------|------|
| code | number | 响应状态码200表示成功 |
| message | string | 响应消息 |
| data | object | 响应数据 |
| data.mailId | string | 邮件ID |
| data.capsuleId | string | 胶囊ID |
| data.status | string | 邮件状态PENDING |
| data.sendTime | string | 发送时间 |
| data.countdown | number | 倒计时秒数 |
| data.updatedAt | string | 更新时间ISO格式时间字符串 |
#### 响应示例
```json
{
"code": 200,
"message": "success",
"data": {
"mailId": "mail_1234567890",
"capsuleId": "capsule_1234567890",
"status": "PENDING",
"sendTime": "2025-12-31T23:59:59Z",
"countdown": 94608000,
"updatedAt": "2023-07-20T10:30:00Z"
}
}
```
### 获取待发送邮件列表
**接口地址:** `GET /api/v1/mails`
**接口描述:** 获取用户的待发送邮件列表
#### 请求参数
| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| type | string | 否 | 邮件类型INBOX、SENT、DRAFT获取待发送时使用SENT |
| status | string | 否 | 状态筛选PENDING、DELIVERING、DELIVERED、DRAFT获取待发送时使用PENDING |
| page | number | 否 | 页码默认为1 |
| size | number | 否 | 每页数量默认为10 |
#### 请求示例
```
GET /api/v1/mails?type=SENT&status=PENDING&page=1&size=10
```
#### 响应参数
| 参数名 | 类型 | 说明 |
|--------|------|------|
| code | number | 响应状态码200表示成功 |
| message | string | 响应消息 |
| data | object | 响应数据 |
| data.list | array | 邮件列表 |
| data.list[].mailId | string | 邮件ID |
| data.list[].title | string | 邮件标题 |
| data.list[].sender | object | 发件人信息 |
| data.list[].recipient | object | 收件人信息 |
| data.list[].sendTime | string | 发送时间 |
| data.list[].deliveryTime | string | 送达时间 |
| data.list[].status | string | 邮件状态 |
| data.list[].hasAttachments | boolean | 是否有附件 |
| data.list[].isEncrypted | boolean | 是否加密 |
| data.list[].capsuleStyle | string | 胶囊样式 |
| data.list[].countdown | number | 倒计时秒数 |
| data.total | number | 总数量 |
| data.page | number | 当前页码 |
| data.size | number | 每页数量 |
#### 响应示例
```json
{
"code": 200,
"message": "success",
"data": {
"list": [
{
"mailId": "mail_1234567890",
"title": "写给未来的自己",
"sender": {
"userId": "user_123",
"username": "张三",
"avatar": "https://example.com/avatar.jpg"
},
"recipient": {
"userId": "user_123",
"username": "张三",
"avatar": "https://example.com/avatar.jpg"
},
"sendTime": "2025-12-31T23:59:59Z",
"deliveryTime": null,
"status": "PENDING",
"hasAttachments": true,
"isEncrypted": false,
"capsuleStyle": "default",
"countdown": 94608000
}
],
"total": 1,
"page": 1,
"size": 10
}
}
```
### 获取待发送邮件详情
**接口地址:** `GET /api/v1/mails/{mailId}`
**接口描述:** 获取指定待发送邮件的详细信息
#### 请求参数
| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| mailId | string | 是 | 邮件ID |
#### 请求示例
```
GET /api/v1/mails/mail_1234567890
```
#### 响应参数
| 参数名 | 类型 | 说明 |
|--------|------|------|
| code | number | 响应状态码200表示成功 |
| message | string | 响应消息 |
| data | object | 响应数据 |
| data.mailId | string | 邮件ID |
| data.title | string | 邮件标题 |
| data.content | string | 邮件内容 |
| data.sender | object | 发件人信息 |
| data.recipient | object | 收件人信息 |
| data.sendTime | string | 发送时间 |
| data.createdAt | string | 创建时间 |
| data.deliveryTime | string | 送达时间 |
| data.status | string | 邮件状态 |
| data.triggerType | string | 触发类型 |
| data.triggerCondition | object | 触发条件 |
| data.attachments | array | 附件列表 |
| data.isEncrypted | boolean | 是否加密 |
| data.capsuleStyle | string | 胶囊样式 |
| data.canEdit | boolean | 是否可编辑待发送状态为false |
| data.canRevoke | boolean | 是否可撤销待发送状态为true |
| data.countdown | number | 倒计时秒数 |
#### 响应示例
```json
{
"code": 200,
"message": "success",
"data": {
"mailId": "mail_1234567890",
"title": "写给未来的自己",
"content": "亲爱的未来的我,当你读到这封信时,希望你已经实现了现在的梦想...",
"sender": {
"userId": "user_123",
"username": "张三",
"avatar": "https://example.com/avatar.jpg",
"email": "zhangsan@example.com"
},
"recipient": {
"userId": "user_123",
"username": "张三",
"avatar": "https://example.com/avatar.jpg",
"email": "zhangsan@example.com"
},
"sendTime": "2025-12-31T23:59:59Z",
"createdAt": "2023-07-20T10:30:00Z",
"deliveryTime": null,
"status": "PENDING",
"triggerType": "TIME",
"triggerCondition": {},
"attachments": [
{
"id": "attach_123",
"type": "IMAGE",
"url": "https://example.com/image.jpg",
"thumbnail": "https://example.com/thumb.jpg",
"size": 1024000
}
],
"isEncrypted": false,
"capsuleStyle": "default",
"canEdit": false,
"canRevoke": true,
"countdown": 94608000
}
}
```
### 撤销待发送邮件
**接口地址:** `POST /api/v1/mails/{mailId}/revoke`
**接口描述:** 撤销待发送的邮件,将状态改回草稿
#### 请求参数
| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| mailId | string | 是 | 邮件ID路径参数 |
#### 请求示例
```
POST /api/v1/mails/mail_1234567890/revoke
```
#### 响应参数
| 参数名 | 类型 | 说明 |
|--------|------|------|
| code | number | 响应状态码200表示成功 |
| message | string | 响应消息 |
| data | object | 响应数据 |
| data.mailId | string | 邮件ID |
| data.status | string | 邮件状态DRAFT |
| data.revokedAt | string | 撤销时间ISO格式时间字符串 |
#### 响应示例
```json
{
"code": 200,
"message": "success",
"data": {
"mailId": "mail_1234567890",
"status": "DRAFT",
"revokedAt": "2023-07-21T14:30:00Z"
}
}
```
## 错误码
| 错误码 | 说明 |
|--------|------|
| 200 | 成功 |
| 400 | 请求参数错误 |
| 401 | 未授权,需要登录 |
| 403 | 权限不足 |
| 404 | 资源不存在 |
| 422 | 验证失败 |
| 500 | 服务器内部错误 |
## 注意事项
1. 发送至未来的邮件状态为PENDING表示等待系统在未来指定时间自动发送
2. 只有草稿状态(DRAFT)的邮件可以设置为发送至未来
3. 发送时间必须晚于当前时间至少1小时
4. 待发送状态的邮件不能编辑内容,但可以撤销发送
5. 撤销后的邮件状态将变回草稿(DRAFT),可以重新编辑或设置发送时间
6. 系统会在发送时间到达前10分钟进入投递中状态(DELIVERING)
7. 免费用户每月最多可设置5封邮件发送至未来
8. 附件大小限制为10MB
9. 加密邮件需要额外验证才能查看内容

View File

@@ -80,6 +80,108 @@ namespace FutureMailAPI.Controllers
result); result);
} }
// 直接接收前端原始格式的创建邮件接口
[HttpPost("create-raw")]
public async Task<IActionResult> CreateMailRaw()
{
try
{
// 读取请求体
var request = HttpContext.Request;
using var reader = new StreamReader(request.Body);
var body = await reader.ReadToEndAsync();
// 解析JSON
var rawMail = System.Text.Json.JsonSerializer.Deserialize<Dictionary<string, object>>(body);
if (rawMail == null)
{
return BadRequest(ApiResponse<SentMailResponseDto>.ErrorResult("请求数据为空"));
}
// 创建兼容DTO
var compatDto = new SentMailCreateCompatDto();
// 解析各个字段
if (rawMail.ContainsKey("title") && rawMail["title"] != null)
compatDto.title = rawMail["title"].ToString() ?? string.Empty;
if (rawMail.ContainsKey("content") && rawMail["content"] != null)
compatDto.content = rawMail["content"].ToString() ?? string.Empty;
if (rawMail.ContainsKey("recipientType") && rawMail["recipientType"] != null)
{
var recipientTypeStr = rawMail["recipientType"].ToString();
if (Enum.TryParse<RecipientTypeEnum>(recipientTypeStr, true, out var recipientType))
compatDto.recipientType = recipientType;
}
if (rawMail.ContainsKey("recipientEmail") && rawMail["recipientEmail"] != null)
compatDto.recipientEmail = rawMail["recipientEmail"].ToString();
if (rawMail.ContainsKey("sendTime") && rawMail["sendTime"] != null)
{
if (DateTime.TryParse(rawMail["sendTime"].ToString(), out var sendTime))
compatDto.sendTime = sendTime;
}
if (rawMail.ContainsKey("triggerType") && rawMail["triggerType"] != null)
{
var triggerTypeStr = rawMail["triggerType"].ToString();
if (Enum.TryParse<TriggerTypeEnum>(triggerTypeStr, true, out var triggerType))
compatDto.triggerType = triggerType;
}
if (rawMail.ContainsKey("triggerCondition"))
compatDto.triggerCondition = rawMail["triggerCondition"];
if (rawMail.ContainsKey("attachments"))
{
try
{
compatDto.attachments = System.Text.Json.JsonSerializer.Deserialize<List<object>>(rawMail["attachments"].ToString());
}
catch
{
compatDto.attachments = new List<object>();
}
}
if (rawMail.ContainsKey("isEncrypted"))
compatDto.isEncrypted = bool.Parse(rawMail["isEncrypted"].ToString());
if (rawMail.ContainsKey("capsuleStyle"))
compatDto.capsuleStyle = rawMail["capsuleStyle"].ToString() ?? "default";
// 从JWT令牌中获取当前用户ID
var currentUserId = GetCurrentUserId();
if (currentUserId <= 0)
{
return Unauthorized(ApiResponse<SentMailResponseDto>.ErrorResult("未授权访问"));
}
// 转换为内部DTO
var internalDto = compatDto.ToInternalDto();
var result = await _mailService.CreateMailAsync(currentUserId, internalDto);
if (!result.Success)
{
return BadRequest(result);
}
return CreatedAtAction(
nameof(GetMail),
new { mailId = result.Data!.Id },
result);
}
catch (Exception ex)
{
_logger.LogError(ex, "创建邮件时发生错误");
return StatusCode(500, ApiResponse<SentMailResponseDto>.ErrorResult("服务器内部错误"));
}
}
[HttpGet("{mailId}")] [HttpGet("{mailId}")]
public async Task<IActionResult> GetMail(int mailId) public async Task<IActionResult> GetMail(int mailId)
{ {
@@ -255,5 +357,198 @@ namespace FutureMailAPI.Controllers
return Ok(result); return Ok(result);
} }
/// <summary>
/// 存入胶囊 - 创建胶囊邮件
/// </summary>
/// <param name="dto">存入胶囊请求</param>
/// <returns>操作结果</returns>
[HttpPost("capsule")]
public async Task<IActionResult> SaveToCapsule([FromBody] SaveToCapsuleDto dto)
{
if (!ModelState.IsValid)
{
return BadRequest(ApiResponse<SaveToCapsuleResponseDto>.ErrorResult("请求参数无效"));
}
var currentUserId = GetCurrentUserId();
if (currentUserId <= 0)
{
return Unauthorized(ApiResponse<SaveToCapsuleResponseDto>.ErrorResult("未授权访问"));
}
var result = await _mailService.SaveToCapsuleAsync(currentUserId, dto);
if (!result.Success)
{
return BadRequest(result);
}
return CreatedAtAction(
nameof(GetCapsuleMail),
new { id = result.Data!.Id },
result);
}
/// <summary>
/// 获取胶囊邮件列表
/// </summary>
/// <param name="pageIndex">页码</param>
/// <param name="pageSize">页大小</param>
/// <param name="status">状态筛选</param>
/// <param name="recipientType">收件人类型筛选</param>
/// <param name="keyword">关键词搜索</param>
/// <param name="startDate">开始日期</param>
/// <param name="endDate">结束日期</param>
/// <returns>胶囊邮件列表</returns>
[HttpGet("capsule")]
public async Task<IActionResult> GetCapsuleMails(
[FromQuery] int pageIndex = 1,
[FromQuery] int pageSize = 10,
[FromQuery] int? status = null,
[FromQuery] int? recipientType = null,
[FromQuery] string? keyword = null,
[FromQuery] DateTime? startDate = null,
[FromQuery] DateTime? endDate = null)
{
var queryDto = new MailListQueryDto
{
PageIndex = pageIndex,
PageSize = pageSize,
Status = status,
RecipientType = recipientType,
Keyword = keyword,
StartDate = startDate,
EndDate = endDate
};
var currentUserId = GetCurrentUserId();
if (currentUserId <= 0)
{
return Unauthorized(ApiResponse<PagedResponse<CapsuleMailListResponseDto>>.ErrorResult("未授权访问"));
}
var result = await _mailService.GetCapsuleMailsAsync(currentUserId, queryDto);
if (!result.Success)
{
return BadRequest(result);
}
return Ok(result);
}
/// <summary>
/// 获取胶囊邮件详情
/// </summary>
/// <param name="id">邮件ID</param>
/// <returns>胶囊邮件详情</returns>
[HttpGet("capsule/{id}")]
public async Task<IActionResult> GetCapsuleMail(int id)
{
var currentUserId = GetCurrentUserId();
if (currentUserId <= 0)
{
return Unauthorized(ApiResponse<CapsuleMailDetailResponseDto>.ErrorResult("未授权访问"));
}
var result = await _mailService.GetCapsuleMailByIdAsync(currentUserId, id);
if (!result.Success)
{
return NotFound(result);
}
return Ok(result);
}
/// <summary>
/// 更新胶囊邮件
/// </summary>
/// <param name="id">邮件ID</param>
/// <param name="dto">更新请求</param>
/// <returns>更新后的胶囊邮件详情</returns>
[HttpPut("capsule/{id}")]
public async Task<IActionResult> UpdateCapsuleMail(int id, [FromBody] UpdateCapsuleMailDto dto)
{
if (!ModelState.IsValid)
{
return BadRequest(ApiResponse<CapsuleMailDetailResponseDto>.ErrorResult("请求参数无效"));
}
var currentUserId = GetCurrentUserId();
if (currentUserId <= 0)
{
return Unauthorized(ApiResponse<CapsuleMailDetailResponseDto>.ErrorResult("未授权访问"));
}
var result = await _mailService.UpdateCapsuleMailAsync(currentUserId, id, dto);
if (!result.Success)
{
return BadRequest(result);
}
return Ok(result);
}
/// <summary>
/// 撤销胶囊邮件
/// </summary>
/// <param name="id">邮件ID</param>
/// <returns>操作结果</returns>
[HttpPost("capsule/{id}/revoke")]
public async Task<IActionResult> RevokeCapsuleMail(int id)
{
var currentUserId = GetCurrentUserId();
if (currentUserId <= 0)
{
return Unauthorized(ApiResponse<bool>.ErrorResult("未授权访问"));
}
var result = await _mailService.RevokeCapsuleMailAsync(currentUserId, id);
if (!result.Success)
{
return BadRequest(result);
}
return Ok(result);
}
/// <summary>
/// 发送至未来 - 将草稿状态的邮件设置为在未来特定时间自动发送
/// </summary>
/// <param name="sendToFutureDto">发送至未来请求DTO</param>
/// <returns>发送至未来响应DTO</returns>
[HttpPost("send-to-future")]
public async Task<IActionResult> SendToFuture([FromBody] SendToFutureDto sendToFutureDto)
{
if (!ModelState.IsValid)
{
return BadRequest(ApiResponse<SendToFutureResponseDto>.ErrorResult("请求参数无效"));
}
var currentUserId = GetCurrentUserId();
if (currentUserId <= 0)
{
return Unauthorized(ApiResponse<SendToFutureResponseDto>.ErrorResult("未授权访问"));
}
var result = await _mailService.SendToFutureAsync(currentUserId, sendToFutureDto);
if (!result.Success)
{
return BadRequest(result);
}
return Ok(result);
}
} }
} }

View File

@@ -189,4 +189,130 @@ namespace FutureMailAPI.DTOs
public DateTime? StartDate { get; set; } public DateTime? StartDate { get; set; }
public DateTime? EndDate { get; set; } public DateTime? EndDate { get; set; }
} }
// 存入胶囊请求DTO
public class SaveToCapsuleDto
{
[Required(ErrorMessage = "标题是必填项")]
[StringLength(200, ErrorMessage = "标题长度不能超过200个字符")]
public string Title { get; set; } = string.Empty;
[Required(ErrorMessage = "内容是必填项")]
public string Content { get; set; } = string.Empty;
[Required(ErrorMessage = "收件人类型是必填项")]
[JsonConverter(typeof(JsonStringEnumConverter))]
public RecipientTypeEnum RecipientType { get; set; }
public string? RecipientEmail { get; set; }
public DateTime? SendTime { get; set; }
[Required(ErrorMessage = "触发条件类型是必填项")]
[JsonConverter(typeof(JsonStringEnumConverter))]
public TriggerTypeEnum TriggerType { get; set; } = TriggerTypeEnum.TIME;
public object? TriggerCondition { get; set; }
public List<object>? Attachments { get; set; }
public bool IsEncrypted { get; set; } = false;
[Required(ErrorMessage = "胶囊样式是必填项")]
public string CapsuleStyle { get; set; } = "default";
}
// 存入胶囊响应DTO
public class SaveToCapsuleResponseDto
{
public int Id { get; set; }
public string MailId { get; set; } = string.Empty;
public string CapsuleId { get; set; } = string.Empty;
public string Status { get; set; } = string.Empty;
public DateTime CreatedAt { get; set; }
}
// 胶囊邮件列表响应DTO
public class CapsuleMailListResponseDto
{
public string MailId { get; set; } = string.Empty;
public string Title { get; set; } = string.Empty;
public UserInfoDto Sender { get; set; } = new();
public UserInfoDto Recipient { get; set; } = new();
public DateTime SendTime { get; set; }
public DateTime? DeliveryTime { get; set; }
public string Status { get; set; } = string.Empty;
public bool HasAttachments { get; set; }
public bool IsEncrypted { get; set; }
public string CapsuleStyle { get; set; } = string.Empty;
public int? Countdown { get; set; } // 倒计时秒数仅status=PENDING时返回
}
// 胶囊邮件详情响应DTO
public class CapsuleMailDetailResponseDto
{
public string MailId { get; set; } = string.Empty;
public string Title { get; set; } = string.Empty;
public string Content { get; set; } = string.Empty;
public UserInfoDto Sender { get; set; } = new();
public UserInfoDto Recipient { get; set; } = new();
public DateTime SendTime { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime? DeliveryTime { get; set; }
public string Status { get; set; } = string.Empty;
public string TriggerType { get; set; } = string.Empty;
public object? TriggerCondition { get; set; }
public List<AttachmentDto> Attachments { get; set; } = new();
public bool IsEncrypted { get; set; }
public string CapsuleStyle { get; set; } = string.Empty;
public bool CanEdit { get; set; } // 是否可编辑(仅草稿状态)
public bool CanRevoke { get; set; } // 是否可撤销(仅待投递状态)
}
// 附件DTO
public class AttachmentDto
{
public string Id { get; set; } = string.Empty;
public string Type { get; set; } = string.Empty;
public string Url { get; set; } = string.Empty;
public string? Thumbnail { get; set; }
public long Size { get; set; }
}
// 更新胶囊邮件DTO
public class UpdateCapsuleMailDto
{
public string? Title { get; set; }
public string? Content { get; set; }
public RecipientTypeEnum? RecipientType { get; set; }
public string? RecipientEmail { get; set; }
public DateTime? SendTime { get; set; }
public TriggerTypeEnum? TriggerType { get; set; }
public object? TriggerCondition { get; set; }
public List<object>? Attachments { get; set; }
public bool? IsEncrypted { get; set; }
public string? CapsuleStyle { get; set; }
}
// 发送至未来请求DTO
public class SendToFutureDto
{
[Required(ErrorMessage = "邮件ID是必填项")]
public int MailId { get; set; }
[Required(ErrorMessage = "投递时间是必填项")]
public DateTime DeliveryTime { get; set; }
}
// 发送至未来响应DTO
public class SendToFutureResponseDto
{
public int MailId { get; set; }
public string Title { get; set; } = string.Empty;
public DateTime DeliveryTime { get; set; }
public string Status { get; set; } = string.Empty;
public DateTime SentAt { get; set; }
}
} }

View File

@@ -60,6 +60,7 @@ namespace FutureMailAPI.DTOs
public int UserId { get; set; } public int UserId { get; set; }
public string Username { get; set; } = string.Empty; public string Username { get; set; } = string.Empty;
public string? Avatar { get; set; } public string? Avatar { get; set; }
public string Email { get; set; } = string.Empty;
} }
public class SubscriptionResponseDto public class SubscriptionResponseDto

View File

@@ -36,7 +36,7 @@ namespace FutureMailAPI.Data
.HasForeignKey(e => e.SenderId) .HasForeignKey(e => e.SenderId)
.OnDelete(DeleteBehavior.Restrict); .OnDelete(DeleteBehavior.Restrict);
entity.HasOne<User>() entity.HasOne(e => e.Recipient)
.WithMany() .WithMany()
.HasForeignKey(e => e.RecipientId) .HasForeignKey(e => e.RecipientId)
.OnDelete(DeleteBehavior.SetNull); .OnDelete(DeleteBehavior.SetNull);
@@ -52,8 +52,8 @@ namespace FutureMailAPI.Data
.HasForeignKey(e => e.SentMailId) .HasForeignKey(e => e.SentMailId)
.OnDelete(DeleteBehavior.Cascade); .OnDelete(DeleteBehavior.Cascade);
entity.HasOne<User>() entity.HasOne(e => e.Recipient)
.WithMany() .WithMany(u => u.ReceivedMails)
.HasForeignKey(e => e.RecipientId) .HasForeignKey(e => e.RecipientId)
.OnDelete(DeleteBehavior.Cascade); .OnDelete(DeleteBehavior.Cascade);
@@ -68,9 +68,10 @@ namespace FutureMailAPI.Data
.HasForeignKey(e => e.UserId) .HasForeignKey(e => e.UserId)
.OnDelete(DeleteBehavior.Cascade); .OnDelete(DeleteBehavior.Cascade);
// 一对一关系配置
entity.HasOne<SentMail>() entity.HasOne<SentMail>()
.WithMany() .WithOne(m => m.TimeCapsule)
.HasForeignKey(e => e.SentMailId) .HasForeignKey<TimeCapsule>(e => e.SentMailId)
.OnDelete(DeleteBehavior.Cascade); .OnDelete(DeleteBehavior.Cascade);
entity.Property(e => e.CreatedAt).HasDefaultValueSql("CURRENT_TIMESTAMP"); entity.Property(e => e.CreatedAt).HasDefaultValueSql("CURRENT_TIMESTAMP");

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,495 @@
// <auto-generated />
using System;
using FutureMailAPI.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace FutureMailAPI.Migrations
{
[DbContext(typeof(FutureMailDbContext))]
[Migration("20251018051334_AddSentMailCreatedAt")]
partial class AddSentMailCreatedAt
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "9.0.9");
modelBuilder.Entity("FutureMailAPI.Models.OAuthClient", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("ClientId")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("TEXT");
b.Property<string>("ClientSecret")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("TEXT");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasDefaultValueSql("CURRENT_TIMESTAMP");
b.Property<bool>("IsActive")
.HasColumnType("INTEGER");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("TEXT");
b.Property<string>("RedirectUris")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("Scopes")
.IsRequired()
.HasColumnType("TEXT");
b.Property<DateTime>("UpdatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasDefaultValueSql("CURRENT_TIMESTAMP");
b.HasKey("Id");
b.HasIndex("ClientId")
.IsUnique();
b.ToTable("OAuthClients");
});
modelBuilder.Entity("FutureMailAPI.Models.OAuthToken", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("AccessToken")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("TEXT");
b.Property<int>("ClientId")
.HasColumnType("INTEGER");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasDefaultValueSql("CURRENT_TIMESTAMP");
b.Property<DateTime>("ExpiresAt")
.HasColumnType("TEXT");
b.Property<string>("RefreshToken")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("TEXT");
b.Property<DateTime?>("RevokedAt")
.HasColumnType("TEXT");
b.Property<string>("Scope")
.HasColumnType("TEXT");
b.Property<string>("TokenType")
.IsRequired()
.HasColumnType("TEXT");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("TEXT");
b.Property<int>("UserId")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("AccessToken")
.IsUnique();
b.HasIndex("ClientId");
b.HasIndex("RefreshToken")
.IsUnique();
b.HasIndex("UserId");
b.ToTable("OAuthTokens");
});
modelBuilder.Entity("FutureMailAPI.Models.ReceivedMail", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<bool>("IsRead")
.HasColumnType("INTEGER");
b.Property<bool>("IsReplied")
.HasColumnType("INTEGER");
b.Property<DateTime?>("ReadAt")
.HasColumnType("TEXT");
b.Property<DateTime>("ReceivedAt")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasDefaultValueSql("CURRENT_TIMESTAMP");
b.Property<int>("RecipientId")
.HasColumnType("INTEGER");
b.Property<int>("RecipientId1")
.HasColumnType("INTEGER");
b.Property<int?>("ReplyMailId")
.HasColumnType("INTEGER");
b.Property<int>("SentMailId")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("RecipientId");
b.HasIndex("RecipientId1");
b.HasIndex("SentMailId");
b.ToTable("ReceivedMails");
});
modelBuilder.Entity("FutureMailAPI.Models.SentMail", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("Attachments")
.HasColumnType("TEXT");
b.Property<string>("Content")
.IsRequired()
.HasColumnType("TEXT");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT");
b.Property<DateTime>("DeliveryTime")
.HasColumnType("TEXT");
b.Property<string>("EncryptionKey")
.HasColumnType("TEXT");
b.Property<bool>("IsEncrypted")
.HasColumnType("INTEGER");
b.Property<int?>("RecipientId")
.HasColumnType("INTEGER");
b.Property<int?>("RecipientId1")
.HasColumnType("INTEGER");
b.Property<int>("RecipientType")
.HasColumnType("INTEGER");
b.Property<int>("SenderId")
.HasColumnType("INTEGER");
b.Property<DateTime>("SentAt")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasDefaultValueSql("CURRENT_TIMESTAMP");
b.Property<int>("Status")
.HasColumnType("INTEGER");
b.Property<string>("Theme")
.HasMaxLength(50)
.HasColumnType("TEXT");
b.Property<string>("Title")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("TEXT");
b.Property<string>("TriggerDetails")
.HasColumnType("TEXT");
b.Property<int>("TriggerType")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("RecipientId");
b.HasIndex("RecipientId1");
b.HasIndex("SenderId");
b.ToTable("SentMails");
});
modelBuilder.Entity("FutureMailAPI.Models.TimeCapsule", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("Color")
.HasMaxLength(20)
.HasColumnType("TEXT");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasDefaultValueSql("CURRENT_TIMESTAMP");
b.Property<double>("GlowIntensity")
.HasColumnType("REAL");
b.Property<double>("Opacity")
.HasColumnType("REAL");
b.Property<double>("PositionX")
.HasColumnType("REAL");
b.Property<double>("PositionY")
.HasColumnType("REAL");
b.Property<double>("PositionZ")
.HasColumnType("REAL");
b.Property<double>("Rotation")
.HasColumnType("REAL");
b.Property<int>("SentMailId")
.HasColumnType("INTEGER");
b.Property<int>("SentMailId1")
.HasColumnType("INTEGER");
b.Property<double>("Size")
.HasColumnType("REAL");
b.Property<int>("Status")
.HasColumnType("INTEGER");
b.Property<string>("Style")
.HasMaxLength(50)
.HasColumnType("TEXT");
b.Property<int>("Type")
.HasColumnType("INTEGER");
b.Property<int>("UserId")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("SentMailId")
.IsUnique();
b.HasIndex("SentMailId1");
b.HasIndex("UserId");
b.ToTable("TimeCapsules");
});
modelBuilder.Entity("FutureMailAPI.Models.User", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("Avatar")
.HasMaxLength(500)
.HasColumnType("TEXT");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasDefaultValueSql("CURRENT_TIMESTAMP");
b.Property<string>("Email")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("TEXT");
b.Property<bool>("IsActive")
.HasColumnType("INTEGER");
b.Property<DateTime?>("LastLoginAt")
.HasColumnType("TEXT");
b.Property<string>("Nickname")
.HasMaxLength(100)
.HasColumnType("TEXT");
b.Property<string>("PasswordHash")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("TEXT");
b.Property<string>("PreferredBackground")
.HasMaxLength(50)
.HasColumnType("TEXT");
b.Property<string>("PreferredScene")
.HasMaxLength(20)
.HasColumnType("TEXT");
b.Property<string>("RefreshToken")
.HasMaxLength(500)
.HasColumnType("TEXT");
b.Property<DateTime?>("RefreshTokenExpiryTime")
.HasColumnType("TEXT");
b.Property<string>("Salt")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("TEXT");
b.Property<string>("Username")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("Email")
.IsUnique();
b.HasIndex("Username")
.IsUnique();
b.ToTable("Users");
});
modelBuilder.Entity("FutureMailAPI.Models.OAuthToken", b =>
{
b.HasOne("FutureMailAPI.Models.OAuthClient", "Client")
.WithMany("Tokens")
.HasForeignKey("ClientId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("FutureMailAPI.Models.User", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Client");
b.Navigation("User");
});
modelBuilder.Entity("FutureMailAPI.Models.ReceivedMail", b =>
{
b.HasOne("FutureMailAPI.Models.User", null)
.WithMany()
.HasForeignKey("RecipientId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("FutureMailAPI.Models.User", "Recipient")
.WithMany("ReceivedMails")
.HasForeignKey("RecipientId1")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("FutureMailAPI.Models.SentMail", "SentMail")
.WithMany()
.HasForeignKey("SentMailId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Recipient");
b.Navigation("SentMail");
});
modelBuilder.Entity("FutureMailAPI.Models.SentMail", b =>
{
b.HasOne("FutureMailAPI.Models.User", null)
.WithMany()
.HasForeignKey("RecipientId")
.OnDelete(DeleteBehavior.SetNull);
b.HasOne("FutureMailAPI.Models.User", "Recipient")
.WithMany()
.HasForeignKey("RecipientId1");
b.HasOne("FutureMailAPI.Models.User", "Sender")
.WithMany("SentMails")
.HasForeignKey("SenderId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.Navigation("Recipient");
b.Navigation("Sender");
});
modelBuilder.Entity("FutureMailAPI.Models.TimeCapsule", b =>
{
b.HasOne("FutureMailAPI.Models.SentMail", null)
.WithOne("TimeCapsule")
.HasForeignKey("FutureMailAPI.Models.TimeCapsule", "SentMailId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("FutureMailAPI.Models.SentMail", "SentMail")
.WithMany()
.HasForeignKey("SentMailId1")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("FutureMailAPI.Models.User", "User")
.WithMany("TimeCapsules")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("SentMail");
b.Navigation("User");
});
modelBuilder.Entity("FutureMailAPI.Models.OAuthClient", b =>
{
b.Navigation("Tokens");
});
modelBuilder.Entity("FutureMailAPI.Models.SentMail", b =>
{
b.Navigation("TimeCapsule");
});
modelBuilder.Entity("FutureMailAPI.Models.User", b =>
{
b.Navigation("ReceivedMails");
b.Navigation("SentMails");
b.Navigation("TimeCapsules");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,71 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace FutureMailAPI.Migrations
{
/// <inheritdoc />
public partial class AddSentMailCreatedAt : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
name: "IX_TimeCapsules_SentMailId",
table: "TimeCapsules");
migrationBuilder.AddColumn<double>(
name: "GlowIntensity",
table: "TimeCapsules",
type: "REAL",
nullable: false,
defaultValue: 0.0);
migrationBuilder.AddColumn<string>(
name: "Style",
table: "TimeCapsules",
type: "TEXT",
maxLength: 50,
nullable: true);
migrationBuilder.AddColumn<DateTime>(
name: "CreatedAt",
table: "SentMails",
type: "TEXT",
nullable: false,
defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified));
migrationBuilder.CreateIndex(
name: "IX_TimeCapsules_SentMailId",
table: "TimeCapsules",
column: "SentMailId",
unique: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
name: "IX_TimeCapsules_SentMailId",
table: "TimeCapsules");
migrationBuilder.DropColumn(
name: "GlowIntensity",
table: "TimeCapsules");
migrationBuilder.DropColumn(
name: "Style",
table: "TimeCapsules");
migrationBuilder.DropColumn(
name: "CreatedAt",
table: "SentMails");
migrationBuilder.CreateIndex(
name: "IX_TimeCapsules_SentMailId",
table: "TimeCapsules",
column: "SentMailId");
}
}
}

View File

@@ -0,0 +1,475 @@
// <auto-generated />
using System;
using FutureMailAPI.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace FutureMailAPI.Migrations
{
[DbContext(typeof(FutureMailDbContext))]
[Migration("20251018071917_FixDuplicateForeignKeys")]
partial class FixDuplicateForeignKeys
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "9.0.9");
modelBuilder.Entity("FutureMailAPI.Models.OAuthClient", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("ClientId")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("TEXT");
b.Property<string>("ClientSecret")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("TEXT");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasDefaultValueSql("CURRENT_TIMESTAMP");
b.Property<bool>("IsActive")
.HasColumnType("INTEGER");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("TEXT");
b.Property<string>("RedirectUris")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("Scopes")
.IsRequired()
.HasColumnType("TEXT");
b.Property<DateTime>("UpdatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasDefaultValueSql("CURRENT_TIMESTAMP");
b.HasKey("Id");
b.HasIndex("ClientId")
.IsUnique();
b.ToTable("OAuthClients");
});
modelBuilder.Entity("FutureMailAPI.Models.OAuthToken", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("AccessToken")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("TEXT");
b.Property<int>("ClientId")
.HasColumnType("INTEGER");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasDefaultValueSql("CURRENT_TIMESTAMP");
b.Property<DateTime>("ExpiresAt")
.HasColumnType("TEXT");
b.Property<string>("RefreshToken")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("TEXT");
b.Property<DateTime?>("RevokedAt")
.HasColumnType("TEXT");
b.Property<string>("Scope")
.HasColumnType("TEXT");
b.Property<string>("TokenType")
.IsRequired()
.HasColumnType("TEXT");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("TEXT");
b.Property<int>("UserId")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("AccessToken")
.IsUnique();
b.HasIndex("ClientId");
b.HasIndex("RefreshToken")
.IsUnique();
b.HasIndex("UserId");
b.ToTable("OAuthTokens");
});
modelBuilder.Entity("FutureMailAPI.Models.ReceivedMail", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<bool>("IsRead")
.HasColumnType("INTEGER");
b.Property<bool>("IsReplied")
.HasColumnType("INTEGER");
b.Property<DateTime?>("ReadAt")
.HasColumnType("TEXT");
b.Property<DateTime>("ReceivedAt")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasDefaultValueSql("CURRENT_TIMESTAMP");
b.Property<int>("RecipientId")
.HasColumnType("INTEGER");
b.Property<int?>("ReplyMailId")
.HasColumnType("INTEGER");
b.Property<int>("SentMailId")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("RecipientId");
b.HasIndex("SentMailId");
b.ToTable("ReceivedMails");
});
modelBuilder.Entity("FutureMailAPI.Models.SentMail", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("Attachments")
.HasColumnType("TEXT");
b.Property<string>("Content")
.IsRequired()
.HasColumnType("TEXT");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT");
b.Property<DateTime>("DeliveryTime")
.HasColumnType("TEXT");
b.Property<string>("EncryptionKey")
.HasColumnType("TEXT");
b.Property<bool>("IsEncrypted")
.HasColumnType("INTEGER");
b.Property<int?>("RecipientId")
.HasColumnType("INTEGER");
b.Property<int>("RecipientType")
.HasColumnType("INTEGER");
b.Property<int>("SenderId")
.HasColumnType("INTEGER");
b.Property<DateTime>("SentAt")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasDefaultValueSql("CURRENT_TIMESTAMP");
b.Property<int>("Status")
.HasColumnType("INTEGER");
b.Property<string>("Theme")
.HasMaxLength(50)
.HasColumnType("TEXT");
b.Property<string>("Title")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("TEXT");
b.Property<string>("TriggerDetails")
.HasColumnType("TEXT");
b.Property<int>("TriggerType")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("RecipientId");
b.HasIndex("SenderId");
b.ToTable("SentMails");
});
modelBuilder.Entity("FutureMailAPI.Models.TimeCapsule", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("Color")
.HasMaxLength(20)
.HasColumnType("TEXT");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasDefaultValueSql("CURRENT_TIMESTAMP");
b.Property<double>("GlowIntensity")
.HasColumnType("REAL");
b.Property<double>("Opacity")
.HasColumnType("REAL");
b.Property<double>("PositionX")
.HasColumnType("REAL");
b.Property<double>("PositionY")
.HasColumnType("REAL");
b.Property<double>("PositionZ")
.HasColumnType("REAL");
b.Property<double>("Rotation")
.HasColumnType("REAL");
b.Property<int>("SentMailId")
.HasColumnType("INTEGER");
b.Property<int>("SentMailId1")
.HasColumnType("INTEGER");
b.Property<double>("Size")
.HasColumnType("REAL");
b.Property<int>("Status")
.HasColumnType("INTEGER");
b.Property<string>("Style")
.HasMaxLength(50)
.HasColumnType("TEXT");
b.Property<int>("Type")
.HasColumnType("INTEGER");
b.Property<int>("UserId")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("SentMailId")
.IsUnique();
b.HasIndex("SentMailId1");
b.HasIndex("UserId");
b.ToTable("TimeCapsules");
});
modelBuilder.Entity("FutureMailAPI.Models.User", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("Avatar")
.HasMaxLength(500)
.HasColumnType("TEXT");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasDefaultValueSql("CURRENT_TIMESTAMP");
b.Property<string>("Email")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("TEXT");
b.Property<bool>("IsActive")
.HasColumnType("INTEGER");
b.Property<DateTime?>("LastLoginAt")
.HasColumnType("TEXT");
b.Property<string>("Nickname")
.HasMaxLength(100)
.HasColumnType("TEXT");
b.Property<string>("PasswordHash")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("TEXT");
b.Property<string>("PreferredBackground")
.HasMaxLength(50)
.HasColumnType("TEXT");
b.Property<string>("PreferredScene")
.HasMaxLength(20)
.HasColumnType("TEXT");
b.Property<string>("RefreshToken")
.HasMaxLength(500)
.HasColumnType("TEXT");
b.Property<DateTime?>("RefreshTokenExpiryTime")
.HasColumnType("TEXT");
b.Property<string>("Salt")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("TEXT");
b.Property<string>("Username")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("Email")
.IsUnique();
b.HasIndex("Username")
.IsUnique();
b.ToTable("Users");
});
modelBuilder.Entity("FutureMailAPI.Models.OAuthToken", b =>
{
b.HasOne("FutureMailAPI.Models.OAuthClient", "Client")
.WithMany("Tokens")
.HasForeignKey("ClientId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("FutureMailAPI.Models.User", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Client");
b.Navigation("User");
});
modelBuilder.Entity("FutureMailAPI.Models.ReceivedMail", b =>
{
b.HasOne("FutureMailAPI.Models.User", "Recipient")
.WithMany("ReceivedMails")
.HasForeignKey("RecipientId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("FutureMailAPI.Models.SentMail", "SentMail")
.WithMany()
.HasForeignKey("SentMailId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Recipient");
b.Navigation("SentMail");
});
modelBuilder.Entity("FutureMailAPI.Models.SentMail", b =>
{
b.HasOne("FutureMailAPI.Models.User", "Recipient")
.WithMany()
.HasForeignKey("RecipientId")
.OnDelete(DeleteBehavior.SetNull);
b.HasOne("FutureMailAPI.Models.User", "Sender")
.WithMany("SentMails")
.HasForeignKey("SenderId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.Navigation("Recipient");
b.Navigation("Sender");
});
modelBuilder.Entity("FutureMailAPI.Models.TimeCapsule", b =>
{
b.HasOne("FutureMailAPI.Models.SentMail", null)
.WithOne("TimeCapsule")
.HasForeignKey("FutureMailAPI.Models.TimeCapsule", "SentMailId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("FutureMailAPI.Models.SentMail", "SentMail")
.WithMany()
.HasForeignKey("SentMailId1")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("FutureMailAPI.Models.User", "User")
.WithMany("TimeCapsules")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("SentMail");
b.Navigation("User");
});
modelBuilder.Entity("FutureMailAPI.Models.OAuthClient", b =>
{
b.Navigation("Tokens");
});
modelBuilder.Entity("FutureMailAPI.Models.SentMail", b =>
{
b.Navigation("TimeCapsule");
});
modelBuilder.Entity("FutureMailAPI.Models.User", b =>
{
b.Navigation("ReceivedMails");
b.Navigation("SentMails");
b.Navigation("TimeCapsules");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,80 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace FutureMailAPI.Migrations
{
/// <inheritdoc />
public partial class FixDuplicateForeignKeys : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_ReceivedMails_Users_RecipientId1",
table: "ReceivedMails");
migrationBuilder.DropForeignKey(
name: "FK_SentMails_Users_RecipientId1",
table: "SentMails");
migrationBuilder.DropIndex(
name: "IX_SentMails_RecipientId1",
table: "SentMails");
migrationBuilder.DropIndex(
name: "IX_ReceivedMails_RecipientId1",
table: "ReceivedMails");
migrationBuilder.DropColumn(
name: "RecipientId1",
table: "SentMails");
migrationBuilder.DropColumn(
name: "RecipientId1",
table: "ReceivedMails");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<int>(
name: "RecipientId1",
table: "SentMails",
type: "INTEGER",
nullable: true);
migrationBuilder.AddColumn<int>(
name: "RecipientId1",
table: "ReceivedMails",
type: "INTEGER",
nullable: false,
defaultValue: 0);
migrationBuilder.CreateIndex(
name: "IX_SentMails_RecipientId1",
table: "SentMails",
column: "RecipientId1");
migrationBuilder.CreateIndex(
name: "IX_ReceivedMails_RecipientId1",
table: "ReceivedMails",
column: "RecipientId1");
migrationBuilder.AddForeignKey(
name: "FK_ReceivedMails_Users_RecipientId1",
table: "ReceivedMails",
column: "RecipientId1",
principalTable: "Users",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
migrationBuilder.AddForeignKey(
name: "FK_SentMails_Users_RecipientId1",
table: "SentMails",
column: "RecipientId1",
principalTable: "Users",
principalColumn: "Id");
}
}
}

View File

@@ -148,9 +148,6 @@ namespace FutureMailAPI.Migrations
b.Property<int>("RecipientId") b.Property<int>("RecipientId")
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");
b.Property<int>("RecipientId1")
.HasColumnType("INTEGER");
b.Property<int?>("ReplyMailId") b.Property<int?>("ReplyMailId")
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");
@@ -161,8 +158,6 @@ namespace FutureMailAPI.Migrations
b.HasIndex("RecipientId"); b.HasIndex("RecipientId");
b.HasIndex("RecipientId1");
b.HasIndex("SentMailId"); b.HasIndex("SentMailId");
b.ToTable("ReceivedMails"); b.ToTable("ReceivedMails");
@@ -181,6 +176,9 @@ namespace FutureMailAPI.Migrations
.IsRequired() .IsRequired()
.HasColumnType("TEXT"); .HasColumnType("TEXT");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT");
b.Property<DateTime>("DeliveryTime") b.Property<DateTime>("DeliveryTime")
.HasColumnType("TEXT"); .HasColumnType("TEXT");
@@ -193,9 +191,6 @@ namespace FutureMailAPI.Migrations
b.Property<int?>("RecipientId") b.Property<int?>("RecipientId")
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");
b.Property<int?>("RecipientId1")
.HasColumnType("INTEGER");
b.Property<int>("RecipientType") b.Property<int>("RecipientType")
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");
@@ -229,8 +224,6 @@ namespace FutureMailAPI.Migrations
b.HasIndex("RecipientId"); b.HasIndex("RecipientId");
b.HasIndex("RecipientId1");
b.HasIndex("SenderId"); b.HasIndex("SenderId");
b.ToTable("SentMails"); b.ToTable("SentMails");
@@ -251,6 +244,9 @@ namespace FutureMailAPI.Migrations
.HasColumnType("TEXT") .HasColumnType("TEXT")
.HasDefaultValueSql("CURRENT_TIMESTAMP"); .HasDefaultValueSql("CURRENT_TIMESTAMP");
b.Property<double>("GlowIntensity")
.HasColumnType("REAL");
b.Property<double>("Opacity") b.Property<double>("Opacity")
.HasColumnType("REAL"); .HasColumnType("REAL");
@@ -278,6 +274,10 @@ namespace FutureMailAPI.Migrations
b.Property<int>("Status") b.Property<int>("Status")
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");
b.Property<string>("Style")
.HasMaxLength(50)
.HasColumnType("TEXT");
b.Property<int>("Type") b.Property<int>("Type")
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");
@@ -286,7 +286,8 @@ namespace FutureMailAPI.Migrations
b.HasKey("Id"); b.HasKey("Id");
b.HasIndex("SentMailId"); b.HasIndex("SentMailId")
.IsUnique();
b.HasIndex("SentMailId1"); b.HasIndex("SentMailId1");
@@ -387,15 +388,9 @@ namespace FutureMailAPI.Migrations
modelBuilder.Entity("FutureMailAPI.Models.ReceivedMail", b => modelBuilder.Entity("FutureMailAPI.Models.ReceivedMail", b =>
{ {
b.HasOne("FutureMailAPI.Models.User", null)
.WithMany()
.HasForeignKey("RecipientId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("FutureMailAPI.Models.User", "Recipient") b.HasOne("FutureMailAPI.Models.User", "Recipient")
.WithMany("ReceivedMails") .WithMany("ReceivedMails")
.HasForeignKey("RecipientId1") .HasForeignKey("RecipientId")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .IsRequired();
@@ -412,15 +407,11 @@ namespace FutureMailAPI.Migrations
modelBuilder.Entity("FutureMailAPI.Models.SentMail", b => modelBuilder.Entity("FutureMailAPI.Models.SentMail", b =>
{ {
b.HasOne("FutureMailAPI.Models.User", null) b.HasOne("FutureMailAPI.Models.User", "Recipient")
.WithMany() .WithMany()
.HasForeignKey("RecipientId") .HasForeignKey("RecipientId")
.OnDelete(DeleteBehavior.SetNull); .OnDelete(DeleteBehavior.SetNull);
b.HasOne("FutureMailAPI.Models.User", "Recipient")
.WithMany()
.HasForeignKey("RecipientId1");
b.HasOne("FutureMailAPI.Models.User", "Sender") b.HasOne("FutureMailAPI.Models.User", "Sender")
.WithMany("SentMails") .WithMany("SentMails")
.HasForeignKey("SenderId") .HasForeignKey("SenderId")
@@ -435,8 +426,8 @@ namespace FutureMailAPI.Migrations
modelBuilder.Entity("FutureMailAPI.Models.TimeCapsule", b => modelBuilder.Entity("FutureMailAPI.Models.TimeCapsule", b =>
{ {
b.HasOne("FutureMailAPI.Models.SentMail", null) b.HasOne("FutureMailAPI.Models.SentMail", null)
.WithMany() .WithOne("TimeCapsule")
.HasForeignKey("SentMailId") .HasForeignKey("FutureMailAPI.Models.TimeCapsule", "SentMailId")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .IsRequired();
@@ -462,6 +453,11 @@ namespace FutureMailAPI.Migrations
b.Navigation("Tokens"); b.Navigation("Tokens");
}); });
modelBuilder.Entity("FutureMailAPI.Models.SentMail", b =>
{
b.Navigation("TimeCapsule");
});
modelBuilder.Entity("FutureMailAPI.Models.User", b => modelBuilder.Entity("FutureMailAPI.Models.User", b =>
{ {
b.Navigation("ReceivedMails"); b.Navigation("ReceivedMails");

View File

@@ -28,6 +28,9 @@ namespace FutureMailAPI.Models
// 发送时间 // 发送时间
public DateTime SentAt { get; set; } = DateTime.UtcNow; public DateTime SentAt { get; set; } = DateTime.UtcNow;
// 创建时间
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
// 投递时间 // 投递时间
[Required] [Required]
public DateTime DeliveryTime { get; set; } public DateTime DeliveryTime { get; set; }
@@ -61,5 +64,7 @@ namespace FutureMailAPI.Models
public virtual User Sender { get; set; } = null!; public virtual User Sender { get; set; } = null!;
public virtual User? Recipient { get; set; } public virtual User? Recipient { get; set; }
public virtual TimeCapsule? TimeCapsule { get; set; }
} }
} }

View File

@@ -32,6 +32,13 @@ namespace FutureMailAPI.Models
// 胶囊旋转角度 // 胶囊旋转角度
public double Rotation { get; set; } = 0; public double Rotation { get; set; } = 0;
// 胶囊样式/皮肤
[MaxLength(50)]
public string? Style { get; set; }
// 发光强度
public double GlowIntensity { get; set; } = 0.8;
// 胶囊状态: 0-未激活, 1-漂浮中, 2-即将到达, 3-已开启 // 胶囊状态: 0-未激活, 1-漂浮中, 2-即将到达, 3-已开启
[Required] [Required]
public int Status { get; set; } = 0; public int Status { get; set; } = 0;

View File

@@ -17,5 +17,15 @@ namespace FutureMailAPI.Services
Task<ApiResponse<bool>> MarkReceivedMailAsReadAsync(int userId, int mailId); Task<ApiResponse<bool>> MarkReceivedMailAsReadAsync(int userId, int mailId);
Task<ApiResponse<bool>> MarkAsReadAsync(int userId, int mailId); Task<ApiResponse<bool>> MarkAsReadAsync(int userId, int mailId);
Task<ApiResponse<bool>> RevokeMailAsync(int userId, int mailId); Task<ApiResponse<bool>> RevokeMailAsync(int userId, int mailId);
// 存入胶囊相关方法
Task<ApiResponse<SaveToCapsuleResponseDto>> SaveToCapsuleAsync(int userId, SaveToCapsuleDto saveToCapsuleDto);
Task<ApiResponse<PagedResponse<CapsuleMailListResponseDto>>> GetCapsuleMailsAsync(int userId, MailListQueryDto queryDto);
Task<ApiResponse<CapsuleMailDetailResponseDto>> GetCapsuleMailByIdAsync(int userId, int mailId);
Task<ApiResponse<CapsuleMailDetailResponseDto>> UpdateCapsuleMailAsync(int userId, int mailId, UpdateCapsuleMailDto updateDto);
Task<ApiResponse<bool>> RevokeCapsuleMailAsync(int userId, int mailId);
// 发送至未来功能
Task<ApiResponse<SendToFutureResponseDto>> SendToFutureAsync(int userId, SendToFutureDto sendToFutureDto);
} }
} }

View File

@@ -2,6 +2,7 @@ using Microsoft.EntityFrameworkCore;
using FutureMailAPI.Data; using FutureMailAPI.Data;
using FutureMailAPI.Models; using FutureMailAPI.Models;
using FutureMailAPI.DTOs; using FutureMailAPI.DTOs;
using System.Text.Json;
namespace FutureMailAPI.Services namespace FutureMailAPI.Services
{ {
@@ -382,6 +383,393 @@ namespace FutureMailAPI.Services
return ApiResponse<bool>.SuccessResult(true, "邮件已撤销"); return ApiResponse<bool>.SuccessResult(true, "邮件已撤销");
} }
// 存入胶囊相关方法实现
public async Task<ApiResponse<SaveToCapsuleResponseDto>> SaveToCapsuleAsync(int userId, SaveToCapsuleDto saveToCapsuleDto)
{
// 验证收件人类型
if (saveToCapsuleDto.RecipientType == RecipientTypeEnum.SPECIFIC && string.IsNullOrEmpty(saveToCapsuleDto.RecipientEmail))
{
return ApiResponse<SaveToCapsuleResponseDto>.ErrorResult("指定收件人时,收件人邮箱是必填项");
}
// 验证发送时间
if (saveToCapsuleDto.SendTime.HasValue && saveToCapsuleDto.SendTime.Value <= DateTime.UtcNow)
{
return ApiResponse<SaveToCapsuleResponseDto>.ErrorResult("发送时间必须是未来时间");
}
// 创建邮件
var mail = new SentMail
{
Title = saveToCapsuleDto.Title,
Content = saveToCapsuleDto.Content,
SenderId = userId,
RecipientType = (int)saveToCapsuleDto.RecipientType,
SentAt = DateTime.UtcNow,
CreatedAt = DateTime.UtcNow,
DeliveryTime = saveToCapsuleDto.SendTime ?? DateTime.UtcNow.AddDays(1), // 默认一天后发送
Status = 0, // 草稿状态
TriggerType = (int)saveToCapsuleDto.TriggerType,
TriggerDetails = saveToCapsuleDto.TriggerCondition != null ? JsonSerializer.Serialize(saveToCapsuleDto.TriggerCondition) : null,
Attachments = saveToCapsuleDto.Attachments != null ? JsonSerializer.Serialize(saveToCapsuleDto.Attachments) : null,
IsEncrypted = saveToCapsuleDto.IsEncrypted,
Theme = saveToCapsuleDto.CapsuleStyle
};
// 如果是指定收件人查找收件人ID
if (saveToCapsuleDto.RecipientType == RecipientTypeEnum.SPECIFIC && !string.IsNullOrEmpty(saveToCapsuleDto.RecipientEmail))
{
var recipient = await _context.Users
.FirstOrDefaultAsync(u => u.Email == saveToCapsuleDto.RecipientEmail);
if (recipient == null)
{
return ApiResponse<SaveToCapsuleResponseDto>.ErrorResult("收件人不存在");
}
mail.RecipientId = recipient.Id;
}
_context.SentMails.Add(mail);
await _context.SaveChangesAsync();
// 创建时间胶囊
var timeCapsule = new TimeCapsule
{
UserId = userId,
SentMailId = mail.Id,
Status = 0, // 草稿状态
CreatedAt = DateTime.UtcNow,
PositionX = 0.5f, // 默认位置
PositionY = 0.5f,
PositionZ = 0.5f,
Style = saveToCapsuleDto.CapsuleStyle,
GlowIntensity = 0.8f // 默认发光强度
};
_context.TimeCapsules.Add(timeCapsule);
await _context.SaveChangesAsync();
var response = new SaveToCapsuleResponseDto
{
Id = mail.Id,
MailId = mail.Id.ToString(),
CapsuleId = timeCapsule.Id.ToString(),
Status = "DRAFT",
CreatedAt = mail.CreatedAt
};
return ApiResponse<SaveToCapsuleResponseDto>.SuccessResult(response, "邮件已存入胶囊");
}
public async Task<ApiResponse<PagedResponse<CapsuleMailListResponseDto>>> GetCapsuleMailsAsync(int userId, MailListQueryDto queryDto)
{
var query = _context.SentMails
.Where(m => m.SenderId == userId && m.Status == 0) // 只查询草稿状态的邮件
.Include(m => m.Sender)
.Include(m => m.Recipient)
.Include(m => m.TimeCapsule)
.AsQueryable();
// 应用筛选条件
if (queryDto.Status.HasValue)
{
query = query.Where(m => m.Status == queryDto.Status.Value);
}
if (queryDto.RecipientType.HasValue)
{
query = query.Where(m => m.RecipientType == queryDto.RecipientType.Value);
}
if (!string.IsNullOrEmpty(queryDto.Keyword))
{
query = query.Where(m => m.Title.Contains(queryDto.Keyword) || m.Content.Contains(queryDto.Keyword));
}
if (queryDto.StartDate.HasValue)
{
query = query.Where(m => m.SentAt >= queryDto.StartDate.Value);
}
if (queryDto.EndDate.HasValue)
{
query = query.Where(m => m.SentAt <= queryDto.EndDate.Value);
}
// 排序
query = query.OrderByDescending(m => m.SentAt);
// 分页
var totalCount = await query.CountAsync();
var mails = await query
.Skip((queryDto.PageIndex - 1) * queryDto.PageSize)
.Take(queryDto.PageSize)
.ToListAsync();
var mailDtos = mails.Select(MapToCapsuleMailListResponseDto).ToList();
var pagedResponse = new PagedResponse<CapsuleMailListResponseDto>(
mailDtos, queryDto.PageIndex, queryDto.PageSize, totalCount);
return ApiResponse<PagedResponse<CapsuleMailListResponseDto>>.SuccessResult(pagedResponse);
}
public async Task<ApiResponse<CapsuleMailDetailResponseDto>> GetCapsuleMailByIdAsync(int userId, int mailId)
{
var mail = await _context.SentMails
.Include(m => m.Sender)
.Include(m => m.Recipient)
.Include(m => m.TimeCapsule)
.FirstOrDefaultAsync(m => m.Id == mailId && m.SenderId == userId);
if (mail == null)
{
return ApiResponse<CapsuleMailDetailResponseDto>.ErrorResult("邮件不存在");
}
var mailDto = MapToCapsuleMailDetailResponseDto(mail);
return ApiResponse<CapsuleMailDetailResponseDto>.SuccessResult(mailDto);
}
public async Task<ApiResponse<CapsuleMailDetailResponseDto>> UpdateCapsuleMailAsync(int userId, int mailId, UpdateCapsuleMailDto updateDto)
{
var mail = await _context.SentMails
.Include(m => m.Sender)
.Include(m => m.Recipient)
.Include(m => m.TimeCapsule)
.FirstOrDefaultAsync(m => m.Id == mailId && m.SenderId == userId);
if (mail == null)
{
return ApiResponse<CapsuleMailDetailResponseDto>.ErrorResult("邮件不存在");
}
// 检查邮件状态,只有草稿状态的邮件才能编辑
if (mail.Status != 0)
{
return ApiResponse<CapsuleMailDetailResponseDto>.ErrorResult("只有草稿状态的邮件才能编辑");
}
// 更新邮件信息
if (!string.IsNullOrEmpty(updateDto.Title))
{
mail.Title = updateDto.Title;
}
if (!string.IsNullOrEmpty(updateDto.Content))
{
mail.Content = updateDto.Content;
}
if (updateDto.RecipientType.HasValue)
{
mail.RecipientType = (int)updateDto.RecipientType.Value;
// 如果是指定收件人查找收件人ID
if (updateDto.RecipientType.Value == RecipientTypeEnum.SPECIFIC && !string.IsNullOrEmpty(updateDto.RecipientEmail))
{
var recipient = await _context.Users
.FirstOrDefaultAsync(u => u.Email == updateDto.RecipientEmail);
if (recipient == null)
{
return ApiResponse<CapsuleMailDetailResponseDto>.ErrorResult("收件人不存在");
}
mail.RecipientId = recipient.Id;
}
}
if (updateDto.SendTime.HasValue)
{
if (updateDto.SendTime.Value <= DateTime.UtcNow)
{
return ApiResponse<CapsuleMailDetailResponseDto>.ErrorResult("发送时间必须是未来时间");
}
mail.DeliveryTime = updateDto.SendTime.Value;
}
if (updateDto.TriggerType.HasValue)
{
mail.TriggerType = (int)updateDto.TriggerType.Value;
}
if (updateDto.TriggerCondition != null)
{
mail.TriggerDetails = JsonSerializer.Serialize(updateDto.TriggerCondition);
}
if (updateDto.Attachments != null)
{
mail.Attachments = JsonSerializer.Serialize(updateDto.Attachments);
}
if (updateDto.IsEncrypted.HasValue)
{
mail.IsEncrypted = updateDto.IsEncrypted.Value;
}
if (!string.IsNullOrEmpty(updateDto.CapsuleStyle))
{
mail.Theme = updateDto.CapsuleStyle;
// 更新时间胶囊样式
if (mail.TimeCapsule != null)
{
mail.TimeCapsule.Style = updateDto.CapsuleStyle;
}
}
await _context.SaveChangesAsync();
var mailResponse = MapToCapsuleMailDetailResponseDto(mail);
return ApiResponse<CapsuleMailDetailResponseDto>.SuccessResult(mailResponse, "胶囊邮件更新成功");
}
public async Task<ApiResponse<bool>> RevokeCapsuleMailAsync(int userId, int mailId)
{
var mail = await _context.SentMails
.Include(m => m.TimeCapsule)
.FirstOrDefaultAsync(m => m.Id == mailId && m.SenderId == userId);
if (mail == null)
{
return ApiResponse<bool>.ErrorResult("邮件不存在");
}
// 检查邮件状态,只有草稿或待投递状态的邮件才能撤销
if (mail.Status > 1)
{
return ApiResponse<bool>.ErrorResult("已投递的邮件不能撤销");
}
// 更新邮件状态为已撤销
mail.Status = 4; // 4-已撤销
await _context.SaveChangesAsync();
// 更新相关的时间胶囊状态
if (mail.TimeCapsule != null)
{
mail.TimeCapsule.Status = 3; // 3-已撤销
await _context.SaveChangesAsync();
}
return ApiResponse<bool>.SuccessResult(true, "胶囊邮件已撤销");
}
private static CapsuleMailListResponseDto MapToCapsuleMailListResponseDto(SentMail mail)
{
return new CapsuleMailListResponseDto
{
MailId = mail.Id.ToString(),
Title = mail.Title,
Sender = MapToUserInfoDto(mail.Sender),
Recipient = mail.Recipient != null ? MapToUserInfoDto(mail.Recipient) : new UserInfoDto(),
SendTime = mail.SentAt,
DeliveryTime = mail.DeliveryTime,
Status = mail.Status switch
{
0 => "DRAFT",
1 => "PENDING",
2 => "DELIVERING",
3 => "DELIVERED",
_ => "UNKNOWN"
},
HasAttachments = !string.IsNullOrEmpty(mail.Attachments),
IsEncrypted = mail.IsEncrypted,
CapsuleStyle = mail.Theme ?? "default",
Countdown = mail.Status == 1 ? (int)(mail.DeliveryTime - DateTime.UtcNow).TotalSeconds : null
};
}
private static CapsuleMailDetailResponseDto MapToCapsuleMailDetailResponseDto(SentMail mail)
{
List<AttachmentDto> attachments = new List<AttachmentDto>();
if (!string.IsNullOrEmpty(mail.Attachments))
{
try
{
var attachmentList = JsonSerializer.Deserialize<List<object>>(mail.Attachments);
if (attachmentList != null)
{
attachments = attachmentList.Select(a => new AttachmentDto
{
Id = Guid.NewGuid().ToString(),
Type = "IMAGE", // 默认类型,实际应根据数据解析
Url = a.ToString() ?? "",
Size = 0 // 默认大小,实际应根据数据解析
}).ToList();
}
}
catch
{
// 解析失败时忽略附件
}
}
object? triggerCondition = null;
if (!string.IsNullOrEmpty(mail.TriggerDetails))
{
try
{
triggerCondition = JsonSerializer.Deserialize<object>(mail.TriggerDetails);
}
catch
{
// 解析失败时忽略触发条件
}
}
return new CapsuleMailDetailResponseDto
{
MailId = mail.Id.ToString(),
Title = mail.Title,
Content = mail.Content,
Sender = MapToUserInfoDto(mail.Sender),
Recipient = mail.Recipient != null ? MapToUserInfoDto(mail.Recipient) : new UserInfoDto(),
SendTime = mail.SentAt,
CreatedAt = mail.CreatedAt,
DeliveryTime = mail.DeliveryTime,
Status = mail.Status switch
{
0 => "DRAFT",
1 => "PENDING",
2 => "DELIVERING",
3 => "DELIVERED",
_ => "UNKNOWN"
},
TriggerType = mail.TriggerType switch
{
0 => "TIME",
1 => "LOCATION",
2 => "EVENT",
_ => "UNKNOWN"
},
TriggerCondition = triggerCondition,
Attachments = attachments,
IsEncrypted = mail.IsEncrypted,
CapsuleStyle = mail.Theme ?? "default",
CanEdit = mail.Status == 0, // 只有草稿状态可编辑
CanRevoke = mail.Status <= 1 // 草稿或待投递状态可撤销
};
}
private static UserInfoDto MapToUserInfoDto(User user)
{
return new UserInfoDto
{
UserId = user.Id,
Username = user.Username,
Avatar = user.Avatar ?? "",
Email = user.Email
};
}
private async Task<SentMailResponseDto> GetSentMailWithDetailsAsync(int mailId) private async Task<SentMailResponseDto> GetSentMailWithDetailsAsync(int mailId)
{ {
var mail = await _context.SentMails var mail = await _context.SentMails
@@ -471,5 +859,70 @@ namespace FutureMailAPI.Services
_ => "未知" _ => "未知"
}; };
} }
/// <summary>
/// 发送至未来 - 将草稿状态的邮件设置为在未来特定时间自动发送
/// </summary>
/// <param name="userId">用户ID</param>
/// <param name="sendToFutureDto">发送至未来请求DTO</param>
/// <returns>发送至未来响应DTO</returns>
public async Task<ApiResponse<SendToFutureResponseDto>> SendToFutureAsync(int userId, SendToFutureDto sendToFutureDto)
{
// 检查投递时间是否在未来
if (sendToFutureDto.DeliveryTime <= DateTime.UtcNow)
{
return ApiResponse<SendToFutureResponseDto>.ErrorResult("投递时间必须是未来时间");
}
// 查找邮件
var mail = await _context.SentMails
.Include(m => m.Sender)
.FirstOrDefaultAsync(m => m.Id == sendToFutureDto.MailId && m.SenderId == userId);
if (mail == null)
{
return ApiResponse<SendToFutureResponseDto>.ErrorResult("邮件不存在");
}
// 检查邮件是否为草稿状态
if (mail.Status != 0)
{
return ApiResponse<SendToFutureResponseDto>.ErrorResult("只有草稿状态的邮件可以设置为发送至未来");
}
// 更新邮件状态和投递时间
mail.DeliveryTime = sendToFutureDto.DeliveryTime;
mail.Status = 1; // 设置为已发送(待投递)状态
mail.SentAt = DateTime.UtcNow; // 设置发送时间为当前时间
await _context.SaveChangesAsync();
// 创建时间胶囊
var timeCapsule = new TimeCapsule
{
UserId = userId,
SentMailId = mail.Id,
PositionX = 0,
PositionY = 0,
PositionZ = 0,
Status = 1, // 漂浮中
Type = 0 // 普通
};
_context.TimeCapsules.Add(timeCapsule);
await _context.SaveChangesAsync();
// 构建响应
var response = new SendToFutureResponseDto
{
MailId = mail.Id,
Title = mail.Title,
DeliveryTime = mail.DeliveryTime,
Status = GetStatusText(mail.Status),
SentAt = mail.SentAt
};
return ApiResponse<SendToFutureResponseDto>.SuccessResult(response, "邮件已设置为发送至未来");
}
} }
} }

View File

@@ -83,6 +83,55 @@
<param name="fileId">文件ID</param> <param name="fileId">文件ID</param>
<returns>文件信息</returns> <returns>文件信息</returns>
</member> </member>
<member name="M:FutureMailAPI.Controllers.MailsController.SaveToCapsule(FutureMailAPI.DTOs.SaveToCapsuleDto)">
<summary>
存入胶囊 - 创建胶囊邮件
</summary>
<param name="dto">存入胶囊请求</param>
<returns>操作结果</returns>
</member>
<member name="M:FutureMailAPI.Controllers.MailsController.GetCapsuleMails(System.Int32,System.Int32,System.Nullable{System.Int32},System.Nullable{System.Int32},System.String,System.Nullable{System.DateTime},System.Nullable{System.DateTime})">
<summary>
获取胶囊邮件列表
</summary>
<param name="pageIndex">页码</param>
<param name="pageSize">页大小</param>
<param name="status">状态筛选</param>
<param name="recipientType">收件人类型筛选</param>
<param name="keyword">关键词搜索</param>
<param name="startDate">开始日期</param>
<param name="endDate">结束日期</param>
<returns>胶囊邮件列表</returns>
</member>
<member name="M:FutureMailAPI.Controllers.MailsController.GetCapsuleMail(System.Int32)">
<summary>
获取胶囊邮件详情
</summary>
<param name="id">邮件ID</param>
<returns>胶囊邮件详情</returns>
</member>
<member name="M:FutureMailAPI.Controllers.MailsController.UpdateCapsuleMail(System.Int32,FutureMailAPI.DTOs.UpdateCapsuleMailDto)">
<summary>
更新胶囊邮件
</summary>
<param name="id">邮件ID</param>
<param name="dto">更新请求</param>
<returns>更新后的胶囊邮件详情</returns>
</member>
<member name="M:FutureMailAPI.Controllers.MailsController.RevokeCapsuleMail(System.Int32)">
<summary>
撤销胶囊邮件
</summary>
<param name="id">邮件ID</param>
<returns>操作结果</returns>
</member>
<member name="M:FutureMailAPI.Controllers.MailsController.SendToFuture(FutureMailAPI.DTOs.SendToFutureDto)">
<summary>
发送至未来 - 将草稿状态的邮件设置为在未来特定时间自动发送
</summary>
<param name="sendToFutureDto">发送至未来请求DTO</param>
<returns>发送至未来响应DTO</returns>
</member>
<member name="M:FutureMailAPI.Controllers.NotificationController.RegisterDevice(FutureMailAPI.DTOs.NotificationDeviceRequestDto)"> <member name="M:FutureMailAPI.Controllers.NotificationController.RegisterDevice(FutureMailAPI.DTOs.NotificationDeviceRequestDto)">
<summary> <summary>
注册设备 注册设备
@@ -207,5 +256,37 @@
<member name="M:FutureMailAPI.Migrations.AddSaltToUser.Down(Microsoft.EntityFrameworkCore.Migrations.MigrationBuilder)"> <member name="M:FutureMailAPI.Migrations.AddSaltToUser.Down(Microsoft.EntityFrameworkCore.Migrations.MigrationBuilder)">
<inheritdoc /> <inheritdoc />
</member> </member>
<member name="T:FutureMailAPI.Migrations.AddSentMailCreatedAt">
<inheritdoc />
</member>
<member name="M:FutureMailAPI.Migrations.AddSentMailCreatedAt.Up(Microsoft.EntityFrameworkCore.Migrations.MigrationBuilder)">
<inheritdoc />
</member>
<member name="M:FutureMailAPI.Migrations.AddSentMailCreatedAt.Down(Microsoft.EntityFrameworkCore.Migrations.MigrationBuilder)">
<inheritdoc />
</member>
<member name="M:FutureMailAPI.Migrations.AddSentMailCreatedAt.BuildTargetModel(Microsoft.EntityFrameworkCore.ModelBuilder)">
<inheritdoc />
</member>
<member name="T:FutureMailAPI.Migrations.FixDuplicateForeignKeys">
<inheritdoc />
</member>
<member name="M:FutureMailAPI.Migrations.FixDuplicateForeignKeys.Up(Microsoft.EntityFrameworkCore.Migrations.MigrationBuilder)">
<inheritdoc />
</member>
<member name="M:FutureMailAPI.Migrations.FixDuplicateForeignKeys.Down(Microsoft.EntityFrameworkCore.Migrations.MigrationBuilder)">
<inheritdoc />
</member>
<member name="M:FutureMailAPI.Migrations.FixDuplicateForeignKeys.BuildTargetModel(Microsoft.EntityFrameworkCore.ModelBuilder)">
<inheritdoc />
</member>
<member name="M:FutureMailAPI.Services.MailService.SendToFutureAsync(System.Int32,FutureMailAPI.DTOs.SendToFutureDto)">
<summary>
发送至未来 - 将草稿状态的邮件设置为在未来特定时间自动发送
</summary>
<param name="userId">用户ID</param>
<param name="sendToFutureDto">发送至未来请求DTO</param>
<returns>发送至未来响应DTO</returns>
</member>
</members> </members>
</doc> </doc>

View File

@@ -1 +1 @@
51372bde626c0ba3aa5386f92d0ea465adcc4b558852e21737182a326708e608 96b95197304e11878aba7d6a0ad52cc38a6b9883e011012e3da5e389d5e93831

View File

@@ -83,6 +83,55 @@
<param name="fileId">文件ID</param> <param name="fileId">文件ID</param>
<returns>文件信息</returns> <returns>文件信息</returns>
</member> </member>
<member name="M:FutureMailAPI.Controllers.MailsController.SaveToCapsule(FutureMailAPI.DTOs.SaveToCapsuleDto)">
<summary>
存入胶囊 - 创建胶囊邮件
</summary>
<param name="dto">存入胶囊请求</param>
<returns>操作结果</returns>
</member>
<member name="M:FutureMailAPI.Controllers.MailsController.GetCapsuleMails(System.Int32,System.Int32,System.Nullable{System.Int32},System.Nullable{System.Int32},System.String,System.Nullable{System.DateTime},System.Nullable{System.DateTime})">
<summary>
获取胶囊邮件列表
</summary>
<param name="pageIndex">页码</param>
<param name="pageSize">页大小</param>
<param name="status">状态筛选</param>
<param name="recipientType">收件人类型筛选</param>
<param name="keyword">关键词搜索</param>
<param name="startDate">开始日期</param>
<param name="endDate">结束日期</param>
<returns>胶囊邮件列表</returns>
</member>
<member name="M:FutureMailAPI.Controllers.MailsController.GetCapsuleMail(System.Int32)">
<summary>
获取胶囊邮件详情
</summary>
<param name="id">邮件ID</param>
<returns>胶囊邮件详情</returns>
</member>
<member name="M:FutureMailAPI.Controllers.MailsController.UpdateCapsuleMail(System.Int32,FutureMailAPI.DTOs.UpdateCapsuleMailDto)">
<summary>
更新胶囊邮件
</summary>
<param name="id">邮件ID</param>
<param name="dto">更新请求</param>
<returns>更新后的胶囊邮件详情</returns>
</member>
<member name="M:FutureMailAPI.Controllers.MailsController.RevokeCapsuleMail(System.Int32)">
<summary>
撤销胶囊邮件
</summary>
<param name="id">邮件ID</param>
<returns>操作结果</returns>
</member>
<member name="M:FutureMailAPI.Controllers.MailsController.SendToFuture(FutureMailAPI.DTOs.SendToFutureDto)">
<summary>
发送至未来 - 将草稿状态的邮件设置为在未来特定时间自动发送
</summary>
<param name="sendToFutureDto">发送至未来请求DTO</param>
<returns>发送至未来响应DTO</returns>
</member>
<member name="M:FutureMailAPI.Controllers.NotificationController.RegisterDevice(FutureMailAPI.DTOs.NotificationDeviceRequestDto)"> <member name="M:FutureMailAPI.Controllers.NotificationController.RegisterDevice(FutureMailAPI.DTOs.NotificationDeviceRequestDto)">
<summary> <summary>
注册设备 注册设备
@@ -207,5 +256,37 @@
<member name="M:FutureMailAPI.Migrations.AddSaltToUser.Down(Microsoft.EntityFrameworkCore.Migrations.MigrationBuilder)"> <member name="M:FutureMailAPI.Migrations.AddSaltToUser.Down(Microsoft.EntityFrameworkCore.Migrations.MigrationBuilder)">
<inheritdoc /> <inheritdoc />
</member> </member>
<member name="T:FutureMailAPI.Migrations.AddSentMailCreatedAt">
<inheritdoc />
</member>
<member name="M:FutureMailAPI.Migrations.AddSentMailCreatedAt.Up(Microsoft.EntityFrameworkCore.Migrations.MigrationBuilder)">
<inheritdoc />
</member>
<member name="M:FutureMailAPI.Migrations.AddSentMailCreatedAt.Down(Microsoft.EntityFrameworkCore.Migrations.MigrationBuilder)">
<inheritdoc />
</member>
<member name="M:FutureMailAPI.Migrations.AddSentMailCreatedAt.BuildTargetModel(Microsoft.EntityFrameworkCore.ModelBuilder)">
<inheritdoc />
</member>
<member name="T:FutureMailAPI.Migrations.FixDuplicateForeignKeys">
<inheritdoc />
</member>
<member name="M:FutureMailAPI.Migrations.FixDuplicateForeignKeys.Up(Microsoft.EntityFrameworkCore.Migrations.MigrationBuilder)">
<inheritdoc />
</member>
<member name="M:FutureMailAPI.Migrations.FixDuplicateForeignKeys.Down(Microsoft.EntityFrameworkCore.Migrations.MigrationBuilder)">
<inheritdoc />
</member>
<member name="M:FutureMailAPI.Migrations.FixDuplicateForeignKeys.BuildTargetModel(Microsoft.EntityFrameworkCore.ModelBuilder)">
<inheritdoc />
</member>
<member name="M:FutureMailAPI.Services.MailService.SendToFutureAsync(System.Int32,FutureMailAPI.DTOs.SendToFutureDto)">
<summary>
发送至未来 - 将草稿状态的邮件设置为在未来特定时间自动发送
</summary>
<param name="userId">用户ID</param>
<param name="sendToFutureDto">发送至未来请求DTO</param>
<returns>发送至未来响应DTO</returns>
</member>
</members> </members>
</doc> </doc>

File diff suppressed because it is too large Load Diff

View File

@@ -1 +1 @@
{"GlobalPropertiesHash":"1nyXR9zdL54Badakr4zt6ZsTCwUunwdqRSmf7XLLUwI=","FingerprintPatternsHash":"gq3WsqcKBUGTSNle7RKKyXRIwh7M8ccEqOqYvIzoM04=","PropertyOverridesHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","InputHashes":["PSBb4S8lcZQPImBE8id7O4eeN8h3whFn6j1jGYFQciQ=","FLPLXKwQVK16UZsWKzZrjH5kq4sMEtCZqAIkpUwa2pU=","qvO3Mvo7bXJuih7HQYVXFfIR0uMf6fdipVLWW\u002BbFgXY=","XAE6ulqLzJRH50\u002Bc9Nteizd/x9s3rvSHUFwFL265XX4=","talZRfyIQIv4aZc27HTn01\u002B12VbY6LMFOy3\u002B3r448jo=","kyazWY\u002B8n0dtdadKg35ovAVMceUhOAzyYnxEaTobs08="],"CachedAssets":{},"CachedCopyCandidates":{}} {"GlobalPropertiesHash":"1nyXR9zdL54Badakr4zt6ZsTCwUunwdqRSmf7XLLUwI=","FingerprintPatternsHash":"gq3WsqcKBUGTSNle7RKKyXRIwh7M8ccEqOqYvIzoM04=","PropertyOverridesHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","InputHashes":["PSBb4S8lcZQPImBE8id7O4eeN8h3whFn6j1jGYFQciQ=","FLPLXKwQVK16UZsWKzZrjH5kq4sMEtCZqAIkpUwa2pU=","ed7if\u002BhRbLX5eYApJ6Bg4oF5be3kjR0VKVKgf\u002BgmyVo=","XAE6ulqLzJRH50\u002Bc9Nteizd/x9s3rvSHUFwFL265XX4=","talZRfyIQIv4aZc27HTn01\u002B12VbY6LMFOy3\u002B3r448jo=","R7hxHyYox4qeYIBY\u002Bo3l0eo8ZIIfGZeN7PS3V61Q7Ks="],"CachedAssets":{},"CachedCopyCandidates":{}}

View File

@@ -1 +1 @@
{"GlobalPropertiesHash":"hRzLFfhtQD0jC2zNthCXf5A5W0LGZjQdHMs0v5Enof8=","FingerprintPatternsHash":"gq3WsqcKBUGTSNle7RKKyXRIwh7M8ccEqOqYvIzoM04=","PropertyOverridesHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","InputHashes":["PSBb4S8lcZQPImBE8id7O4eeN8h3whFn6j1jGYFQciQ=","FLPLXKwQVK16UZsWKzZrjH5kq4sMEtCZqAIkpUwa2pU=","qvO3Mvo7bXJuih7HQYVXFfIR0uMf6fdipVLWW\u002BbFgXY=","XAE6ulqLzJRH50\u002Bc9Nteizd/x9s3rvSHUFwFL265XX4=","talZRfyIQIv4aZc27HTn01\u002B12VbY6LMFOy3\u002B3r448jo=","kyazWY\u002B8n0dtdadKg35ovAVMceUhOAzyYnxEaTobs08="],"CachedAssets":{},"CachedCopyCandidates":{}} {"GlobalPropertiesHash":"hRzLFfhtQD0jC2zNthCXf5A5W0LGZjQdHMs0v5Enof8=","FingerprintPatternsHash":"gq3WsqcKBUGTSNle7RKKyXRIwh7M8ccEqOqYvIzoM04=","PropertyOverridesHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","InputHashes":["PSBb4S8lcZQPImBE8id7O4eeN8h3whFn6j1jGYFQciQ=","FLPLXKwQVK16UZsWKzZrjH5kq4sMEtCZqAIkpUwa2pU=","ed7if\u002BhRbLX5eYApJ6Bg4oF5be3kjR0VKVKgf\u002BgmyVo=","XAE6ulqLzJRH50\u002Bc9Nteizd/x9s3rvSHUFwFL265XX4=","talZRfyIQIv4aZc27HTn01\u002B12VbY6LMFOy3\u002B3r448jo=","R7hxHyYox4qeYIBY\u002Bo3l0eo8ZIIfGZeN7PS3V61Q7Ks="],"CachedAssets":{},"CachedCopyCandidates":{}}

307
simple_test.html Normal file
View File

@@ -0,0 +1,307 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>API测试</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
.section {
margin-bottom: 30px;
padding: 15px;
border: 1px solid #ddd;
border-radius: 5px;
}
.form-group {
margin-bottom: 15px;
}
label {
display: block;
margin-bottom: 5px;
font-weight: bold;
}
input, textarea, select, button {
width: 100%;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
box-sizing: border-box;
}
button {
background-color: #4CAF50;
color: white;
border: none;
cursor: pointer;
margin-top: 10px;
}
button:hover {
background-color: #45a049;
}
.result {
margin-top: 15px;
padding: 10px;
background-color: #f9f9f9;
border-radius: 4px;
white-space: pre-wrap;
font-family: monospace;
}
.error {
background-color: #ffebee;
color: #c62828;
}
.success {
background-color: #e8f5e9;
color: #2e7d32;
}
</style>
</head>
<body>
<h1>API测试</h1>
<div class="section">
<h2>用户注册</h2>
<div class="form-group">
<label for="username">用户名:</label>
<input type="text" id="username" value="testuser123">
</div>
<div class="form-group">
<label for="email">邮箱:</label>
<input type="email" id="email" value="test123@example.com">
</div>
<div class="form-group">
<label for="password">密码:</label>
<input type="password" id="password" value="password123">
</div>
<button onclick="register()">注册</button>
<div id="registerResult" class="result"></div>
</div>
<div class="section">
<h2>用户登录</h2>
<div class="form-group">
<label for="loginUsername">用户名或邮箱:</label>
<input type="text" id="loginUsername" value="testuser123">
</div>
<div class="form-group">
<label for="loginPassword">密码:</label>
<input type="password" id="loginPassword" value="password123">
</div>
<button onclick="login()">登录</button>
<div id="loginResult" class="result"></div>
</div>
<div class="section">
<h2>创建邮件</h2>
<div class="form-group">
<label for="mailTitle">标题:</label>
<input type="text" id="mailTitle" value="测试邮件">
</div>
<div class="form-group">
<label for="mailContent">内容:</label>
<textarea id="mailContent" rows="4">这是一封测试邮件内容</textarea>
</div>
<div class="form-group">
<label for="recipientType">收件人类型:</label>
<select id="recipientType">
<option value="SELF">自己</option>
<option value="SPECIFIC">指定用户</option>
<option value="PUBLIC">公开时间胶囊</option>
</select>
</div>
<div class="form-group">
<label for="recipientEmail">收件人邮箱 (如果是指定用户):</label>
<input type="email" id="recipientEmail">
</div>
<div class="form-group">
<label for="sendTime">发送时间:</label>
<input type="datetime-local" id="sendTime">
</div>
<div class="form-group">
<label for="triggerType">触发类型:</label>
<select id="triggerType">
<option value="TIME">时间</option>
<option value="LOCATION">地点</option>
<option value="EVENT">事件</option>
</select>
</div>
<div class="form-group">
<label for="capsuleStyle">胶囊样式:</label>
<input type="text" id="capsuleStyle" value="default">
</div>
<button onclick="createMail()">创建邮件</button>
<div id="createMailResult" class="result"></div>
</div>
<div class="section">
<h2>获取邮件列表</h2>
<div class="form-group">
<label for="mailType">邮件类型:</label>
<select id="mailType">
<option value="SENT">已发送</option>
<option value="DRAFT">草稿</option>
</select>
</div>
<button onclick="getMails()">获取邮件列表</button>
<div id="getMailsResult" class="result"></div>
</div>
<script>
const API_BASE_URL = 'http://localhost:5003/api/v1';
let authToken = '';
// 设置默认发送时间为明天
document.getElementById('sendTime').value = new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString().slice(0, 16);
// 显示结果
function showResult(elementId, data, isError = false) {
const element = document.getElementById(elementId);
element.textContent = typeof data === 'object' ? JSON.stringify(data, null, 2) : data;
element.className = 'result ' + (isError ? 'error' : 'success');
}
// 注册
async function register() {
const username = document.getElementById('username').value;
const email = document.getElementById('email').value;
const password = document.getElementById('password').value;
try {
const response = await fetch(`${API_BASE_URL}/auth/register`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
username,
email,
password
})
});
const data = await response.json();
if (response.ok) {
authToken = data.data.token;
showResult('registerResult', data);
} else {
showResult('registerResult', data, true);
}
} catch (error) {
showResult('registerResult', '错误: ' + error.message, true);
}
}
// 登录
async function login() {
const usernameOrEmail = document.getElementById('loginUsername').value;
const password = document.getElementById('loginPassword').value;
try {
const response = await fetch(`${API_BASE_URL}/auth/login`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
usernameOrEmail,
password
})
});
const data = await response.json();
if (response.ok) {
authToken = data.data.token;
showResult('loginResult', data);
} else {
showResult('loginResult', data, true);
}
} catch (error) {
showResult('loginResult', '错误: ' + error.message, true);
}
}
// 创建邮件
async function createMail() {
if (!authToken) {
showResult('createMailResult', '请先登录', true);
return;
}
const title = document.getElementById('mailTitle').value;
const content = document.getElementById('mailContent').value;
const recipientType = document.getElementById('recipientType').value;
const recipientEmail = document.getElementById('recipientEmail').value;
const sendTime = document.getElementById('sendTime').value;
const triggerType = document.getElementById('triggerType').value;
const capsuleStyle = document.getElementById('capsuleStyle').value;
try {
const response = await fetch(`${API_BASE_URL}/mails/create`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${authToken}`
},
body: JSON.stringify({
title,
content,
recipientType,
recipientEmail,
sendTime: new Date(sendTime).toISOString(),
triggerType,
triggerCondition: {},
attachments: [],
isEncrypted: false,
capsuleStyle
})
});
const data = await response.json();
if (response.ok) {
showResult('createMailResult', data);
} else {
showResult('createMailResult', data, true);
}
} catch (error) {
showResult('createMailResult', '错误: ' + error.message, true);
}
}
// 获取邮件列表
async function getMails() {
if (!authToken) {
showResult('getMailsResult', '请先登录', true);
return;
}
const mailType = document.getElementById('mailType').value;
try {
const response = await fetch(`${API_BASE_URL}/mails?type=${mailType}&page=1&size=20`, {
method: 'GET',
headers: {
'Authorization': `Bearer ${authToken}`
}
});
const data = await response.json();
if (response.ok) {
showResult('getMailsResult', data);
} else {
showResult('getMailsResult', data, true);
}
} catch (error) {
showResult('getMailsResult', '错误: ' + error.message, true);
}
}
</script>
</body>
</html>

53
test_api.js Normal file
View File

@@ -0,0 +1,53 @@
const axios = require('axios');
const API_BASE_URL = 'http://localhost:5003/api/v1';
async function testAPI() {
try {
// 1. 注册用户
console.log('1. 注册用户...');
const registerResponse = await axios.post(`${API_BASE_URL}/auth/register`, {
username: 'testuser',
email: 'test@example.com',
password: 'password123'
});
const token = registerResponse.data.data.token;
console.log('注册成功获取到token:', token);
// 2. 创建邮件
console.log('\n2. 创建邮件...');
const mailData = {
title: '测试邮件',
content: '这是一封测试邮件内容',
recipientType: 'SELF',
sendTime: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(), // 明天这个时候
triggerType: 'TIME',
triggerCondition: {},
attachments: [],
isEncrypted: false,
capsuleStyle: 'default'
};
const createMailResponse = await axios.post(`${API_BASE_URL}/mails/create`, mailData, {
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
console.log('创建邮件成功:', createMailResponse.data);
// 3. 获取邮件列表
console.log('\n3. 获取邮件列表...');
const mailsResponse = await axios.get(`${API_BASE_URL}/mails?type=SENT&page=1&size=20`, {
headers: {
'Authorization': `Bearer ${token}`
}
});
console.log('获取邮件列表成功:', mailsResponse.data);
} catch (error) {
console.error('测试失败:', error.response?.data || error.message);
}
}
testAPI();

56
test_api.ps1 Normal file
View File

@@ -0,0 +1,56 @@
# 测试API的PowerShell脚本
$API_BASE_URL = "http://localhost:5003/api/v1"
try {
# 1. 注册用户
Write-Host "1. 注册用户..."
$registerBody = @{
username = "testuser"
email = "test@example.com"
password = "password123"
} | ConvertTo-Json
$registerResponse = Invoke-RestMethod -Uri "$API_BASE_URL/auth/register" -Method Post -Body $registerBody -ContentType "application/json"
$token = $registerResponse.data.token
Write-Host "注册成功获取到token: $token"
# 2. 创建邮件
Write-Host "`n2. 创建邮件..."
$sendTime = (Get-Date).AddDays(1).ToString("yyyy-MM-ddTHH:mm:ss.fffZ")
$mailData = @{
title = "测试邮件"
content = "这是一封测试邮件内容"
recipientType = "SELF"
sendTime = $sendTime
triggerType = "TIME"
triggerCondition = @{}
attachments = @()
isEncrypted = $false
capsuleStyle = "default"
} | ConvertTo-Json -Depth 10
$headers = @{
"Authorization" = "Bearer $token"
"Content-Type" = "application/json"
}
$createMailResponse = Invoke-RestMethod -Uri "$API_BASE_URL/mails/create" -Method Post -Body $mailData -Headers $headers
Write-Host "创建邮件成功:"
$createMailResponse | ConvertTo-Json -Depth 10
# 3. 获取邮件列表
Write-Host "`n3. 获取邮件列表..."
$mailsResponse = Invoke-RestMethod -Uri "$API_BASE_URL/mails?type=SENT&page=1&size=20" -Method Get -Headers $headers
Write-Host "获取邮件列表成功:"
$mailsResponse | ConvertTo-Json -Depth 10
} catch {
Write-Host "测试失败: $($_.Exception.Message)"
if ($_.Exception.Response) {
$reader = New-Object System.IO.StreamReader($_.Exception.Response.GetResponseStream())
$reader.BaseStream.Position = 0
$errorBody = $reader.ReadToEnd()
Write-Host "错误详情: $errorBody"
}
}