feat(challenges): 更新自定义挑战功能,支持时间戳转换及数据模型调整

This commit is contained in:
richarjiang
2025-11-26 10:43:42 +08:00
parent 93b4fcf553
commit 029b8f46b9
4 changed files with 918 additions and 35 deletions

View File

@@ -0,0 +1,887 @@
# 自定义挑战 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-10000null表示无限制 |
**成功响应**: `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高亮副标题
- ctaLabelCTA文字
**成功响应**: `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. 编辑限制
-

View File

@@ -166,8 +166,8 @@ export class ChallengesService {
requirementLabel: challenge.requirementLabel, requirementLabel: challenge.requirementLabel,
status, status,
unit: challenge.progressUnit, unit: challenge.progressUnit,
startAt: challenge.startAt, startAt: new Date(challenge.startAt).getTime(),
endAt: challenge.endAt, endAt: new Date(challenge.endAt).getTime(),
participantsCount: participantsCountMap.get(challenge.id) ?? 0, participantsCount: participantsCountMap.get(challenge.id) ?? 0,
rankingDescription: challenge.rankingDescription, rankingDescription: challenge.rankingDescription,
highlightTitle: challenge.highlightTitle, highlightTitle: challenge.highlightTitle,
@@ -617,7 +617,7 @@ export class ChallengesService {
} }
private computeStatus(startAt: number, endAt: number): ChallengeStatus { private computeStatus(startAt: Date | number, endAt: Date | number): ChallengeStatus {
const now = dayjs(); const now = dayjs();
const start = dayjs(startAt); const start = dayjs(startAt);
const end = dayjs(endAt); const end = dayjs(endAt);
@@ -822,6 +822,15 @@ export class ChallengesService {
throw new BadRequestException('结束时间必须晚于开始时间'); throw new BadRequestException('结束时间必须晚于开始时间');
} }
// 将毫秒时间戳转换为 Date 对象,以匹配数据库 DATETIME 类型
const startAtDate = new Date(dto.startAt);
const endAtDate = new Date(dto.endAt);
// 验证日期是否有效
if (isNaN(startAtDate.getTime()) || isNaN(endAtDate.getTime())) {
throw new BadRequestException('无效的时间戳');
}
// 检查创建频率限制(每天最多创建 5 个) // 检查创建频率限制(每天最多创建 5 个)
const recentCount = await this.challengeModel.count({ const recentCount = await this.challengeModel.count({
where: { where: {
@@ -839,13 +848,14 @@ export class ChallengesService {
// 生成分享码 // 生成分享码
const shareCode = await this.generateUniqueShareCode(); const shareCode = await this.generateUniqueShareCode();
// 创建挑战 // 创建挑战
const challenge = await this.challengeModel.create({ const challenge = await this.challengeModel.create({
title: dto.title, title: dto.title,
type: dto.type, type: dto.type,
image: dto.image || null, image: dto.image || null,
startAt: dto.startAt, startAt: startAtDate,
endAt: dto.endAt, endAt: endAtDate,
periodLabel: dto.periodLabel || null, periodLabel: dto.periodLabel || null,
durationLabel: dto.durationLabel, durationLabel: dto.durationLabel,
requirementLabel: dto.requirementLabel, requirementLabel: dto.requirementLabel,
@@ -854,9 +864,9 @@ export class ChallengesService {
progressUnit: dto.progressUnit || '天', progressUnit: dto.progressUnit || '天',
minimumCheckInDays: dto.minimumCheckInDays, minimumCheckInDays: dto.minimumCheckInDays,
rankingDescription: dto.rankingDescription || '连续打卡榜', rankingDescription: dto.rankingDescription || '连续打卡榜',
highlightTitle: dto.highlightTitle || '坚持挑战', highlightTitle: dto.title,
highlightSubtitle: dto.highlightSubtitle || '养成好习惯', highlightSubtitle: dto.summary || '养成好习惯',
ctaLabel: dto.ctaLabel || '立即加入', ctaLabel: '立即加入',
source: ChallengeSource.CUSTOM, source: ChallengeSource.CUSTOM,
creatorId: userId, creatorId: userId,
shareCode, shareCode,
@@ -865,7 +875,10 @@ export class ChallengesService {
challengeState: ChallengeState.ACTIVE, challengeState: ChallengeState.ACTIVE,
}); });
this.winstonLogger.info('创建自定义挑战成功', { // 创建者自动加入挑战
await this.joinChallenge(userId, challenge.id);
this.winstonLogger.info('创建自定义挑战成功,创建者已自动加入', {
context: 'createCustomChallenge', context: 'createCustomChallenge',
userId, userId,
challengeId: challenge.id, challengeId: challenge.id,
@@ -1115,8 +1128,8 @@ export class ChallengesService {
creatorId: challenge.creatorId, creatorId: challenge.creatorId,
shareCode: challenge.shareCode, shareCode: challenge.shareCode,
image: challenge.image, image: challenge.image,
startAt: challenge.startAt, startAt: new Date(challenge.startAt).getTime(),
endAt: challenge.endAt, endAt: new Date(challenge.endAt).getTime(),
periodLabel: challenge.periodLabel, periodLabel: challenge.periodLabel,
durationLabel: challenge.durationLabel, durationLabel: challenge.durationLabel,
requirementLabel: challenge.requirementLabel, requirementLabel: challenge.requirementLabel,

View File

@@ -77,23 +77,6 @@ export class CreateCustomChallengeDto {
@MaxLength(255) @MaxLength(255)
rankingDescription?: string; rankingDescription?: string;
@ApiProperty({ description: '高亮标题', example: '坚持21天', required: false })
@IsString()
@IsOptional()
@MaxLength(255)
highlightTitle?: string;
@ApiProperty({ description: '高亮副标题', example: '养成好习惯', required: false })
@IsString()
@IsOptional()
@MaxLength(255)
highlightSubtitle?: string;
@ApiProperty({ description: 'CTA 按钮文字', example: '立即加入', required: false })
@IsString()
@IsOptional()
@MaxLength(128)
ctaLabel?: string;
@ApiProperty({ description: '是否公开(可通过分享码加入)', default: true }) @ApiProperty({ description: '是否公开(可通过分享码加入)', default: true })
@IsBoolean() @IsBoolean()

View File

@@ -55,18 +55,18 @@ export class Challenge extends Model {
declare image: string; declare image: string;
@Column({ @Column({
type: DataType.BIGINT, type: DataType.DATE,
allowNull: false, allowNull: false,
comment: '挑战开始时间(时间戳)', comment: '挑战开始时间',
}) })
declare startAt: number; declare startAt: Date;
@Column({ @Column({
type: DataType.BIGINT, type: DataType.DATE,
allowNull: false, allowNull: false,
comment: '挑战结束时间(时间戳)', comment: '挑战结束时间',
}) })
declare endAt: number; declare endAt: Date;
@Column({ @Column({
type: DataType.STRING(128), type: DataType.STRING(128),
@@ -142,7 +142,7 @@ export class Challenge extends Model {
@Column({ @Column({
type: DataType.STRING(128), type: DataType.STRING(128),
allowNull: false, allowNull: true,
comment: 'CTA 按钮文字', comment: 'CTA 按钮文字',
}) })
declare ctaLabel: string; declare ctaLabel: string;