fix: align maixiang asset search with home search
This commit is contained in:
parent
998d311254
commit
efc8f35a83
@ -9,6 +9,7 @@ import {
|
||||
validateApiKey,
|
||||
} from '@/lib/external-api'
|
||||
import { getPublicAppOrigin } from '@/lib/public-app-url'
|
||||
import { buildMaterialSearchOrConditions } from '@/lib/material-query'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
import { rewriteStorageUrlForClient } from '@/lib/url-rewrite'
|
||||
import { LEGACY_COMPATIBLE_VIDEO_MATERIAL_TYPES } from '@/lib/video-material-types'
|
||||
@ -246,10 +247,11 @@ export async function GET(request: NextRequest) {
|
||||
}
|
||||
|
||||
try {
|
||||
const searchOrConditions = await buildMaterialSearchOrConditions(trace.normalizedSpu)
|
||||
const materials = await prisma.material.findMany({
|
||||
where: {
|
||||
deletedAt: null,
|
||||
productModel: trace.normalizedSpu,
|
||||
...(searchOrConditions ? { OR: searchOrConditions } : {}),
|
||||
materialType: {
|
||||
in: [BUYER_SHOW_MATERIAL_TYPE, ...VIDEO_MATERIAL_TYPES],
|
||||
},
|
||||
|
||||
@ -90,7 +90,7 @@ X-API-Key: your-api-key-here
|
||||
|
||||
### 脉象平台:按 SPU 查询买家秀图片和视频下载链接
|
||||
|
||||
脉象平台传入 SPU,素材平台返回该 SPU 下全部 `买家秀图片` 和全部视频类素材的预览地址与正式下载链接。
|
||||
脉象平台传入搜索关键词,通常是 SPU,素材平台按首页搜索栏同口径模糊匹配,并返回命中结果中的全部 `买家秀图片` 和全部视频类素材的预览地址与正式下载链接。
|
||||
|
||||
**请求**
|
||||
|
||||
@ -108,7 +108,9 @@ GET /api/external/maixiang-spu-assets?spu={SPU}
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|-----|------|-----|------|
|
||||
| spu | string | 是 | SPU 型号,接口会转为大写并做精确匹配 |
|
||||
| spu | string | 是 | 搜索关键词,通常传 SPU;接口会转为大写并使用与首页搜索栏一致的模糊匹配 |
|
||||
|
||||
匹配范围与首页搜索栏一致:`SPU / 任务号 / 报告标题 / 店铺 / 场景 / 作者 / SKU 名称 / 上传人姓名`。接口只返回买家秀和视频素材;如果关键词只命中其它素材类型,仍返回 `404 SPU_ASSETS_NOT_FOUND`。
|
||||
|
||||
**视频素材范围**
|
||||
|
||||
|
||||
263
docs/脉象平台SPU素材下载接口文档.md
Normal file
263
docs/脉象平台SPU素材下载接口文档.md
Normal file
@ -0,0 +1,263 @@
|
||||
# 脉象平台 SPU 素材下载接口文档
|
||||
|
||||
版本日期:2026-05-21
|
||||
|
||||
## 1. 接口说明
|
||||
|
||||
脉象平台传入一个搜索关键词,通常是 SPU,素材平台按首页搜索栏同口径模糊匹配,并返回命中结果中的:
|
||||
|
||||
- 买家秀图片
|
||||
- 视频类素材
|
||||
|
||||
接口不会返回 `3D场景`、`任务-详情`、`标准模型`、`实拍图片`、`任务-3D原图` 等其它素材类型。
|
||||
|
||||
返回结构中,`buyerShow` 为买家秀图片,放在前面;`videos` 为视频类素材,放在后面。
|
||||
|
||||
## 2. 请求地址
|
||||
|
||||
```http
|
||||
GET https://sc.jiansun.vip/api/external/maixiang-spu-assets?spu={SPU}
|
||||
```
|
||||
|
||||
示例:
|
||||
|
||||
```http
|
||||
GET https://sc.jiansun.vip/api/external/maixiang-spu-assets?spu=GBGJ-DNZ0366
|
||||
```
|
||||
|
||||
## 3. 鉴权方式
|
||||
|
||||
使用请求头 `X-API-Key` 鉴权。
|
||||
|
||||
```http
|
||||
X-API-Key: your-api-key-here
|
||||
```
|
||||
|
||||
注意:
|
||||
|
||||
- API Key 必须放在请求头中。
|
||||
- 不支持把 API Key 放在 URL 参数中。
|
||||
- 正式 API Key 请通过单独的安全方式提供,不要写入代码仓库或公开文档。
|
||||
|
||||
## 4. 请求参数
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|---|---|---|---|
|
||||
| spu | string | 是 | 搜索关键词。通常传 SPU,接口会自动去掉首尾空格并转为大写,然后使用与首页搜索栏一致的模糊匹配。 |
|
||||
|
||||
说明:
|
||||
|
||||
- 只支持单个关键词查询。
|
||||
- 匹配方式与素材库首页搜索栏一致:会模糊匹配 `SPU / 任务号 / 报告标题 / 店铺 / 场景 / 作者 / SKU 名称 / 上传人姓名`。
|
||||
- 即使命中其它类型素材,接口也只返回 `买家秀图片` 和视频类素材。
|
||||
- 组合 SPU 中的 `+` 会被保留,例如 `GBSFY-AD0297+GBSFY-CZ0102`。
|
||||
|
||||
## 5. 返回素材范围
|
||||
|
||||
### 5.1 买家秀图片
|
||||
|
||||
只返回素材类型为:
|
||||
|
||||
- `买家秀图片`
|
||||
|
||||
### 5.2 视频类素材
|
||||
|
||||
返回以下视频类素材:
|
||||
|
||||
- `淘系-主图视频`
|
||||
- `安装视频`
|
||||
- `淘系-逛逛视频`
|
||||
- `抖音-千川素材`
|
||||
- `抖音-账号维护`
|
||||
- `主图视频`
|
||||
- `种草视频`
|
||||
- `视频`
|
||||
- `VIDEO`
|
||||
|
||||
## 6. 请求示例
|
||||
|
||||
```bash
|
||||
curl -X GET \
|
||||
'https://sc.jiansun.vip/api/external/maixiang-spu-assets?spu=GBGJ-DNZ0366' \
|
||||
-H 'X-API-Key: your-api-key-here'
|
||||
```
|
||||
|
||||
## 7. 成功响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"spu": "GBGJ-DNZ0366",
|
||||
"buyerShow": {
|
||||
"total": 1,
|
||||
"items": [
|
||||
{
|
||||
"materialId": "buyer-material-id",
|
||||
"productModel": "GBGJ-DNZ0366",
|
||||
"materialType": "买家秀图片",
|
||||
"categoryName": "顾家",
|
||||
"shopName": "顾家",
|
||||
"sceneName": "客厅",
|
||||
"fileName": "buyer-show.zip",
|
||||
"fileSize": 1024000,
|
||||
"uploadedAt": "2026-05-13T10:00:00.000Z",
|
||||
"coverUrl": "https://s3.jiansun.vip/gongxiangpan/example-cover.webp",
|
||||
"previewUrls": [
|
||||
"https://s3.jiansun.vip/gongxiangpan/example-1.webp",
|
||||
"https://s3.jiansun.vip/gongxiangpan/example-2.webp"
|
||||
],
|
||||
"downloadUrl": "https://sc.jiansun.vip/api/external/download?token=xxx"
|
||||
}
|
||||
]
|
||||
},
|
||||
"videos": {
|
||||
"total": 1,
|
||||
"items": [
|
||||
{
|
||||
"materialId": "video-material-id",
|
||||
"productModel": "GBGJ-DNZ0366",
|
||||
"materialType": "抖音-千川素材",
|
||||
"categoryName": "顾家",
|
||||
"shopName": "顾家",
|
||||
"sceneName": "客厅",
|
||||
"fileName": "video.mp4",
|
||||
"fileSize": 2048000,
|
||||
"duration": 18,
|
||||
"uploadedAt": "2026-05-13T10:05:00.000Z",
|
||||
"coverUrl": "https://s3.jiansun.vip/gongxiangpan/video-cover.jpg",
|
||||
"previewUrl": "https://s3.jiansun.vip/gongxiangpan/video-preview.mp4",
|
||||
"previewClipUrl": "https://s3.jiansun.vip/gongxiangpan/video-clip-10s.mp4",
|
||||
"downloadUrl": "https://sc.jiansun.vip/api/external/download?token=yyy"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 8. 响应字段说明
|
||||
|
||||
### 8.1 顶层字段
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| success | boolean | 请求是否成功 |
|
||||
| data.spu | string | 本次查询的标准化关键词,通常是 SPU |
|
||||
| data.buyerShow.total | number | 买家秀图片数量 |
|
||||
| data.buyerShow.items | array | 买家秀图片列表 |
|
||||
| data.videos.total | number | 视频素材数量 |
|
||||
| data.videos.items | array | 视频素材列表 |
|
||||
|
||||
### 8.2 买家秀图片字段
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| materialId | string | 素材 ID |
|
||||
| productModel | string | SPU 型号 |
|
||||
| materialType | string | 素材类型,固定为 `买家秀图片` |
|
||||
| categoryName | string | 店铺/分类名称 |
|
||||
| shopName | string | 店铺名称 |
|
||||
| sceneName | string | 场景名称 |
|
||||
| fileName | string | 下载包文件名 |
|
||||
| fileSize | number | 文件大小,单位 byte |
|
||||
| uploadedAt | string | 上传时间,ISO 8601 格式 |
|
||||
| coverUrl | string \| null | 封面或首图预览地址 |
|
||||
| previewUrls | string[] | 预览图地址数组 |
|
||||
| downloadUrl | string | 正式下载链接,下载买家秀 ZIP 包 |
|
||||
|
||||
### 8.3 视频字段
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| materialId | string | 素材 ID |
|
||||
| productModel | string | SPU 型号 |
|
||||
| materialType | string | 视频素材类型 |
|
||||
| categoryName | string | 店铺/分类名称 |
|
||||
| shopName | string | 店铺名称 |
|
||||
| sceneName | string | 场景名称 |
|
||||
| fileName | string | 视频文件名 |
|
||||
| fileSize | number | 文件大小,单位 byte |
|
||||
| duration | number \| null | 视频时长,单位秒 |
|
||||
| uploadedAt | string | 上传时间,ISO 8601 格式 |
|
||||
| coverUrl | string \| null | 视频封面地址 |
|
||||
| previewUrl | string \| null | 视频预览文件地址 |
|
||||
| previewClipUrl | string \| null | 10 秒预览视频地址 |
|
||||
| downloadUrl | string | 正式下载链接,下载原视频文件 |
|
||||
|
||||
## 9. 错误响应
|
||||
|
||||
### 9.1 缺少 SPU
|
||||
|
||||
HTTP 状态码:`400`
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"error": {
|
||||
"code": "MISSING_SPU",
|
||||
"message": "SPU 为必填项"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 9.2 API Key 无效或缺失
|
||||
|
||||
HTTP 状态码:`401`
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"error": {
|
||||
"code": "INVALID_API_KEY",
|
||||
"message": "API Key 无效或缺失"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 9.3 没有找到买家秀或视频素材
|
||||
|
||||
HTTP 状态码:`404`
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"error": {
|
||||
"code": "SPU_ASSETS_NOT_FOUND",
|
||||
"message": "该 SPU 下暂无买家秀图片或视频素材"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 9.4 服务异常
|
||||
|
||||
HTTP 状态码:`500`
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"error": {
|
||||
"code": "SERVER_ERROR",
|
||||
"message": "服务器内部错误"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 10. 错误码汇总
|
||||
|
||||
| HTTP 状态 | code | 说明 |
|
||||
|---|---|---|
|
||||
| 400 | MISSING_SPU | 缺少 SPU 参数 |
|
||||
| 401 | INVALID_API_KEY | API Key 无效或缺失 |
|
||||
| 404 | SPU_ASSETS_NOT_FOUND | 该 SPU 下暂无买家秀图片或视频素材 |
|
||||
| 500 | SERVER_ERROR | 服务器内部错误 |
|
||||
|
||||
## 11. 对接注意事项
|
||||
|
||||
- 同一 SPU 命中多条素材时,接口会全部返回,并按上传时间倒序排列。
|
||||
- 如果只有买家秀图片,没有视频,接口仍返回 `200`,`videos.total` 为 `0`。
|
||||
- 如果只有视频,没有买家秀图片,接口仍返回 `200`,`buyerShow.total` 为 `0`。
|
||||
- 如果首页搜索能搜到该关键词,但命中的素材没有 `买家秀图片` 或视频类素材,接口仍返回 `404`。
|
||||
- `downloadUrl` 是正式下载链接,脉象平台可直接请求该链接下载文件。
|
||||
- `previewUrl`、`previewClipUrl`、`coverUrl`、`previewUrls` 是预览地址,不等同于正式下载地址。
|
||||
- 不建议脉象平台解析 `downloadUrl` 里的 token,只需要把它当成完整 URL 使用。
|
||||
@ -10,6 +10,12 @@ jest.mock('@/lib/prisma', () => ({
|
||||
material: {
|
||||
findMany: jest.fn(),
|
||||
},
|
||||
materialSku: {
|
||||
findMany: jest.fn(),
|
||||
},
|
||||
user: {
|
||||
findMany: jest.fn(),
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
||||
@ -54,6 +60,8 @@ describe('external maixiang-spu-assets route', () => {
|
||||
isActive: true,
|
||||
})
|
||||
;(prisma.material.findMany as jest.Mock).mockResolvedValue([])
|
||||
;(prisma.materialSku.findMany as jest.Mock).mockResolvedValue([])
|
||||
;(prisma.user.findMany as jest.Mock).mockResolvedValue([])
|
||||
})
|
||||
|
||||
function createRequest(url: string, apiKey = 'test-key') {
|
||||
@ -102,19 +110,60 @@ describe('external maixiang-spu-assets route', () => {
|
||||
expect(prisma.material.findMany).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
test('应使用标准化后的 SPU 精确匹配,不做包含搜索', async () => {
|
||||
test('应使用与首页搜索栏一致的模糊匹配条件', async () => {
|
||||
;(prisma.material.findMany as jest.Mock).mockResolvedValue([])
|
||||
;(prisma.materialSku.findMany as jest.Mock).mockResolvedValue([
|
||||
{ materialId: 'sku-hit-material' },
|
||||
])
|
||||
;(prisma.user.findMany as jest.Mock).mockResolvedValue([
|
||||
{ id: 'author-user-id' },
|
||||
])
|
||||
|
||||
const response = await GET(
|
||||
createRequest('https://example.com/api/external/maixiang-spu-assets?spu=gbsfy-ad0297%20%2B%20gbsfy-cz0102'),
|
||||
)
|
||||
|
||||
expect(response.status).toBe(404)
|
||||
expect(prisma.materialSku.findMany).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
where: expect.objectContaining({
|
||||
skuName: {
|
||||
contains: 'GBSFY-AD0297+GBSFY-CZ0102',
|
||||
mode: 'insensitive',
|
||||
},
|
||||
deletedAt: null,
|
||||
}),
|
||||
select: { materialId: true },
|
||||
take: 1000,
|
||||
}),
|
||||
)
|
||||
expect(prisma.user.findMany).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
where: {
|
||||
OR: [
|
||||
{ realName: { contains: 'GBSFY-AD0297+GBSFY-CZ0102', mode: 'insensitive' } },
|
||||
{ username: { contains: 'GBSFY-AD0297+GBSFY-CZ0102', mode: 'insensitive' } },
|
||||
],
|
||||
},
|
||||
select: { id: true },
|
||||
take: 1000,
|
||||
}),
|
||||
)
|
||||
expect(prisma.material.findMany).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
where: expect.objectContaining({
|
||||
deletedAt: null,
|
||||
productModel: 'GBSFY-AD0297+GBSFY-CZ0102',
|
||||
materialType: expect.any(Object),
|
||||
OR: expect.arrayContaining([
|
||||
{ productModel: { contains: 'GBSFY-AD0297+GBSFY-CZ0102', mode: 'insensitive' } },
|
||||
{ reportTitle: { contains: 'GBSFY-AD0297+GBSFY-CZ0102', mode: 'insensitive' } },
|
||||
{ taskNo: { contains: 'GBSFY-AD0297+GBSFY-CZ0102', mode: 'insensitive' } },
|
||||
{ categoryName: { contains: 'GBSFY-AD0297+GBSFY-CZ0102', mode: 'insensitive' } },
|
||||
{ sceneName: { contains: 'GBSFY-AD0297+GBSFY-CZ0102', mode: 'insensitive' } },
|
||||
{ authorName: { contains: 'GBSFY-AD0297+GBSFY-CZ0102', mode: 'insensitive' } },
|
||||
{ id: { in: ['sku-hit-material'] } },
|
||||
{ userId: { in: ['author-user-id'] } },
|
||||
]),
|
||||
}),
|
||||
}),
|
||||
)
|
||||
|
||||
@ -135,7 +135,7 @@ export async function canAccessDeletedMaterials(user: MaterialSessionUser): Prom
|
||||
return isSystemAdmin(user.role || '')
|
||||
}
|
||||
|
||||
async function buildSearchOrConditions(search: string): Promise<any[] | undefined> {
|
||||
export async function buildMaterialSearchOrConditions(search: string): Promise<any[] | undefined> {
|
||||
const keyword = search.trim()
|
||||
if (!keyword) {
|
||||
return undefined
|
||||
@ -228,7 +228,7 @@ export async function buildMaterialWhereFromSearchParams(
|
||||
const productCategory = searchParams.get('productCategory') || ''
|
||||
const month = searchParams.get('month') || ''
|
||||
|
||||
const searchOrConditions = await buildSearchOrConditions(search)
|
||||
const searchOrConditions = await buildMaterialSearchOrConditions(search)
|
||||
if (searchOrConditions) {
|
||||
where.OR = searchOrConditions
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user