add hierarchy endpoint and mobile integration guide
Expose a dedicated human-language hierarchy endpoint aligned to category/subcategory/course/level/module/sub-module structure and add a complete learner mobile integration guide. Made-with: Cursor
This commit is contained in:
parent
9c9ab41f41
commit
4055ad46f6
292
docs/HUMAN_LANGUAGE_MOBILE_INTEGRATION_GUIDE.md
Normal file
292
docs/HUMAN_LANGUAGE_MOBILE_INTEGRATION_GUIDE.md
Normal file
|
|
@ -0,0 +1,292 @@
|
|||
# Human Language Mobile Integration Guide
|
||||
|
||||
This guide explains how to integrate the new **Human Language** feature in the **Yimaru learner mobile app** (not admin).
|
||||
|
||||
It is designed to keep the existing non-language hierarchy intact while introducing a dedicated CEFR-based flow for language learning.
|
||||
|
||||
---
|
||||
|
||||
## 1) Scope and Goals
|
||||
|
||||
### What is new
|
||||
|
||||
- A dedicated backend API namespace for Human Language:
|
||||
- `GET /api/v1/course-management/human-language/courses/:courseId/lessons?cefr_level=A1..C3`
|
||||
- `POST /api/v1/course-management/human-language/lessons` (admin/instructor side)
|
||||
- `PATCH /api/v1/course-management/human-language/lessons/:id` (admin/instructor side)
|
||||
- CEFR levels are fixed to:
|
||||
- `A1, A2, A3, B1, B2, B3, C1, C2, C3`
|
||||
- No custom sub-levels in Human Language flow.
|
||||
|
||||
### What remains unchanged
|
||||
|
||||
- Existing non-human-language content hierarchy and APIs.
|
||||
- Existing course/category/sub-course endpoints for non-language domains (programming, etc.).
|
||||
|
||||
---
|
||||
|
||||
## 2) Target Hierarchy for Human Language
|
||||
|
||||
- Course Category (Human Language)
|
||||
- Course (e.g., English)
|
||||
- CEFR Lesson Unit (A1..C3 only)
|
||||
- Intro/lesson videos
|
||||
- Practices
|
||||
- Audio questions
|
||||
|
||||
Backend implementation stores CEFR lesson units using the existing sub-course model, with CEFR mapped internally and validated strictly.
|
||||
|
||||
---
|
||||
|
||||
## 3) Authentication and Access
|
||||
|
||||
All Human Language endpoints are under `/api/v1` and require bearer token auth.
|
||||
|
||||
- Header:
|
||||
- `Authorization: Bearer <access_token>`
|
||||
|
||||
### Permission notes
|
||||
|
||||
- **Learner mobile app** typically needs only the `GET` endpoint.
|
||||
- `POST` and `PATCH` are content-management endpoints and should generally be used by admin/instructor clients, not learner clients.
|
||||
|
||||
If learner roles do not currently have `learning_tree.get`, coordinate RBAC assignment for read-only access to the `GET` endpoint.
|
||||
|
||||
---
|
||||
|
||||
## 4) Endpoint Contracts
|
||||
|
||||
## 4.1 Fetch lessons by CEFR level (learner-facing)
|
||||
|
||||
### Request
|
||||
|
||||
`GET /api/v1/course-management/human-language/courses/{courseId}/lessons?cefr_level={A1|A2|A3|B1|B2|B3|C1|C2|C3}`
|
||||
|
||||
### Example
|
||||
|
||||
`GET /api/v1/course-management/human-language/courses/12/lessons?cefr_level=B1`
|
||||
|
||||
### Response (success)
|
||||
|
||||
```json
|
||||
{
|
||||
"message": "Human-language lessons retrieved successfully",
|
||||
"data": {
|
||||
"course_id": 12,
|
||||
"course_title": "English",
|
||||
"cefr_level": "B1",
|
||||
"lessons": [
|
||||
{
|
||||
"id": 201,
|
||||
"course_id": 12,
|
||||
"title": "B1 Module 1",
|
||||
"description": "Intermediate conversational patterns",
|
||||
"thumbnail": "https://.../thumb.jpg",
|
||||
"display_order": 1,
|
||||
"level": "B1",
|
||||
"video_count": 3,
|
||||
"practice_count": 2,
|
||||
"videos": [
|
||||
{
|
||||
"id": 9001,
|
||||
"title": "B1 Intro",
|
||||
"description": "Lesson intro",
|
||||
"video_url": "https://...",
|
||||
"duration": 420,
|
||||
"resolution": "1080p",
|
||||
"display_order": 1
|
||||
}
|
||||
],
|
||||
"practices": [
|
||||
{
|
||||
"id": 4401,
|
||||
"title": "B1 Speaking Practice",
|
||||
"status": "PUBLISHED",
|
||||
"question_count": 10
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"success": true,
|
||||
"status_code": 200,
|
||||
"metadata": null
|
||||
}
|
||||
```
|
||||
|
||||
### Validation errors
|
||||
|
||||
- Invalid CEFR level:
|
||||
- `400` with error message: `Use one of: A1,A2,A3,B1,B2,B3,C1,C2,C3`
|
||||
- Course not in human language category:
|
||||
- `400` with error message indicating invalid human-language course.
|
||||
|
||||
---
|
||||
|
||||
## 4.2 Create Human Language lesson unit (admin/instructor)
|
||||
|
||||
### Request
|
||||
|
||||
`POST /api/v1/course-management/human-language/lessons`
|
||||
|
||||
```json
|
||||
{
|
||||
"course_id": 12,
|
||||
"title": "A2 Module 1",
|
||||
"description": "A2 speaking fundamentals",
|
||||
"thumbnail": "https://...",
|
||||
"display_order": 1,
|
||||
"cefr_level": "A2"
|
||||
}
|
||||
```
|
||||
|
||||
### Response
|
||||
|
||||
Returns created lesson metadata with CEFR-normalized level behavior.
|
||||
|
||||
---
|
||||
|
||||
## 4.3 Update Human Language lesson unit (admin/instructor)
|
||||
|
||||
### Request
|
||||
|
||||
`PATCH /api/v1/course-management/human-language/lessons/{lessonId}`
|
||||
|
||||
```json
|
||||
{
|
||||
"title": "A2 Module 1 - Updated",
|
||||
"description": "Updated description",
|
||||
"cefr_level": "A3",
|
||||
"is_active": true
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5) Recommended Mobile App Integration Flow
|
||||
|
||||
## Step 1: Discover courses under Human Language category
|
||||
|
||||
Use existing endpoints:
|
||||
|
||||
1. `GET /course-management/categories`
|
||||
2. Pick category where name indicates human language.
|
||||
3. `GET /course-management/categories/:categoryId/courses`
|
||||
|
||||
## Step 2: Select CEFR level in UI
|
||||
|
||||
Use static list in app:
|
||||
|
||||
- `A1, A2, A3, B1, B2, B3, C1, C2, C3`
|
||||
|
||||
Do not allow custom levels in mobile UI.
|
||||
|
||||
## Step 3: Fetch lessons by selected CEFR level
|
||||
|
||||
Call:
|
||||
|
||||
- `GET /course-management/human-language/courses/:courseId/lessons?cefr_level=<selected>`
|
||||
|
||||
Render grouped lessons with nested videos/practices from response.
|
||||
|
||||
## Step 4: Navigate to learning/practice screens
|
||||
|
||||
- Lesson video playback uses returned `videos[]`.
|
||||
- Practice entry uses returned `practices[]` IDs and existing practice-question endpoints.
|
||||
|
||||
---
|
||||
|
||||
## 6) Mobile UI/UX Recommendations
|
||||
|
||||
- Use CEFR tabs/segmented control (`A1`...`C3`) at top.
|
||||
- Cache last selected level per course.
|
||||
- Show empty state per level:
|
||||
- "No lessons available for this level yet."
|
||||
- Sort by `display_order` and maintain backend order.
|
||||
- Show badges:
|
||||
- video count
|
||||
- practice count
|
||||
|
||||
---
|
||||
|
||||
## 7) Error Handling and Resilience
|
||||
|
||||
- `400` invalid level: reset selection to previous valid CEFR value and show toast/snackbar.
|
||||
- `401/403`: trigger token refresh / re-login flow.
|
||||
- `5xx`: show retry UI with exponential backoff.
|
||||
- If category/course fetch succeeds but level fetch fails, keep course visible and allow manual retry.
|
||||
|
||||
---
|
||||
|
||||
## 8) Data Model Mapping (Mobile DTO)
|
||||
|
||||
Suggested DTO for learner app:
|
||||
|
||||
```ts
|
||||
type CefrLevel = "A1"|"A2"|"A3"|"B1"|"B2"|"B3"|"C1"|"C2"|"C3";
|
||||
|
||||
interface HumanLanguageLessonDTO {
|
||||
id: number;
|
||||
courseId: number;
|
||||
title: string;
|
||||
description?: string;
|
||||
thumbnail?: string;
|
||||
order: number;
|
||||
level: CefrLevel;
|
||||
videoCount: number;
|
||||
practiceCount: number;
|
||||
videos: {
|
||||
id: number;
|
||||
title: string;
|
||||
url: string;
|
||||
duration: number;
|
||||
order: number;
|
||||
}[];
|
||||
practices: {
|
||||
id: number;
|
||||
title: string;
|
||||
status: string;
|
||||
questionCount: number;
|
||||
}[];
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9) Backward Compatibility
|
||||
|
||||
- Existing non-language content (programming and other categories) continues to use current APIs and hierarchy unchanged.
|
||||
- New Human Language endpoint is additive and isolated.
|
||||
- Mobile app can progressively enable the new flow for language categories only.
|
||||
|
||||
---
|
||||
|
||||
## 10) QA Checklist for Mobile Team
|
||||
|
||||
- [ ] Category discovery correctly identifies Human Language category.
|
||||
- [ ] CEFR selector only allows A1..C3.
|
||||
- [ ] Fetch by CEFR level returns only matching lessons.
|
||||
- [ ] Video/practice counts match rendered lists.
|
||||
- [ ] Empty-level state works.
|
||||
- [ ] Unauthorized/session-expired flow works.
|
||||
- [ ] Non-language courses still load via existing app flow.
|
||||
|
||||
---
|
||||
|
||||
## 11) Example Call Matrix
|
||||
|
||||
- Load language categories/courses:
|
||||
- `GET /course-management/categories`
|
||||
- `GET /course-management/categories/:id/courses`
|
||||
- Load A1 lessons:
|
||||
- `GET /course-management/human-language/courses/:courseId/lessons?cefr_level=A1`
|
||||
- Load B2 lessons:
|
||||
- `GET /course-management/human-language/courses/:courseId/lessons?cefr_level=B2`
|
||||
|
||||
---
|
||||
|
||||
For backend ownership questions, refer to:
|
||||
|
||||
- `internal/web_server/handlers/course_management.go`
|
||||
- `internal/web_server/routes.go`
|
||||
|
||||
|
|
@ -697,6 +697,42 @@ type getHumanLanguageLessonsRes struct {
|
|||
Lessons []humanLanguageLessonRes `json:"lessons"`
|
||||
}
|
||||
|
||||
type humanLanguageSubModuleRes struct {
|
||||
ID int64 `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Videos []domain.LearningPathVideo `json:"videos"`
|
||||
Practices []domain.LearningPathPractice `json:"practices"`
|
||||
}
|
||||
|
||||
type humanLanguageModuleRes struct {
|
||||
ID int64 `json:"id"`
|
||||
Title string `json:"title"`
|
||||
SubModules []humanLanguageSubModuleRes `json:"sub_modules"`
|
||||
}
|
||||
|
||||
type humanLanguageLevelRes struct {
|
||||
Level string `json:"level"`
|
||||
Modules []humanLanguageModuleRes `json:"modules"`
|
||||
}
|
||||
|
||||
type humanLanguageCourseTreeRes struct {
|
||||
CourseID int64 `json:"course_id"`
|
||||
CourseName string `json:"course_name"`
|
||||
Levels []humanLanguageLevelRes `json:"levels"`
|
||||
}
|
||||
|
||||
type humanLanguageSubCategoryTreeRes struct {
|
||||
SubCategoryID int64 `json:"sub_category_id"`
|
||||
SubCategoryName string `json:"sub_category_name"`
|
||||
Courses []humanLanguageCourseTreeRes `json:"courses"`
|
||||
}
|
||||
|
||||
type humanLanguageHierarchyRes struct {
|
||||
CategoryID int64 `json:"category_id"`
|
||||
CategoryName string `json:"category_name"`
|
||||
SubCategories []humanLanguageSubCategoryTreeRes `json:"sub_categories"`
|
||||
}
|
||||
|
||||
// CreateHumanLanguageLesson godoc
|
||||
// @Summary Create human-language lesson unit
|
||||
// @Description Creates a lesson unit under a human-language course using CEFR level (A1..C3)
|
||||
|
|
@ -860,6 +896,116 @@ func (h *Handler) GetHumanLanguageLessonsByCourse(c *fiber.Ctx) error {
|
|||
})
|
||||
}
|
||||
|
||||
// GetHumanLanguageHierarchy godoc
|
||||
// @Summary Get full human-language hierarchy
|
||||
// @Description Returns Category -> SubCategory -> Course -> Level -> Module -> SubModule with videos/practices
|
||||
// @Tags human-language
|
||||
// @Produce json
|
||||
// @Success 200 {object} domain.Response
|
||||
// @Failure 500 {object} domain.ErrorResponse
|
||||
// @Router /api/v1/course-management/human-language/hierarchy [get]
|
||||
func (h *Handler) GetHumanLanguageHierarchy(c *fiber.Ctx) error {
|
||||
categories, _, err := h.courseMgmtSvc.GetAllCourseCategories(c.Context(), 1000, 0)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||
Message: "Failed to load course categories",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
var humanCategory *domain.CourseCategory
|
||||
for _, cat := range categories {
|
||||
name := strings.ToLower(strings.TrimSpace(cat.Name))
|
||||
if strings.Contains(name, "human language") || strings.Contains(name, "language") {
|
||||
cc := cat
|
||||
humanCategory = &cc
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if humanCategory == nil {
|
||||
return c.JSON(domain.Response{
|
||||
Message: "Human-language hierarchy retrieved successfully",
|
||||
Data: humanLanguageHierarchyRes{
|
||||
CategoryID: 0,
|
||||
CategoryName: "Human Language",
|
||||
SubCategories: []humanLanguageSubCategoryTreeRes{},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
courses, _, err := h.courseMgmtSvc.GetCoursesByCategory(c.Context(), humanCategory.ID, 1000, 0)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||
Message: "Failed to load human-language subcategories",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
subCategories := make([]humanLanguageSubCategoryTreeRes, 0, len(courses))
|
||||
for _, course := range courses {
|
||||
path, pathErr := h.courseMgmtSvc.GetCourseLearningPath(c.Context(), course.ID)
|
||||
if pathErr != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
levelsMap := map[string][]humanLanguageModuleRes{}
|
||||
for _, sc := range path.SubCourses {
|
||||
levelKey := strings.ToUpper(strings.TrimSpace(sc.SubLevel))
|
||||
if !isValidHumanLanguageCEFRLevel(levelKey) {
|
||||
continue
|
||||
}
|
||||
|
||||
module := humanLanguageModuleRes{
|
||||
ID: sc.ID,
|
||||
Title: sc.Title,
|
||||
SubModules: []humanLanguageSubModuleRes{
|
||||
{
|
||||
ID: sc.ID,
|
||||
Title: sc.Title,
|
||||
Videos: sc.Videos,
|
||||
Practices: sc.Practices,
|
||||
},
|
||||
},
|
||||
}
|
||||
levelsMap[levelKey] = append(levelsMap[levelKey], module)
|
||||
}
|
||||
|
||||
levels := make([]humanLanguageLevelRes, 0, 9)
|
||||
for _, cefr := range []string{
|
||||
string(domain.SubCourseSubLevelA1), string(domain.SubCourseSubLevelA2), string(domain.SubCourseSubLevelA3),
|
||||
string(domain.SubCourseSubLevelB1), string(domain.SubCourseSubLevelB2), string(domain.SubCourseSubLevelB3),
|
||||
string(domain.SubCourseSubLevelC1), string(domain.SubCourseSubLevelC2), string(domain.SubCourseSubLevelC3),
|
||||
} {
|
||||
levels = append(levels, humanLanguageLevelRes{
|
||||
Level: cefr,
|
||||
Modules: levelsMap[cefr],
|
||||
})
|
||||
}
|
||||
|
||||
subCategories = append(subCategories, humanLanguageSubCategoryTreeRes{
|
||||
SubCategoryID: course.ID,
|
||||
SubCategoryName: course.Title,
|
||||
Courses: []humanLanguageCourseTreeRes{
|
||||
{
|
||||
CourseID: course.ID,
|
||||
CourseName: course.Title,
|
||||
Levels: levels,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return c.JSON(domain.Response{
|
||||
Message: "Human-language hierarchy retrieved successfully",
|
||||
Data: humanLanguageHierarchyRes{
|
||||
CategoryID: humanCategory.ID,
|
||||
CategoryName: humanCategory.Name,
|
||||
SubCategories: subCategories,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// CreateSubCourse godoc
|
||||
// @Summary Create a new sub-course
|
||||
// @Description Creates a new sub-course under a specific course
|
||||
|
|
|
|||
|
|
@ -128,6 +128,7 @@ func (a *App) initAppRoutes() {
|
|||
// Learning Tree
|
||||
groupV1.Get("/course-management/learning-tree", a.authMiddleware, a.RequirePermission("learning_tree.get"), h.GetFullLearningTree)
|
||||
groupV1.Get("/course-management/courses/:courseId/learning-path", a.authMiddleware, a.RequirePermission("learning_tree.get"), h.GetCourseLearningPath)
|
||||
groupV1.Get("/course-management/human-language/hierarchy", a.authMiddleware, a.RequirePermission("learning_tree.get"), h.GetHumanLanguageHierarchy)
|
||||
groupV1.Get("/course-management/human-language/courses/:courseId/lessons", a.authMiddleware, a.RequirePermission("learning_tree.get"), h.GetHumanLanguageLessonsByCourse)
|
||||
groupV1.Post("/course-management/human-language/lessons", a.authMiddleware, a.RequirePermission("subcourses.create"), h.CreateHumanLanguageLesson)
|
||||
groupV1.Patch("/course-management/human-language/lessons/:id", a.authMiddleware, a.RequirePermission("subcourses.update"), h.UpdateHumanLanguageLesson)
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user