📚 目录

1. 概述

服务器的作用

三长两短App使用CloudKit作为主要存储,自建服务器作为备份方案

💡 设计原理
主要存储:iCloud CloudKit(快速、安全、免费)
备份方案:自建服务器(当CloudKit不可用时)
自动切换:App会自动在两者之间切换

服务器功能

技术栈

组件 技术 说明
服务器 Node.js + Express RESTful API服务
数据库 PostgreSQL 主数据存储
缓存 Redis 分享码缓存和查询优化
App端 Swift + SwiftUI iOS原生应用

2. 服务器配置

2.1 环境要求

软件 版本要求 说明
Node.js >= 18.0 必需
PostgreSQL >= 13.0 必需
Redis >= 6.0 可选但推荐
Nginx >= 1.18 推荐作为反向代理

2.2 快速部署

方式1:使用Docker(推荐)

# 1. 克隆代码
cd /path/to/server

# 2. 配置环境变量
cp .env.example .env
nano .env  # 编辑配置

# 3. 启动服务
docker-compose up -d

# 4. 检查状态
docker-compose ps

方式2:传统部署

# 1. 安装依赖
npm install

# 2. 配置数据库
createdb threelongtwo
psql threelongtwo < schema.sql

# 3. 配置环境变量
cp .env.example .env
nano .env

# 4. 启动服务
npm run start

# 或使用PM2(生产环境推荐)
pm2 start src/app.js --name threelongtwo

2.3 环境变量配置

# .env 文件示例

# 服务器配置
PORT=3000
NODE_ENV=production

# 数据库配置
DATABASE_URL=postgresql://user:password@localhost:5432/threelongtwo

# Redis配置(可选)
REDIS_URL=redis://localhost:6379

# 安全配置
JWT_SECRET=your-super-secret-key-change-this
ENCRYPTION_KEY=your-encryption-key-32-bytes

# CORS配置(允许App访问)
CORS_ORIGIN=https://yourdomain.com
⚠️ 安全提示
• 请修改 JWT_SECRETENCRYPTION_KEY
• 不要将 .env 文件提交到Git
• 生产环境务必使用HTTPS

2.4 配置Nginx(可选但推荐)

server {
    listen 80;
    server_name um.dazitai.com;
    
    # 重定向到HTTPS
    return 301 https://$server_name$request_uri;
}

server {
    listen 443 ssl http2;
    server_name um.dazitai.com;
    
    # SSL证书
    ssl_certificate /path/to/cert.pem;
    ssl_certificate_key /path/to/key.pem;
    
    # 静态文件(官网)
    location / {
        root /www/wwwroot/um.dazitai.com;
        index index.php index.html;
        try_files $uri $uri/ /index.php?$query_string;
    }
    
    # API代理
    location /api/ {
        proxy_pass http://localhost:3000;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $host;
        proxy_cache_bypass $http_upgrade;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

3. App端配置

3.1 在App中配置服务器URL

📱 App配置位置
打开App → 设置 → 服务器配置 → 输入服务器URL
  1. 打开设置页面
    在App主界面点击右上角的"设置"按钮
  2. 找到"服务器配置"选项
    在设置页面中找到"服务器配置"或"高级设置"部分
  3. 输入服务器URL
    输入您的服务器地址,例如:https://um.dazitai.com
    注意:必须使用 https:// 开头(生产环境)
  4. 测试连接
    点击"测试连接"按钮,确认服务器可访问
  5. 保存配置
    测试成功后,点击"保存"按钮

3.2 代码实现(Swift)

在SettingsView.swift中配置

// SettingsView.swift

struct SettingsView: View {
    @AppStorage("serverURL") private var serverURL: String = ""
    @State private var testStatus: String = ""
    @State private var isTesting = false
    
    var body: some View {
        Form {
            Section(header: Text("服务器配置")) {
                TextField("服务器URL", text: $serverURL)
                    .textInputAutocapitalization(.never)
                    .autocorrectionDisabled()
                    .placeholder("https://um.dazitai.com")
                
                Button("测试连接") {
                    testConnection()
                }
                .disabled(isTesting || serverURL.isEmpty)
                
                if !testStatus.isEmpty {
                    Text(testStatus)
                        .font(.caption)
                        .foregroundColor(testStatus.contains("成功") ? .green : .red)
                }
            }
        }
    }
    
    private func testConnection() {
        guard let url = URL(string: serverURL + "/api/health") else {
            testStatus = "❌ URL格式错误"
            return
        }
        
        isTesting = true
        testStatus = "正在测试..."
        
        URLSession.shared.dataTask(with: url) { data, response, error in
            DispatchQueue.main.async {
                isTesting = false
                
                if let error = error {
                    testStatus = "❌ 连接失败:\(error.localizedDescription)"
                    return
                }
                
                if let httpResponse = response as? HTTPURLResponse,
                   httpResponse.statusCode == 200 {
                    testStatus = "✅ 连接成功"
                } else {
                    testStatus = "❌ 服务器响应异常"
                }
            }
        }.resume()
    }
}

在ServerService.swift中使用

// ServerService.swift

class ServerService: ObservableObject {
    @AppStorage("serverURL") private var serverURL: String = ""
    
    private var baseURL: String {
        return serverURL.isEmpty ? "https://um.dazitai.com" : serverURL
    }
    
    // 检查分享码唯一性
    func checkShareCodeUniqueness(_ shareCode: String) async throws -> Bool {
        let url = URL(string: "\(baseURL)/api/share-code/check")!
        
        var request = URLRequest(url: url)
        request.httpMethod = "POST"
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        
        let body = ["shareCode": shareCode]
        request.httpBody = try JSONEncoder().encode(body)
        
        let (data, response) = try await URLSession.shared.data(for: request)
        
        guard let httpResponse = response as? HTTPURLResponse,
              httpResponse.statusCode == 200 else {
            throw ServerError.invalidResponse
        }
        
        let result = try JSONDecoder().decode(CheckResponse.self, from: data)
        return result.isUnique
    }
    
    // 发送状态请求
    func sendStatusRequest(_ request: StatusRequest) async throws {
        let url = URL(string: "\(baseURL)/api/status-request")!
        
        var urlRequest = URLRequest(url: url)
        urlRequest.httpMethod = "POST"
        urlRequest.setValue("application/json", forHTTPHeaderField: "Content-Type")
        
        urlRequest.httpBody = try JSONEncoder().encode(request)
        
        let (_, response) = try await URLSession.shared.data(for: urlRequest)
        
        guard let httpResponse = response as? HTTPURLResponse,
              httpResponse.statusCode == 200 else {
            throw ServerError.requestFailed
        }
    }
}

4. API接口说明

4.1 健康检查

GET /api/health

说明:检查服务器是否正常运行

响应

{
  "status": "ok",
  "timestamp": "2024-01-20T10:00:00Z",
  "version": "1.0.0"
}

4.2 检查分享码唯一性

POST /api/share-code/check

说明:检查分享码是否已被使用

请求体

{
  "shareCode": "A1B2C3"
}

响应

{
  "isUnique": true,
  "shareCode": "A1B2C3"
}

4.3 注册分享码

POST /api/share-code/register

说明:注册新的分享码

请求体

{
  "umid": "abc123...",
  "shareCode": "A1B2C3",
  "encryptedData": "...",
  "validityMinutes": 5,
  "expiresAt": "2024-01-20T10:05:00Z"
}

响应

{
  "success": true,
  "shareCode": "A1B2C3",
  "expiresAt": "2024-01-20T10:05:00Z"
}

4.4 发送状态请求

POST /api/status-request

说明:发送用户状态查询请求

请求体

{
  "requestId": "uuid-here",
  "fromUserId": "user-uuid",
  "toUserId": "target-uuid",
  "message": "查询您的状态"
}

响应

{
  "success": true,
  "requestId": "uuid-here",
  "status": "pending"
}

4.5 记录碰撞

POST /api/collision/record

说明:记录分享码碰撞事件

请求体

{
  "shareCode": "A1B2C3",
  "attempts": 3,
  "codeLength": 6,
  "timestamp": "2024-01-20T10:00:00Z"
}

4.6 错误响应

所有API在出错时返回统一格式:

{
  "error": {
    "code": "ERROR_CODE",
    "message": "错误描述",
    "details": "详细信息(可选)"
  }
}

常见错误码

错误码 HTTP状态 说明
INVALID_REQUEST 400 请求参数无效
NOT_FOUND 404 资源不存在
SERVER_ERROR 500 服务器内部错误
DATABASE_ERROR 500 数据库操作失败

5. 数据流程

5.1 生成分享码流程

App端:
1. 用户点击"生成分享码"
2. App生成6位随机分享码
3. 检查CloudKit中是否唯一
   ├─ 如果CloudKit可用 → 使用CloudKit检查
   └─ 如果CloudKit不可用 → 调用服务器API检查
4. 如果不唯一,重新生成(最多尝试3次)
5. 如果仍然碰撞,增加位数到8位
6. 保存到CloudKit + 服务器(双重备份)
7. 显示二维码和分享码给用户

5.2 扫描分享码流程

App端:
1. 用户扫描二维码或输入分享码
2. 解密第三层获取姓名
3. 验证有效期(检查时间戳)
4. 显示姓名的第一个字,要求输入最后一个字
5. 用户输入后,验证姓名是否完整匹配
6. 如果匹配,发送状态请求
   ├─ 优先发送到CloudKit
   └─ 失败时发送到服务器
7. 等待对方确认

5.3 CloudKit与服务器切换逻辑

// StatusSharingService.swift

func generateNewShareCode() async {
    // 1. 先尝试使用CloudKit
    do {
        let isUnique = try await cloudKitService.checkShareCodeUniqueness(shareCode)
        if isUnique {
            // 保存到CloudKit
            try await cloudKitService.saveShareInfo(...)
            // 同时备份到服务器
            try? await serverService.registerShareCode(...)
        }
    } catch {
        // 2. CloudKit失败,切换到服务器
        print("CloudKit不可用,使用服务器备份")
        do {
            let isUnique = try await serverService.checkShareCodeUniqueness(shareCode)
            if isUnique {
                try await serverService.registerShareCode(...)
            }
        } catch {
            // 3. 服务器也失败,只保存本地
            print("服务器也不可用,仅保存本地")
            saveToLocalOnly()
        }
    }
}

6. 测试验证

6.1 测试服务器连接

# 测试健康检查接口
curl https://um.dazitai.com/api/health

# 应该返回
# {"status":"ok","timestamp":"...","version":"1.0.0"}

6.2 测试分享码检查

# 测试分享码唯一性检查
curl -X POST https://um.dazitai.com/api/share-code/check \
  -H "Content-Type: application/json" \
  -d '{"shareCode":"TEST01"}'

# 应该返回
# {"isUnique":true,"shareCode":"TEST01"}

6.3 在App中测试

  1. 配置服务器URL
    打开设置,输入服务器地址,测试连接
  2. 生成分享码
    点击"生成分享码",观察日志确认是否调用服务器API
  3. 查看日志
    在Xcode控制台查看网络请求日志
  4. 测试备份切换
    断开iCloud,再次生成分享码,确认使用服务器

6.4 监控服务器日志

# 查看实时日志
pm2 logs threelongtwo

# 或Docker方式
docker-compose logs -f app

# Nginx访问日志
tail -f /var/log/nginx/access.log

7. 常见问题

问题1:App无法连接服务器

症状:测试连接时显示"连接失败"

解决方案
1. 检查服务器URL是否正确(必须https://)
2. 确认服务器已启动:pm2 status
3. 检查防火墙是否开放端口
4. 查看服务器日志:pm2 logs
5. 测试API:curl https://um.dazitai.com/api/health

问题2:CORS错误

症状:浏览器控制台显示CORS错误

解决方案
在服务器 .env 文件中设置:
CORS_ORIGIN=* 或指定App的域名

问题3:数据未同步

症状:App生成分享码后,服务器没有记录

解决方案
1. 检查网络连接
2. 查看App日志,确认是否调用了服务器API
3. 检查服务器数据库:SELECT * FROM share_codes;
4. 确认CloudKit可用时,服务器只作为备份

问题4:SSL证书问题

症状:App显示"证书无效"

解决方案
1. 使用Let's Encrypt申请免费证书
2. 或在开发环境使用自签名证书(需要在App中信任)
3. 生产环境必须使用受信任的SSL证书

需要帮助?

获取支持
• 查看完整文档:文档中心
• 查看服务器日志定位问题
• 检查数据库连接状态
• 使用Postman测试API接口
← 返回首页