feat: 初始化项目

This commit is contained in:
richarjiang
2025-08-13 15:17:33 +08:00
commit 4f9d648a50
72 changed files with 29051 additions and 0 deletions

View File

@@ -0,0 +1,190 @@
# App Store 服务器通知接收接口
## 概述
本接口用于接收来自苹果App Store的服务器通知App Store Server Notifications V2处理用户订阅状态的变化包括订阅、续订、过期、退款等事件。
## 接口地址
```
POST /api/users/app-store-notifications
```
## 配置要求
### 1. 在App Store Connect中配置
1. 登录 [App Store Connect](https://appstoreconnect.apple.com)
2. 选择你的应用
3. 在左侧菜单中选择 "App信息"
4. 滚动到 "App Store服务器通知" 部分
5. 设置生产环境URL`https://your-domain.com/api/users/app-store-notifications`
6. 设置沙盒环境URL`https://your-domain.com/api/users/app-store-notifications`
7. 选择 "Version 2" 通知格式
8. 保存设置
### 2. 环境变量配置
确保以下环境变量已正确设置:
```bash
# Apple相关配置
APPLE_BUNDLE_ID=com.yourcompany.yourapp
APPLE_KEY_ID=your_key_id
APPLE_ISSUER_ID=your_issuer_id
APPLE_PRIVATE_KEY_PATH=path/to/your/private/key.p8
APPLE_APP_SHARED_SECRET=your_shared_secret
```
## 请求格式
### 请求头
```
Content-Type: application/json
```
### 请求体
```json
{
"signedPayload": "eyJhbGciOiJFUzI1NiIsImtpZCI6IjEyMzQ1Njc4OTAiLCJ0eXAiOiJKV1QifQ..."
}
```
## 响应格式
### 成功响应
```json
{
"code": 200,
"message": "通知处理成功",
"data": {
"processed": true,
"notificationType": "SUBSCRIBED",
"notificationUUID": "12345678-1234-1234-1234-123456789012",
"userId": "user_12345",
"transactionId": "1000000123456789"
}
}
```
### 错误响应
```json
{
"code": 500,
"message": "处理通知失败: 无法解码通知负载",
"data": {
"processed": false
}
}
```
## 支持的通知类型
### 1. SUBSCRIBED订阅
- **描述**: 用户首次订阅或重新订阅
- **处理**: 更新用户的订阅状态和到期时间
### 2. DID_RENEW续订
- **描述**: 订阅成功续订
- **处理**: 延长用户的订阅到期时间
### 3. EXPIRED过期
- **描述**: 订阅已过期
- **处理**: 可以发送邮件通知或降级用户权限
### 4. DID_FAIL_TO_RENEW续订失败
- **描述**: 订阅续订失败(通常是支付问题)
- **处理**: 发送付款提醒或进入宽限期
### 5. REFUND退款
- **描述**: 用户获得退款
- **处理**: 立即撤销用户的订阅权限
### 6. REVOKE撤销
- **描述**: 通过家庭共享获得的权限被撤销
- **处理**: 撤销用户的订阅权限
### 7. DID_CHANGE_RENEWAL_STATUS续订状态变更
- **描述**: 用户更改了自动续订设置
- **处理**: 记录用户的续订偏好
### 8. TEST测试
- **描述**: 测试通知
- **处理**: 仅记录日志,无特殊处理
## 安全考虑
### 1. 签名验证
- 接口会验证通知的JWS签名
- 确保通知来自苹果官方服务器
- 验证bundleId匹配
### 2. 重复处理
- 建议实现幂等性处理
- 根据notificationUUID去重
- 避免重复处理相同通知
### 3. 错误处理
- 对于无法处理的通知返回适当的HTTP状态码
- 苹果会根据响应状态码决定是否重试
## 日志记录
接口会记录以下信息:
- 收到的通知类型和UUID
- 解码后的交易信息
- 处理结果和任何错误
- 用户ID和交易ID如果可用
## 测试
### 1. 使用App Store Connect测试
1. 在App Store Connect中请求测试通知
2. 检查服务器日志确认收到通知
3. 验证通知处理逻辑是否正确
### 2. 沙盒环境测试
1. 使用沙盒环境进行购买测试
2. 观察通知是否正确触发
3. 验证用户状态是否正确更新
## 故障排除
### 常见问题
1. **收不到通知**
- 检查App Store Connect中的URL配置
- 确保服务器可以从外网访问
- 检查防火墙设置
2. **通知解码失败**
- 验证私钥文件路径和格式
- 检查环境变量配置
- 确认bundleId匹配
3. **用户状态未更新**
- 检查appAccountToken是否正确设置
- 验证用户ID映射逻辑
- 查看数据库更新日志
### 日志查看
```bash
# 查看相关日志
grep "App Store" /path/to/your/logs/app.log
grep "processAppStoreNotification" /path/to/your/logs/app.log
```
## 扩展功能
可以根据业务需求扩展以下功能:
1. **邮件通知**: 在特定事件发生时发送邮件
2. **数据分析**: 收集订阅数据用于分析
3. **第三方集成**: 将事件发送到其他系统
4. **自定义业务逻辑**: 根据不同的通知类型执行特定操作
## 相关文档
- [App Store Server Notifications](https://developer.apple.com/documentation/appstoreservernotifications)
- [App Store Server API](https://developer.apple.com/documentation/appstoreserverapi)
- [StoreKit 2](https://developer.apple.com/documentation/storekit)

View File

@@ -0,0 +1,223 @@
# iOS端加密对接指南
## 概述
本文档描述了如何在iOS端实现与服务端兼容的AES-256-GCM加解密功能。
## 加密规格
- **算法**: AES-256-GCM
- **密钥长度**: 256位 (32字节)
- **IV长度**: 96位 (12字节)
- **认证标签长度**: 128位 (16字节)
- **数据格式**: Base64编码的 `IV + AuthTag + Ciphertext`
## 环境变量配置
在服务端设置环境变量 `ENCRYPTION_KEY`,确保密钥安全:
```bash
export ENCRYPTION_KEY="your-32-character-secret-key-here"
```
## iOS Swift 实现
### 1. 创建加密工具类
```swift
import Foundation
import CryptoKit
class EncryptionHelper {
private static let keyLength = 32
private static let ivLength = 12
private static let tagLength = 16
private static let additionalData = "additional-data".data(using: .utf8)!
// 从配置或钥匙串获取密钥
private static let encryptionKey: SymmetricKey = {
let keyString = "your-32-character-secret-key-here" // 应从安全存储获取
let keyData = keyString.prefix(keyLength).padding(toLength: keyLength, withPad: "0", startingAt: 0).data(using: .utf8)!
return SymmetricKey(data: keyData)
}()
/// 加密数据
/// - Parameter plaintext: 要加密的明文字符串
/// - Returns: Base64编码的加密数据格式iv + tag + ciphertext
static func encrypt(_ plaintext: String) throws -> String {
guard let data = plaintext.data(using: .utf8) else {
throw EncryptionError.invalidInput
}
// 生成随机IV
let iv = Data((0..<ivLength).map { _ in UInt8.random(in: 0...255) })
// 使用AES-GCM加密
let sealedBox = try AES.GCM.seal(data, using: encryptionKey, nonce: AES.GCM.Nonce(data: iv), authenticating: additionalData)
// 组合数据IV + Tag + Ciphertext
var combined = Data()
combined.append(iv)
combined.append(sealedBox.tag)
combined.append(sealedBox.ciphertext)
return combined.base64EncodedString()
}
/// 解密数据
/// - Parameter encryptedData: Base64编码的加密数据
/// - Returns: 解密后的明文字符串
static func decrypt(_ encryptedData: String) throws -> String {
guard let combined = Data(base64Encoded: encryptedData) else {
throw EncryptionError.invalidInput
}
guard combined.count >= ivLength + tagLength else {
throw EncryptionError.invalidDataLength
}
// 提取各部分
let iv = combined.prefix(ivLength)
let tag = combined.dropFirst(ivLength).prefix(tagLength)
let ciphertext = combined.dropFirst(ivLength + tagLength)
// 创建密封盒进行解密
let sealedBox = try AES.GCM.SealedBox(nonce: AES.GCM.Nonce(data: iv), ciphertext: ciphertext, tag: tag)
let decryptedData = try AES.GCM.open(sealedBox, using: encryptionKey, authenticating: additionalData)
guard let plaintext = String(data: decryptedData, encoding: .utf8) else {
throw EncryptionError.decodingFailed
}
return plaintext
}
}
enum EncryptionError: Error {
case invalidInput
case invalidDataLength
case decodingFailed
}
```
### 2. 使用示例
```swift
// 创建用户数据
struct CreateUserRequest: Codable {
let id: String
let name: String
let mail: String
}
// 加密请求体
struct EncryptedRequest: Codable {
let encryptedData: String
}
// 加密响应体
struct EncryptedResponse: Codable {
let success: Bool
let message: String
let encryptedData: String?
}
// 使用示例
func createUserWithEncryption() async {
do {
// 1. 准备用户数据
let userData = CreateUserRequest(
id: "1234567890",
name: "张三",
mail: "zhangsan@example.com"
)
// 2. 序列化为JSON
let jsonData = try JSONEncoder().encode(userData)
let jsonString = String(data: jsonData, encoding: .utf8)!
// 3. 加密数据
let encryptedData = try EncryptionHelper.encrypt(jsonString)
// 4. 创建请求体
let request = EncryptedRequest(encryptedData: encryptedData)
// 5. 发送请求
let response = try await sendEncryptedRequest(request)
// 6. 解密响应(如果有加密数据)
if let encryptedResponseData = response.encryptedData {
let decryptedResponse = try EncryptionHelper.decrypt(encryptedResponseData)
print("解密后的响应: \(decryptedResponse)")
}
} catch {
print("加密通信失败: \(error)")
}
}
func sendEncryptedRequest(_ request: EncryptedRequest) async throws -> EncryptedResponse {
let url = URL(string: "https://your-server.com/users/encrypted")!
var urlRequest = URLRequest(url: url)
urlRequest.httpMethod = "POST"
urlRequest.setValue("application/json", forHTTPHeaderField: "Content-Type")
let requestData = try JSONEncoder().encode(request)
urlRequest.httpBody = requestData
let (data, _) = try await URLSession.shared.data(for: urlRequest)
return try JSONDecoder().decode(EncryptedResponse.self, from: data)
}
```
## API 接口
### 加密版创建用户接口
**端点**: `POST /users/encrypted`
**请求体**:
```json
{
"encryptedData": "base64编码的加密数据"
}
```
**响应体**:
```json
{
"success": true,
"message": "用户创建成功",
"encryptedData": "base64编码的加密响应数据"
}
```
## 安全建议
1. **密钥管理**:
- 生产环境必须使用环境变量或安全密钥管理服务
- iOS端应将密钥存储在钥匙串中
- 定期轮换密钥
2. **传输安全**:
- 始终使用HTTPS
- 考虑添加请求签名验证
3. **错误处理**:
- 不要在错误信息中暴露加密细节
- 记录加密失败日志用于监控
4. **测试**:
- 确保iOS端和服务端使用相同的密钥
- 测试各种边界情况和错误场景
## 故障排除
1. **解密失败**: 检查密钥是否一致
2. **格式错误**: 确认Base64编码格式正确
3. **数据长度错误**: 验证IV和Tag长度是否正确

230
docs/topic-favorite-api.md Normal file
View File

@@ -0,0 +1,230 @@
# 话题收藏功能 API 文档
## 概述
话题收藏功能允许用户收藏喜欢的话题方便后续查看和使用。本文档描述了话题收藏相关的所有API接口。
## 功能特性
- ✅ 收藏话题
- ✅ 取消收藏话题
- ✅ 获取收藏话题列表
- ✅ 话题列表中显示收藏状态
- ✅ 防重复收藏
- ✅ 完整的错误处理
## API 接口
### 1. 收藏话题
**接口地址:** `POST /api/topic/favorite`
**请求参数:**
```json
{
"topicId": 123
}
```
**响应示例:**
```json
{
"code": 200,
"message": "收藏成功",
"data": {
"success": true,
"isFavorited": true,
"topicId": 123
}
}
```
**错误情况:**
- 话题不存在:`{ "code": 400, "message": "话题不存在" }`
- 已收藏:`{ "code": 200, "message": "已经收藏过该话题" }`
### 2. 取消收藏话题
**接口地址:** `POST /api/topic/unfavorite`
**请求参数:**
```json
{
"topicId": 123
}
```
**响应示例:**
```json
{
"code": 200,
"message": "取消收藏成功",
"data": {
"success": true,
"isFavorited": false,
"topicId": 123
}
}
```
### 3. 获取收藏话题列表
**接口地址:** `POST /api/topic/favorites`
**请求参数:**
```json
{
"page": 1,
"pageSize": 10
}
```
**响应示例:**
```json
{
"code": 200,
"message": "success",
"data": {
"list": [
{
"id": 123,
"topic": "约会话题",
"opening": {
"text": "今天天气真不错...",
"scenarios": ["咖啡厅", "公园"]
},
"scriptType": "初识破冰",
"scriptTopic": "天气",
"keywords": "天气,约会,轻松",
"isFavorited": true,
"createdAt": "2024-01-01T00:00:00.000Z",
"updatedAt": "2024-01-01T00:00:00.000Z"
}
],
"total": 5,
"page": 1,
"pageSize": 10
}
}
```
### 4. 获取话题列表(已包含收藏状态)
**接口地址:** `POST /api/topic/list`
现在所有话题列表都会包含 `isFavorited` 字段,表示当前用户是否已收藏该话题。
**响应示例:**
```json
{
"code": 200,
"message": "success",
"data": {
"list": [
{
"id": 123,
"topic": "约会话题",
"opening": {
"text": "今天天气真不错...",
"scenarios": ["咖啡厅", "公园"]
},
"scriptType": "初识破冰",
"scriptTopic": "天气",
"keywords": "天气,约会,轻松",
"isFavorited": true, // ← 新增的收藏状态字段
"createdAt": "2024-01-01T00:00:00.000Z",
"updatedAt": "2024-01-01T00:00:00.000Z"
},
{
"id": 124,
"topic": "工作话题",
"opening": "关于工作的开场白...",
"scriptType": "深度交流",
"scriptTopic": "职业",
"keywords": "工作,职业,发展",
"isFavorited": false, // ← 未收藏
"createdAt": "2024-01-01T00:00:00.000Z",
"updatedAt": "2024-01-01T00:00:00.000Z"
}
],
"total": 20,
"page": 1,
"pageSize": 10
}
}
```
## 数据库变更
### 新增表t_topic_favorites
```sql
CREATE TABLE `t_topic_favorites` (
`id` int NOT NULL AUTO_INCREMENT,
`user_id` varchar(255) NOT NULL COMMENT '用户ID',
`topic_id` int NOT NULL COMMENT '话题ID',
`created_at` datetime DEFAULT CURRENT_TIMESTAMP,
`updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `unique_user_topic_favorite` (`user_id`, `topic_id`),
KEY `idx_user_id` (`user_id`),
KEY `idx_topic_id` (`topic_id`),
FOREIGN KEY (`user_id`) REFERENCES `t_users` (`id`) ON DELETE CASCADE,
FOREIGN KEY (`topic_id`) REFERENCES `t_topic_library` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
```
## 使用场景
### 1. 用户收藏话题
```javascript
// 收藏话题
const response = await fetch('/api/topic/favorite', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer your-jwt-token'
},
body: JSON.stringify({ topicId: 123 })
});
```
### 2. 取消收藏话题
```javascript
// 取消收藏
const response = await fetch('/api/topic/unfavorite', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer your-jwt-token'
},
body: JSON.stringify({ topicId: 123 })
});
```
### 3. 查看收藏列表
```javascript
// 获取收藏的话题
const response = await fetch('/api/topic/favorites', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer your-jwt-token'
},
body: JSON.stringify({ page: 1, pageSize: 10 })
});
```
## 注意事项
1. **防重复收藏:** 数据库层面通过唯一索引保证同一用户不能重复收藏同一话题
2. **级联删除:** 用户删除或话题删除时,相关收藏记录会自动删除
3. **性能优化:** 获取话题列表时通过单次查询获取用户所有收藏状态避免N+1查询问题
4. **权限控制:** 所有接口都需要用户登录JWT认证
## 错误码说明
- `200`: 操作成功
- `400`: 请求参数错误(如话题不存在)
- `401`: 未授权(需要登录)
- `500`: 服务器内部错误

View File

@@ -0,0 +1,233 @@
# Winston Logger 配置指南
本项目已配置了基于 Winston 的日志系统,支持日志文件输出、按日期滚动和自动清理。
## 功能特性
-**日志文件输出**: 自动将日志写入文件
-**按日期滚动**: 每天生成新的日志文件
-**自动清理**: 保留最近7天的日志文件
-**分级日志**: 支持不同级别的日志分离
-**结构化日志**: 支持JSON格式的结构化日志
-**异常处理**: 自动记录未捕获的异常和Promise拒绝
## 日志文件结构
```
logs/
├── app-2025-07-21.log # 应用日志 (info级别及以上)
├── error-2025-07-21.log # 错误日志 (error级别)
├── debug-2025-07-21.log # 调试日志 (仅开发环境)
├── exceptions-2025-07-21.log # 未捕获异常
├── rejections-2025-07-21.log # 未处理的Promise拒绝
└── .audit-*.json # 日志轮转审计文件
```
## 配置说明
### 日志级别
- **生产环境**: `info` 及以上级别
- **开发环境**: `debug` 及以上级别
### 文件轮转配置
- **日期模式**: `YYYY-MM-DD`
- **保留天数**: 7天
- **单文件大小**: 最大20MB
- **自动压缩**: 支持
## 使用方法
### 1. 在服务中使用 NestJS Logger
```typescript
import { Injectable, Logger } from '@nestjs/common';
@Injectable()
export class YourService {
private readonly logger = new Logger(YourService.name);
someMethod() {
this.logger.log('这是一条信息日志');
this.logger.warn('这是一条警告日志');
this.logger.error('这是一条错误日志');
this.logger.debug('这是一条调试日志');
}
}
```
### 2. 直接使用 Winston Logger (推荐用于结构化日志)
```typescript
import { Injectable, Inject } from '@nestjs/common';
import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
import { Logger as WinstonLogger } from 'winston';
@Injectable()
export class YourService {
constructor(
@Inject(WINSTON_MODULE_PROVIDER)
private readonly winstonLogger: WinstonLogger,
) {}
someMethod() {
// 结构化日志
this.winstonLogger.info('用户登录', {
context: 'AuthService',
userId: 'user123',
email: 'user@example.com',
timestamp: new Date().toISOString()
});
// 错误日志
this.winstonLogger.error('数据库连接失败', {
context: 'DatabaseService',
error: 'Connection timeout',
retryCount: 3
});
}
}
```
### 3. 在控制器中使用
```typescript
import { Controller, Logger } from '@nestjs/common';
@Controller('users')
export class UsersController {
private readonly logger = new Logger(UsersController.name);
@Get()
findAll() {
this.logger.log('获取用户列表请求');
// 业务逻辑
}
}
```
## 日志格式
### 控制台输出格式
```
2025-07-21 10:08:38 info [ServiceName] 日志消息
```
### 文件输出格式
```
2025-07-21 10:08:38 [INFO] [ServiceName] 日志消息
```
### 结构化日志格式
```json
{
"timestamp": "2025-07-21 10:08:38",
"level": "info",
"message": "用户登录",
"context": "AuthService",
"userId": "user123",
"email": "user@example.com"
}
```
## 环境变量配置
可以通过环境变量调整日志行为:
```bash
# 日志级别 (development: debug, production: info)
NODE_ENV=production
# 自定义日志目录 (可选)
LOG_DIR=/var/log/love-tips-server
```
## 最佳实践
### 1. 使用合适的日志级别
- `error`: 错误和异常
- `warn`: 警告信息
- `info`: 重要的业务信息
- `debug`: 调试信息 (仅开发环境)
### 2. 结构化日志
对于重要的业务事件,使用结构化日志:
```typescript
this.winstonLogger.info('订单创建', {
context: 'OrderService',
orderId: order.id,
userId: user.id,
amount: order.amount,
currency: 'CNY'
});
```
### 3. 错误日志包含上下文
```typescript
try {
// 业务逻辑
} catch (error) {
this.winstonLogger.error('处理订单失败', {
context: 'OrderService',
orderId: order.id,
error: error.message,
stack: error.stack
});
}
```
### 4. 避免敏感信息
不要在日志中记录密码、令牌等敏感信息:
```typescript
// ❌ 错误
this.logger.log(`用户登录: ${JSON.stringify(loginData)}`);
// ✅ 正确
this.logger.log(`用户登录: ${loginData.email}`);
```
## 监控和维护
### 查看实时日志
```bash
# 查看应用日志
tail -f logs/app-$(date +%Y-%m-%d).log
# 查看错误日志
tail -f logs/error-$(date +%Y-%m-%d).log
```
### 日志分析
```bash
# 统计错误数量
grep -c "ERROR" logs/app-*.log
# 查找特定用户的日志
grep "userId.*user123" logs/app-*.log
```
### 清理旧日志
日志系统会自动清理7天前的日志文件无需手动维护。
## 故障排除
### 1. 日志文件未生成
- 检查 `logs` 目录权限
- 确认应用有写入权限
- 查看控制台是否有错误信息
### 2. 日志级别不正确
- 检查 `NODE_ENV` 环境变量
- 确认 winston 配置中的日志级别设置
### 3. 日志文件过大
- 检查日志轮转配置
- 确认 `maxSize``maxFiles` 设置
## 相关文件
- [`src/common/logger/winston.config.ts`](../src/common/logger/winston.config.ts) - Winston 配置
- [`src/common/logger/logger.module.ts`](../src/common/logger/logger.module.ts) - Logger 模块
- [`src/main.ts`](../src/main.ts) - 应用启动配置
- [`src/app.module.ts`](../src/app.module.ts) - 应用模块配置