HarmonyOS应用<趣答>开发第2篇:User模型设计与用户数据管理——用户系统深度解析

📖 引言
在知识问答学习应用中,用户数据是连接所有功能的核心纽带。从用户登录、学习进度追踪,到成就系统、排行榜,都离不开完善的用户数据模型。
本文将详细讲解 User 模型的完整设计,包括用户基本信息、学习统计、成就管理、个性化设置等核心内容。通过本文,你将掌握:
- 如何设计完整且可扩展的用户数据模型
- 如何使用嵌套接口组织复杂数据结构
- 如何实现学习统计和成就系统
- 如何在实际项目中管理用户数据
🎯 学习目标
完成本文后,你将能够:
- ✅ 理解 User 模型的完整字段定义和用途
- ✅ 掌握嵌套接口的设计和使用
- ✅ 实现用户数据的校验逻辑
- ✅ 在实际项目中创建和管理用户数据
💡 需求分析
功能模块设计
| 模块 | 功能描述 | 技术要点 |
|---|---|---|
| 用户基本信息 | 存储用户的基本信息,如昵称、头像、等级等 | 基础字段、数据类型 |
| 学习进度追踪 | 记录用户的关卡解锁、完成状态和星级 | 数组、Map 结构 |
| 成就系统 | 管理用户获得的成就列表 | 字符串数组、成就触发 |
| 个性化设置 | 存储用户的偏好配置 | 嵌套接口、枚举 |
| 学习统计 | 记录用户的学习数据和统计信息 | 嵌套接口、Map 结构 |
🛠️ 核心实现
步骤1: 定义 User 主接口
功能说明
User 接口是用户数据的核心模型,包含了用户的所有信息,从基本信息到学习统计,从成就系统到个性化设置。
完整代码
// models/User.ts
/**
* 年龄组枚举
*/
export enum AgeGroup {
CHILD = '少儿',
TEENAGER = '青少年',
ADULT = '成人'
}
/**
* 用户工具道具接口
*/
export interface UserTools {
removeError: number; // 消除错误次数
extendTime: number; // 延长时间次数
hint: number; // 提示次数
skip: number; // 跳过次数
}
/**
* 用户设置接口
*/
export interface UserSettings {
soundEnabled: boolean; // 音效开关
vibrationEnabled: boolean; // 震动开关
questionTimer: number; // 答题计时器(秒)
showExplanation: boolean; // 是否显示解析
ageGroup: AgeGroup; // 年龄组
difficultyLock: boolean; // 难度锁定
}
/**
* 学科统计接口
*/
export interface SubjectStat {
subjectId: string; // 学科 ID
totalQuestions: number; // 该学科答题数
correctQuestions: number; // 该学科正确数
accuracy: number; // 该学科正确率
}
/**
* 用户统计接口
*/
export interface UserStatistics {
totalQuestions: number; // 总答题数
correctQuestions: number; // 正确答题数
totalStudyTime: number; // 总学习时长(分钟)
averageAccuracy: number; // 平均正确率
subjectStats: Map<string, SubjectStat>; // 各学科统计
}
/**
* 每日记录接口
*/
export interface DailyRecord {
date: string; // 日期,格式 YYYY-MM-DD
studyTime: number; // 学习时长(分钟)
questionsAnswered: number; // 答题数量
correctCount: number; // 正确数量
}
/**
* 答题结果接口
*/
export interface QuestionResult {
questionId: string; // 题目 ID
userAnswer: string | string[]; // 用户答案
isCorrect: boolean; // 是否正确
timeUsed: number; // 答题用时(秒)
usedTools: string[]; // 使用的工具列表
}
/**
* 测验记录接口
*/
export interface QuizRecord {
id: string; // 记录 ID
userId: string; // 用户 ID
levelId: string; // 关卡 ID
startTime: string; // 开始时间
endTime: string; // 结束时间
totalQuestions: number; // 总题数
correctQuestions: number; // 正确数
accuracy: number; // 正确率
score: number; // 得分
experience: number; // 获得经验
stars: number; // 获得星级(1-3)
timeUsed: number; // 用时(秒)
questionResults: QuestionResult[]; // 每题结果
subjectId?: string; // 学科 ID(可选)
difficulty?: string; // 难度等级(可选)
timestamp?: string; // 时间戳(可选)
}
/**
* 用户接口
*/
export interface User {
id: string; // 用户唯一标识
nickname: string; // 用户昵称
avatar: string; // 头像 URL
level: number; // 用户等级
totalScore: number; // 总积分
totalExperience: number; // 总经验值
rank: number; // 排行榜排名
consecutiveDays: number; // 连续登录天数
lastLoginDate: string; // 最后登录日期
createdAt: string; // 创建时间
settings: UserSettings; // 用户设置
statistics: UserStatistics; // 学习统计
unlockedLevels: string[]; // 已解锁关卡 ID 列表
completedLevels: string[]; // 已完成关卡 ID 列表
levelStars: Map<string, number>; // 各关卡星级 Map
achievements: string[]; // 已获得成就 ID 列表
wrongQuestions: string[]; // 错题 ID 列表
favorites: string[]; // 收藏题目 ID 列表
tools: UserTools; // 工具数量
quizHistory: QuizRecord[]; // 测验记录列表
dailyRecords: DailyRecord[]; // 每日学习记录
ownedItems: string[]; // 拥有物品 ID 列表
}
代码解析
1. User 接口字段说明
| 字段名 | 类型 | 必填 | 说明 |
|---|---|---|---|
id |
string |
✅ | 用户唯一标识,UUID 格式 |
nickname |
string |
✅ | 用户显示名称 |
avatar |
string |
✅ | 头像图片 URL |
level |
number |
✅ | 用户等级,从 1 开始 |
totalScore |
number |
✅ | 累计获得的积分 |
totalExperience |
number |
✅ | 累计获得的经验值 |
rank |
number |
✅ | 当前排行榜排名 |
consecutiveDays |
number |
✅ | 连续登录天数 |
lastLoginDate |
string |
✅ | 最后登录日期,格式 YYYY-MM-DD |
createdAt |
string |
✅ | 用户创建时间,格式 YYYY-MM-DD |
settings |
UserSettings |
✅ | 用户个性化设置 |
statistics |
UserStatistics |
✅ | 学习统计数据 |
unlockedLevels |
string[] |
✅ | 已解锁的关卡 ID 数组 |
completedLevels |
string[] |
✅ | 已完成的关卡 ID 数组 |
levelStars |
Map<string, number> |
✅ | 关卡星级映射 |
achievements |
string[] |
✅ | 已获得成就 ID 数组 |
wrongQuestions |
string[] |
✅ | 错题 ID 数组 |
favorites |
string[] |
✅ | 收藏题目 ID 数组 |
tools |
UserTools |
✅ | 工具道具数量 |
quizHistory |
QuizRecord[] |
✅ | 测验历史记录 |
dailyRecords |
DailyRecord[] |
✅ | 每日学习记录 |
ownedItems |
string[] |
✅ | 拥有的物品 ID 数组 |
2. 嵌套接口设计
// ✅ 正确:使用嵌套接口组织数据
export interface User {
settings: UserSettings; // 用户设置
statistics: UserStatistics; // 学习统计
tools: UserTools; // 工具道具
}
// ❌ 错误:将所有字段都放在主接口中
export interface User {
soundEnabled: boolean;
vibrationEnabled: boolean;
questionTimer: number;
showExplanation: boolean;
ageGroup: AgeGroup;
difficultyLock: boolean;
// ... 其他字段
}
原理:
- 使用嵌套接口将相关字段分组
- 提高代码可读性和可维护性
- 便于功能扩展和修改
注意事项:
- 嵌套接口应该有明确的语义
- 避免过度嵌套(不超过 3 层)
- 保持接口的单一职责
步骤2: 创建用户实例
功能说明
创建一个完整的用户实例,包含所有必要的数据。
完整代码
// 创建用户实例示例
const user: User = {
id: 'user_001',
nickname: '知识小达人',
avatar: 'https://example.com/avatar.png',
level: 5,
totalScore: 2580,
totalExperience: 12500,
rank: 128,
consecutiveDays: 15,
lastLoginDate: '2024-01-15',
createdAt: '2024-01-01',
settings: {
soundEnabled: true,
vibrationEnabled: false,
questionTimer: 60,
showExplanation: true,
ageGroup: AgeGroup.TEENAGER,
difficultyLock: false
},
statistics: {
totalQuestions: 520,
correctQuestions: 442,
totalStudyTime: 1800,
averageAccuracy: 0.85,
subjectStats: new Map([
['math', {
subjectId: 'math',
totalQuestions: 150,
correctQuestions: 135,
accuracy: 0.9
}],
['history', {
subjectId: 'history',
totalQuestions: 120,
correctQuestions: 96,
accuracy: 0.8
}]
])
},
unlockedLevels: ['math_level_1', 'math_level_2', 'history_level_1'],
completedLevels: ['math_level_1', 'history_level_1'],
levelStars: new Map([
['math_level_1', 3],
['history_level_1', 2]
]),
achievements: ['achievement_first_login', 'achievement_100_questions'],
wrongQuestions: ['math_1001', 'history_2003'],
favorites: ['math_1005', 'science_3001'],
tools: {
removeError: 5,
extendTime: 3,
hint: 10,
skip: 2
},
quizHistory: [],
dailyRecords: [],
ownedItems: ['item_avatar_001']
};
代码解析
1. UserSettings 接口
settings: {
soundEnabled: true, // 音效开关
vibrationEnabled: false, // 震动开关
questionTimer: 60, // 答题计时器(秒)
showExplanation: true, // 是否显示解析
ageGroup: AgeGroup.TEENAGER, // 年龄组
difficultyLock: false // 难度锁定
}
原理:
soundEnabled: 控制音效播放vibrationEnabled: 控制震动反馈questionTimer: 每道题的答题时间限制showExplanation: 答题后是否显示解析ageGroup: 根据年龄推荐适合的内容difficultyLock: 是否锁定难度等级
示例:
// ✅ 正确:使用枚举值
ageGroup: AgeGroup.TEENAGER
// ❌ 错误:使用字符串
ageGroup: '青少年' as AgeGroup
2. UserStatistics 接口
statistics: {
totalQuestions: 520, // 总答题数
correctQuestions: 442, // 正确答题数
totalStudyTime: 1800, // 总学习时长(分钟)
averageAccuracy: 0.85, // 平均正确率
subjectStats: new Map([ // 各学科统计
['math', {
subjectId: 'math',
totalQuestions: 150,
correctQuestions: 135,
accuracy: 0.9
}]
])
}
原理:
averageAccuracy应该等于correctQuestions / totalQuestionssubjectStats使用 Map 存储各学科的统计数据- 每个学科的
accuracy应该等于该学科的correctQuestions / totalQuestions
注意事项:
averageAccuracy范围:0-1subjectStats的键是学科 ID- 保持统计数据的一致性
3. levelStars Map 设计
levelStars: new Map([
['math_level_1', 3], // 数学入门级:3星
['history_level_1', 2] // 历史入门级:2星
])
原理:
- 使用 Map 存储关卡星级
- 键:关卡 ID
- 值:星级(1-3)
示例:
// ✅ 正确:使用 Map
levelStars: new Map([
['math_level_1', 3]
])
// ❌ 错误:使用对象
levelStars: {
'math_level_1': 3
}
// 获取星级
const stars = user.levelStars.get('math_level_1'); // 3
// 设置星级
user.levelStars.set('math_level_2', 2);
步骤3: 创建测验记录
功能说明
创建一个完整的测验记录,记录用户在一次测验中的表现。
完整代码
// 创建测验记录示例
const quizRecord: QuizRecord = {
id: 'quiz_001',
userId: 'user_001',
levelId: 'math_level_2',
startTime: '2024-01-15 10:00:00',
endTime: '2024-01-15 10:05:30',
totalQuestions: 10,
correctQuestions: 8,
accuracy: 0.8,
score: 80,
experience: 200,
stars: 2,
timeUsed: 330,
questionResults: [
{
questionId: 'math_1001',
userAnswer: 'B',
isCorrect: true,
timeUsed: 25,
usedTools: []
},
{
questionId: 'math_1002',
userAnswer: 'A',
isCorrect: false,
timeUsed: 30,
usedTools: ['hint']
}
],
subjectId: 'math',
difficulty: '2',
timestamp: '2024-01-15T10:05:30Z'
};
代码解析
1. QuizRecord 接口字段说明
| 字段名 | 类型 | 必填 | 说明 |
|---|---|---|---|
id |
string |
✅ | 记录唯一标识 |
userId |
string |
✅ | 用户 ID |
levelId |
string |
✅ | 关卡 ID |
startTime |
string |
✅ | 开始时间 |
endTime |
string |
✅ | 结束时间 |
totalQuestions |
number |
✅ | 总题数 |
correctQuestions |
number |
✅ | 正确数 |
accuracy |
number |
✅ | 正确率 |
score |
number |
✅ | 得分 |
experience |
number |
✅ | 获得经验 |
stars |
number |
✅ | 获得星级(1-3) |
timeUsed |
number |
✅ | 用时(秒) |
questionResults |
QuestionResult[] |
✅ | 每题结果 |
subjectId |
string |
❌ | 学科 ID(可选) |
difficulty |
string |
❌ | 难度等级(可选) |
timestamp |
string |
❌ | 时间戳(可选) |
2. QuestionResult 接口
questionResults: [
{
questionId: 'math_1001', // 题目 ID
userAnswer: 'B', // 用户答案
isCorrect: true, // 是否正确
timeUsed: 25, // 答题用时(秒)
usedTools: [] // 使用的工具列表
}
]
原理:
questionId: 题目的唯一标识userAnswer: 用户的答案isCorrect: 答案是否正确timeUsed: 答题用时usedTools: 使用的工具列表(如['hint'])
注意事项:
usedTools可以包含多个工具timeUsed单位为秒userAnswer格式与题目类型相关
3. 星级计算逻辑
// 星级计算示例
function calculateStars(accuracy: number): number {
if (accuracy >= 0.9) {
return 3; // 90% 以上:3星
} else if (accuracy >= 0.7) {
return 2; // 70%-90%:2星
} else {
return 1; // 70% 以下:1星
}
}
原理:
- 3 星:正确率 ≥ 90%
- 2 星:正确率 ≥ 70%
- 1 星:正确率 < 70%
示例:
calculateStars(0.95); // 3
calculateStars(0.8); // 2
calculateStars(0.6); // 1
步骤4: 实现数据校验
功能说明
为了确保用户数据的完整性和正确性,需要实现数据校验逻辑。
完整代码
// utils/UserValidator.ts
import { User, UserStatistics, AgeGroup } from '../models/User';
/**
* 用户数据校验类
*/
export class UserValidator {
/**
* 校验用户数据
* @param user 待校验的用户
* @returns 校验结果
*/
static validate(user: User): { isValid: boolean; errors: string[] } {
const errors: string[] = [];
// 1. 检查必填字段
if (!user.id) {
errors.push('用户 ID 不能为空');
}
if (!user.nickname) {
errors.push('用户昵称不能为空');
}
if (!user.avatar) {
errors.push('用户头像不能为空');
}
// 2. 检查等级范围
if (user.level < 1) {
errors.push('用户等级必须大于等于 1');
}
// 3. 检查统计数据一致性
if (user.statistics.totalQuestions > 0) {
const calculatedAccuracy = user.statistics.correctQuestions / user.statistics.totalQuestions;
if (Math.abs(calculatedAccuracy - user.statistics.averageAccuracy) > 0.01) {
errors.push('平均正确率与统计数据不一致');
}
}
// 4. 检查关卡星级范围
for (const [levelId, stars] of user.levelStars) {
if (stars < 1 || stars > 3) {
errors.push(`关卡 ${levelId} 的星级必须在 1-3 之间`);
}
}
// 5. 检查日期格式
const dateRegex = /^\d{4}-\d{2}-\d{2}$/;
if (!dateRegex.test(user.lastLoginDate)) {
errors.push('最后登录日期格式必须为 YYYY-MM-DD');
}
if (!dateRegex.test(user.createdAt)) {
errors.push('创建时间格式必须为 YYYY-MM-DD');
}
// 6. 检查年龄组
if (!Object.values(AgeGroup).includes(user.settings.ageGroup)) {
errors.push('年龄组必须是有效的枚举值');
}
// 7. 检查工具数量
if (user.tools.removeError < 0) {
errors.push('消除错误次数不能为负数');
}
if (user.tools.extendTime < 0) {
errors.push('延长时间次数不能为负数');
}
if (user.tools.hint < 0) {
errors.push('提示次数不能为负数');
}
if (user.tools.skip < 0) {
errors.push('跳过次数不能为负数');
}
return {
isValid: errors.length === 0,
errors
};
}
/**
* 校验测验记录
* @param record 待校验的测验记录
* @returns 校验结果
*/
static validateQuizRecord(record: QuizRecord): { isValid: boolean; errors: string[] } {
const errors: string[] = [];
// 1. 检查必填字段
if (!record.id) {
errors.push('记录 ID 不能为空');
}
if (!record.userId) {
errors.push('用户 ID 不能为空');
}
if (!record.levelId) {
errors.push('关卡 ID 不能为空');
}
// 2. 检查数值范围
if (record.totalQuestions <= 0) {
errors.push('总题数必须大于 0');
}
if (record.correctQuestions < 0 || record.correctQuestions > record.totalQuestions) {
errors.push('正确题数必须在 0 到总题数之间');
}
if (record.accuracy < 0 || record.accuracy > 1) {
errors.push('正确率必须在 0 到 1 之间');
}
if (record.stars < 1 || record.stars > 3) {
errors.push('星级必须在 1 到 3 之间');
}
// 3. 检查统计数据一致性
const calculatedAccuracy = record.correctQuestions / record.totalQuestions;
if (Math.abs(calculatedAccuracy - record.accuracy) > 0.01) {
errors.push('正确率与统计数据不一致');
}
return {
isValid: errors.length === 0,
errors
};
}
}
代码解析
1. 必填字段检查
if (!user.id) {
errors.push('用户 ID 不能为空');
}
原理:
- 检查所有必填字段是否有值
- 空字符串、
null、undefined都会被判定为无效 - 收集所有错误信息,一次性返回
示例:
// ✅ 正确:所有必填字段都有值
const user: User = {
id: 'user_001',
nickname: '知识小达人',
avatar: 'https://example.com/avatar.png',
// ...
};
// ❌ 错误:缺少必填字段
const user: User = {
id: 'user_001',
// 缺少 nickname
avatar: 'https://example.com/avatar.png',
// ...
};
2. 统计数据一致性检查
if (user.statistics.totalQuestions > 0) {
const calculatedAccuracy = user.statistics.correctQuestions / user.statistics.totalQuestions;
if (Math.abs(calculatedAccuracy - user.statistics.averageAccuracy) > 0.01) {
errors.push('平均正确率与统计数据不一致');
}
}
原理:
- 计算实际正确率:
correctQuestions / totalQuestions - 与存储的
averageAccuracy比较 - 允许 0.01 的误差(浮点数精度问题)
示例:
// ✅ 正确:统计数据一致
statistics: {
totalQuestions: 100,
correctQuestions: 85,
averageAccuracy: 0.85 // 85/100 = 0.85
}
// ❌ 错误:统计数据不一致
statistics: {
totalQuestions: 100,
correctQuestions: 85,
averageAccuracy: 0.9 // 85/100 = 0.85 ≠ 0.9
}
3. 关卡星级范围检查
for (const [levelId, stars] of user.levelStars) {
if (stars < 1 || stars > 3) {
errors.push(`关卡 ${levelId} 的星级必须在 1-3 之间`);
}
}
原理:
- 遍历
levelStarsMap - 检查每个关卡的星级是否在 1-3 之间
示例:
// ✅ 正确:星级在 1-3 之间
levelStars: new Map([
['math_level_1', 3],
['history_level_1', 2]
])
// ❌ 错误:星级超出范围
levelStars: new Map([
['math_level_1', 4], // 错误:最大值为 3
['history_level_1', 0] // 错误:最小值为 1
])
4. 年龄组校验
if (!Object.values(AgeGroup).includes(user.settings.ageGroup)) {
errors.push('年龄组必须是有效的枚举值');
}
原理:
- 检查
ageGroup是否是AgeGroup枚举的有效值 - 防止使用无效的年龄组
示例:
// ✅ 正确:使用枚举值
ageGroup: AgeGroup.TEENAGER
// ❌ 错误:使用无效值
ageGroup: '中年' as AgeGroup
⚠️ 常见问题与解决方案
问题1: averageAccuracy 与统计数据不一致
现象:
用户的 averageAccuracy 与 correctQuestions / totalQuestions 的计算结果不一致。
错误代码:
// ❌ 错误:统计数据不一致
const user: User = {
statistics: {
totalQuestions: 100,
correctQuestions: 85,
averageAccuracy: 0.9 // 错误:应该是 0.85
}
};
正确代码:
// ✅ 正确:统计数据一致
const user: User = {
statistics: {
totalQuestions: 100,
correctQuestions: 85,
averageAccuracy: 0.85 // 正确:85/100 = 0.85
}
};
规则/建议:
averageAccuracy应该等于correctQuestions / totalQuestions- 使用
UserValidator.validate()进行校验 - 更新统计数据时同步更新
averageAccuracy
问题2: levelStars 使用对象而不是 Map
现象:
使用普通对象存储关卡星级,导致类型错误。
错误代码:
// ❌ 错误:使用对象
const user: User = {
levelStars: {
'math_level_1': 3,
'history_level_1': 2
} as any // 强制类型转换
};
正确代码:
// ✅ 正确:使用 Map
const user: User = {
levelStars: new Map([
['math_level_1', 3],
['history_level_1', 2]
])
};
规则/建议:
levelStars必须使用Map<string, number>类型- 使用
Map的get()和set()方法操作 - 避免使用类型强制转换
问题3: 工具数量为负数
现象:
用户的工具数量为负数,导致逻辑错误。
错误代码:
// ❌ 错误:工具数量为负数
const user: User = {
tools: {
removeError: -1, // 错误:不能为负数
extendTime: 3,
hint: 10,
skip: 2
}
};
正确代码:
// ✅ 正确:工具数量为非负数
const user: User = {
tools: {
removeError: 0, // 正确:可以为 0
extendTime: 3,
hint: 10,
skip: 2
}
};
规则/建议:
- 所有工具数量必须 ≥ 0
- 使用工具时先检查数量
- 数量不足时提示用户
问题4: 日期格式不正确
现象:
日期字段使用不标准的格式,导致解析错误。
错误代码:
// ❌ 错误:日期格式不标准
const user: User = {
lastLoginDate: '2024/01/15', // 使用斜杠
createdAt: '2024-1-1' // 月份和日期没有补零
};
正确代码:
// ✅ 正确:使用标准格式 YYYY-MM-DD
const user: User = {
lastLoginDate: '2024-01-15', // 标准格式
createdAt: '2024-01-01' // 标准格式
};
规则/建议:
- 日期格式:
YYYY-MM-DD - 月份和日期必须补零
- 使用正则表达式校验:
/^\d{4}-\d{2}-\d{2}$/
问题5: 测验记录的星级计算错误
现象:
测验记录的星级与正确率不匹配。
错误代码:
// ❌ 错误:星级与正确率不匹配
const quizRecord: QuizRecord = {
accuracy: 0.8, // 正确率 80%
stars: 3 // 错误:应该是 2 星
};
正确代码:
// ✅ 正确:星级与正确率匹配
const quizRecord: QuizRecord = {
accuracy: 0.8, // 正确率 80%
stars: 2 // 正确:2 星
};
规则/建议:
- 3 星:正确率 ≥ 90%
- 2 星:正确率 ≥ 70%
- 1 星:正确率 < 70%
- 使用统一的星级计算函数
📝 本章小结
核心知识点
本文详细讲解了 User 模型的设计与实现,主要包括:
1. User 接口设计
- 包含 24 个字段,覆盖用户的所有信息
- 使用嵌套接口组织复杂数据结构
- 支持可选字段(
subjectId、difficulty、timestamp)
2. 嵌套接口设计
UserSettings: 用户个性化设置UserStatistics: 学习统计数据UserTools: 工具道具数量QuizRecord: 测验记录QuestionResult: 答题结果
3. 数据校验
- 必填字段检查
- 统计数据一致性检查
- 数值范围检查
- 日期格式检查
最佳实践总结
✅ 使用 Map 存储关卡星级
levelStars: new Map([
['math_level_1', 3]
])
✅ 统计数据一致性
averageAccuracy: correctQuestions / totalQuestions
✅ 星级计算规则
function calculateStars(accuracy: number): number {
if (accuracy >= 0.9) return 3;
if (accuracy >= 0.7) return 2;
return 1;
}
✅ 数据校验
const result = UserValidator.validate(user);
if (!result.isValid) {
console.error(result.errors);
}
下一步预告
在下一篇文章中,我们将:
- 🎨 讲解 Level 模型设计与关卡系统
- 📚 介绍关卡解锁和完成机制
- 🏷️ 探索关卡难度和知识点管理
🔗 相关链接
- 项目源码: Atomgit仓库
💡 提示: 建议结合项目源码阅读,动手实践效果更好!
更多推荐


所有评论(0)