888 lines
22 KiB
Markdown
888 lines
22 KiB
Markdown
# 自定义挑战 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<CustomChallengeResponse>.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<ChallengeProgress>.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<CustomChallengeResponse> {
|
||
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<ChallengeProgress> {
|
||
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<ChallengeDetail> {
|
||
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. 编辑限制
|
||
|
||
-
|