1. 概述
服务器的作用
三长两短App使用CloudKit作为主要存储,自建服务器作为备份方案:
💡 设计原理
• 主要存储:iCloud CloudKit(快速、安全、免费)
• 备份方案:自建服务器(当CloudKit不可用时)
• 自动切换:App会自动在两者之间切换
• 主要存储:iCloud CloudKit(快速、安全、免费)
• 备份方案:自建服务器(当CloudKit不可用时)
• 自动切换:App会自动在两者之间切换
服务器功能
- ✅ 备份存储 - 当CloudKit不可用时使用
- ✅ 分享码验证 - 检查分享码唯一性
- ✅ 状态请求 - 处理用户状态查询
- ✅ 碰撞记录 - 记录分享码冲突
技术栈
| 组件 | 技术 | 说明 |
|---|---|---|
| 服务器 | 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
⚠️ 安全提示
• 请修改
• 不要将
• 生产环境务必使用HTTPS
• 请修改
JWT_SECRET 和 ENCRYPTION_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
打开App → 设置 → 服务器配置 → 输入服务器URL
-
打开设置页面
在App主界面点击右上角的"设置"按钮 -
找到"服务器配置"选项
在设置页面中找到"服务器配置"或"高级设置"部分 -
输入服务器URL
输入您的服务器地址,例如:https://um.dazitai.com
注意:必须使用https://开头(生产环境) -
测试连接
点击"测试连接"按钮,确认服务器可访问 -
保存配置
测试成功后,点击"保存"按钮
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中测试
-
配置服务器URL
打开设置,输入服务器地址,测试连接 -
生成分享码
点击"生成分享码",观察日志确认是否调用服务器API -
查看日志
在Xcode控制台查看网络请求日志 -
测试备份切换
断开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. 确认服务器已启动:
3. 检查防火墙是否开放端口
4. 查看服务器日志:
5. 测试API:
解决方案:
1. 检查服务器URL是否正确(必须https://)
2. 确认服务器已启动:
pm2 status3. 检查防火墙是否开放端口
4. 查看服务器日志:
pm2 logs5. 测试API:
curl https://um.dazitai.com/api/health
问题2:CORS错误
症状:浏览器控制台显示CORS错误
解决方案:
在服务器
解决方案:
在服务器
.env 文件中设置:CORS_ORIGIN=* 或指定App的域名
问题3:数据未同步
症状:App生成分享码后,服务器没有记录
解决方案:
1. 检查网络连接
2. 查看App日志,确认是否调用了服务器API
3. 检查服务器数据库:
4. 确认CloudKit可用时,服务器只作为备份
解决方案:
1. 检查网络连接
2. 查看App日志,确认是否调用了服务器API
3. 检查服务器数据库:
SELECT * FROM share_codes;4. 确认CloudKit可用时,服务器只作为备份
问题4:SSL证书问题
症状:App显示"证书无效"
解决方案:
1. 使用Let's Encrypt申请免费证书
2. 或在开发环境使用自签名证书(需要在App中信任)
3. 生产环境必须使用受信任的SSL证书
解决方案:
1. 使用Let's Encrypt申请免费证书
2. 或在开发环境使用自签名证书(需要在App中信任)
3. 生产环境必须使用受信任的SSL证书