阶段 16:动作分类、动作库、动作收藏查询接口
合辑 :Django 系统阶段开发手册 | 阶段 :16 / 18 | 定位 :为动作库与用户动作收藏提供受登录保护的查询与收藏接口 | 建议学时:6--8 小时
大家好,我是小马不起床。
阶段导语
本阶段在 course 模块(阶段 15)完成课程类查询接口之后展开,将同一套视图组织方式、序列化规范与登录保护机制迁移到动作领域。动作模块沿用 RBAC 的 login_required 装饰器与统一响应封装,接口契约与课程模块保持一致,前端无需针对领域差异编写额外的适配逻辑。
本阶段交付六个接口:动作分类列表、动作列表(支持多条件筛选)、动作详情、当前用户收藏列表、收藏动作、取消收藏。收藏数据与用户身份绑定,所有读写操作均以 JWT 解析出的当前用户为作用域边界。
学习目标
完成本阶段后,应能够:
- 基于
t_action_category、t_action、y_user_action_favorite三张表构建动作领域的查询接口 - 为动作列表实现按分类、难度、适用门店类型筛选与名称模糊搜索
- 在序列化函数中关联当前用户,输出
is_favorite与favorite_id字段 - 使用
get_or_create实现幂等收藏,避免重复记录引发数据库异常 - 将收藏记录 ID 与动作 ID 区分,取消收藏仅作用于当前用户的记录
- 通过
exercise/urls.py与项目总路由挂载/api/exercise/路由前缀
前置知识
本阶段依赖阶段 15(课程类查询接口)的接口组织方式,以及阶段 12、阶段 13 的模型与数据初始化:
| 前置条目 | 具体要求 |
|---|---|
| 阶段 15:课程类查询接口 | 视图类结构、序列化函数、login_required 与 json_response 的使用方式 |
| 阶段 12:数据模型与迁移 | exercise 应用的动作分类表、动作表、用户动作收藏表已迁移建表 |
| 阶段 13:业务数据初始化 | init_business_data 命令已生成动作分类与动作测试数据 |
| JWT 登录 | 能获取 access_token 并在请求头中携带 |
目录
1. 阶段目标与数据模型
1.1 阶段目标
本阶段实现以下六项能力:
text
1. 查询动作分类列表
2. 查询动作列表
3. 查询动作详情
4. 查询当前用户收藏的动作
5. 收藏动作
6. 取消收藏动作
1.2 数据模型基础
本阶段建立在前面阶段设计的三张表之上:
text
t_action_category 动作分类表
t_action 动作库条目表
y_user_action_favorite 用户动作收藏表
三张表的职责与关联关系如下:
| 表名 | 用途 | 关键字段与关联 |
|---|---|---|
t_action_category |
动作分类主数据 | category_id 作为分类主键 |
t_action |
动作主数据 | 通过 category_id 关联动作分类 |
y_user_action_favorite |
用户动作收藏关系 | 通过 user_id 关联用户、action_id 关联动作 |
要点:
- 要点:动作模块的数据边界由三张表构成,分类与动作为一对多,收藏为用户与动作的多对多关系表。
- 要点 :
t_action与t_action_category通过category_id建立外键关联,序列化时需select_related以减少查询次数。 - 要点:收藏关系表的复合主体为「用户 + 动作」,所有收藏读写均以当前登录用户过滤。
2. 接口清单与响应结构
动作相关接口统一归入 /api/exercise/ 前缀,全部要求登录。
2.1 接口总览
| 方法 | 路径 | 参数 | 返回说明 | 是否需要登录 |
|---|---|---|---|---|
| GET | /api/exercise/categories/ |
无 | 动作分类列表 | 需要 |
| GET | /api/exercise/actions/ |
category_id、action_difficulty、applicable_store_type、keyword(均可选) |
动作列表,含当前用户收藏状态 | 需要 |
| GET | /api/exercise/actions/<action_id>/ |
路径参数 action_id |
单个动作详情 | 需要 |
| GET | /api/exercise/my-favorites/ |
无 | 当前用户收藏的动作列表 | 需要 |
| POST | /api/exercise/favorites/ |
请求体 action_id |
收藏动作,返回收藏记录 | 需要 |
| DELETE | /api/exercise/favorites/<favorite_id>/ |
路径参数 favorite_id |
取消收藏动作 | 需要 |
2.2 动作分类列表
text
GET /api/exercise/categories/
返回结构:
json
{
"code": 200,
"message": "获取动作分类成功",
"data": [
{
"category_id": 1,
"category_name": "胸部",
"category_image_url": "https://example.com/action-chest.jpg"
}
]
}
2.3 动作列表
text
GET /api/exercise/actions/
支持以下筛选参数:
| 参数 | 作用 |
|---|---|
category_id |
按动作分类筛选 |
action_difficulty |
按动作难度筛选 |
applicable_store_type |
按适用门店类型筛选 |
keyword |
按动作名称模糊搜索 |
请求示例:
text
GET /api/exercise/actions/?category_id=1
GET /api/exercise/actions/?keyword=深蹲
GET /api/exercise/actions/?applicable_store_type=1
2.4 动作详情
text
GET /api/exercise/actions/1/
返回指定动作的详细信息。
2.5 我的动作收藏
text
GET /api/exercise/my-favorites/
后端根据 JWT 定位当前用户,返回该用户收藏的动作。
2.6 收藏动作
text
POST /api/exercise/favorites/
请求体:
json
{
"action_id": 1
}
2.7 取消收藏动作
text
DELETE /api/exercise/favorites/1/
路径中的 1 为收藏记录的 favorite_id,而非动作 ID。
要点:
- 要点:动作列表接口以查询参数实现可选筛选,未传参数时返回全量数据。
- 要点 :
applicable_store_type与门店类型枚举对应:1为铁馆、2为商业私教馆。 - 要点 :收藏与取消收藏以
action_id(新增)和favorite_id(删除)为入参,二者语义不同,不可混用。
3. 视图层实现
打开文件:
text
backend/exercise/views.py
将内容替换为以下完整代码。
3.1 backend/exercise/views.py
python
import json
from django.views import View
from django.views.decorators.csrf import csrf_exempt
from django.utils.decorators import method_decorator
from rbac.auth import login_required
from rbac.responses import json_response
from .models import (
ActionCategory,
Action,
UserActionFavorite,
)
def get_json_body(request):
"""
获取 JSON 请求体
"""
try:
body = request.body.decode("utf-8")
if not body:
return {}
return json.loads(body)
except json.JSONDecodeError:
return None
def format_datetime_value(value):
"""
datetime 转字符串
"""
if value is None:
return None
return value.strftime("%Y-%m-%d %H:%M:%S")
def serialize_category(category):
"""
动作分类对象转 JSON 字典
"""
return {
"category_id": category.category_id,
"category_name": category.category_name,
"category_image_url": category.category_image_url,
}
def serialize_action(action, current_user=None):
"""
动作对象转 JSON 字典
"""
is_favorite = False
favorite_id = None
if current_user:
favorite = UserActionFavorite.objects.filter(
user=current_user,
action=action
).first()
if favorite:
is_favorite = True
favorite_id = favorite.favorite_id
return {
"action_id": action.action_id,
"action_name": action.action_name,
"category_id": action.category_id,
"category_name": action.category.category_name,
"action_difficulty": action.action_difficulty,
"action_image_url": action.action_image_url,
"action_steps": action.action_steps,
"attention_points": action.attention_points,
"applicable_equipment": action.applicable_equipment,
"applicable_store_type": action.applicable_store_type,
"applicable_store_type_text": (
action.get_applicable_store_type_display()
if action.applicable_store_type is not None
else None
),
"is_favorite": is_favorite,
"favorite_id": favorite_id,
}
def serialize_favorite(favorite):
"""
用户动作收藏对象转 JSON 字典
"""
action = favorite.action
return {
"favorite_id": favorite.favorite_id,
"created_at": format_datetime_value(favorite.created_at),
"action": serialize_action(action, favorite.user),
}
class ActionCategoryListView(View):
"""
动作分类列表接口
GET /api/exercise/categories/
"""
@login_required
def get(self, request):
categories = ActionCategory.objects.all().order_by("category_id")
data = []
for category in categories:
data.append(serialize_category(category))
return json_response(
data=data,
message="获取动作分类成功",
)
class ActionListView(View):
"""
动作列表接口
GET /api/exercise/actions/
可选查询参数:
category_id
action_difficulty
applicable_store_type
keyword
"""
@login_required
def get(self, request):
current_user = request.current_user
queryset = Action.objects.select_related("category").all().order_by("action_id")
category_id = request.GET.get("category_id")
action_difficulty = request.GET.get("action_difficulty")
applicable_store_type = request.GET.get("applicable_store_type")
keyword = request.GET.get("keyword")
if category_id:
queryset = queryset.filter(category_id=category_id)
if action_difficulty:
queryset = queryset.filter(action_difficulty=action_difficulty)
if applicable_store_type:
queryset = queryset.filter(applicable_store_type=applicable_store_type)
if keyword:
queryset = queryset.filter(action_name__icontains=keyword)
data = []
for action in queryset:
data.append(serialize_action(action, current_user))
return json_response(
data=data,
message="获取动作列表成功",
)
class ActionDetailView(View):
"""
动作详情接口
GET /api/exercise/actions/<action_id>/
"""
@login_required
def get(self, request, action_id):
current_user = request.current_user
action = Action.objects.select_related("category").filter(
action_id=action_id
).first()
if action is None:
return json_response(
message="动作不存在",
code=404,
status=404,
)
return json_response(
data=serialize_action(action, current_user),
message="获取动作详情成功",
)
class MyActionFavoriteListView(View):
"""
当前用户动作收藏列表接口
GET /api/exercise/my-favorites/
"""
@login_required
def get(self, request):
current_user = request.current_user
favorites = UserActionFavorite.objects.select_related(
"user",
"action",
"action__category",
).filter(
user=current_user
).order_by("-created_at")
data = []
for favorite in favorites:
data.append(serialize_favorite(favorite))
return json_response(
data=data,
message="获取我的动作收藏成功",
)
@method_decorator(csrf_exempt, name="dispatch")
class ActionFavoriteCreateView(View):
"""
收藏动作接口
POST /api/exercise/favorites/
请求体:
{
"action_id": 1
}
"""
@login_required
def post(self, request):
current_user = request.current_user
data = get_json_body(request)
if data is None:
return json_response(
message="请求体不是合法 JSON",
code=400,
status=400,
)
action_id = data.get("action_id")
if not action_id:
return json_response(
message="action_id 不能为空",
code=400,
status=400,
)
action = Action.objects.select_related("category").filter(
action_id=action_id
).first()
if action is None:
return json_response(
message="动作不存在",
code=404,
status=404,
)
favorite, created = UserActionFavorite.objects.get_or_create(
user=current_user,
action=action,
)
if not created:
return json_response(
data=serialize_favorite(favorite),
message="你已经收藏过该动作",
)
return json_response(
data=serialize_favorite(favorite),
message="收藏动作成功",
)
@method_decorator(csrf_exempt, name="dispatch")
class ActionFavoriteDeleteView(View):
"""
取消收藏动作接口
DELETE /api/exercise/favorites/<favorite_id>/
"""
@login_required
def delete(self, request, favorite_id):
current_user = request.current_user
favorite = UserActionFavorite.objects.select_related(
"user",
"action",
"action__category",
).filter(
favorite_id=favorite_id,
user=current_user,
).first()
if favorite is None:
return json_response(
message="收藏记录不存在",
code=404,
status=404,
)
favorite.delete()
return json_response(
message="取消收藏成功",
)
3.2 实现要点说明
序列化与视图类的设计遵循以下原则:
| 组件 | 设计要点 | 目的 |
|---|---|---|
serialize_action |
接收可选 current_user,查询收藏关系后注入 is_favorite、favorite_id |
使列表与详情自带收藏状态 |
ActionListView |
以 select_related("category") 预取分类,icontains 实现模糊搜索 |
减少 N+1 查询、支持名称检索 |
ActionFavoriteCreateView |
使用 get_or_create,命中已有记录时返回提示而非报错 |
保证收藏操作幂等 |
ActionFavoriteDeleteView |
查询条件同时限定 favorite_id 与 user=current_user |
仅允许删除本人收藏 |
| 写操作视图 | 以 @method_decorator(csrf_exempt, name="dispatch") 装饰 |
放行无 CSRF 令牌的 JSON 请求 |
要点:
- 要点 :
serialize_action是收藏状态注入的核心,current_user为空时输出is_favorite=false、favorite_id=null。 - 要点 :
get_or_create将「重复收藏」转化为正常返回,避免触发数据库唯一约束异常。 - 要点 :取消收藏以
user=current_user作为查询条件之一,天然阻断越权删除他人收藏。 - 要点 :涉及请求体的 POST/DELETE 视图需免除 CSRF 校验,读取请求体统一经
get_json_body处理。
4. 路由层配置
4.1 创建 exercise/urls.py
新建文件:
text
backend/exercise/urls.py
写入:
python
from django.urls import path
from .views import (
ActionCategoryListView,
ActionListView,
ActionDetailView,
MyActionFavoriteListView,
ActionFavoriteCreateView,
ActionFavoriteDeleteView,
)
urlpatterns = [
path("categories/", ActionCategoryListView.as_view()),
path("actions/", ActionListView.as_view()),
path("actions/<int:action_id>/", ActionDetailView.as_view()),
path("my-favorites/", MyActionFavoriteListView.as_view()),
path("favorites/", ActionFavoriteCreateView.as_view()),
path("favorites/<int:favorite_id>/", ActionFavoriteDeleteView.as_view()),
]
4.2 修改项目总路由
打开文件:
text
backend/config/urls.py
修改前内容为:
python
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path("admin/", admin.site.urls),
path("api/", include("rbac.urls")),
path("api/store/", include("store.urls")),
path("api/course/", include("course.urls")),
]
修改为:
python
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path("admin/", admin.site.urls),
path("api/", include("rbac.urls")),
path("api/store/", include("store.urls")),
path("api/course/", include("course.urls")),
path("api/exercise/", include("exercise.urls")),
]
动作接口的最终地址为:
text
/api/exercise/categories/
/api/exercise/actions/
/api/exercise/actions/<action_id>/
/api/exercise/my-favorites/
/api/exercise/favorites/
/api/exercise/favorites/<favorite_id>/
要点:
- 要点 :
exercise/urls.py内的路径不含/api/exercise/前缀,前缀由项目总路由的include提供。 - 要点 :
<int:action_id>、<int:favorite_id>约束路径参数为整数,非法值在路由匹配阶段即被拒绝。 - 要点 :项目总路由新增一行
path("api/exercise/", include("exercise.urls"))即可挂载全部动作接口。
5. 后端启动与接口测试
5.1 启动后端
确认当前工作目录位于:
text
rbac_project/backend
启动后端:
bash
python manage.py runserver
5.2 登录 admin 获取 token
macOS / Linux:
bash
curl -X POST http://127.0.0.1:8000/api/login/ \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"123456"}'
Windows PowerShell:
powershell
curl -Method POST "http://127.0.0.1:8000/api/login/" `
-Headers @{"Content-Type"="application/json"} `
-Body '{"username":"admin","password":"123456"}'
从返回结果中复制 access_token,后续请求以该值替换占位符 你的admin_access_token。
5.3 查询动作分类
macOS / Linux:
bash
curl -X GET http://127.0.0.1:8000/api/exercise/categories/ \
-H "Authorization: Bearer 你的admin_access_token"
Windows PowerShell:
powershell
curl -Method GET "http://127.0.0.1:8000/api/exercise/categories/" `
-Headers @{"Authorization"="Bearer 你的admin_access_token"}
预期返回包含:
text
胸部
腿部
核心
5.4 查询动作列表
macOS / Linux:
bash
curl -X GET http://127.0.0.1:8000/api/exercise/actions/ \
-H "Authorization: Bearer 你的admin_access_token"
Windows PowerShell:
powershell
curl -Method GET "http://127.0.0.1:8000/api/exercise/actions/" `
-Headers @{"Authorization"="Bearer 你的admin_access_token"}
预期返回包含:
text
俯卧撑
深蹲
平板支撑
5.5 按关键词搜索动作
bash
curl -X GET "http://127.0.0.1:8000/api/exercise/actions/?keyword=深蹲" \
-H "Authorization: Bearer 你的admin_access_token"
预期仅返回:
text
深蹲
5.6 按分类筛选动作
以 胸部 的 category_id 为 1 为例:
bash
curl -X GET "http://127.0.0.1:8000/api/exercise/actions/?category_id=1" \
-H "Authorization: Bearer 你的admin_access_token"
预期返回胸部分类下的动作:
text
俯卧撑
5.7 按门店类型筛选动作
门店类型取值:
| 取值 | 含义 |
|---|---|
| 1 | 铁馆 |
| 2 | 商业私教馆 |
请求:
bash
curl -X GET "http://127.0.0.1:8000/api/exercise/actions/?applicable_store_type=1" \
-H "Authorization: Bearer 你的admin_access_token"
预期返回适合铁馆的动作。
5.8 查询动作详情
以动作 ID 为 1 为例:
bash
curl -X GET http://127.0.0.1:8000/api/exercise/actions/1/ \
-H "Authorization: Bearer 你的admin_access_token"
正常返回:
json
{
"code": 200,
"message": "获取动作详情成功",
"data": {
"action_id": 1,
"action_name": "俯卧撑",
"category_id": 1,
"category_name": "胸部",
"action_difficulty": 1,
"action_steps": "双手撑地,身体保持直线,屈肘下降后推起。",
"attention_points": "腰背保持稳定,不要塌腰。",
"applicable_equipment": "徒手",
"applicable_store_type": 1,
"applicable_store_type_text": "铁馆",
"is_favorite": true,
"favorite_id": 1
}
}
5.9 查询我的动作收藏
bash
curl -X GET http://127.0.0.1:8000/api/exercise/my-favorites/ \
-H "Authorization: Bearer 你的admin_access_token"
admin 正常应看到:
text
俯卧撑
5.10 收藏动作
以动作 ID 为 2 为例。
macOS / Linux:
bash
curl -X POST http://127.0.0.1:8000/api/exercise/favorites/ \
-H "Content-Type: application/json" \
-H "Authorization: Bearer 你的admin_access_token" \
-d '{"action_id":2}'
Windows PowerShell:
powershell
curl -Method POST "http://127.0.0.1:8000/api/exercise/favorites/" `
-Headers @{
"Content-Type"="application/json"
"Authorization"="Bearer 你的admin_access_token"
} `
-Body '{"action_id":2}'
正常返回:
json
{
"code": 200,
"message": "收藏动作成功",
"data": {
"favorite_id": 3
}
}
若已收藏过,返回:
json
{
"code": 200,
"message": "你已经收藏过该动作"
}
5.11 取消收藏动作
以收藏记录 ID 为 3 为例:
bash
curl -X DELETE http://127.0.0.1:8000/api/exercise/favorites/3/ \
-H "Authorization: Bearer 你的admin_access_token"
正常返回:
json
{
"code": 200,
"message": "取消收藏成功",
"data": null
}
取消收藏仅作用于当前登录用户自身的收藏记录,无法删除他人的收藏。
要点:
- 要点:测试顺序为登录取 token → 读接口 → 写接口 → 删除接口,写删除依赖读接口返回的 ID。
- 要点 :收藏使用
action_id,取消收藏使用favorite_id,二者来源不同不可互换。 - 要点:重复收藏返回 200 与提示文案,而非数据库错误。
6. 用户隔离与匿名请求验证
6.1 测试 zhangsan
先登录 zhangsan:
bash
curl -X POST http://127.0.0.1:8000/api/login/ \
-H "Content-Type: application/json" \
-d '{"username":"zhangsan","password":"123456"}'
复制 zhangsan 的 access_token。
查询收藏:
bash
curl -X GET http://127.0.0.1:8000/api/exercise/my-favorites/ \
-H "Authorization: Bearer 你的zhangsan_access_token"
zhangsan 正常应看到:
text
深蹲
收藏动作:
bash
curl -X POST http://127.0.0.1:8000/api/exercise/favorites/ \
-H "Content-Type: application/json" \
-H "Authorization: Bearer 你的zhangsan_access_token" \
-d '{"action_id":1}'
该请求使 zhangsan 收藏:
text
俯卧撑
6.2 不带 token 的请求
直接请求:
bash
curl -X GET http://127.0.0.1:8000/api/exercise/actions/
应返回:
json
{
"code": 401,
"message": "未提供 access_token",
"data": null
}
该结果表明动作接口已受登录保护。
要点:
- 要点 :
my-favorites依据 token 解析用户身份,不同账号返回各自的收藏集合。 - 要点:admin 与 zhangsan 的收藏数据相互隔离,越权访问他人收藏在查询层即被过滤。
- 要点 :缺失 token 的请求统一返回 401,由
login_required拦截器处理。
7. 阶段验收与模块进度
7.1 阶段 16 完成标准
需确认以下各项:
text
1. backend/exercise/views.py 已编写完成
2. backend/exercise/urls.py 已创建
3. backend/config/urls.py 已引入 exercise.urls
4. GET /api/exercise/categories/ 可返回动作分类
5. GET /api/exercise/actions/ 可返回动作列表
6. GET /api/exercise/actions/<id>/ 可返回动作详情
7. GET /api/exercise/my-favorites/ 可返回当前用户收藏动作
8. POST /api/exercise/favorites/ 可收藏动作
9. DELETE /api/exercise/favorites/<favorite_id>/ 可取消收藏动作
10. 不带 token 请求动作接口返回 401
7.2 当前模块进度
text
rbac/
已完成:
- 用户
- 角色
- 权限
- JWT 登录
- RBAC 权限拦截
- 用户档案接口
store/
已完成:
- 省份表
- 门店表
- 用户门店关系表
- 省份查询接口
- 门店查询接口
- 当前用户门店查询接口
course/
已完成:
- 课程分类表
- 课程表
- 用户课程收藏表
- 门店课程排期表
- 课程分类查询接口
- 课程查询接口
- 课程详情接口
- 我的课程收藏接口
- 收藏 / 取消收藏课程接口
- 门店课程排期查询接口
exercise/
已完成:
- 动作分类表
- 动作表
- 用户动作收藏表
- 动作分类查询接口
- 动作查询接口
- 动作详情接口
- 我的动作收藏接口
- 收藏 / 取消收藏动作接口
要点:
- 要点:完成标准覆盖视图、路由、总路由挂载与六类接口的行为验证。
- 要点 :
exercise模块补齐后,后端四大业务领域(rbac、store、course、exercise)查询接口全部就绪。 - 要点:后端接口层已完整,下一阶段转入前端对接。
阶段小结
| 环节 | 核心内容 | 在系统中的作用 |
|---|---|---|
| 数据模型 | 动作分类表、动作表、用户动作收藏表 | 支撑动作查询与收藏读写 |
| 视图层 | serialize_action 注入收藏状态、六类视图类 |
统一响应结构、隔离用户数据 |
| 幂等收藏 | get_or_create 处理重复收藏 |
避免数据库唯一约束异常 |
| 取消收藏 | 查询限定 favorite_id 与 user |
阻止越权删除他人收藏 |
| 路由配置 | exercise/urls.py + 总路由 include |
挂载 /api/exercise/ 接口 |
| 接口测试 | 登录取 token、读接口、写接口、删除接口 | 验证接口契约与登录保护 |
常见问题与排查
| 现象 | 原因 | 处理方式 |
|---|---|---|
报错 No module named exercise.urls |
未创建 backend/exercise/urls.py,或 config/urls.py 中 include("exercise.urls") 书写有误 |
补建 exercise/urls.py,核对 path("api/exercise/", include("exercise.urls")) |
报错 Table 'rbac_db.t_action' doesn't exist |
阶段 12 的迁移未执行成功 | 执行 python manage.py makemigrations exercise 与 python manage.py migrate,再执行 python manage.py init_business_data |
| 动作列表为空 | 阶段 13 的业务测试数据未初始化 | 执行 python manage.py init_business_data |
| 收藏动作报重复错误 | 正常流程不会触发数据库错误,接口已用 get_or_create |
若已收藏会返回「你已经收藏过该动作」,无需额外处理 |
| 取消收藏返回 404 | favorite_id 错误、收藏非本人记录,或记录已被删除 |
先请求 GET /api/exercise/my-favorites/ 取得正确的 favorite_id 再删除 |
下一阶段衔接
阶段 16 完成后,store、course、exercise 三大业务领域的查询与收藏接口已在后端全部就绪。这些接口目前仅以 curl 验证,尚无页面承载。
阶段 17(阶段 17:Vue3 对接门店、课程、动作接口.md)将在前端以 Vue3 建立门店列表、课程列表、动作列表与我的收藏页面,通过封装的请求工具调用本阶段的动作接口,把后端数据呈现为可交互界面。