版本: v1.0.0
更新日期: 2025-11-07
适用项目: Roamio + Ralendar
维护: Roamio Team
本文档是 Roamio 生态系统的统一 API 文档,涵盖:
- Roamio:旅行规划与内容分享平台
- Ralendar:智能日历与时间管理工具
两个项目共享用户认证体系,未来将深度融合,实现数据互通和功能联动。
┌─────────────────────────────────────────────────────────────┐
│ Roamio Ecosystem Architecture │
└─────────────────────────────────────────────────────────────┘
统一用户认证系统
(JWT + OAuth2)
│
┌──────────────────┼──────────────────┐
│ │ │
Roamio API Ralendar API 未来产品
(旅行规划) (日历管理) (Rote, Rapture...)
│ │ │
└──────────────────┼──────────────────┘
│
共享数据层(可选)
User, OAuth绑定
- 独立开发,统一部署 - 各项目独立开发,共享认证体系
- API 优先 - RESTful 设计,便于跨端调用
- 数据互通 - 预留接口,支持未来数据联动
- 可扩展性 - 模块化设计,易于添加新产品
认证方式:Bearer Token
Header 格式:
Authorization: Bearer <access_token>
Token 生命周期:
- Access Token:5 分钟(短期,安全)
- Refresh Token:15 天(长期,便利)
端点: POST /api/v1/token/ (Roamio) 或 POST /api/auth/login/ (Ralendar)
请求:
{
"username": "string",
"password": "string"
}响应:
{
"access": "eyJ0eXAiOiJKV1QiLCJhbGc...",
"refresh": "eyJ0eXAiOiJKV1QiLCJhbGc...",
"user": {
"id": 1,
"username": "张三",
"email": "user@example.com"
}
}端点: POST /api/v1/token/refresh/ (Roamio) 或 POST /api/auth/refresh/ (Ralendar)
请求:
{
"refresh": "eyJ0eXAiOiJKV1QiLCJhbGc..."
}响应:
{
"access": "eyJ0eXAiOiJKV1QiLCJhbGc..."
}Base URL: https://roamio.com/api/v1/
端点: POST /api/v1/auth/register/
权限: 匿名
请求:
{
"username": "string",
"email": "email@example.com",
"password": "string",
"password_confirm": "string"
}响应 (201 Created):
{
"user": {
"id": 1,
"username": "张三",
"email": "user@example.com",
"profile": {
"avatar": "https://...",
"bio": "",
"level": "novice"
}
},
"access": "eyJ0eXAiOiJKV1QiLCJhbGc...",
"refresh": "eyJ0eXAiOiJKV1QiLCJhbGc..."
}端点: POST /api/v1/auth/login/
权限: 匿名
请求:
{
"username": "string", // 或 email
"password": "string"
}响应 (200 OK):
{
"access": "eyJ0eXAiOiJKV1QiLCJhbGc...",
"refresh": "eyJ0eXAiOiJKV1QiLCJhbGc...",
"user": {
"id": 1,
"username": "张三",
"email": "user@example.com"
}
}端点: GET /api/v1/auth/me/
权限: 已认证
响应 (200 OK):
{
"id": 1,
"username": "张三",
"email": "user@example.com",
"profile": {
"avatar": "https://...",
"bio": "旅行爱好者",
"birthday": "1990-01-01",
"tags": "摄影,美食",
"level": "explorer",
"email_verified": true
},
"social_accounts": [
{
"provider": "qq",
"nickname": "QQ昵称",
"avatar_url": "https://..."
}
],
"trips_count": 5,
"comments_count": 23
}端点: POST /api/v1/auth/send_verification_code/
权限: 匿名
请求:
{
"email": "user@example.com",
"type": "register" // register | reset_password | verify_email
}频率限制:
- 同一邮箱:5分钟内最多3次
- 同一IP:1小时内最多10次
响应 (200 OK):
{
"success": true,
"message": "验证码已发送到您的邮箱",
"expires_in": 600 // 10分钟
}响应 (429 Too Many Requests):
{
"error": "发送过于频繁,请稍后再试",
"remaining_seconds": 180
}端点: POST /api/v1/auth/verify_code/
权限: 匿名
请求:
{
"email": "user@example.com",
"code": "123456",
"type": "register"
}响应 (200 OK):
{
"success": true,
"verified": true,
"message": "验证成功",
"email": "user@example.com",
"verification_token": "abcd1234...", // 临时token,5分钟有效
"expires_in": 300
}端点: GET /api/v1/auth/qq_login_url/
权限: 匿名
响应 (200 OK):
{
"authorize_url": "https://graph.qq.com/oauth2.0/authorize?...",
"state": "random_state_string"
}端点: POST /api/v1/auth/qq_callback/
权限: 匿名
请求:
{
"code": "authorization_code",
"state": "state_string"
}响应 (200 OK):
{
"success": true,
"access": "eyJ0eXAiOiJKV1QiLCJhbGc...",
"refresh": "eyJ0eXAiOiJKV1QiLCJhbGc...",
"user": {
"id": 1,
"username": "QQ用户",
"email": ""
},
"message": "QQ登录成功",
"email_optional": true,
"tip": "建议在个人中心绑定邮箱"
}端点: POST /api/v1/auth/qq_bind_existing/
权限: 已认证
请求:
{
"code": "authorization_code",
"state": "state_string"
}响应 (200 OK):
{
"success": true,
"message": "QQ绑定成功"
}端点: DELETE /api/v1/auth/qq_unbind/
权限: 已认证
响应 (200 OK):
{
"success": true,
"message": "QQ解绑成功"
}端点: POST /api/v1/auth/reset_password/
权限: 匿名
请求:
{
"email": "user@example.com",
"verification_token": "token_from_verify_code",
"new_password": "new_password",
"password_confirm": "new_password"
}响应 (200 OK):
{
"success": true,
"message": "密码重置成功,请使用新密码登录"
}端点: GET /api/v1/users/
权限: 匿名(只读)
查询参数:
page: 页码(默认 1)page_size: 每页数量(默认 20)
响应 (200 OK):
{
"count": 100,
"next": "https://roamio.com/api/v1/users/?page=2",
"previous": null,
"results": [
{
"id": 1,
"username": "张三",
"profile": {
"avatar": "https://...",
"bio": "旅行爱好者",
"level": "explorer"
},
"trips_count": 5,
"public_trips_count": 3
}
]
}端点: GET /api/v1/users/{id}/
权限: 匿名
响应 (200 OK):
{
"id": 1,
"username": "张三",
"profile": {
"avatar": "https://...",
"bio": "旅行爱好者,走过30个国家",
"birthday": "1990-01-01",
"tags": "摄影,美食,徒步",
"level": "wanderer",
"visited_countries": "中国,日本,泰国"
},
"trips": [
{
"id": 1,
"slug": "abc123def456",
"title": "云南之旅",
"icon": "🏔️",
"start_date": "2025-11-15",
"end_date": "2025-11-20"
}
],
"trips_count": 5,
"public_trips_count": 3,
"comments_count": 45,
"joined_at": "2025-01-01T00:00:00Z"
}端点: PATCH /api/v1/users/{id}/profile/
权限: 本人或管理员
请求:
{
"bio": "string",
"birthday": "1990-01-01",
"tags": "摄影,美食"
}响应 (200 OK):
{
"success": true,
"message": "资料更新成功",
"profile": {
"bio": "...",
"birthday": "1990-01-01",
"tags": "摄影,美食"
}
}端点: POST /api/v1/users/{id}/avatar/
权限: 本人或管理员
请求:
Content-Type: multipart/form-data
avatar: <file>
响应 (200 OK):
{
"success": true,
"message": "头像上传成功",
"avatar_url": "https://cos.ap-beijing.myqcloud.com/..."
}端点: GET /api/v1/trips/
权限: 匿名
查询参数:
visibility: 可见性筛选(public | private)author: 作者IDstatus: 状态筛选(draft | published)page: 页码page_size: 每页数量
响应 (200 OK):
{
"count": 50,
"next": "https://roamio.com/api/v1/trips/?page=2",
"previous": null,
"results": [
{
"slug": "abc123def456",
"title": "云南秘境探索",
"description": "一场说走就走的旅行",
"icon": "🏔️",
"author": {
"id": 1,
"username": "张三",
"avatar": "https://..."
},
"start_date": "2025-11-15",
"end_date": "2025-11-20",
"days_count": 6,
"status": "published",
"visibility": "public",
"views": 1234,
"likes": 56,
"checked_in": true,
"comments_count": 23,
"created_at": "2025-11-01T10:00:00Z"
}
]
}端点: GET /api/v1/trips/{slug}/
权限: 匿名(公开),本人或管理员(私有)
响应 (200 OK):
{
"slug": "abc123def456",
"title": "云南秘境探索",
"description": "一场说走就走的旅行...",
"icon": "🏔️",
"author": {
"id": 1,
"username": "张三",
"avatar": "https://..."
},
"start_date": "2025-11-15",
"end_date": "2025-11-20",
"days_count": 6,
"status": "published",
"visibility": "public",
"config": {
"enabledModules": ["basicInfo", "highlights", "itinerary", "budget", "tips"]
},
"overview": {
"basicInfo": {
"destination": "云南",
"duration": "6天5晚",
"budget": 5000
},
"highlights": [
"香格里拉",
"丽江古城",
"泸沽湖"
],
"itinerary": [
{
"day": 1,
"title": "抵达昆明",
"activities": ["机场接机", "酒店入住"]
}
]
},
"theme_color": "#f0e68c",
"background_music": "https://...",
"views": 1235, // 自动+1
"likes": 56,
"checked_in": true,
"comments_count": 23,
"created_at": "2025-11-01T10:00:00Z",
"updated_at": "2025-11-06T15:30:00Z"
}端点: POST /api/v1/trip-plans/
权限: 已认证
请求:
{
"title": "string",
"description": "string (可选)",
"icon": "🗺️ (可选)",
"start_date": "2025-11-15 (可选)",
"end_date": "2025-11-20 (可选)",
"visibility": "private", // private | public
"status": "draft", // draft | published
"config": {
"enabledModules": ["basicInfo", "highlights"]
},
"overview": {}
}响应 (201 Created):
{
"slug": "abc123def456", // 自动生成
"title": "云南之旅",
"author": 1,
"created_at": "2025-11-07T10:00:00Z"
}端点: PATCH /api/v1/trip-plans/{slug}/
权限: 作者或管理员
请求:
{
"title": "新标题 (可选)",
"description": "新描述 (可选)",
"overview": { // 部分更新
"basicInfo": {
"destination": "云南",
"duration": "6天5晚"
}
}
}响应 (200 OK):
{
"slug": "abc123def456",
"title": "新标题",
"updated_at": "2025-11-07T11:00:00Z"
}端点: DELETE /api/v1/trip-plans/{slug}/
权限: 作者或管理员
响应 (204 No Content)
端点: POST /api/v1/trips/{slug}/like/
权限: 匿名
响应 (200 OK):
{
"likes": 57
}端点: POST /api/v1/trips/{slug}/checkin/
权限: 匿名
响应 (200 OK):
{
"checked_in": true
}端点: GET /api/v1/trips/{slug}/stats/
权限: 匿名
响应 (200 OK):
{
"page": "trip_abc123",
"views": 1234,
"likes": 56,
"checked_in": true,
"comments_count": 23
}端点: GET /api/v1/comments/
权限: 匿名
查询参数:
page_filter: 筛选页面(如 "trip_abc123")parent_id: 筛选回复(父评论ID)page: 页码page_size: 每页数量
响应 (200 OK):
{
"count": 50,
"results": [
{
"id": 1,
"user": {
"id": 2,
"username": "李四",
"avatar": "https://..."
},
"content": "写得真好!",
"image": "https://...",
"video": null,
"page": "trip_abc123",
"parent": null,
"replies_count": 3,
"likes": 12,
"liked_by_me": false,
"is_pinned": false,
"timestamp": "2025-11-06T10:00:00Z"
}
]
}端点: POST /api/v1/comments/
权限: 已认证
请求:
{
"content": "评论内容",
"page": "trip_abc123",
"parent": null, // 父评论ID(回复时填写)
"image": "https://... (可选)",
"video": "https://... (可选)"
}响应 (201 Created):
{
"id": 1,
"user": {
"id": 1,
"username": "张三"
},
"content": "评论内容",
"timestamp": "2025-11-07T12:00:00Z"
}端点: POST /api/v1/comments/{id}/like/
权限: 已认证
响应 (200 OK):
{
"success": true,
"likes": 13,
"liked_by_me": true
}端点: DELETE /api/v1/comments/{id}/
权限: 作者或管理员
响应 (204 No Content)
Base URL: https://app7626.acapp.acwing.com.cn/api/
端点: POST /api/auth/register/
权限: 匿名
请求:
{
"username": "string",
"password": "string",
"email": "email@example.com (可选)"
}响应 (201 Created):
{
"id": 1,
"username": "张三",
"email": "user@example.com"
}端点: POST /api/auth/login/
权限: 匿名
请求:
{
"username": "string",
"password": "string"
}响应 (200 OK):
{
"access": "eyJ0eXAiOiJKV1QiLCJhbGc...",
"refresh": "eyJ0eXAiOiJKV1QiLCJhbGc...",
"user": {
"id": 1,
"username": "张三",
"email": "user@example.com",
"photo": "https://..." // AcWing/QQ头像
}
}端点: GET /api/auth/me/
权限: 已认证
响应 (200 OK):
{
"id": 1,
"username": "张三",
"email": "user@example.com",
"photo": "https://..."
}端点: POST /api/auth/acwing/login/
权限: 匿名
请求:
{
"code": "authorization_code",
"state": "state_string (可选)"
}响应 (200 OK):
{
"access": "eyJ0eXAiOiJKV1QiLCJhbGc...",
"refresh": "eyJ0eXAiOiJKV1QiLCJhbGc...",
"user": {
"id": 1,
"username": "AcWing用户",
"email": "",
"photo": "https://cdn.acwing.com/..."
}
}端点: GET /api/oauth2/receive_code/
权限: 匿名
查询参数:
code: 授权码state: 状态码
响应 (200 OK):
{
"code": "authorization_code",
"state": "state_string"
}说明:此端点专为 AcWingOS API 设计,返回纯 JSON(不是 HTML)
端点: POST /api/auth/qq/login/
权限: 匿名
请求:
{
"code": "authorization_code",
"state": "state_string (可选)"
}响应 (200 OK):
{
"access": "eyJ0eXAiOiJKV1QiLCJhbGc...",
"refresh": "eyJ0eXAiOiJKV1QiLCJhbGc...",
"user": {
"id": 1,
"username": "QQ用户",
"email": "",
"photo": "http://qzapp.qlogo.cn/..."
}
}端点: GET /api/user/stats/
权限: 已认证
响应 (200 OK):
{
"total_events": 45,
"today_events": 3,
"upcoming_events": 12 // 未来7天
}端点: GET /api/user/bindings/
权限: 已认证
响应 (200 OK):
{
"has_acwing": true,
"has_qq": true,
"has_password": true,
"acwing_username": "AcWing用户",
"qq_nickname": "QQ昵称"
}端点: PATCH /api/user/profile/
权限: 已认证
请求:
{
"username": "新用户名 (可选)",
"email": "new@example.com (可选)"
}响应 (200 OK):
{
"id": 1,
"username": "新用户名",
"email": "new@example.com"
}端点: POST /api/user/change-password/
权限: 已认证
请求:
{
"old_password": "string",
"new_password": "string"
}响应 (200 OK):
{
"message": "密码修改成功,请重新登录"
}说明:OAuth 账号(无密码)无法调用此接口
端点: DELETE /api/user/unbind/acwing/
权限: 已认证
响应 (200 OK):
{
"message": "AcWing 账号已解绑"
}错误 (400 Bad Request):
{
"error": "至少保留一种登录方式"
}端点: DELETE /api/user/unbind/qq/
权限: 已认证
响应 (200 OK):
{
"message": "QQ 账号已解绑"
}端点: GET /api/events/
权限: 已认证(自己的日程),匿名只读(公开日程)
查询参数:
start_date: 开始日期(YYYY-MM-DD)end_date: 结束日期(YYYY-MM-DD)page: 页码page_size: 每页数量
响应 (200 OK):
[
{
"id": 1,
"user": 1,
"title": "团队会议",
"description": "讨论Q4计划",
"start_time": "2025-11-07T14:00:00Z",
"end_time": "2025-11-07T15:30:00Z",
"location": "公司会议室",
"reminder_minutes": 15,
"created_at": "2025-11-01T10:00:00Z",
"updated_at": "2025-11-06T15:30:00Z"
}
]端点: GET /api/events/{id}/
权限: 本人
响应 (200 OK):
{
"id": 1,
"user": 1,
"title": "团队会议",
"description": "讨论Q4计划",
"start_time": "2025-11-07T14:00:00Z",
"end_time": "2025-11-07T15:30:00Z",
"location": "公司会议室",
"reminder_minutes": 15,
"created_at": "2025-11-01T10:00:00Z",
"updated_at": "2025-11-06T15:30:00Z"
}端点: POST /api/events/
权限: 已认证
请求:
{
"title": "string",
"description": "string (可选)",
"start_time": "2025-11-07T14:00:00Z",
"end_time": "2025-11-07T15:30:00Z (可选)",
"location": "string (可选)",
"reminder_minutes": 15 // 0, 5, 15, 30, 60, 1440, 10080
}响应 (201 Created):
{
"id": 1,
"title": "团队会议",
"start_time": "2025-11-07T14:00:00Z",
"created_at": "2025-11-07T10:00:00Z"
}端点: PUT /api/events/{id}/ 或 PATCH /api/events/{id}/
权限: 本人
请求:
{
"title": "新标题 (可选)",
"start_time": "2025-11-07T15:00:00Z (可选)"
}响应 (200 OK):
{
"id": 1,
"title": "新标题",
"updated_at": "2025-11-07T12:00:00Z"
}端点: DELETE /api/events/{id}/
权限: 本人
响应 (204 No Content)
端点: GET /api/calendars/
权限: 匿名
响应 (200 OK):
[
{
"id": 1,
"name": "中国法定假日",
"url_slug": "china-holidays",
"description": "2025年中国法定假日日历",
"is_public": true,
"created_by": 1,
"events_count": 11,
"created_at": "2025-01-01T00:00:00Z"
}
]端点: POST /api/calendars/
权限: 已认证
请求:
{
"name": "string",
"url_slug": "unique-slug",
"description": "string (可选)",
"is_public": true
}响应 (201 Created):
{
"id": 1,
"name": "我的课程表",
"url_slug": "my-schedule",
"created_at": "2025-11-07T10:00:00Z"
}端点: POST /api/calendars/{id}/subscribe/
权限: 已认证
响应 (200 OK):
{
"success": true,
"message": "订阅成功",
"synced_events_count": 11
}端点: GET /api/lunar/
权限: 匿名
查询参数:
year: 年份(如 2025)month: 月份(1-12)day: 日期(1-31)
响应 (200 OK):
{
"year": 2025,
"month": 9,
"day": 16,
"isleap": false,
"lunarDate": "2025年九月十六"
}端点: POST /api/v1/trips/{slug}/sync-to-calendar/
权限: 作者
请求:
{
"create_events": true, // 是否创建日程
"calendar_api_base": "https://ralendar.com/api/" // Ralendar API 地址
}响应 (200 OK):
{
"success": true,
"synced_events": [
{
"id": 1,
"title": "飞机起飞",
"start_time": "2025-11-15T10:00:00Z"
}
],
"synced_count": 3
}实现逻辑:
def sync_to_calendar(trip):
events = []
# 从旅行计划提取日程
if trip.overview.get('itinerary'):
for item in trip.overview['itinerary']:
event = {
'title': item['title'],
'start_time': item['time'],
'location': item.get('location', ''),
'description': f"来自旅行:{trip.title}"
}
events.append(event)
# 调用 Ralendar API 批量创建
# POST /api/events/batch/
return events端点: PATCH /api/events/{id}/link-trip/
权限: 本人
请求:
{
"trip_slug": "abc123def456",
"roamio_api_base": "https://roamio.com/api/v1/"
}响应 (200 OK):
{
"success": true,
"event_id": 1,
"trip_slug": "abc123def456",
"linked": true
}端点: POST /api/sync/user-info/
权限: 系统级(需要 API Key)
请求:
{
"user_id": 1,
"source": "roamio", // roamio | ralendar
"data": {
"username": "张三",
"email": "user@example.com",
"avatar": "https://..."
}
}响应 (200 OK):
{
"success": true,
"synced_fields": ["username", "email", "avatar"]
}User (Django 内置)
├── UserProfile (一对一)
│ ├── avatar
│ ├── bio
│ ├── level
│ └── email_verified
├── SocialAccount (一对多)
│ ├── provider (qq/wechat/github)
│ ├── uid
│ └── avatar_url
├── Trip (一对多)
│ ├── slug
│ ├── title
│ ├── overview (JSON)
│ └── config (JSON)
└── Comment (一对多)
├── content
├── page
└── parent (自关联)User (Django 内置) - 与 Roamio 共享
├── AcWingUser (一对一)
│ ├── openid
│ └── photo_url
├── QQUser (一对一)
│ ├── openid
│ └── nickname
├── Event (一对多)
│ ├── title
│ ├── start_time
│ ├── end_time (可选)
│ ├── location
│ └── reminder_minutes
└── PublicCalendar (一对多)
├── name
├── url_slug
└── events (多对多)# Ralendar Event 扩展
class Event(models.Model):
# 原有字段
user = ForeignKey(User)
title = CharField()
start_time = DateTimeField()
# 新增:关联旅行
related_trip_slug = CharField(null=True, blank=True) # Roamio Trip slug
sync_from = CharField(choices=['manual', 'roamio']) # 来源
# Roamio Trip 扩展
class Trip(models.Model):
# 原有字段
slug = SlugField()
title = CharField()
# 新增:关联日程
synced_to_calendar = BooleanField(default=False)
calendar_events_ids = JSONField(default=list) # Ralendar Event IDs- 资源名词化:
/api/events/而不是/api/get-events/ - HTTP 方法语义化:GET 查询,POST 创建,PATCH 部分更新,PUT 完整更新,DELETE 删除
- 状态码标准化:200 成功,201 创建,400 客户端错误,401 未认证,403 无权限,404 未找到,500 服务器错误
成功响应:
{
"data": {...},
"message": "操作成功 (可选)"
}错误响应:
{
"error": "错误描述",
"error_code": "ERROR_CODE (可选)",
"details": {...} (可选)
}查询参数:
page: 页码(从 1 开始)page_size: 每页数量(默认 20,最大 100)
响应格式:
{
"count": 100,
"next": "https://.../api/resource/?page=2",
"previous": null,
"results": [...]
}过滤参数:
- 字段名直接作为查询参数:
?status=published&visibility=public - 时间范围:
?start_date=2025-11-01&end_date=2025-11-30
排序参数:
ordering: 排序字段,加-表示降序- 示例:
?ordering=-created_at(最新的在前)
标准格式: ISO 8601
- 日期时间:
2025-11-07T14:30:00Z(UTC) - 日期:
2025-11-07 - 时间:
14:30:00
# roamio/backend/utils/ralendar_client.py
import requests
from django.conf import settings
class RalendarClient:
"""Ralendar API 客户端"""
def __init__(self, user_token=None):
self.base_url = settings.RALENDAR_API_BASE
self.token = user_token
def get_headers(self):
"""获取请求头"""
headers = {'Content-Type': 'application/json'}
if self.token:
headers['Authorization'] = f'Bearer {self.token}'
return headers
def create_event(self, event_data):
"""创建日程"""
url = f'{self.base_url}/events/'
response = requests.post(
url,
json=event_data,
headers=self.get_headers(),
timeout=10
)
return response.json()
def sync_trip_events(self, trip, user_token):
"""同步旅行计划到日历"""
events = []
# 从旅行计划提取日程
if trip.overview.get('itinerary'):
for item in trip.overview['itinerary']:
event_data = {
'title': item['title'],
'start_time': item['time'],
'location': item.get('location', ''),
'description': f"来自旅行:{trip.title}",
'related_trip_slug': trip.slug
}
result = self.create_event(event_data)
events.append(result)
return events
# 使用示例
client = RalendarClient(user_token=request.user.get_jwt_token())
synced_events = client.sync_trip_events(trip, user_token)# settings.py(两个项目配置相同数据库)
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': 'roamio_ecosystem', # 共享数据库
'USER': 'roamio_user',
'PASSWORD': 'password',
'HOST': 'localhost',
'PORT': '5432',
}
}
# JWT 配置(两个项目使用相同的 SECRET_KEY)
SECRET_KEY = 'same-secret-key-for-both-projects'
# 这样生成的 JWT Token 可以在两个项目中互通优势:
- ✅ 一个账号,两个产品
- ✅ Token 互通
- ✅ 用户信息同步
# ralendar/backend/api/middleware/roamio_auth.py
class RoamioTokenMiddleware:
"""验证来自 Roamio 的 Token"""
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
# 检查是否来自 Roamio
if request.META.get('HTTP_X_SOURCE') == 'roamio':
token = request.META.get('HTTP_X_ROAMIO_TOKEN')
# 调用 Roamio API 验证 Token
user = self.verify_roamio_token(token)
if user:
request.user = user
return self.get_response(request)
def verify_roamio_token(self, token):
"""调用 Roamio API 验证 Token"""
response = requests.get(
'https://roamio.com/api/v1/auth/verify-token/',
headers={'Authorization': f'Bearer {token}'}
)
if response.status_code == 200:
user_data = response.json()
# 在本地创建/更新用户
return self.get_or_create_user(user_data)
return None# roamio/backend/utils/webhooks.py
class WebhookService:
"""Webhook 服务"""
@staticmethod
def trigger_event(event_type, data):
"""触发 Webhook 事件"""
webhooks = [
{
'name': 'ralendar',
'url': 'https://ralendar.com/api/webhooks/roamio/',
'events': ['trip.created', 'trip.updated', 'trip.deleted']
}
]
for webhook in webhooks:
if event_type in webhook['events']:
try:
requests.post(
webhook['url'],
json={
'event': event_type,
'data': data,
'timestamp': timezone.now().isoformat()
},
headers={
'X-Webhook-Signature': generate_signature(data)
},
timeout=5
)
except:
pass # 失败不影响主流程
# 使用示例
def create_trip(request):
trip = Trip.objects.create(...)
# 触发 Webhook
WebhookService.trigger_event('trip.created', {
'trip_id': trip.id,
'slug': trip.slug,
'title': trip.title,
'start_date': trip.start_date,
'end_date': trip.end_date
})
return Response(...)server {
listen 443 ssl;
server_name roamio.com;
# SSL 证书
ssl_certificate /path/to/cert.pem;
ssl_certificate_key /path/to/key.pem;
# Roamio 前端
location / {
root /var/www/roamio/dist;
try_files $uri $uri/ /index.html;
}
# Roamio API
location /api/v1/ {
proxy_pass http://127.0.0.1:8000/api/v1/;
proxy_set_header Host $host;
proxy_set_header Authorization $http_authorization;
}
# Ralendar 前端(子路径)
location /calendar/ {
root /var/www/ralendar/dist;
try_files $uri $uri/ /calendar/index.html;
}
# Ralendar API(代理到另一个端口)
location /api/calendar/ {
proxy_pass http://127.0.0.1:8001/api/;
proxy_set_header Host $host;
proxy_set_header Authorization $http_authorization;
}
# 静态文件
location /static/ {
alias /var/www/static/;
expires 30d;
}
}# roamio/.env
# Django
SECRET_KEY=your-secret-key-here
DEBUG=False
ALLOWED_HOSTS=roamio.com,*.roamio.com
# Database
DB_ENGINE=postgresql
DB_NAME=roamio_ecosystem
DB_USER=roamio_user
DB_PASSWORD=your-password
DB_HOST=localhost
DB_PORT=5432
# QQ OAuth
QQ_APPID=102814915
QQ_APPKEY=your-qq-appkey
# 腾讯云 COS
TENCENT_COS_SECRET_ID=your-secret-id
TENCENT_COS_SECRET_KEY=your-secret-key
TENCENT_COS_BUCKET=roamio-1234567
TENCENT_COS_REGION=ap-beijing
# 邮件服务
EMAIL_HOST=smtp.qq.com
EMAIL_PORT=587
EMAIL_HOST_USER=your-email@qq.com
EMAIL_HOST_PASSWORD=your-smtp-password
# Ralendar API(用于跨项目调用)
RALENDAR_API_BASE=https://roamio.com/api/calendar/# ralendar/.env
# Django
SECRET_KEY=same-as-roamio # 如果共享JWT,必须相同
DEBUG=False
ALLOWED_HOSTS=roamio.com,app7626.acapp.acwing.com.cn
# Database(可以共享 Roamio 的数据库)
DB_ENGINE=postgresql
DB_NAME=roamio_ecosystem # 与 Roamio 共享
DB_USER=roamio_user
DB_PASSWORD=your-password
DB_HOST=localhost
DB_PORT=5432
# AcWing OAuth
ACWING_APPID=7626
ACWING_SECRET=your-acwing-secret
# QQ OAuth
QQ_APPID=102818448
QQ_APPKEY=your-qq-appkey
# Roamio API(用于跨项目调用)
ROAMIO_API_BASE=https://roamio.com/api/v1/# roamio/backend/views/trip_views.py
from ..utils.ralendar_client import RalendarClient
class TripViewSet(viewsets.ModelViewSet):
@action(detail=True, methods=['post'])
def sync_to_calendar(self, request, slug=None):
"""同步旅行计划到日历"""
trip = self.get_object()
# 获取用户的 JWT Token
auth_header = request.META.get('HTTP_AUTHORIZATION', '')
user_token = auth_header.replace('Bearer ', '')
# 初始化 Ralendar 客户端
ralendar = RalendarClient(user_token=user_token)
# 提取行程中的事件
events_to_create = []
if trip.overview.get('itinerary'):
for day_item in trip.overview['itinerary']:
for activity in day_item.get('activities', []):
event = {
'title': f"{trip.title} - {activity['name']}",
'start_time': f"{trip.start_date}T{activity.get('time', '09:00')}:00Z",
'location': activity.get('location', ''),
'description': f"来自旅行计划:{trip.title}",
'reminder_minutes': 60 # 提前1小时提醒
}
events_to_create.append(event)
# 批量创建日程
synced_events = []
for event_data in events_to_create:
try:
result = ralendar.create_event(event_data)
synced_events.append(result)
except Exception as e:
print(f"Failed to sync event: {e}")
# 记录同步状态
trip.synced_to_calendar = True
trip.calendar_events_ids = [e['id'] for e in synced_events]
trip.save()
return Response({
'success': True,
'synced_events_count': len(synced_events),
'events': synced_events
})# ralendar/backend/api/views/events.py
from ..utils.roamio_client import RoamioClient
class EventViewSet(viewsets.ModelViewSet):
@action(detail=True, methods=['get'])
def related_trip(self, request, pk=None):
"""获取关联的旅行信息"""
event = self.get_object()
if not event.related_trip_slug:
return Response({
'has_related_trip': False
})
# 调用 Roamio API 获取旅行信息
auth_header = request.META.get('HTTP_AUTHORIZATION', '')
user_token = auth_header.replace('Bearer ', '')
roamio = RoamioClient(user_token=user_token)
trip_info = roamio.get_trip(event.related_trip_slug)
if trip_info:
return Response({
'has_related_trip': True,
'trip': {
'slug': trip_info['slug'],
'title': trip_info['title'],
'start_date': trip_info['start_date'],
'end_date': trip_info['end_date'],
'url': f'https://roamio.com/trips/{trip_info["slug"]}/'
}
})
return Response({
'has_related_trip': False,
'error': '关联的旅行不存在或无权访问'
})| 端点 | 方法 | 功能 | 权限 |
|---|---|---|---|
/api/v1/auth/register/ |
POST | 用户注册 | 匿名 |
/api/v1/auth/login/ |
POST | 用户登录 | 匿名 |
/api/v1/auth/me/ |
GET | 获取当前用户 | 已认证 |
/api/v1/auth/logout/ |
POST | 用户登出 | 已认证 |
/api/v1/auth/send_verification_code/ |
POST | 发送验证码 | 匿名 |
/api/v1/auth/verify_code/ |
POST | 验证验证码 | 匿名 |
/api/v1/auth/qq_login_url/ |
GET | 获取QQ登录URL | 匿名 |
/api/v1/auth/qq_callback/ |
POST | QQ登录回调 | 匿名 |
/api/v1/auth/qq_bind_existing/ |
POST | 绑定QQ(已有账号) | 已认证 |
/api/v1/auth/qq_unbind/ |
DELETE | 解绑QQ | 已认证 |
/api/v1/auth/reset_password/ |
POST | 重置密码 | 匿名 |
/api/v1/users/ |
GET | 获取用户列表 | 匿名 |
/api/v1/users/{id}/ |
GET | 获取用户详情 | 匿名 |
/api/v1/users/{id}/profile/ |
PATCH | 更新个人资料 | 本人 |
/api/v1/users/{id}/avatar/ |
POST | 上传头像 | 本人 |
/api/v1/trips/ |
GET | 获取旅行列表 | 匿名 |
/api/v1/trips/{slug}/ |
GET | 获取旅行详情 | 条件 |
/api/v1/trips/{slug}/like/ |
POST | 点赞 | 匿名 |
/api/v1/trips/{slug}/checkin/ |
POST | 打卡 | 匿名 |
/api/v1/trips/{slug}/stats/ |
GET | 获取统计 | 匿名 |
/api/v1/trips/{slug}/comments/ |
GET | 获取评论 | 匿名 |
/api/v1/trip-plans/ |
GET, POST | 旅行计划CRUD | 条件 |
/api/v1/trip-plans/{slug}/ |
GET, PATCH, DELETE | 旅行计划操作 | 作者 |
/api/v1/comments/ |
GET, POST | 评论CRUD | 条件 |
/api/v1/comments/{id}/ |
GET, DELETE | 评论操作 | 条件 |
/api/v1/comments/{id}/like/ |
POST | 点赞评论 | 已认证 |
/api/v1/token/ |
POST | 获取Token | 匿名 |
/api/v1/token/refresh/ |
POST | 刷新Token | 匿名 |
总计: 30+ 个端点
| 端点 | 方法 | 功能 | 权限 |
|---|---|---|---|
/api/auth/register/ |
POST | 用户注册 | 匿名 |
/api/auth/login/ |
POST | 用户登录 | 匿名 |
/api/auth/refresh/ |
POST | 刷新Token | 匿名 |
/api/auth/me/ |
GET | 获取当前用户 | 已认证 |
/api/auth/acwing/login/ |
POST | AcWing登录 | 匿名 |
/api/oauth2/receive_code/ |
GET | AcWing回调 | 匿名 |
/api/auth/qq/login/ |
POST | QQ登录 | 匿名 |
/api/user/stats/ |
GET | 用户统计 | 已认证 |
/api/user/bindings/ |
GET | 绑定状态 | 已认证 |
/api/user/profile/ |
PATCH | 更新资料 | 已认证 |
/api/user/change-password/ |
POST | 修改密码 | 已认证 |
/api/user/unbind/acwing/ |
DELETE | 解绑AcWing | 已认证 |
/api/user/unbind/qq/ |
DELETE | 解绑QQ | 已认证 |
/api/events/ |
GET, POST | 日程CRUD | 条件 |
/api/events/{id}/ |
GET, PUT, PATCH, DELETE | 日程操作 | 本人 |
/api/calendars/ |
GET, POST | 公开日历CRUD | 条件 |
/api/calendars/{id}/ |
GET, PUT, PATCH, DELETE | 日历操作 | 条件 |
/api/lunar/ |
GET | 农历转换 | 匿名 |
总计: 20+ 个端点
统一错误格式:
{
"error": "错误描述",
"error_code": "INVALID_INPUT",
"details": {
"field": "具体字段错误"
}
}常见错误码:
INVALID_INPUT- 输入无效UNAUTHORIZED- 未认证FORBIDDEN- 无权限NOT_FOUND- 资源不存在RATE_LIMIT_EXCEEDED- 超过频率限制INTERNAL_ERROR- 服务器内部错误
URL 版本化:
- Roamio:
/api/v1/... - Ralendar:
/api/...(当前版本) - 未来:
/api/v2/...(向后兼容)
废弃策略:
- 提前3个月通知
- 返回
Deprecated头 - 提供迁移指南
分页:
- 所有列表接口必须分页
- 默认 20 条/页
- 最大 100 条/页
缓存:
- 使用 Redis 缓存热点数据
- 旅行详情缓存 5 分钟
- 评论列表缓存 1 分钟
查询优化:
- 使用
select_related预加载外键 - 使用
prefetch_related预加载多对多 - 添加数据库索引
CORS 配置:
CORS_ALLOWED_ORIGINS = [
'https://roamio.com',
'https://www.roamio.com',
'https://app7626.acapp.acwing.com.cn',
'https://www.acwing.com',
]频率限制:
- 匿名用户:100 请求/小时
- 已认证用户:1000 请求/小时
- 敏感操作(发送验证码):特殊限制
SQL 注入防护:
- 使用 Django ORM(自动转义)
- 避免原始 SQL 查询
XSS 防护:
- 用户输入内容转义
- 使用 DRF Serializer 验证
ROAMIO_BASE_URL = https://roamio.com
RALENDAR_BASE_URL = https://app7626.acapp.acwing.com.cn
ACCESS_TOKEN = (登录后获取)
1. 注册/登录
POST {{ROAMIO_BASE_URL}}/api/v1/auth/register/
→ 获取 access_token
2. 创建旅行
POST {{ROAMIO_BASE_URL}}/api/v1/trip-plans/
Headers: Authorization: Bearer {{ACCESS_TOKEN}}
3. 同步到日历
POST {{ROAMIO_BASE_URL}}/api/v1/trips/{slug}/sync-to-calendar/
4. 查看日程
GET {{RALENDAR_BASE_URL}}/api/events/
Headers: Authorization: Bearer {{ACCESS_TOKEN}}
Roamio: https://roamio.com/api/docs/
Ralendar: (待集成 drf-yasg)
端点: GET /api/v1/recommendations/trips/
功能: 基于用户历史推荐旅行
端点: POST /api/events/{id}/geocode/
功能: 地址 → 坐标转换
端点: POST /api/ai/create-event/
功能: 语音转文字 → 创建日程
端点: GET /api/analytics/user-insights/
功能: 用户行为分析
- Roamio 项目: https://github.com/yourusername/roamio
- Ralendar 项目: https://github.com/yourusername/kotlin-calendar
- API 在线文档: https://roamio.com/api/docs/
- 问题反馈: https://github.com/yourusername/roamio/issues
技术支持: tech@roamio.com
商务合作: business@roamio.com
API 文档维护: Roamio Team
最后更新: 2025-11-07
文档版本: v1.0.0
作者: Roamio Team
© 2025 Roamio. 用心打造有温度的数字生活系统。 🌏✨