REST Admin API 分页机制

REST Admin API 分页机制基于游标实现分页查询。所有支持分页的接口在分页控制(查询参数与响应头)上均遵循统一的 Schema 规范,并支持以下两种模式:

  • since_id 模式:主要用于首次请求(第一页)。用于指定数据拉取的起始位置。
  • page_info 模式:主要用于后续请求(非第一页)。用于获取下一页或上一页数据。

在响应分页请求时,REST Admin API 会在响应头的 link 字段中返回当前可用的上一页(rel="previous")或下一页(rel="next")的完整请求 URL。你只需解析并调用 link 响应头中的完整 URL 即可完成翻页。

上述分页规则适用于所有版本的 REST Admin API。


通用查询参数​

进行分页查询时,你可以使用以下通用查询参数来控制数据翻页、单页数据量以及返回字段:

  • page_info:特定页面的唯一标识 ID,须从 HTTP 响应头的 link 字段中提取。
  • limit:每页返回的记录数量限制。默认值:50。单次请求最多返回 1,000 条。
  • fields:指定响应中要返回的字段,多个字段用英文逗号分隔。默认返回查询目标资源对象的全部字段。
注意

当请求 URL 中包含 page_info 参数时,系统将自动继承首次请求的筛选上下文,此时除 limit 和 fields 外的所有过滤字段都将被忽略。


如何分页​

以检索 SHOPLINE 店铺中的商品列表为例,假设设置每页返回 10 条数据,并需要获取第二页数据,完整操作步骤如下:

步骤一:发起第一页请求​

调用 批量查询商品 接口,并在查询参数中传入 limit=10:

GET https://{handle}.myshopline.com/admin/openapi/{version}/products/products.json?limit=10

检查 HTTP 响应头中的 link 字段。

仅包含指向下一页的请求 URL 时,示例返回结构如下:

link: <https://{handle}.myshopline.com/admin/openapi/{version}/products/products.json?limit=1&page_info=eyJzaW5jZUlkIjoiMTYwNTc1OTAxNTM4OTA4Mjk1MjExMTI3ODgiLCJkaXJlY3Rpb24iOiJuZXh0IiwibGltaXQiOjF9>; rel="next"

若同时包含指向上一页和下一页的请求 URL,示例返回结构如下:

link: <https://{handle}.myshopline.com/admin/openapi/{version}/products/products.json?limit=1&page_info=eyJzaW5jZUlkIjoiMTYwNTc2NjAxNzI1MjczOTI4MDEwOTI3ODgiLCJkaXJlY3Rpb24iOiJuZXh0IiwibGltaXQiOjF9>; rel="next", <https://{handle}.myshopline.com/admin/openapi/{version}/products/products.json?limit=1&page_info=eyJzaW5jZUlkIjoiMTYwNTc2NjAxNzI1MjczOTI4MDEwOTI3ODgiLCJkaXJlY3Rpb24iOiJwcmV2IiwibGltaXQiOjF9>; rel="previous"
提示

若响应头中不包含 link 字段,说明已到达最后一页,无需继续翻页。

link 字段中的 rel 属性标识了对应 URL 的页面关系:

  • rel="next":下一页的请求 URL
  • rel="previous":上一页的请求 URL

步骤三:提取 URL 并发起第二页请求​

当响应中 link 字段如下时:

link: <https://{handle}.myshopline.com/admin/openapi/{version}/products/products.json?limit=1&page_info=eyJzaW5jZUlkIjoiMTYwNTc2NjAxNzI1MjczOTI4MDEwOTI3ODgiLCJkaXJlY3Rpb24iOiJuZXh0IiwibGltaXQiOjF9>; rel="next", <https://{handle}.myshopline.com/admin/openapi/{version}/products/products.json?limit=1&page_info=eyJzaW5jZUlkIjoiMTYwNTc2NjAxNzI1MjczOTI4MDEwOTI3ODgiLCJkaXJlY3Rpb24iOiJwcmV2IiwibGltaXQiOjF9>; rel="previous"

从 rel="next" 对应的尖括号 <> 中提取出完整的下一页请求 URL:

https://{handle}.myshopline.com/admin/openapi/{version}/products/products.json?limit=1&page_info=eyJzaW5jZUlkIjoiMTYwNTc2NjAxNzI1MjczOTI4MDEwOTI3ODgiLCJkaXJlY3Rpb24iOiJuZXh0IiwibGltaXQiOjF9

对解析得到的 URL 发起 GET 请求,即可获取第二页数据。

提示

在 Java 中,你可以通过正则表达式 <([^>]+)>;\s*rel="([^"]+)" 快速匹配并提取出包含 page_info 的目标 URL。


详细参数描述​

since_id 模式​

查询参数如下表所示:

参数类型必填描述
since_idString否起始资源 ID,如商品 ID 和订单 ID。接口将返回 ID 大于该值的记录。
默认值:0
limitLong否单页返回的记录数量。默认值以具体接口 Schema 为准。

link 响应头信息如下表所示:

参数类型必填描述
linkString否包含指向下一页数据或上一页数据的请求 URL。
例子:<https://{handle}.myshopline.com/admin/openapi/{version}/products.json?page_info={page_info}&limit={limit}>;rel="next", <https://{handle}.myshopline.com/admin/openapi/{version}/products.json?page_info={page_info}&limit={limit}>;rel="previous"
包含如下参数:
  • handle:店铺唯一标识。
  • version:接口版本标识。
  • page_info:当前查询条件的上一页或下一页数据的编码串。
  • limit:单页返回记录的数量限制。

page_info 模式​

查询参数如下表所示:

参数类型必填描述
page_infoString否当前查询条件的上一页或下一页数据的编码串。
limitLong否单页返回的记录数量。默认值以具体接口 Schema 为准。

link 响应头信息如下表所示:

参数类型必填描述
linkString否包含指向下一页数据或上一页数据的请求 URL。
例子:<https://{handle}.myshopline.com/admin/openapi/{version}/products.json?page_info={page_info}&limit={limit}>;rel="next", <https://{handle}.myshopline.com/admin/openapi/{version}/products.json?page_info={page_info}&limit={limit}>; rel="previous"
包含如下参数:
  • handle:店铺唯一标识。
  • version:接口版本标识。
  • page_info:当前查询条件的上一页或下一页数据的编码串。
  • limit:单页返回记录的数量限制。

以下提供了 Java、PHP、Python、Node.js 及 Go 五种常用后端语言对 link 响应头进行解析的核心工具类及使用示例。

Java​

解析工具​

package com.shopline.client.api.internal.util;
import com.shopline.client.api.constant.Constants;
import com.shopline.client.api.model.ListOptions;
import com.shopline.client.api.model.Pagination;
import org.apache.commons.lang3.StringUtils;
import java.net.URI;
import java.net.URLDecoder;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Pagination
* Chinese:https://developer.shopline.com/zh-hans-cn/docs/apps/api-instructions-for-use/paging-mechanism/
* English:https://developer.shopline.com/docs/apps/api-instructions-for-use/paging-mechanism/
*/
public abstract class PaginationUtils {
// link pattern
private static final Pattern PATTERN = Pattern.compile("^ *<([^>]+)>; rel=\"(previous|prev|next)\" *$");
/**
* Parse linkHeader
*
* @param linkHeader
* @return Pagination
* @throws Exception
*/
public static Pagination parse(String linkHeader) throws Exception {
if (StringUtils.isBlank(linkHeader)) {
return null;
}
Pagination pagination = new Pagination();
String[] links = linkHeader.split(",");
for (String link : links) {
Matcher matcher = PATTERN.matcher(link);
if (matcher.matches()) {
String url = matcher.group(1);
String nav = matcher.group(2);
ListOptions option = buildListOption(url);
if ("next".equalsIgnoreCase(nav)) {
pagination.setNext(option);
} else {
pagination.setPrevious(option);
}
}
}
return pagination;
}
// url https://example.myshopline.com/admin/openapi/v20270301/products/products.json?limit=1&page_info=eyJzaW5jZUlkIjoiMTYwNTc1OTAxNTM4OTA4Mjk1MjExMTI3ODgiLCJkaXJlY3Rpb24iOiJuZXh0IiwibGltaXQiOjF9
private static ListOptions buildListOption(String url) throws Exception {
URI uri = new URI(url);
Map<String, String> queryParams = parseQueryParams(uri.getQuery());
String pageInfo = queryParams.get("page_info");
ListOptions option = new ListOptions();
option.setPageInfo(pageInfo);
String limit = queryParams.get("limit");
String sinceId = queryParams.get("since_id");
String fields = queryParams.get("fields");
if (StringUtils.isNotBlank(sinceId)) {
option.setSinceId(Long.parseLong(sinceId));
}
option.setFields(fields);
if (StringUtils.isNotBlank(limit)) {
option.setLimit(Integer.parseInt(limit));
}
return option;
}
private static Map<String, String> parseQueryParams(String query) throws Exception {
if (query == null || query.isEmpty()) {
return Collections.emptyMap();
}
Map<String, String> params = new HashMap<>();
String[] pairs = query.split("&");
for (String pair : pairs) {
String[] keyValue = pair.split("=", 2);
String key = URLDecoder.decode(keyValue[0], Constants.DEFAULT_ENCODING);
String value = keyValue.length > 1 ? URLDecoder.decode(keyValue[1], Constants.DEFAULT_ENCODING) : "";
params.put(key, value);
}
return params;
}
}

解析示例​

// Parse next
String linkHeader = "<https://example.myshopline.com/admin/openapi/v20270301/products/products.json?limit=1&page_info=eyJzaW5jZUlkIjoiMTYwNTc1OTAxNTM4OTA4Mjk1MjExMTI3ODgiLCJkaXJlY3Rpb24iOiJuZXh0IiwibGltaXQiOjF9>; rel=\"next\"";
Pagination pagination = PaginationUtils.parse(linkHeader);
// Parse next && previous
String linkHeader = "<https://example.myshopline.com/admin/openapi/v20270301/products/products.json?limit=1&page_info=eyJzaW5jZUlkIjoiMTYwNTc1OTAxNTM4OTA4Mjk1MjExMTI3ODgiLCJkaXJlY3Rpb24iOiJuZXh0IiwibGltaXQiOjF9>; rel=\"next\",<https://example.myshopline.com/admin/openapi/v20270301/products/products.json?limit=1&page_info=eyJzaW5jZUlkIjoiMTYwNTc2NjAxNzI1MjczOTI4MDEwOTI3ODgiLCJkaXJlY3Rpb24iOiJwcmV2IiwibGltaXQiOjF9>; rel=\"previous\"";
Pagination pagination = PaginationUtils.parse(linkHeader);

PHP​

解析工具​

<?php
declare(strict_types=1);
namespace Shopline\Client;
// Pagination of results
// For more details, see:
// https://developer.shopline.com/docs/apps/api-instructions-for-use/paging-mechanism
class PaginationParser
{
// linkRegex parses pagination links from SHOPLINE API responses.
private const link_REGEX = '/ *<([^>]+)>; rel="(previous|prev|next)" */';
/**
* Parse link header from a SHOPLINE API response.
*
* Example linkHeader:
* <https://example.myshopline.com/admin/openapi/v20270301/products/products.json?limit=1&page_info=xxx>; rel="next"
*
* @param string $linkHeader
* @return Pagination|null
* @throws ResponseDecodingError
*/
public static function parsePagination(string $linkHeader): ?Pagination
{
if ($linkHeader === '') {
return null;
}
$pagination = new Pagination();
foreach (explode(',', $linkHeader) as $link) {
self::parseListOptions($link, $pagination);
}
return $pagination;
}
/**
* @throws ResponseDecodingError
*/
private static function parseListOptions(string $link, Pagination $pagination): void
{
if (!preg_match(self::link_REGEX, $link, $matches)) {
// We expect $matches[1] = URL, $matches[2] = 'previous'|'prev'|'next'
throw new ResponseDecodingError('could not extract pagination link header');
}
$urlStr = $matches[1];
$rel = $matches[2];
$parsed = parse_url($urlStr);
if ($parsed === false || empty($parsed['scheme'])) {
throw new ResponseDecodingError('pagination does not contain a valid URL');
}
parse_str($parsed['query'] ?? '', $params);
if (empty($params['page_info'])) {
throw new ResponseDecodingError('The page_info is missing');
}
$listOptions = new ListOptions();
$listOptions->pageInfo = $params['page_info'];
if (isset($params['limit']) && $params['limit'] !== '') {
$listOptions->limit = (int) $params['limit'];
}
if (!empty($params['fields'])) {
$listOptions->fields = $params['fields'];
}
if ($rel === 'next') {
$pagination->next = $listOptions;
} elseif ($rel === 'previous' || $rel === 'prev') {
$pagination->previous = $listOptions;
} else {
throw new \InvalidArgumentException("Invalid pagination link format, rel: {$rel}");
}
}
}

解析示例​

<?php
declare(strict_types=1);
require_once __DIR__ . '/../src/ResponseDecodingError.php';
require_once __DIR__ . '/../src/ListOptions.php';
require_once __DIR__ . '/../src/Pagination.php';
require_once __DIR__ . '/../src/PaginationParser.php';
use Shopline\Client\PaginationParser;
use Shopline\Client\ResponseDecodingError;
$passed = 0;
$failed = 0;
function ok(bool $cond, string $label): void
{
global $passed, $failed;
if ($cond) {
echo "PASS: {$label}\n";
$passed++;
} else {
echo "FAIL: {$label}\n";
$failed++;
}
}
// Case 1: invalid header string → throws ResponseDecodingError
try {
PaginationParser::parsePagination('linkHeader');
ok(false, 'case1 - invalid header should throw');
} catch (ResponseDecodingError $e) {
ok(true, 'case1 - invalid header throws ResponseDecodingError');
}
// Case 2: empty string → returns null
$result = PaginationParser::parsePagination('');
ok($result === null, 'case2 - empty header returns null');
// Case 3: both next and previous links
$linkHeader =
'<https://example.myshopline.com/admin/openapi/v20270301/products/products.json' .
'?limit=1&page_info=eyJzaW5jZUlkIjoiMTYwNTc1OTAxNTM4OTA4Mjk1MjExMTI3ODgiLCJkaXJlY3Rpb24iOiJuZXh0IiwibGltaXQiOjF9' .
'>; rel="next",' .
'<https://example.myshopline.com/admin/openapi/v20270301/products/products.json' .
'?limit=1&page_info=eyJzaW5jZUlkIjoiMTYwNTc2NjAxNzI1MjczOTI4MDEwOTI3ODgiLCJkaXJlY3Rpb24iOiJwcmV2IiwibGltaXQiOjF9' .
'>; rel="previous"';
$pagination = PaginationParser::parsePagination($linkHeader);
ok($pagination !== null, 'case3 - pagination not null');
ok($pagination->previous !== null, 'case3 - previous not null');
ok(
$pagination->previous->pageInfo === 'eyJzaW5jZUlkIjoiMTYwNTc2NjAxNzI1MjczOTI4MDEwOTI3ODgiLCJkaXJlY3Rpb24iOiJwcmV2IiwibGltaXQiOjF9',
'case3 - previous pageInfo correct',
);
ok($pagination->previous->limit === 1, 'case3 - previous limit = 1');
ok($pagination->next !== null, 'case3 - next not null');
ok(
$pagination->next->pageInfo === 'eyJzaW5jZUlkIjoiMTYwNTc1OTAxNTM4OTA4Mjk1MjExMTI3ODgiLCJkaXJlY3Rpb24iOiJuZXh0IiwibGltaXQiOjF9',
'case3 - next pageInfo correct',
);
ok($pagination->next->limit === 1, 'case3 - next limit = 1');
// Case 4: only next link
$linkHeader =
'<https://example.myshopline.com/admin/openapi/v20270301/products/products.json' .
'?limit=1&page_info=eyJzaW5jZUlkIjoiMTYwNTc1OTAxNTM4OTA4Mjk1MjExMTI3ODgiLCJkaXJlY3Rpb24iOiJuZXh0IiwibGltaXQiOjF9' .
'>; rel="next"';
$pagination = PaginationParser::parsePagination($linkHeader);
ok($pagination !== null, 'case4 - pagination not null');
ok($pagination->next !== null, 'case4 - next not null');
ok($pagination->next->pageInfo !== '', 'case4 - next pageInfo not empty');
ok($pagination->next->limit === 1, 'case4 - next limit = 1');
// Case 5: rel="prev" treated as previous
$linkHeader =
'<https://example.myshopline.com/admin/openapi/v20270301/products/products.json' .
'?limit=1&page_info=eyJzaW5jZUlkIjoiMTYwNTc1OTAxNTM4OTA4Mjk1MjExMTI3ODgiLCJkaXJlY3Rpb24iOiJuZXh0IiwibGltaXQiOjF9' .
'>; rel="next",' .
'<https://example.myshopline.com/admin/openapi/v20270301/products/products.json' .
'?limit=1&page_info=eyJzaW5jZUlkIjoiMTYwNTc2NjAxNzI1MjczOTI4MDEwOTI3ODgiLCJkaXJlY3Rpb24iOiJwcmV2IiwibGltaXQiOjF9' .
'>; rel="prev"';
$pagination = PaginationParser::parsePagination($linkHeader);
ok($pagination !== null, 'case5 - pagination not null');
ok($pagination->previous !== null, 'case5 - previous not null (rel=prev)');
ok($pagination->next !== null, 'case5 - next not null');
// Summary
echo "\n{$passed} passed, {$failed} failed\n";
exit($failed > 0 ? 1 : 0);

Python​

解析工具​

// Code content
import re
from dataclasses import dataclass, field
from typing import Optional
from urllib.parse import urlparse, parse_qs
# Pagination of results
# For more details, see:
# https://developer.shopline.com/docs/apps/api-instructions-for-use/paging-mechanism
class ResponseDecodingError(Exception):
def __init__(self, message: str):
self.message = message
super().__init__(message)
@dataclass
class ListOptions:
# PageInfo is used with new pagination search.
page_info: str = ""
since_id: Optional[int] = None
limit: int = 0
fields: str = ""
@dataclass
class Pagination:
next: Optional[ListOptions] = None
previous: Optional[ListOptions] = None
# linkRegex is used to parse the pagination link from SHOPLINE API search results.
_link_REGEX = re.compile(r'^ *<([^>]+)>; rel="(previous|prev|next)" *$')
def parse_pagination(link_header: str) -> Optional[Pagination]:
"""Parse link header from SHOPLINE API response.
Example link_header:
<https://example.myshopline.com/admin/openapi/v20270301/products/products.json?limit=1&page_info=xxx>; rel="next"
"""
if not link_header:
return None
pagination = Pagination()
for link in link_header.split(","):
_parse_list_options(link, pagination)
return pagination
def _parse_list_options(link: str, pagination: Pagination) -> None:
match = _link_REGEX.match(link)
# We expect match[1] = URL, match[2] = 'previous'|'prev'|'next'
if not match:
raise ResponseDecodingError("could not extract pagination link header")
url_str = match.group(1)
rel = match.group(2)
try:
parsed_url = urlparse(url_str)
if not parsed_url.scheme:
raise ValueError("no scheme")
except Exception:
raise ResponseDecodingError("pagination does not contain a valid URL")
params = parse_qs(parsed_url.query)
list_options = ListOptions()
page_info_values = params.get("page_info", [])
if not page_info_values:
raise ResponseDecodingError("The page_info is missing")
list_options.page_info = page_info_values[0]
limit_values = params.get("limit", [])
if limit_values:
list_options.limit = int(limit_values[0])
fields_values = params.get("fields", [])
if fields_values:
list_options.fields = fields_values[0]
if rel == "next":
pagination.next = list_options
elif rel in ("previous", "prev"):
pagination.previous = list_options
else:
raise ValueError(f"Invalid pagination link format, rel: {rel}")

解析示例​

// Code content
import sys
import os
import unittest
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
from client.pagination import parse_pagination, ResponseDecodingError
class TestParsePagination(unittest.TestCase):
def test_next_and_previous(self):
# Case 1: both next and previous links
link_header = (
'<https://example.myshopline.com/admin/openapi/v20270301/products/products.json'
'?limit=1&page_info=eyJzaW5jZUlkIjoiMTYwNTc1OTAxNTM4OTA4Mjk1MjExMTI3ODgiLCJkaXJlY3Rpb24iOiJuZXh0IiwibGltaXQiOjF9'
'>; rel="next",'
'<https://example.myshopline.com/admin/openapi/v20270301/products/products.json'
'?limit=1&page_info=eyJzaW5jZUlkIjoiMTYwNTc2NjAxNzI1MjczOTI4MDEwOTI3ODgiLCJkaXJlY3Rpb24iOiJwcmV2IiwibGltaXQiOjF9'
'>; rel="previous"'
)
pagination = parse_pagination(link_header)
self.assertIsNotNone(pagination)
self.assertIsNotNone(pagination.previous)
self.assertEqual(
pagination.previous.page_info,
"eyJzaW5jZUlkIjoiMTYwNTc2NjAxNzI1MjczOTI4MDEwOTI3ODgiLCJkaXJlY3Rpb24iOiJwcmV2IiwibGltaXQiOjF9",
)
self.assertEqual(pagination.previous.limit, 1)
self.assertIsNotNone(pagination.next)
self.assertEqual(
pagination.next.page_info,
"eyJzaW5jZUlkIjoiMTYwNTc1OTAxNTM4OTA4Mjk1MjExMTI3ODgiLCJkaXJlY3Rpb24iOiJuZXh0IiwibGltaXQiOjF9",
)
self.assertEqual(pagination.next.limit, 1)
def test_next_only(self):
# Case 2: only next link
link_header = (
'<https://example.myshopline.com/admin/openapi/v20270301/products/products.json'
'?limit=1&page_info=eyJzaW5jZUlkIjoiMTYwNTc1OTAxNTM4OTA4Mjk1MjExMTI3ODgiLCJkaXJlY3Rpb24iOiJuZXh0IiwibGltaXQiOjF9'
'>; rel="next"'
)
pagination = parse_pagination(link_header)
self.assertIsNotNone(pagination)
self.assertIsNotNone(pagination.next)
self.assertNotEqual(pagination.next.page_info, "")
self.assertEqual(pagination.next.limit, 1)
def test_rel_prev_alias(self):
# Case 3: rel="prev" treated as previous
link_header = (
'<https://example.myshopline.com/admin/openapi/v20270301/products/products.json'
'?limit=1&page_info=eyJzaW5jZUlkIjoiMTYwNTc1OTAxNTM4OTA4Mjk1MjExMTI3ODgiLCJkaXJlY3Rpb24iOiJuZXh0IiwibGltaXQiOjF9'
'>; rel="next",'
'<https://example.myshopline.com/admin/openapi/v20270301/products/products.json'
'?limit=1&page_info=eyJzaW5jZUlkIjoiMTYwNTc2NjAxNzI1MjczOTI4MDEwOTI3ODgiLCJkaXJlY3Rpb24iOiJwcmV2IiwibGltaXQiOjF9'
'>; rel="prev"'
)
pagination = parse_pagination(link_header)
self.assertIsNotNone(pagination)
self.assertIsNotNone(pagination.previous)
self.assertIsNotNone(pagination.next)
if __name__ == "__main__":
unittest.main()

Node.js​

解析工具​

// Code content
// Pagination of results
// For more details, see:
// https://developer.shopline.com/docs/apps/api-instructions-for-use/paging-mechanism
export class ResponseDecodingError extends Error {
constructor(message) {
super(message);
this.name = 'ResponseDecodingError';
}
}
export class ListOptions {
/** @type {string} */
pageInfo = '';
/** @type {number|null} */
sinceId = null;
/** @type {number} */
limit = 0;
/** @type {string} */
fields = '';
}
export class Pagination {
/** @type {ListOptions|null} */
next = null;
/** @type {ListOptions|null} */
previous = null;
}
// linkRegex parses pagination links from SHOPLINE API responses.
const link_REGEX = /^ *<([^>]+)>; rel="(previous|prev|next)" *$/;
/**
* Parse link header from a SHOPLINE API response.
*
* Example linkHeader:
* <https://example.myshopline.com/admin/openapi/v20270301/products/products.json?limit=1&page_info=xxx>; rel="next"
*
* @param {string} linkHeader
* @returns {Pagination|null}
*/
export function parsePagination(linkHeader) {
if (!linkHeader) {
return null;
}
const pagination = new Pagination();
for (const link of linkHeader.split(',')) {
_parseListOptions(link, pagination);
}
return pagination;
}
/**
* @param {string} link
* @param {Pagination} pagination
*/
function _parseListOptions(link, pagination) {
const match = link_REGEX.exec(link);
// We expect match[1] = URL, match[2] = 'previous'|'prev'|'next'
if (!match) {
throw new ResponseDecodingError('could not extract pagination link header');
}
let parsedURL;
try {
parsedURL = new URL(match[1]);
} catch {
throw new ResponseDecodingError('pagination does not contain a valid URL');
}
const pageInfo = parsedURL.searchParams.get('page_info');
if (!pageInfo) {
throw new ResponseDecodingError('The page_info is missing');
}
const listOptions = new ListOptions();
listOptions.pageInfo = pageInfo;
const limit = parsedURL.searchParams.get('limit');
if (limit !== null) {
listOptions.limit = parseInt(limit, 10);
}
const fields = parsedURL.searchParams.get('fields');
if (fields !== null) {
listOptions.fields = fields;
}
const rel = match[2];
if (rel === 'next') {
pagination.next = listOptions;
} else if (rel === 'previous' || rel === 'prev') {
pagination.previous = listOptions;
} else {
throw new Error(`Invalid pagination link format, rel: ${rel}`);
}
}

解析示例​

import { test } from 'node:test';
import assert from 'node:assert/strict';
import { parsePagination, ResponseDecodingError } from '../client/pagination.js';
// Case 1: both next and previous links
test('parsePagination - next and previous', () => {
const linkHeader =
'<https://example.myshopline.com/admin/openapi/v20270301/products/products.json' +
'?limit=1&page_info=eyJzaW5jZUlkIjoiMTYwNTc1OTAxNTM4OTA4Mjk1MjExMTI3ODgiLCJkaXJlY3Rpb24iOiJuZXh0IiwibGltaXQiOjF9' +
'>; rel="next",' +
'<https://example.myshopline.com/admin/openapi/v20270301/products/products.json' +
'?limit=1&page_info=eyJzaW5jZUlkIjoiMTYwNTc2NjAxNzI1MjczOTI4MDEwOTI3ODgiLCJkaXJlY3Rpb24iOiJwcmV2IiwibGltaXQiOjF9' +
'>; rel="previous"';
const pagination = parsePagination(linkHeader);
assert.ok(pagination);
assert.ok(pagination.previous);
assert.equal(
pagination.previous.pageInfo,
'eyJzaW5jZUlkIjoiMTYwNTc2NjAxNzI1MjczOTI4MDEwOTI3ODgiLCJkaXJlY3Rpb24iOiJwcmV2IiwibGltaXQiOjF9',
);
assert.equal(pagination.previous.limit, 1);
assert.ok(pagination.next);
assert.equal(
pagination.next.pageInfo,
'eyJzaW5jZUlkIjoiMTYwNTc1OTAxNTM4OTA4Mjk1MjExMTI3ODgiLCJkaXJlY3Rpb24iOiJuZXh0IiwibGltaXQiOjF9',
);
assert.equal(pagination.next.limit, 1);
});
// Case 2: only next link
test('parsePagination - next only', () => {
const linkHeader =
'<https://example.myshopline.com/admin/openapi/v20270301/products/products.json' +
'?limit=1&page_info=eyJzaW5jZUlkIjoiMTYwNTc1OTAxNTM4OTA4Mjk1MjExMTI3ODgiLCJkaXJlY3Rpb24iOiJuZXh0IiwibGltaXQiOjF9' +
'>; rel="next"';
const pagination = parsePagination(linkHeader);
assert.ok(pagination);
assert.ok(pagination.next);
assert.ok(pagination.next.pageInfo);
assert.equal(pagination.next.limit, 1);
});
// Case 3: rel="prev" treated as previous
test('parsePagination - rel=prev alias', () => {
const linkHeader =
'<https://example.myshopline.com/admin/openapi/v20270301/products/products.json' +
'?limit=1&page_info=eyJzaW5jZUlkIjoiMTYwNTc1OTAxNTM4OTA4Mjk1MjExMTI3ODgiLCJkaXJlY3Rpb24iOiJuZXh0IiwibGltaXQiOjF9' +
'>; rel="next",' +
'<https://example.myshopline.com/admin/openapi/v20270301/products/products.json' +
'?limit=1&page_info=eyJzaW5jZUlkIjoiMTYwNTc2NjAxNzI1MjczOTI4MDEwOTI3ODgiLCJkaXJlY3Rpb24iOiJwcmV2IiwibGltaXQiOjF9' +
'>; rel="prev"';
const pagination = parsePagination(linkHeader);
assert.ok(pagination);
assert.ok(pagination.previous);
assert.ok(pagination.next);
});

Go​

解析工具​

解析工具源码参见 shopline-sdk-go/client/pagination.go。

package client
import (
"errors"
"fmt"
"net/url"
"regexp"
"strconv"
"strings"
)
// Pagination of results
// For more details, see:
// Chinese: https://developer.shopline.com/zh-hans-cn/docs/apps/api-instructions-for-use/paging-mechanism
// English: https://developer.shopline.com/docs/apps/api-instructions-for-use/paging-mechanism
type Pagination struct {
Next *ListOptions
Previous *ListOptions
}
// ListOptions
// General list options that can be used for most collections of entities.
type ListOptions struct {
// PageInfo is used with new pagination search.
PageInfo string `url:"page_info,omitempty"`
SinceId *uint64 `url:"since_id,omitempty"`
Limit int `url:"limit,omitempty"`
Fields string `url:"fields,omitempty"`
}
// linkRegex is used to parse the pagination link from SHOPLINE API search results.
var linkRegex = regexp.MustCompile(`^ *<([^>]+)>; rel="(previous|prev|next)" *$`)
// ParsePagination
// linkHeader eg: <https://example.myshopline.com/admin/openapi/v20270301/products/products.json?limit=1&page_info=eyJzaW5jZUlkIjoiMTYwNTc1OTAxNTM4OTA4Mjk1MjExMTI3ODgiLCJkaXJlY3Rpb24iOiJuZXh0IiwibGltaXQiOjF9>; rel="next"
// Chinese: https://developer.shopline.com/zh-hans-cn/docs/apps/api-instructions-for-use/paging-mechanism
// English: https://developer.shopline.com/docs/apps/api-instructions-for-use/paging-mechanism
func ParsePagination(linkHeader string) (*Pagination, error) {
if linkHeader == "" {
return nil, nil
}
pagination := new(Pagination)
for _, link := range strings.Split(linkHeader, ",") {
err := parseListOptions(link, pagination)
if err != nil {
return nil, err
}
}
return pagination, nil
}
// link <https://{handle}.myshopline.com/admin/openapi/{version}/products/products.json?limit=1&page_info=eyJzaW5jZUlkIjoiMTYwNTc1OTAxNTM4OTA4Mjk1MjExMTI3ODgiLCJkaXJlY3Rpb24iOiJuZXh0IiwibGltaXQiOjF9>; rel="next"
func parseListOptions(link string, pagination *Pagination) error {
match := linkRegex.FindStringSubmatch(link)
// Make sure the link is not empty or invalid
if len(match) != 3 {
// We expect 3 values:
// match[0] = full match
// match[1] is the URL and match[2] is either 'previous' or 'next'
err := ResponseDecodingError{
Message: "could not extract pagination link header",
}
return err
}
queryURL, err := url.Parse(match[1])
if err != nil {
err = ResponseDecodingError{
Message: "pagination does not contain a valid URL",
}
return err
}
params, err := url.ParseQuery(queryURL.RawQuery)
if err != nil {
return err
}
paginationListOptions := ListOptions{}
paginationListOptions.PageInfo = params.Get("page_info")
if paginationListOptions.PageInfo == "" {
err = ResponseDecodingError{
Message: "The page_info is missing",
}
return err
}
limit := params.Get("limit")
if limit != "" {
paginationListOptions.Limit, err = strconv.Atoi(params.Get("limit"))
if err != nil {
return err
}
}
fields := params.Get("fields")
if fields != "" {
paginationListOptions.Fields = params.Get("fields")
}
// 'rel' is either next or previous
if match[2] == "next" {
pagination.Next = &paginationListOptions
} else if match[2] == "previous" || match[2] == "prev" {
pagination.Previous = &paginationListOptions
} else {
return errors.New(fmt.Sprintf("Invalid pagination link format, rel: %s", match[2]))
}
return nil
}

解析示例​

package client
import (
"github.com/stretchr/testify/assert"
"testing"
)
func TestParsePagination(t *testing.T) {
// Case 1
linkHeader := "linkHeader"
_, err := ParsePagination(linkHeader)
a := assert.New(t)
a.NotNil(err)
// Case 2
linkHeader = ""
_, err = ParsePagination(linkHeader)
a.Nil(err)
// Case 3
linkHeader = "<https://example.myshopline.com/admin/openapi/v20270301/products/products.json?limit=1&page_info=eyJzaW5jZUlkIjoiMTYwNTc1OTAxNTM4OTA4Mjk1MjExMTI3ODgiLCJkaXJlY3Rpb24iOiJuZXh0IiwibGltaXQiOjF9>; rel=\"next\",<https://example.myshopline.com/admin/openapi/v20270301/products/products.json?limit=1&page_info=eyJzaW5jZUlkIjoiMTYwNTc2NjAxNzI1MjczOTI4MDEwOTI3ODgiLCJkaXJlY3Rpb24iOiJwcmV2IiwibGltaXQiOjF9>; rel=\"previous\""
pagination, err := ParsePagination(linkHeader)
a.Nil(err)
a.NotNil(pagination)
a.NotNil(pagination.Previous)
a.Equal(pagination.Previous.PageInfo, "eyJzaW5jZUlkIjoiMTYwNTc2NjAxNzI1MjczOTI4MDEwOTI3ODgiLCJkaXJlY3Rpb24iOiJwcmV2IiwibGltaXQiOjF9")
a.Equal(pagination.Previous.Limit, 1)
a.NotNil(pagination.Next)
a.NotEmpty(pagination.Next.PageInfo)
a.Equal(pagination.Next.PageInfo, "eyJzaW5jZUlkIjoiMTYwNTc1OTAxNTM4OTA4Mjk1MjExMTI3ODgiLCJkaXJlY3Rpb24iOiJuZXh0IiwibGltaXQiOjF9")
a.Equal(pagination.Next.Limit, 1)
// Case 4
linkHeader = "<https://example.myshopline.com/admin/openapi/v20270301/products/products.json?limit=1&page_info=eyJzaW5jZUlkIjoiMTYwNTc1OTAxNTM4OTA4Mjk1MjExMTI3ODgiLCJkaXJlY3Rpb24iOiJuZXh0IiwibGltaXQiOjF9>; rel=\"next\""
pagination, err = ParsePagination(linkHeader)
a.Nil(err)
a.NotNil(pagination)
a.NotNil(pagination.Next)
a.NotEmpty(pagination.Next.PageInfo)
a.Equal(pagination.Next.Limit, 1)
}
这篇文章对你有帮助吗?