feat: 初始化项目
This commit is contained in:
190
docs/app-store-server-notifications.md
Normal file
190
docs/app-store-server-notifications.md
Normal 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)
|
||||
223
docs/ios-encryption-guide.md
Normal file
223
docs/ios-encryption-guide.md
Normal 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
230
docs/topic-favorite-api.md
Normal 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`: 服务器内部错误
|
||||
233
docs/winston-logger-guide.md
Normal file
233
docs/winston-logger-guide.md
Normal 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) - 应用模块配置
|
||||
Reference in New Issue
Block a user