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"`
|
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
|
// CreateHumanLanguageLesson godoc
|
||||||
// @Summary Create human-language lesson unit
|
// @Summary Create human-language lesson unit
|
||||||
// @Description Creates a lesson unit under a human-language course using CEFR level (A1..C3)
|
// @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
|
// CreateSubCourse godoc
|
||||||
// @Summary Create a new sub-course
|
// @Summary Create a new sub-course
|
||||||
// @Description Creates a new sub-course under a specific course
|
// @Description Creates a new sub-course under a specific course
|
||||||
|
|
|
||||||
|
|
@ -128,6 +128,7 @@ func (a *App) initAppRoutes() {
|
||||||
// Learning Tree
|
// Learning Tree
|
||||||
groupV1.Get("/course-management/learning-tree", a.authMiddleware, a.RequirePermission("learning_tree.get"), h.GetFullLearningTree)
|
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/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.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.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)
|
groupV1.Patch("/course-management/human-language/lessons/:id", a.authMiddleware, a.RequirePermission("subcourses.update"), h.UpdateHumanLanguageLesson)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user