# 自定义挑战 API 接口文档 **版本**: v1.0 **更新日期**: 2025-01-25 **基础URL**: `https://your-domain.com/api` ## 目录 - [概述](#概述) - [认证](#认证) - [数据模型](#数据模型) - [API 接口](#api-接口) - [创建自定义挑战](#1-创建自定义挑战) - [通过分享码加入挑战](#2-通过分享码加入挑战) - [获取分享码对应的挑战信息](#3-获取分享码对应的挑战信息) - [获取我创建的挑战列表](#4-获取我创建的挑战列表) - [更新自定义挑战](#5-更新自定义挑战) - [归档自定义挑战](#6-归档自定义挑战) - [重新生成分享码](#7-重新生成分享码) - [错误码说明](#错误码说明) - [客户端集成示例](#客户端集成示例) --- ## 概述 自定义挑战功能允许用户创建自己的挑战,并通过分享码邀请其他用户参与。该功能完全兼容现有的系统挑战。 ### 核心特性 - ✅ 用户可以自由创建挑战 - ✅ 自动生成6位唯一分享码 - ✅ 支持公开和私密两种模式 - ✅ 可设置参与人数限制 - ✅ 完全兼容现有的打卡、排行榜系统 --- ## 认证 大部分接口需要用户认证。在请求头中添加 JWT Token: ```http Authorization: Bearer {your_jwt_token} ``` **公开接口**(无需认证): - 获取分享码对应的挑战信息 --- ## 数据模型 ### ChallengeType(挑战类型) ```typescript enum ChallengeType { WATER = "water", // 喝水 EXERCISE = "exercise", // 运动 DIET = "diet", // 饮食 MOOD = "mood", // 心情 SLEEP = "sleep", // 睡眠 WEIGHT = "weight", // 体重 } ``` ### ChallengeSource(挑战来源) ```typescript enum ChallengeSource { SYSTEM = "system", // 系统预设挑战 CUSTOM = "custom", // 用户自定义挑战 } ``` ### ChallengeState(挑战状态) ```typescript enum ChallengeState { DRAFT = "draft", // 草稿(预留) ACTIVE = "active", // 活跃 ARCHIVED = "archived", // 已归档 } ``` ### CustomChallengeResponse(自定义挑战响应) ```typescript interface CustomChallengeResponse { id: string; // 挑战ID title: string; // 挑战标题 type: ChallengeType; // 挑战类型 source: ChallengeSource; // 挑战来源 creatorId: string | null; // 创建者ID shareCode: string | null; // 分享码(6位大写字母和数字) image: string | null; // 封面图URL startAt: number; // 开始时间戳(毫秒) endAt: number; // 结束时间戳(毫秒) periodLabel: string | null; // 周期标签,如"21天挑战" durationLabel: string; // 持续时间标签,如"持续21天" requirementLabel: string; // 要求标签,如"每日喝水8杯" summary: string | null; // 挑战说明 targetValue: number; // 每日目标值 progressUnit: string; // 进度单位,默认"天" minimumCheckInDays: number; // 最少打卡天数 rankingDescription: string | null; // 排行榜描述 highlightTitle: string; // 高亮标题 highlightSubtitle: string; // 高亮副标题 ctaLabel: string; // CTA按钮文字 isPublic: boolean; // 是否公开 maxParticipants: number | null; // 最大参与人数(null=无限制) challengeState: ChallengeState; // 挑战状态 participantsCount: number; // 当前参与人数 progress?: { // 用户进度(仅加入后有值) completed: number; // 已完成天数 target: number; // 目标天数 remaining: number; // 剩余天数 checkedInToday: boolean; // 今日是否已打卡 }; isJoined: boolean; // 当前用户是否已加入 isCreator: boolean; // 当前用户是否为创建者 createdAt: Date; // 创建时间 updatedAt: Date; // 更新时间 } ``` --- ## API 接口 ### 1. 创建自定义挑战 创建一个新的自定义挑战,系统会自动生成唯一的分享码。 **接口地址**: `POST /challenges/custom` **是否需要认证**: ✅ 是 **请求头**: ```http Content-Type: application/json Authorization: Bearer {token} ``` **请求体**: ```json { "title": "21天喝水挑战", "type": "water", "image": "https://example.com/image.jpg", "startAt": 1704067200000, "endAt": 1705881600000, "targetValue": 8, "minimumCheckInDays": 21, "durationLabel": "持续21天", "requirementLabel": "每日喝水8杯", "summary": "坚持每天喝足8杯水,养成健康好习惯", "progressUnit": "天", "periodLabel": "21天挑战", "rankingDescription": "连续打卡榜", "highlightTitle": "坚持21天", "highlightSubtitle": "养成喝水好习惯", "ctaLabel": "立即加入", "isPublic": true, "maxParticipants": 100 } ``` **参数说明**: | 参数 | 类型 | 必填 | 说明 | | ------------------ | ------------- | ---- | ------------------------------------- | | title | string | ✅ | 挑战标题,最长100字符 | | type | ChallengeType | ✅ | 挑战类型 | | startAt | number | ✅ | 开始时间戳(毫秒),必须是未来时间 | | endAt | number | ✅ | 结束时间戳(毫秒),必须晚于开始时间 | | targetValue | number | ✅ | 每日目标值,1-1000 | | minimumCheckInDays | number | ✅ | 最少打卡天数,1-365 | | durationLabel | string | ✅ | 持续时间标签,最长128字符 | | requirementLabel | string | ✅ | 要求标签,最长255字符 | | image | string | ❌ | 封面图URL,最长512字符 | | summary | string | ❌ | 挑战说明 | | progressUnit | string | ❌ | 进度单位,默认"天",最长64字符 | | periodLabel | string | ❌ | 周期标签,最长128字符 | | rankingDescription | string | ❌ | 排行榜描述,最长255字符 | | highlightTitle | string | ❌ | 高亮标题,最长255字符 | | highlightSubtitle | string | ❌ | 高亮副标题,最长255字符 | | ctaLabel | string | ❌ | CTA按钮文字,最长128字符 | | isPublic | boolean | ❌ | 是否公开,默认true | | maxParticipants | number | ❌ | 最大参与人数,2-10000,null表示无限制 | **成功响应**: `200 OK` ```json { "code": 0, "message": "创建挑战成功", "data": { "id": "550e8400-e29b-41d4-a716-446655440000", "shareCode": "A3K9P2", "title": "21天喝水挑战", "type": "water", "source": "custom", "creatorId": "user_123", "isPublic": true, "maxParticipants": 100, "participantsCount": 0, "isJoined": false, "isCreator": true, ... } } ``` **错误响应**: ```json { "code": 1, "message": "每天最多创建 5 个挑战,请明天再试", "data": null } ``` --- ### 2. 通过分享码加入挑战 使用分享码加入他人创建的挑战。 **接口地址**: `POST /challenges/join-by-code` **是否需要认证**: ✅ 是 **请求头**: ```http Content-Type: application/json Authorization: Bearer {token} ``` **请求体**: ```json { "shareCode": "A3K9P2" } ``` **参数说明**: | 参数 | 类型 | 必填 | 说明 | | --------- | ------ | ---- | ------------------------------------ | | shareCode | string | ✅ | 6-12位分享码,只能包含大写字母和数字 | **成功响应**: `200 OK` ```json { "code": 0, "message": "加入挑战成功", "data": { "completed": 0, "target": 21, "remaining": 21, "checkedInToday": false } } ``` **错误响应**: ```json { "code": 1, "message": "分享码无效或挑战不存在", "data": null } ``` ```json { "code": 1, "message": "挑战人数已满", "data": null } ``` --- ### 3. 获取分享码对应的挑战信息 通过分享码查看挑战详情(公开接口,无需登录)。 **接口地址**: `GET /challenges/share/{shareCode}` **是否需要认证**: ❌ 否(但提供token可获取更多信息) **路径参数**: - `shareCode`: 分享码,如 `A3K9P2` **请求示例**: ```http GET /challenges/share/A3K9P2 ``` **成功响应**: `200 OK` ```json { "code": 0, "message": "获取挑战信息成功", "data": { "id": "550e8400-e29b-41d4-a716-446655440000", "title": "21天喝水挑战", "image": "https://example.com/image.jpg", "periodLabel": "21天挑战", "durationLabel": "持续21天", "requirementLabel": "每日喝水8杯", "summary": "坚持每天喝足8杯水", "rankingDescription": "连续打卡榜", "highlightTitle": "坚持21天", "highlightSubtitle": "养成喝水好习惯", "ctaLabel": "立即加入", "minimumCheckInDays": 21, "participantsCount": 15, "progress": null, "rankings": [...], "userRank": null, "unit": "天", "type": "water" } } ``` **错误响应**: ```json { "code": 1, "message": "分享码无效或挑战不存在", "data": null } ``` --- ### 4. 获取我创建的挑战列表 获取当前用户创建的所有挑战。 **接口地址**: `GET /challenges/my/created` **是否需要认证**: ✅ 是 **请求头**: ```http Authorization: Bearer {token} ``` **查询参数**: | 参数 | 类型 | 必填 | 说明 | | -------- | -------------- | ---- | ------------------------- | | page | number | ❌ | 页码,默认1 | | pageSize | number | ❌ | 每页数量,默认20,最大100 | | state | ChallengeState | ❌ | 挑战状态筛选 | **请求示例**: ```http GET /challenges/my/created?page=1&pageSize=20&state=active ``` **成功响应**: `200 OK` ```json { "code": 0, "message": "获取我创建的挑战列表成功", "data": { "items": [ { "id": "550e8400-e29b-41d4-a716-446655440000", "shareCode": "A3K9P2", "title": "21天喝水挑战", "type": "water", "source": "custom", "participantsCount": 15, "challengeState": "active", "isCreator": true, ... } ], "total": 5, "page": 1, "pageSize": 20 } } ``` --- ### 5. 更新自定义挑战 更新自定义挑战信息(仅创建者可操作)。 **接口地址**: `PUT /challenges/custom/{id}` **是否需要认证**: ✅ 是(仅创建者) **路径参数**: - `id`: 挑战ID **请求头**: ```http Content-Type: application/json Authorization: Bearer {token} ``` **请求体**: ```json { "title": "新的挑战标题", "image": "https://example.com/new-image.jpg", "summary": "更新的挑战说明", "isPublic": false, "maxParticipants": 50, "highlightTitle": "新的高亮标题", "highlightSubtitle": "新的高亮副标题", "ctaLabel": "快来加入" } ``` **参数说明**: | 参数 | 类型 | 必填 | 说明 | | ----------------- | ------- | ---- | ------------ | | title | string | ❌ | 挑战标题 | | image | string | ❌ | 封面图URL | | summary | string | ❌ | 挑战说明 | | isPublic | boolean | ❌ | 是否公开 | | maxParticipants | number | ❌ | 最大参与人数 | | highlightTitle | string | ❌ | 高亮标题 | | highlightSubtitle | string | ❌ | 高亮副标题 | | ctaLabel | string | ❌ | CTA按钮文字 | **⚠️ 重要**: 挑战开始后,只能编辑以下字段: - summary(挑战说明) - isPublic(公开性) - highlightTitle(高亮标题) - highlightSubtitle(高亮副标题) - ctaLabel(CTA文字) **成功响应**: `200 OK` ```json { "code": 0, "message": "更新挑战成功", "data": { "id": "550e8400-e29b-41d4-a716-446655440000", "title": "新的挑战标题", ... } } ``` **错误响应**: ```json { "code": 1, "message": "只有创建者才能编辑挑战", "data": null } ``` ```json { "code": 1, "message": "挑战已开始,只能编辑概要、公开性和展示文案", "data": null } ``` --- ### 6. 归档自定义挑战 归档(软删除)自定义挑战(仅创建者可操作)。 **接口地址**: `DELETE /challenges/custom/{id}` **是否需要认证**: ✅ 是(仅创建者) **路径参数**: - `id`: 挑战ID **请求头**: ```http Authorization: Bearer {token} ``` **请求示例**: ```http DELETE /challenges/custom/550e8400-e29b-41d4-a716-446655440000 ``` **成功响应**: `200 OK` ```json { "code": 0, "message": "归档挑战成功", "data": true } ``` **错误响应**: ```json { "code": 1, "message": "只有创建者才能归档挑战", "data": null } ``` --- ### 7. 重新生成分享码 为挑战重新生成一个新的分享码(仅创建者可操作)。 **接口地址**: `POST /challenges/custom/{id}/regenerate-code` **是否需要认证**: ✅ 是(仅创建者) **路径参数**: - `id`: 挑战ID **请求头**: ```http Authorization: Bearer {token} ``` **请求示例**: ```http POST /challenges/custom/550e8400-e29b-41d4-a716-446655440000/regenerate-code ``` **成功响应**: `200 OK` ```json { "code": 0, "message": "重新生成分享码成功", "data": { "shareCode": "B7M4N9" } } ``` **使用场景**: - 分享码泄露,需要更换 - 想要限制旧分享码的传播 - 重新组织挑战参与者 --- ## 错误码说明 ### 通用错误码 | code | message | 说明 | | ---- | ------- | ------------------------------- | | 0 | Success | 请求成功 | | 1 | Error | 业务错误(message中有具体说明) | ### 常见业务错误 | 错误信息 | 说明 | 解决方案 | | -------------------------------------------- | ---------------------- | -------------------- | | "每天最多创建 5 个挑战,请明天再试" | 创建频率限制 | 提示用户明天再试 | | "分享码无效或挑战不存在" | 分享码错误或挑战已归档 | 提示用户检查分享码 | | "挑战人数已满" | 达到最大参与人数 | 提示用户挑战已满 | | "挑战已过期,无法加入" | 挑战已结束 | 提示挑战已结束 | | "只有创建者才能编辑挑战" | 权限不足 | 提示只有创建者可操作 | | "挑战已开始,只能编辑概要、公开性和展示文案" | 限制编辑 | 提示可编辑字段 | | "已加入该挑战" | 重复加入 | 跳转到挑战详情页 | ### HTTP 状态码 | 状态码 | 说明 | | ------ | -------------------- | | 200 | 请求成功 | | 400 | 请求参数错误 | | 401 | 未授权(需要登录) | | 403 | 禁止访问(权限不足) | | 404 | 资源不存在 | | 500 | 服务器内部错误 | --- ## 客户端集成示例 ### Swift (iOS) #### 1. 创建挑战 ```swift struct CreateChallengeRequest: Codable { let title: String let type: String let startAt: Int64 let endAt: Int64 let targetValue: Int let minimumCheckInDays: Int let durationLabel: String let requirementLabel: String let isPublic: Bool } func createChallenge(request: CreateChallengeRequest) async throws -> CustomChallengeResponse { guard let url = URL(string: "\(baseURL)/challenges/custom") else { throw NetworkError.invalidURL } var urlRequest = URLRequest(url: url) urlRequest.httpMethod = "POST" urlRequest.setValue("application/json", forHTTPHeaderField: "Content-Type") urlRequest.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") urlRequest.httpBody = try JSONEncoder().encode(request) let (data, response) = try await URLSession.shared.data(for: urlRequest) guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else { throw NetworkError.requestFailed } let result = try JSONDecoder().decode(APIResponse.self, from: data) return result.data } ``` #### 2. 通过分享码加入 ```swift func joinByShareCode(_ shareCode: String) async throws { guard let url = URL(string: "\(baseURL)/challenges/join-by-code") else { throw NetworkError.invalidURL } var urlRequest = URLRequest(url: url) urlRequest.httpMethod = "POST" urlRequest.setValue("application/json", forHTTPHeaderField: "Content-Type") urlRequest.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") let body = ["shareCode": shareCode] urlRequest.httpBody = try JSONEncoder().encode(body) let (data, response) = try await URLSession.shared.data(for: urlRequest) guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else { throw NetworkError.requestFailed } let result = try JSONDecoder().decode(APIResponse.self, from: data) // Handle success } ``` #### 3. 分享功能 ```swift func shareChallengeCode(_ shareCode: String, title: String) { let message = "邀请你加入挑战:\(title)\n分享码:\(shareCode)" let activityVC = UIActivityViewController( activityItems: [message], applicationActivities: nil ) present(activityVC, animated: true) } ``` ### Kotlin (Android) #### 1. 创建挑战 ```kotlin data class CreateChallengeRequest( val title: String, val type: String, val startAt: Long, val endAt: Long, val targetValue: Int, val minimumCheckInDays: Int, val durationLabel: String, val requirementLabel: String, val isPublic: Boolean = true ) suspend fun createChallenge(request: CreateChallengeRequest): CustomChallengeResponse { return withContext(Dispatchers.IO) { val response = apiService.createChallenge(request) if (response.code == 0) { response.data } else { throw Exception(response.message) } } } ``` #### 2. 通过分享码加入 ```kotlin suspend fun joinByShareCode(shareCode: String): ChallengeProgress { return withContext(Dispatchers.IO) { val request = JoinByCodeRequest(shareCode) val response = apiService.joinByShareCode(request) if (response.code == 0) { response.data } else { throw Exception(response.message) } } } ``` #### 3. 分享功能 ```kotlin fun shareChallenge(context: Context, shareCode: String, title: String) { val message = "邀请你加入挑战:$title\n分享码:$shareCode" val shareIntent = Intent().apply { action = Intent.ACTION_SEND putExtra(Intent.EXTRA_TEXT, message) type = "text/plain" } context.startActivity(Intent.createChooser(shareIntent, "分享挑战")) } ``` ### TypeScript/JavaScript #### 1. API 客户端封装 ```typescript class ChallengesAPI { private baseURL: string; private token: string; constructor(baseURL: string, token: string) { this.baseURL = baseURL; this.token = token; } async createChallenge( data: CreateChallengeRequest ): Promise { const response = await fetch(`${this.baseURL}/challenges/custom`, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${this.token}`, }, body: JSON.stringify(data), }); const result = await response.json(); if (result.code !== 0) { throw new Error(result.message); } return result.data; } async joinByShareCode(shareCode: string): Promise { const response = await fetch(`${this.baseURL}/challenges/join-by-code`, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${this.token}`, }, body: JSON.stringify({ shareCode }), }); const result = await response.json(); if (result.code !== 0) { throw new Error(result.message); } return result.data; } async getChallengeByShareCode(shareCode: string): Promise { const response = await fetch( `${this.baseURL}/challenges/share/${shareCode}` ); const result = await response.json(); if (result.code !== 0) { throw new Error(result.message); } return result.data; } } ``` #### 2. 使用示例 ```typescript const api = new ChallengesAPI("https://api.example.com/api", userToken); // 创建挑战 try { const challenge = await api.createChallenge({ title: "21天喝水挑战", type: "water", startAt: Date.now(), endAt: Date.now() + 21 * 24 * 60 * 60 * 1000, targetValue: 8, minimumCheckInDays: 21, durationLabel: "持续21天", requirementLabel: "每日喝水8杯", isPublic: true, }); console.log("分享码:", challenge.shareCode); } catch (error) { console.error("创建失败:", error.message); } ``` --- ## 注意事项 ### 1. 时间戳格式 - 所有时间戳均为**毫秒级**(JavaScript: `Date.now()`) - 示例: `1704067200000`(2024-01-01 00:00:00) ### 2. 分享码规则 - 长度: 6-12位字符 - 字符集: 大写字母和数字(A-Z, 2-9) - 排除易混淆字符: 0/O, 1/I/l - 示例: `A3K9P2`, `B7M4N9` ### 3. 创建频率限制 - 每个用户每天最多创建 **5 个挑战** - 超出限制会返回错误,建议提示用户 ### 4. 人数限制 - `maxParticipants` 为 `null` 表示无限制 - 最小值: 2 人 - 最大值: 10000 人 ### 5. 编辑限制 -