refactor(dramas): 重构短剧预搜索功能,改为搜索短剧本身
1. 将预搜索接口从剧集维度改为短剧维度,移除剧集相关逻辑 2. 新增短剧列表按标题筛选的能力 3. 更新测试用例与接口文档,修正命名与逻辑
This commit is contained in:
590
docs/apifox-app-apis.openapi.json
Normal file
590
docs/apifox-app-apis.openapi.json
Normal file
@@ -0,0 +1,590 @@
|
||||
{
|
||||
"openapi": "3.0.3",
|
||||
"info": {
|
||||
"title": "CTH TK App APIs",
|
||||
"description": "Apifox import file for mini app drama and history APIs.",
|
||||
"version": "1.0.0"
|
||||
},
|
||||
"servers": [
|
||||
{
|
||||
"url": "http://localhost:3000",
|
||||
"description": "Local backend"
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
{
|
||||
"name": "App Dramas",
|
||||
"description": "Mini app drama APIs"
|
||||
},
|
||||
{
|
||||
"name": "App History",
|
||||
"description": "Mini app playback history APIs"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"/api/app/v1/dramas": {
|
||||
"get": {
|
||||
"tags": ["App Dramas"],
|
||||
"summary": "短剧列表",
|
||||
"operationId": "listAppDramas",
|
||||
"parameters": [
|
||||
{ "$ref": "#/components/parameters/Page" },
|
||||
{ "$ref": "#/components/parameters/PageSize10" }
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"allOf": [
|
||||
{ "$ref": "#/components/schemas/ApiResponseBase" },
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"$ref": "#/components/schemas/AppDramaPage"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/app/v1/dramas/recommended": {
|
||||
"get": {
|
||||
"tags": ["App Dramas"],
|
||||
"summary": "推荐短剧列表",
|
||||
"operationId": "listRecommendedAppDramas",
|
||||
"parameters": [
|
||||
{ "$ref": "#/components/parameters/Page" },
|
||||
{ "$ref": "#/components/parameters/PageSize10" }
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"allOf": [
|
||||
{ "$ref": "#/components/schemas/ApiResponseBase" },
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"$ref": "#/components/schemas/AppDramaPage"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/app/v1/dramas/presearch": {
|
||||
"get": {
|
||||
"tags": ["App Dramas"],
|
||||
"summary": "关键字预查询视频列表",
|
||||
"operationId": "presearchAppDramaVideos",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "keyword",
|
||||
"in": "query",
|
||||
"description": "用户输入关键字;空值返回空列表",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"example": "skyline"
|
||||
},
|
||||
{ "$ref": "#/components/parameters/Page" },
|
||||
{ "$ref": "#/components/parameters/PageSize10" }
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"allOf": [
|
||||
{ "$ref": "#/components/schemas/ApiResponseBase" },
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"$ref": "#/components/schemas/VideoPresearchPage"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/app/v1/dramas/{id}/episodes": {
|
||||
"get": {
|
||||
"tags": ["App Dramas"],
|
||||
"summary": "短剧剧集列表",
|
||||
"operationId": "listAppDramaEpisodes",
|
||||
"parameters": [
|
||||
{ "$ref": "#/components/parameters/DramaIdPath" },
|
||||
{ "$ref": "#/components/parameters/Page" },
|
||||
{ "$ref": "#/components/parameters/PageSize20" }
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"allOf": [
|
||||
{ "$ref": "#/components/schemas/ApiResponseBase" },
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"$ref": "#/components/schemas/AppEpisodePage"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": { "$ref": "#/components/responses/ErrorResponse" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/app/v1/dramas/{id}": {
|
||||
"get": {
|
||||
"tags": ["App Dramas"],
|
||||
"summary": "短剧详情",
|
||||
"operationId": "getAppDrama",
|
||||
"parameters": [
|
||||
{ "$ref": "#/components/parameters/DramaIdPath" }
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"allOf": [
|
||||
{ "$ref": "#/components/schemas/ApiResponseBase" },
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"$ref": "#/components/schemas/AppDramaDetail"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": { "$ref": "#/components/responses/ErrorResponse" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/app/v1/history": {
|
||||
"get": {
|
||||
"tags": ["App History"],
|
||||
"summary": "播放历史列表",
|
||||
"operationId": "listAppHistory",
|
||||
"security": [
|
||||
{
|
||||
"bearerAuth": []
|
||||
}
|
||||
],
|
||||
"parameters": [
|
||||
{ "$ref": "#/components/parameters/Page" },
|
||||
{ "$ref": "#/components/parameters/PageSize20" }
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"allOf": [
|
||||
{ "$ref": "#/components/schemas/ApiResponseBase" },
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"$ref": "#/components/schemas/PlaybackHistoryPage"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": { "$ref": "#/components/responses/ErrorResponse" }
|
||||
}
|
||||
},
|
||||
"post": {
|
||||
"tags": ["App History"],
|
||||
"summary": "上报播放历史",
|
||||
"operationId": "upsertAppHistory",
|
||||
"security": [
|
||||
{
|
||||
"bearerAuth": []
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/UpsertPlaybackHistoryRequest"
|
||||
},
|
||||
"example": {
|
||||
"episodeId": 123,
|
||||
"progressSeconds": 86,
|
||||
"completed": false,
|
||||
"occurredAt": "2026-07-02T10:00:00.000Z"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"201": {
|
||||
"description": "Created",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"allOf": [
|
||||
{ "$ref": "#/components/schemas/ApiResponseBase" },
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"$ref": "#/components/schemas/PlaybackHistoryRecord"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": { "$ref": "#/components/responses/ErrorResponse" },
|
||||
"401": { "$ref": "#/components/responses/ErrorResponse" },
|
||||
"404": { "$ref": "#/components/responses/ErrorResponse" }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"securitySchemes": {
|
||||
"bearerAuth": {
|
||||
"type": "http",
|
||||
"scheme": "bearer",
|
||||
"bearerFormat": "JWT"
|
||||
}
|
||||
},
|
||||
"parameters": {
|
||||
"DramaIdPath": {
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"minimum": 1
|
||||
},
|
||||
"example": 1
|
||||
},
|
||||
"Page": {
|
||||
"name": "page",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"default": 1
|
||||
}
|
||||
},
|
||||
"PageSize10": {
|
||||
"name": "pageSize",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"maximum": 100,
|
||||
"default": 10
|
||||
}
|
||||
},
|
||||
"PageSize20": {
|
||||
"name": "pageSize",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"maximum": 100,
|
||||
"default": 20
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"ErrorResponse": {
|
||||
"description": "Error response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"allOf": [
|
||||
{ "$ref": "#/components/schemas/ApiResponseBase" },
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"code": { "type": "integer", "example": 404 },
|
||||
"data": { "type": "object", "example": {} },
|
||||
"message": { "type": "string", "example": "Not found" }
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"schemas": {
|
||||
"ApiResponseBase": {
|
||||
"type": "object",
|
||||
"required": ["code", "data", "message", "timestamp", "requestId", "cost"],
|
||||
"properties": {
|
||||
"code": {
|
||||
"type": "integer",
|
||||
"description": "业务响应码;成功为 0,错误为 HTTP 状态码",
|
||||
"example": 0
|
||||
},
|
||||
"data": {
|
||||
"type": "object"
|
||||
},
|
||||
"message": {
|
||||
"type": "string",
|
||||
"example": "success"
|
||||
},
|
||||
"timestamp": {
|
||||
"type": "integer",
|
||||
"format": "int64",
|
||||
"example": 1782986400000
|
||||
},
|
||||
"requestId": {
|
||||
"type": "string",
|
||||
"example": "550e8400-e29b-41d4-a716-446655440000"
|
||||
},
|
||||
"cost": {
|
||||
"type": "string",
|
||||
"example": "12ms"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Pagination": {
|
||||
"type": "object",
|
||||
"required": ["page", "pageSize", "total"],
|
||||
"properties": {
|
||||
"page": { "type": "integer", "example": 1 },
|
||||
"pageSize": { "type": "integer", "example": 10 },
|
||||
"total": { "type": "integer", "example": 100 }
|
||||
}
|
||||
},
|
||||
"AppDrama": {
|
||||
"type": "object",
|
||||
"required": ["id", "title", "coverUrl", "type", "tags", "status", "sortOrder", "isRecommended", "onlineVersion", "totalEpisodes", "publishedEpisodes", "createdAt", "updatedAt"],
|
||||
"properties": {
|
||||
"id": { "type": "integer", "example": 1 },
|
||||
"tiktokAlbumId": { "type": "string", "nullable": true },
|
||||
"title": { "type": "string", "example": "Published Drama" },
|
||||
"description": { "type": "string", "nullable": true },
|
||||
"coverId": { "type": "integer", "nullable": true },
|
||||
"coverUrl": { "type": "string", "example": "https://cdn.example.com/cover.jpg" },
|
||||
"type": { "type": "string", "example": "romance" },
|
||||
"tags": { "type": "array", "items": { "type": "string" } },
|
||||
"region": { "type": "string", "nullable": true },
|
||||
"language": { "type": "string", "nullable": true },
|
||||
"year": { "type": "integer", "nullable": true },
|
||||
"status": { "type": "string", "enum": ["draft", "scheduled", "published", "offline"], "example": "published" },
|
||||
"sortOrder": { "type": "integer", "example": 0 },
|
||||
"isRecommended": { "type": "boolean", "example": true },
|
||||
"onlineVersion": { "type": "integer", "example": 1 },
|
||||
"totalEpisodes": { "type": "integer", "example": 20 },
|
||||
"publishedEpisodes": { "type": "integer", "example": 10 },
|
||||
"createdAt": { "type": "string", "format": "date-time" },
|
||||
"updatedAt": { "type": "string", "format": "date-time" }
|
||||
}
|
||||
},
|
||||
"AppDramaDetail": {
|
||||
"allOf": [
|
||||
{ "$ref": "#/components/schemas/AppDrama" },
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"portraitCoverUrl": { "type": "string", "nullable": true },
|
||||
"landscapeCoverUrl": { "type": "string", "nullable": true }
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"AppDramaPage": {
|
||||
"type": "object",
|
||||
"required": ["list", "pagination"],
|
||||
"properties": {
|
||||
"list": {
|
||||
"type": "array",
|
||||
"items": { "$ref": "#/components/schemas/AppDrama" }
|
||||
},
|
||||
"pagination": { "$ref": "#/components/schemas/Pagination" }
|
||||
}
|
||||
},
|
||||
"AppEpisode": {
|
||||
"type": "object",
|
||||
"required": ["id", "dramaId", "albumId", "episodeNo", "seq", "title", "coverUrl", "durationSeconds", "isPaid", "freeType", "price", "status", "reviewStatus", "publishStatus", "createdAt", "updatedAt"],
|
||||
"properties": {
|
||||
"id": { "type": "integer", "example": 1 },
|
||||
"dramaId": { "type": "integer", "example": 1 },
|
||||
"albumId": { "type": "integer", "example": 1 },
|
||||
"tiktokEpisodeId": { "type": "string", "nullable": true },
|
||||
"episodeNo": { "type": "integer", "example": 1 },
|
||||
"seq": { "type": "integer", "example": 1 },
|
||||
"title": { "type": "string", "example": "Episode 1" },
|
||||
"description": { "type": "string", "nullable": true },
|
||||
"byteplusVid": { "type": "string", "nullable": true },
|
||||
"coverId": { "type": "integer", "nullable": true },
|
||||
"coverUrl": { "type": "string", "example": "https://cdn.example.com/episode.jpg" },
|
||||
"videoUrl": { "type": "string", "nullable": true },
|
||||
"storageBucket": { "type": "string", "nullable": true },
|
||||
"objectKey": { "type": "string", "nullable": true },
|
||||
"trialDurationSeconds": { "type": "integer", "nullable": true },
|
||||
"durationSeconds": { "type": "integer", "example": 300 },
|
||||
"width": { "type": "integer", "nullable": true },
|
||||
"height": { "type": "integer", "nullable": true },
|
||||
"isPaid": { "type": "boolean", "example": false },
|
||||
"freeType": { "type": "string", "enum": ["free", "paid"], "example": "free" },
|
||||
"unlockPrice": { "type": "integer", "nullable": true },
|
||||
"price": { "type": "integer", "example": 0 },
|
||||
"publishAt": { "type": "string", "format": "date-time", "nullable": true },
|
||||
"nextReleaseAt": { "type": "string", "format": "date-time", "nullable": true },
|
||||
"status": { "type": "string", "enum": ["draft", "scheduled", "published", "offline"], "example": "published" },
|
||||
"reviewStatus": { "type": "string", "enum": ["not_submitted", "pending", "reviewing", "approved", "rejected"], "example": "approved" },
|
||||
"reviewMessage": { "type": "string", "nullable": true },
|
||||
"publishStatus": { "type": "string", "enum": ["draft", "scheduled", "published", "offline"], "example": "published" },
|
||||
"createdAt": { "type": "string", "format": "date-time" },
|
||||
"updatedAt": { "type": "string", "format": "date-time" }
|
||||
}
|
||||
},
|
||||
"AppEpisodePage": {
|
||||
"type": "object",
|
||||
"required": ["list", "pagination"],
|
||||
"properties": {
|
||||
"list": {
|
||||
"type": "array",
|
||||
"items": { "$ref": "#/components/schemas/AppEpisode" }
|
||||
},
|
||||
"pagination": { "$ref": "#/components/schemas/Pagination" }
|
||||
}
|
||||
},
|
||||
"VideoPresearchItem": {
|
||||
"type": "object",
|
||||
"required": ["id", "dramaId", "episodeNo", "title", "coverUrl", "durationSeconds", "freeType", "price", "dramaTitle", "dramaCoverUrl"],
|
||||
"properties": {
|
||||
"id": { "type": "integer", "example": 1 },
|
||||
"dramaId": { "type": "integer", "example": 1 },
|
||||
"episodeNo": { "type": "integer", "example": 1 },
|
||||
"title": { "type": "string", "example": "Pilot Video Match" },
|
||||
"description": { "type": "string", "nullable": true },
|
||||
"coverUrl": { "type": "string", "example": "https://cdn.example.com/episode.jpg" },
|
||||
"durationSeconds": { "type": "integer", "example": 180 },
|
||||
"byteplusVid": { "type": "string", "nullable": true },
|
||||
"videoUrl": { "type": "string", "nullable": true },
|
||||
"freeType": { "type": "string", "enum": ["free", "paid"], "example": "free" },
|
||||
"price": { "type": "integer", "example": 0 },
|
||||
"dramaTitle": { "type": "string", "example": "Skyline Keyword Saga" },
|
||||
"dramaCoverUrl": { "type": "string", "example": "https://cdn.example.com/drama.jpg" }
|
||||
}
|
||||
},
|
||||
"VideoPresearchPage": {
|
||||
"type": "object",
|
||||
"required": ["list", "pagination"],
|
||||
"properties": {
|
||||
"list": {
|
||||
"type": "array",
|
||||
"items": { "$ref": "#/components/schemas/VideoPresearchItem" }
|
||||
},
|
||||
"pagination": { "$ref": "#/components/schemas/Pagination" }
|
||||
}
|
||||
},
|
||||
"UpsertPlaybackHistoryRequest": {
|
||||
"type": "object",
|
||||
"required": ["episodeId", "progressSeconds"],
|
||||
"properties": {
|
||||
"episodeId": { "type": "integer", "minimum": 1, "example": 123 },
|
||||
"progressSeconds": { "type": "integer", "minimum": 0, "example": 86 },
|
||||
"completed": { "type": "boolean", "example": false },
|
||||
"occurredAt": { "type": "string", "format": "date-time", "example": "2026-07-02T10:00:00.000Z" }
|
||||
}
|
||||
},
|
||||
"PlaybackHistoryRecord": {
|
||||
"type": "object",
|
||||
"required": ["id", "userId", "dramaId", "episodeId", "progressSeconds", "durationSeconds", "completed", "lastPlayedAt", "createdAt", "updatedAt"],
|
||||
"properties": {
|
||||
"id": { "type": "integer", "example": 1 },
|
||||
"userId": { "type": "integer", "example": 1 },
|
||||
"dramaId": { "type": "integer", "example": 1 },
|
||||
"episodeId": { "type": "integer", "example": 123 },
|
||||
"progressSeconds": { "type": "integer", "example": 86 },
|
||||
"durationSeconds": { "type": "integer", "example": 300 },
|
||||
"completed": { "type": "boolean", "example": false },
|
||||
"lastPlayedAt": { "type": "string", "format": "date-time" },
|
||||
"createdAt": { "type": "string", "format": "date-time" },
|
||||
"updatedAt": { "type": "string", "format": "date-time" }
|
||||
}
|
||||
},
|
||||
"PlaybackHistoryListItem": {
|
||||
"type": "object",
|
||||
"required": ["id", "dramaId", "episodeId", "episodeNo", "title", "coverUrl", "progressSeconds", "durationSeconds", "completed", "lastPlayedAt", "dramaTitle", "dramaCoverUrl"],
|
||||
"properties": {
|
||||
"id": { "type": "integer", "example": 1 },
|
||||
"dramaId": { "type": "integer", "example": 1 },
|
||||
"episodeId": { "type": "integer", "example": 123 },
|
||||
"episodeNo": { "type": "integer", "example": 1 },
|
||||
"title": { "type": "string", "example": "History Episode" },
|
||||
"coverUrl": { "type": "string", "example": "https://cdn.example.com/history-episode.jpg" },
|
||||
"progressSeconds": { "type": "integer", "example": 86 },
|
||||
"durationSeconds": { "type": "integer", "example": 300 },
|
||||
"completed": { "type": "boolean", "example": false },
|
||||
"lastPlayedAt": { "type": "string", "format": "date-time" },
|
||||
"dramaTitle": { "type": "string", "example": "History Drama" },
|
||||
"dramaCoverUrl": { "type": "string", "example": "https://cdn.example.com/history-drama.jpg" }
|
||||
}
|
||||
},
|
||||
"PlaybackHistoryPage": {
|
||||
"type": "object",
|
||||
"required": ["list", "pagination"],
|
||||
"properties": {
|
||||
"list": {
|
||||
"type": "array",
|
||||
"items": { "$ref": "#/components/schemas/PlaybackHistoryListItem" }
|
||||
},
|
||||
"pagination": { "$ref": "#/components/schemas/Pagination" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
351
docs/apifox-app-apis.swagger.json
Normal file
351
docs/apifox-app-apis.swagger.json
Normal file
@@ -0,0 +1,351 @@
|
||||
{
|
||||
"swagger": "2.0",
|
||||
"info": {
|
||||
"title": "CTH TK App APIs",
|
||||
"description": "Apifox import file for mini app drama and history APIs.",
|
||||
"version": "1.0.0"
|
||||
},
|
||||
"host": "localhost:3000",
|
||||
"basePath": "/",
|
||||
"schemes": ["http"],
|
||||
"consumes": ["application/json"],
|
||||
"produces": ["application/json"],
|
||||
"securityDefinitions": {
|
||||
"Bearer": {
|
||||
"type": "apiKey",
|
||||
"name": "Authorization",
|
||||
"in": "header",
|
||||
"description": "Bearer <JWT>"
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
{ "name": "App Dramas", "description": "Mini app drama APIs" },
|
||||
{ "name": "App History", "description": "Mini app playback history APIs" }
|
||||
],
|
||||
"paths": {
|
||||
"/api/app/v1/dramas": {
|
||||
"get": {
|
||||
"tags": ["App Dramas"],
|
||||
"summary": "短剧列表",
|
||||
"operationId": "listAppDramas",
|
||||
"parameters": [
|
||||
{ "name": "page", "in": "query", "type": "integer", "default": 1, "minimum": 1 },
|
||||
{ "name": "pageSize", "in": "query", "type": "integer", "default": 10, "minimum": 1, "maximum": 100 }
|
||||
],
|
||||
"responses": {
|
||||
"200": { "description": "OK", "schema": { "$ref": "#/definitions/AppDramaPageResponse" } }
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/app/v1/dramas/recommended": {
|
||||
"get": {
|
||||
"tags": ["App Dramas"],
|
||||
"summary": "推荐短剧列表",
|
||||
"operationId": "listRecommendedAppDramas",
|
||||
"parameters": [
|
||||
{ "name": "page", "in": "query", "type": "integer", "default": 1, "minimum": 1 },
|
||||
{ "name": "pageSize", "in": "query", "type": "integer", "default": 10, "minimum": 1, "maximum": 100 }
|
||||
],
|
||||
"responses": {
|
||||
"200": { "description": "OK", "schema": { "$ref": "#/definitions/AppDramaPageResponse" } }
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/app/v1/dramas/presearch": {
|
||||
"get": {
|
||||
"tags": ["App Dramas"],
|
||||
"summary": "短剧名称预查询",
|
||||
"operationId": "presearchAppDramasByTitle",
|
||||
"parameters": [
|
||||
{ "name": "keyword", "in": "query", "type": "string", "description": "短剧名称关键字;空值返回空列表" },
|
||||
{ "name": "page", "in": "query", "type": "integer", "default": 1, "minimum": 1 },
|
||||
{ "name": "pageSize", "in": "query", "type": "integer", "default": 10, "minimum": 1, "maximum": 100 }
|
||||
],
|
||||
"responses": {
|
||||
"200": { "description": "OK", "schema": { "$ref": "#/definitions/AppDramaPageResponse" } }
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/app/v1/dramas/{id}/episodes": {
|
||||
"get": {
|
||||
"tags": ["App Dramas"],
|
||||
"summary": "短剧剧集列表",
|
||||
"operationId": "listAppDramaEpisodes",
|
||||
"parameters": [
|
||||
{ "name": "id", "in": "path", "required": true, "type": "integer", "minimum": 1 },
|
||||
{ "name": "page", "in": "query", "type": "integer", "default": 1, "minimum": 1 },
|
||||
{ "name": "pageSize", "in": "query", "type": "integer", "default": 20, "minimum": 1, "maximum": 100 }
|
||||
],
|
||||
"responses": {
|
||||
"200": { "description": "OK", "schema": { "$ref": "#/definitions/AppEpisodePageResponse" } },
|
||||
"404": { "description": "Not Found", "schema": { "$ref": "#/definitions/ErrorResponse" } }
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/app/v1/dramas/{id}": {
|
||||
"get": {
|
||||
"tags": ["App Dramas"],
|
||||
"summary": "短剧详情",
|
||||
"operationId": "getAppDrama",
|
||||
"parameters": [
|
||||
{ "name": "id", "in": "path", "required": true, "type": "integer", "minimum": 1 }
|
||||
],
|
||||
"responses": {
|
||||
"200": { "description": "OK", "schema": { "$ref": "#/definitions/AppDramaDetailResponse" } },
|
||||
"404": { "description": "Not Found", "schema": { "$ref": "#/definitions/ErrorResponse" } }
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/app/v1/history": {
|
||||
"get": {
|
||||
"tags": ["App History"],
|
||||
"summary": "播放历史列表",
|
||||
"operationId": "listAppHistory",
|
||||
"security": [{ "Bearer": [] }],
|
||||
"parameters": [
|
||||
{ "name": "page", "in": "query", "type": "integer", "default": 1, "minimum": 1 },
|
||||
{ "name": "pageSize", "in": "query", "type": "integer", "default": 20, "minimum": 1, "maximum": 100 }
|
||||
],
|
||||
"responses": {
|
||||
"200": { "description": "OK", "schema": { "$ref": "#/definitions/PlaybackHistoryPageResponse" } },
|
||||
"401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/ErrorResponse" } }
|
||||
}
|
||||
},
|
||||
"post": {
|
||||
"tags": ["App History"],
|
||||
"summary": "上报播放历史",
|
||||
"operationId": "upsertAppHistory",
|
||||
"security": [{ "Bearer": [] }],
|
||||
"parameters": [
|
||||
{ "name": "body", "in": "body", "required": true, "schema": { "$ref": "#/definitions/UpsertPlaybackHistoryRequest" } }
|
||||
],
|
||||
"responses": {
|
||||
"201": { "description": "Created", "schema": { "$ref": "#/definitions/PlaybackHistoryRecordResponse" } },
|
||||
"400": { "description": "Bad Request", "schema": { "$ref": "#/definitions/ErrorResponse" } },
|
||||
"401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/ErrorResponse" } },
|
||||
"404": { "description": "Not Found", "schema": { "$ref": "#/definitions/ErrorResponse" } }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"definitions": {
|
||||
"ApiResponseBase": {
|
||||
"type": "object",
|
||||
"required": ["code", "data", "message", "timestamp", "requestId", "cost"],
|
||||
"properties": {
|
||||
"code": { "type": "integer", "description": "成功为 0,错误为 HTTP 状态码", "example": 0 },
|
||||
"message": { "type": "string", "example": "success" },
|
||||
"timestamp": { "type": "integer", "format": "int64", "example": 1782986400000 },
|
||||
"requestId": { "type": "string", "example": "550e8400-e29b-41d4-a716-446655440000" },
|
||||
"cost": { "type": "string", "example": "12ms" }
|
||||
}
|
||||
},
|
||||
"Pagination": {
|
||||
"type": "object",
|
||||
"required": ["page", "pageSize", "total"],
|
||||
"properties": {
|
||||
"page": { "type": "integer", "example": 1 },
|
||||
"pageSize": { "type": "integer", "example": 10 },
|
||||
"total": { "type": "integer", "example": 100 }
|
||||
}
|
||||
},
|
||||
"AppDrama": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": { "type": "integer", "example": 1 },
|
||||
"tiktokAlbumId": { "type": "string" },
|
||||
"title": { "type": "string", "example": "Published Drama" },
|
||||
"description": { "type": "string" },
|
||||
"coverId": { "type": "integer" },
|
||||
"coverUrl": { "type": "string", "example": "https://cdn.example.com/cover.jpg" },
|
||||
"type": { "type": "string", "example": "romance" },
|
||||
"tags": { "type": "array", "items": { "type": "string" } },
|
||||
"region": { "type": "string" },
|
||||
"language": { "type": "string" },
|
||||
"year": { "type": "integer" },
|
||||
"status": { "type": "string", "enum": ["draft", "scheduled", "published", "offline"], "example": "published" },
|
||||
"sortOrder": { "type": "integer", "example": 0 },
|
||||
"isRecommended": { "type": "boolean", "example": true },
|
||||
"onlineVersion": { "type": "integer", "example": 1 },
|
||||
"totalEpisodes": { "type": "integer", "example": 20 },
|
||||
"publishedEpisodes": { "type": "integer", "example": 10 },
|
||||
"createdAt": { "type": "string", "format": "date-time" },
|
||||
"updatedAt": { "type": "string", "format": "date-time" }
|
||||
}
|
||||
},
|
||||
"AppDramaDetail": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": { "type": "integer", "example": 1 },
|
||||
"tiktokAlbumId": { "type": "string" },
|
||||
"title": { "type": "string", "example": "Published Drama" },
|
||||
"description": { "type": "string" },
|
||||
"coverId": { "type": "integer" },
|
||||
"coverUrl": { "type": "string", "example": "https://cdn.example.com/cover.jpg" },
|
||||
"portraitCoverUrl": { "type": "string" },
|
||||
"landscapeCoverUrl": { "type": "string" },
|
||||
"type": { "type": "string", "example": "romance" },
|
||||
"tags": { "type": "array", "items": { "type": "string" } },
|
||||
"region": { "type": "string" },
|
||||
"language": { "type": "string" },
|
||||
"year": { "type": "integer" },
|
||||
"status": { "type": "string", "enum": ["draft", "scheduled", "published", "offline"], "example": "published" },
|
||||
"sortOrder": { "type": "integer", "example": 0 },
|
||||
"isRecommended": { "type": "boolean", "example": true },
|
||||
"onlineVersion": { "type": "integer", "example": 1 },
|
||||
"totalEpisodes": { "type": "integer", "example": 20 },
|
||||
"publishedEpisodes": { "type": "integer", "example": 10 },
|
||||
"createdAt": { "type": "string", "format": "date-time" },
|
||||
"updatedAt": { "type": "string", "format": "date-time" }
|
||||
}
|
||||
},
|
||||
"AppEpisode": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": { "type": "integer", "example": 1 },
|
||||
"dramaId": { "type": "integer", "example": 1 },
|
||||
"albumId": { "type": "integer", "example": 1 },
|
||||
"tiktokEpisodeId": { "type": "string" },
|
||||
"episodeNo": { "type": "integer", "example": 1 },
|
||||
"seq": { "type": "integer", "example": 1 },
|
||||
"title": { "type": "string", "example": "Episode 1" },
|
||||
"description": { "type": "string" },
|
||||
"byteplusVid": { "type": "string" },
|
||||
"coverId": { "type": "integer" },
|
||||
"coverUrl": { "type": "string", "example": "https://cdn.example.com/episode.jpg" },
|
||||
"videoUrl": { "type": "string" },
|
||||
"trialDurationSeconds": { "type": "integer" },
|
||||
"durationSeconds": { "type": "integer", "example": 300 },
|
||||
"isPaid": { "type": "boolean", "example": false },
|
||||
"freeType": { "type": "string", "enum": ["free", "paid"], "example": "free" },
|
||||
"unlockPrice": { "type": "integer" },
|
||||
"price": { "type": "integer", "example": 0 },
|
||||
"status": { "type": "string", "enum": ["draft", "scheduled", "published", "offline"], "example": "published" },
|
||||
"reviewStatus": { "type": "string", "enum": ["not_submitted", "pending", "reviewing", "approved", "rejected"], "example": "approved" },
|
||||
"publishStatus": { "type": "string", "enum": ["draft", "scheduled", "published", "offline"], "example": "published" },
|
||||
"createdAt": { "type": "string", "format": "date-time" },
|
||||
"updatedAt": { "type": "string", "format": "date-time" }
|
||||
}
|
||||
},
|
||||
"PlaybackHistoryListItem": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": { "type": "integer", "example": 1 },
|
||||
"dramaId": { "type": "integer", "example": 1 },
|
||||
"episodeId": { "type": "integer", "example": 123 },
|
||||
"episodeNo": { "type": "integer", "example": 1 },
|
||||
"title": { "type": "string", "example": "History Episode" },
|
||||
"coverUrl": { "type": "string", "example": "https://cdn.example.com/history-episode.jpg" },
|
||||
"progressSeconds": { "type": "integer", "example": 86 },
|
||||
"durationSeconds": { "type": "integer", "example": 300 },
|
||||
"completed": { "type": "boolean", "example": false },
|
||||
"lastPlayedAt": { "type": "string", "format": "date-time" },
|
||||
"dramaTitle": { "type": "string", "example": "History Drama" },
|
||||
"dramaCoverUrl": { "type": "string", "example": "https://cdn.example.com/history-drama.jpg" }
|
||||
}
|
||||
},
|
||||
"PlaybackHistoryRecord": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": { "type": "integer", "example": 1 },
|
||||
"userId": { "type": "integer", "example": 1 },
|
||||
"dramaId": { "type": "integer", "example": 1 },
|
||||
"episodeId": { "type": "integer", "example": 123 },
|
||||
"progressSeconds": { "type": "integer", "example": 86 },
|
||||
"durationSeconds": { "type": "integer", "example": 300 },
|
||||
"completed": { "type": "boolean", "example": false },
|
||||
"lastPlayedAt": { "type": "string", "format": "date-time" },
|
||||
"createdAt": { "type": "string", "format": "date-time" },
|
||||
"updatedAt": { "type": "string", "format": "date-time" }
|
||||
}
|
||||
},
|
||||
"UpsertPlaybackHistoryRequest": {
|
||||
"type": "object",
|
||||
"required": ["episodeId", "progressSeconds"],
|
||||
"properties": {
|
||||
"episodeId": { "type": "integer", "minimum": 1, "example": 123 },
|
||||
"progressSeconds": { "type": "integer", "minimum": 0, "example": 86 },
|
||||
"completed": { "type": "boolean", "example": false },
|
||||
"occurredAt": { "type": "string", "format": "date-time", "example": "2026-07-02T10:00:00.000Z" }
|
||||
}
|
||||
},
|
||||
"AppDramaPage": {
|
||||
"type": "object",
|
||||
"properties": { "list": { "type": "array", "items": { "$ref": "#/definitions/AppDrama" } }, "pagination": { "$ref": "#/definitions/Pagination" } }
|
||||
},
|
||||
"AppEpisodePage": {
|
||||
"type": "object",
|
||||
"properties": { "list": { "type": "array", "items": { "$ref": "#/definitions/AppEpisode" } }, "pagination": { "$ref": "#/definitions/Pagination" } }
|
||||
},
|
||||
"PlaybackHistoryPage": {
|
||||
"type": "object",
|
||||
"properties": { "list": { "type": "array", "items": { "$ref": "#/definitions/PlaybackHistoryListItem" } }, "pagination": { "$ref": "#/definitions/Pagination" } }
|
||||
},
|
||||
"AppDramaPageResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"code": { "type": "integer", "example": 0 },
|
||||
"data": { "$ref": "#/definitions/AppDramaPage" },
|
||||
"message": { "type": "string", "example": "success" },
|
||||
"timestamp": { "type": "integer", "format": "int64" },
|
||||
"requestId": { "type": "string" },
|
||||
"cost": { "type": "string", "example": "12ms" }
|
||||
}
|
||||
},
|
||||
"AppDramaDetailResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"code": { "type": "integer", "example": 0 },
|
||||
"data": { "$ref": "#/definitions/AppDramaDetail" },
|
||||
"message": { "type": "string", "example": "success" },
|
||||
"timestamp": { "type": "integer", "format": "int64" },
|
||||
"requestId": { "type": "string" },
|
||||
"cost": { "type": "string", "example": "12ms" }
|
||||
}
|
||||
},
|
||||
"AppEpisodePageResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"code": { "type": "integer", "example": 0 },
|
||||
"data": { "$ref": "#/definitions/AppEpisodePage" },
|
||||
"message": { "type": "string", "example": "success" },
|
||||
"timestamp": { "type": "integer", "format": "int64" },
|
||||
"requestId": { "type": "string" },
|
||||
"cost": { "type": "string", "example": "12ms" }
|
||||
}
|
||||
},
|
||||
"PlaybackHistoryPageResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"code": { "type": "integer", "example": 0 },
|
||||
"data": { "$ref": "#/definitions/PlaybackHistoryPage" },
|
||||
"message": { "type": "string", "example": "success" },
|
||||
"timestamp": { "type": "integer", "format": "int64" },
|
||||
"requestId": { "type": "string" },
|
||||
"cost": { "type": "string", "example": "12ms" }
|
||||
}
|
||||
},
|
||||
"PlaybackHistoryRecordResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"code": { "type": "integer", "example": 0 },
|
||||
"data": { "$ref": "#/definitions/PlaybackHistoryRecord" },
|
||||
"message": { "type": "string", "example": "success" },
|
||||
"timestamp": { "type": "integer", "format": "int64" },
|
||||
"requestId": { "type": "string" },
|
||||
"cost": { "type": "string", "example": "12ms" }
|
||||
}
|
||||
},
|
||||
"ErrorResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"code": { "type": "integer", "example": 404 },
|
||||
"data": { "type": "object" },
|
||||
"message": { "type": "string", "example": "Not found" },
|
||||
"timestamp": { "type": "integer", "format": "int64" },
|
||||
"requestId": { "type": "string" },
|
||||
"cost": { "type": "string", "example": "12ms" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -113,10 +113,11 @@ export class DramasController {
|
||||
}
|
||||
|
||||
@Get("/api/app/v1/dramas")
|
||||
listPublishedDramas(@Query("page") page?: string, @Query("pageSize") pageSize?: string) {
|
||||
listPublishedDramas(@Query("page") page?: string, @Query("pageSize") pageSize?: string, @Query("title") title?: string) {
|
||||
return this.dramasService.listPublishedDramas({
|
||||
page: Number(page) || 1,
|
||||
pageSize: Number(pageSize) || 10,
|
||||
title,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -124,13 +125,13 @@ export class DramasController {
|
||||
listRecommendedDramas(@Query("page") page?: string, @Query("pageSize") pageSize?: string) {
|
||||
return this.dramasService.listRecommendedDramas({
|
||||
page: Number(page) || 1,
|
||||
pageSize: Number(pageSize) || 10,
|
||||
pageSize: Number(pageSize) || 10
|
||||
});
|
||||
}
|
||||
|
||||
@Get("/api/app/v1/dramas/presearch")
|
||||
presearchVideos(@Query("keyword") keyword?: string, @Query("page") page?: string, @Query("pageSize") pageSize?: string) {
|
||||
return this.dramasService.presearchVideos({
|
||||
presearchDramas(@Query("keyword") keyword?: string, @Query("page") page?: string, @Query("pageSize") pageSize?: string) {
|
||||
return this.dramasService.presearchDramas({
|
||||
page: Number(page) || 1,
|
||||
pageSize: Number(pageSize) || 10,
|
||||
keyword,
|
||||
|
||||
@@ -24,28 +24,12 @@ interface DramaListInput extends PaginationInput {
|
||||
|
||||
type PublishedDramaListInput = DramaListInput;
|
||||
|
||||
interface VideoPresearchInput extends PaginationInput {
|
||||
interface DramaPresearchInput extends PaginationInput {
|
||||
keyword?: string;
|
||||
}
|
||||
|
||||
type PublicDramaRecord = Omit<DramaRecord, "portraitCoverUrl" | "landscapeCoverUrl">;
|
||||
|
||||
export interface VideoPresearchRecord {
|
||||
id: number;
|
||||
dramaId: number;
|
||||
episodeNo: number;
|
||||
title: string;
|
||||
description?: string;
|
||||
coverUrl: string;
|
||||
durationSeconds: number;
|
||||
byteplusVid?: string;
|
||||
videoUrl?: string;
|
||||
freeType: "free" | "paid";
|
||||
price: number;
|
||||
dramaTitle: string;
|
||||
dramaCoverUrl: string;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class DramasService {
|
||||
private dramaSequence = 1;
|
||||
@@ -537,15 +521,15 @@ export class DramasService {
|
||||
return episode;
|
||||
}
|
||||
|
||||
async listPublishedDramas(input: PublishedDramaListInput): Promise<PaginatedResponse<PublicDramaRecord>> {
|
||||
const title = input.title?.trim();
|
||||
async listPublishedDramas(data: PublishedDramaListInput): Promise<PaginatedResponse<PublicDramaRecord>> {
|
||||
const title = data.title?.trim();
|
||||
if (this.hasDatabase()) {
|
||||
const { page, pageSize, skip, take } = this.resolvePagination(input);
|
||||
const { page, pageSize, skip, take } = this.resolvePagination(data);
|
||||
const [records, total] = await this.dataSource.getRepository(Drama).findAndCount({
|
||||
where: {
|
||||
status: PublishStatus.Published,
|
||||
...(title ? { title: Like(`%${title}%`) } : {}),
|
||||
...(input.isRecommended !== undefined ? { isRecommended: input.isRecommended } : {}),
|
||||
...(data.isRecommended !== undefined ? { isRecommended: data.isRecommended } : {}),
|
||||
},
|
||||
order: { sortOrder: "DESC", id: "DESC" },
|
||||
skip,
|
||||
@@ -560,10 +544,10 @@ export class DramasService {
|
||||
this.dramas
|
||||
.filter((item) => item.status === PublishStatus.Published)
|
||||
.filter((item) => !title || item.title.includes(title))
|
||||
.filter((item) => input.isRecommended === undefined || item.isRecommended === input.isRecommended)
|
||||
.filter((item) => data.isRecommended === undefined || item.isRecommended === data.isRecommended)
|
||||
.map((drama) => this.withEpisodeCounts(drama)),
|
||||
),
|
||||
input,
|
||||
data,
|
||||
);
|
||||
return {
|
||||
...page,
|
||||
@@ -578,7 +562,7 @@ export class DramasService {
|
||||
});
|
||||
}
|
||||
|
||||
async presearchVideos(input: VideoPresearchInput): Promise<PaginatedResponse<VideoPresearchRecord>> {
|
||||
async presearchDramas(input: DramaPresearchInput): Promise<PaginatedResponse<PublicDramaRecord>> {
|
||||
const keyword = input.keyword?.trim();
|
||||
if (!keyword) {
|
||||
const { page, pageSize } = this.resolvePagination(input);
|
||||
@@ -587,82 +571,30 @@ export class DramasService {
|
||||
|
||||
if (this.hasDatabase()) {
|
||||
const { page, pageSize, skip, take } = this.resolvePagination(input);
|
||||
const likeKeyword = `%${keyword}%`;
|
||||
const baseQuery = this.dataSource
|
||||
.getRepository(Episode)
|
||||
.createQueryBuilder("episode")
|
||||
.innerJoin(Drama, "drama", "drama.id = episode.dramaId")
|
||||
.where("drama.status = :published", {
|
||||
published: PublishStatus.Published,
|
||||
})
|
||||
.andWhere("episode.status = :published", {
|
||||
published: PublishStatus.Published,
|
||||
})
|
||||
.andWhere("episode.publishStatus = :published", {
|
||||
published: PublishStatus.Published,
|
||||
})
|
||||
.andWhere("episode.reviewStatus = :approved", {
|
||||
approved: "approved",
|
||||
})
|
||||
.andWhere("(episode.title LIKE :keyword OR episode.description LIKE :keyword OR drama.title LIKE :keyword OR drama.description LIKE :keyword)", { keyword: likeKeyword });
|
||||
|
||||
const total = await baseQuery.clone().getCount();
|
||||
const rows = await baseQuery
|
||||
.clone()
|
||||
.select([
|
||||
"episode.id AS id",
|
||||
"episode.dramaId AS dramaId",
|
||||
"episode.episodeNo AS episodeNo",
|
||||
"episode.title AS title",
|
||||
"episode.description AS description",
|
||||
"episode.coverUrl AS coverUrl",
|
||||
"episode.durationSeconds AS durationSeconds",
|
||||
"episode.byteplusVid AS byteplusVid",
|
||||
"episode.videoUrl AS videoUrl",
|
||||
"episode.freeType AS freeType",
|
||||
"episode.price AS price",
|
||||
"drama.title AS dramaTitle",
|
||||
"drama.coverUrl AS dramaCoverUrl",
|
||||
])
|
||||
.orderBy("drama.sortOrder", "DESC")
|
||||
.addOrderBy("drama.id", "DESC")
|
||||
.addOrderBy("episode.episodeNo", "ASC")
|
||||
.offset(skip)
|
||||
.limit(take)
|
||||
.getRawMany<VideoPresearchRecord>();
|
||||
const [records, total] = await this.dataSource.getRepository(Drama).findAndCount({
|
||||
where: {
|
||||
status: PublishStatus.Published,
|
||||
title: Like(`%${keyword}%`),
|
||||
},
|
||||
order: { sortOrder: "DESC", id: "DESC" },
|
||||
skip,
|
||||
take,
|
||||
});
|
||||
const list = await Promise.all(records.map((drama) => this.toDramaRecordWithCounts(drama)));
|
||||
|
||||
return {
|
||||
list: rows.map((row) => ({
|
||||
...row,
|
||||
id: Number(row.id),
|
||||
dramaId: Number(row.dramaId),
|
||||
episodeNo: Number(row.episodeNo),
|
||||
durationSeconds: Number(row.durationSeconds),
|
||||
price: Number(row.price),
|
||||
})),
|
||||
list: list.map((drama) => this.toPublicDramaRecord(drama)),
|
||||
pagination: { page, pageSize, total },
|
||||
};
|
||||
}
|
||||
|
||||
const normalizedKeyword = keyword.toLowerCase();
|
||||
const records = this.episodes
|
||||
.filter((episode) => episode.status === PublishStatus.Published && episode.publishStatus === PublishStatus.Published && episode.reviewStatus === "approved")
|
||||
.map((episode) => {
|
||||
const drama = this.dramas.find((item) => item.id === episode.dramaId);
|
||||
return { episode, drama };
|
||||
})
|
||||
.filter((item): item is { episode: EpisodeRecord; drama: DramaRecord } => Boolean(item.drama) && item.drama?.status === PublishStatus.Published)
|
||||
.filter(({ episode, drama }) => [episode.title, episode.description, drama.title, drama.description].some((value) => value?.toLowerCase().includes(normalizedKeyword)))
|
||||
.sort((left, right) => {
|
||||
if (right.drama.sortOrder !== left.drama.sortOrder) {
|
||||
return right.drama.sortOrder - left.drama.sortOrder;
|
||||
}
|
||||
if (right.drama.id !== left.drama.id) {
|
||||
return right.drama.id - left.drama.id;
|
||||
}
|
||||
return left.episode.episodeNo - right.episode.episodeNo;
|
||||
})
|
||||
.map(({ episode, drama }) => this.toVideoPresearchRecord(episode, drama));
|
||||
const records = this.sortDramas(
|
||||
this.dramas
|
||||
.filter((drama) => drama.status === PublishStatus.Published)
|
||||
.filter((drama) => drama.title.toLowerCase().includes(normalizedKeyword))
|
||||
.map((drama) => this.withEpisodeCounts(drama)),
|
||||
).map((drama) => this.toPublicDramaRecord(drama));
|
||||
|
||||
return this.paginate(records, input);
|
||||
}
|
||||
@@ -901,24 +833,6 @@ export class DramasService {
|
||||
return publicRecord;
|
||||
}
|
||||
|
||||
private toVideoPresearchRecord(episode: EpisodeRecord, drama: DramaRecord): VideoPresearchRecord {
|
||||
return {
|
||||
id: episode.id,
|
||||
dramaId: episode.dramaId,
|
||||
episodeNo: episode.episodeNo,
|
||||
title: episode.title,
|
||||
description: episode.description,
|
||||
coverUrl: episode.coverUrl,
|
||||
durationSeconds: episode.durationSeconds,
|
||||
byteplusVid: episode.byteplusVid,
|
||||
videoUrl: episode.videoUrl,
|
||||
freeType: episode.freeType,
|
||||
price: episode.price,
|
||||
dramaTitle: drama.title,
|
||||
dramaCoverUrl: drama.coverUrl,
|
||||
};
|
||||
}
|
||||
|
||||
private toEpisodeRecord(entity: Episode): EpisodeRecord {
|
||||
const dramaId = Number(entity.dramaId);
|
||||
return {
|
||||
|
||||
@@ -419,8 +419,8 @@ describe('Dramas and episodes', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('presearches published approved videos by keyword for mini app clients', async () => {
|
||||
const drama = await request(app.getHttpServer())
|
||||
it('presearches published dramas by drama title for mini app clients', async () => {
|
||||
await request(app.getHttpServer())
|
||||
.post('/api/admin/v1/dramas')
|
||||
.set('Authorization', `Bearer ${adminToken}`)
|
||||
.send({
|
||||
@@ -432,14 +432,25 @@ describe('Dramas and episodes', () => {
|
||||
})
|
||||
.expect(201);
|
||||
|
||||
const episodeOnlyDrama = await request(app.getHttpServer())
|
||||
.post('/api/admin/v1/dramas')
|
||||
.set('Authorization', `Bearer ${adminToken}`)
|
||||
.send({
|
||||
title: 'Episode Only Drama',
|
||||
coverUrl: 'https://cdn.example.com/episode-only-drama.jpg',
|
||||
type: 'romance',
|
||||
status: 'published',
|
||||
})
|
||||
.expect(201);
|
||||
|
||||
await request(app.getHttpServer())
|
||||
.post('/api/admin/v1/episodes')
|
||||
.set('Authorization', `Bearer ${adminToken}`)
|
||||
.send({
|
||||
dramaId: drama.body.data.id,
|
||||
dramaId: episodeOnlyDrama.body.data.id,
|
||||
episodeNo: 1,
|
||||
title: 'Pilot Video Match',
|
||||
description: 'The first skyline episode.',
|
||||
title: 'Skyline Episode Title Only',
|
||||
description: 'This should not make the drama match.',
|
||||
coverUrl: 'https://cdn.example.com/skyline-ep1.jpg',
|
||||
videoUrl: 'https://cdn.example.com/skyline-ep1.mp4',
|
||||
byteplusVid: 'vid_skyline_001',
|
||||
@@ -453,20 +464,13 @@ describe('Dramas and episodes', () => {
|
||||
.expect(201);
|
||||
|
||||
await request(app.getHttpServer())
|
||||
.post('/api/admin/v1/episodes')
|
||||
.post('/api/admin/v1/dramas')
|
||||
.set('Authorization', `Bearer ${adminToken}`)
|
||||
.send({
|
||||
dramaId: drama.body.data.id,
|
||||
episodeNo: 2,
|
||||
title: 'Hidden Draft Video',
|
||||
description: 'Contains skyline but is not published.',
|
||||
coverUrl: 'https://cdn.example.com/skyline-ep2.jpg',
|
||||
videoUrl: 'https://cdn.example.com/skyline-ep2.mp4',
|
||||
byteplusVid: 'vid_skyline_002',
|
||||
durationSeconds: 180,
|
||||
title: 'Skyline Draft Drama',
|
||||
coverUrl: 'https://cdn.example.com/skyline-draft-drama.jpg',
|
||||
type: 'romance',
|
||||
status: 'draft',
|
||||
publishStatus: 'draft',
|
||||
reviewStatus: 'approved',
|
||||
})
|
||||
.expect(201);
|
||||
|
||||
@@ -478,19 +482,17 @@ describe('Dramas and episodes', () => {
|
||||
expect(response.body.data.pagination.pageSize).toBe(10);
|
||||
expect(response.body.data.list).toEqual([
|
||||
expect.objectContaining({
|
||||
dramaId: drama.body.data.id,
|
||||
episodeNo: 1,
|
||||
title: 'Pilot Video Match',
|
||||
coverUrl: 'https://cdn.example.com/skyline-ep1.jpg',
|
||||
videoUrl: 'https://cdn.example.com/skyline-ep1.mp4',
|
||||
byteplusVid: 'vid_skyline_001',
|
||||
durationSeconds: 180,
|
||||
freeType: 'free',
|
||||
price: 0,
|
||||
dramaTitle: 'Skyline Keyword Saga',
|
||||
dramaCoverUrl: 'https://cdn.example.com/skyline-drama.jpg',
|
||||
title: 'Skyline Keyword Saga',
|
||||
coverUrl: 'https://cdn.example.com/skyline-drama.jpg',
|
||||
status: 'published',
|
||||
}),
|
||||
]);
|
||||
expect(response.body.data.list).not.toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ title: 'Episode Only Drama' }),
|
||||
expect.objectContaining({ title: 'Skyline Draft Drama' }),
|
||||
]),
|
||||
);
|
||||
|
||||
const emptyKeyword = await request(app.getHttpServer())
|
||||
.get('/api/app/v1/dramas/presearch')
|
||||
|
||||
Reference in New Issue
Block a user