feat: 支持主列表选择每页条数 (#242)
This commit is contained in:
@@ -34,15 +34,21 @@ func ParseCatalogStatus(raw string) model.CatalogImportStatus {
|
||||
}
|
||||
|
||||
func ListCatalogHistory(q repository.Execer, source, statusRaw string, page int) (CatalogHistoryResult, error) {
|
||||
return ListCatalogHistoryWithPageSize(q, source, statusRaw, page, DefaultPageSize)
|
||||
}
|
||||
|
||||
// ListCatalogHistoryWithPageSize 按白名单页容量返回商品目录导入记录。
|
||||
func ListCatalogHistoryWithPageSize(q repository.Execer, source, statusRaw string, page, pageSize int) (CatalogHistoryResult, error) {
|
||||
pageSize = NormalizePageSize(pageSize)
|
||||
status := ParseCatalogStatus(statusRaw)
|
||||
totalPages := 1
|
||||
_, total, err := repository.ListCatalogImportRuns(q, strings.TrimSpace(source), status, 1, 0)
|
||||
if err != nil {
|
||||
return CatalogHistoryResult{}, err
|
||||
}
|
||||
totalPages = TotalPages(total)
|
||||
totalPages = TotalPages(total, pageSize)
|
||||
page = ClampPage(page, totalPages)
|
||||
runs, _, err := repository.ListCatalogImportRuns(q, strings.TrimSpace(source), status, PageSize, (page-1)*PageSize)
|
||||
runs, _, err := repository.ListCatalogImportRuns(q, strings.TrimSpace(source), status, pageSize, (page-1)*pageSize)
|
||||
if err != nil {
|
||||
return CatalogHistoryResult{}, err
|
||||
}
|
||||
|
||||
@@ -42,8 +42,8 @@ func ApplyInnerCodes(ctx context.Context, db *sql.DB, writer InnerCodeWriter, id
|
||||
if len(ids) == 0 {
|
||||
return nil, fmt.Errorf("没有选择可回写记录")
|
||||
}
|
||||
if len(ids) > PageSize {
|
||||
return nil, fmt.Errorf("单次最多回写当前页 %d 条记录", PageSize)
|
||||
if len(ids) > InnerCodeRemoteBatchLimit {
|
||||
return nil, fmt.Errorf("单次最多回写 %d 条记录", InnerCodeRemoteBatchLimit)
|
||||
}
|
||||
result := &InnerCodeApplyResult{Requested: len(ids)}
|
||||
for _, id := range ids {
|
||||
|
||||
@@ -20,6 +20,16 @@ type fakeInnerCodeWriter struct {
|
||||
updatedCode string
|
||||
}
|
||||
|
||||
func TestApplyInnerCodes_远程批量上限不随页面容量放宽(t *testing.T) {
|
||||
ids := make([]int64, InnerCodeRemoteBatchLimit+1)
|
||||
for i := range ids {
|
||||
ids[i] = int64(i + 1)
|
||||
}
|
||||
if _, err := ApplyInnerCodes(context.Background(), nil, nil, ids, "user-1"); err == nil {
|
||||
t.Fatal("超过 20 条时应该在访问数据库或顺运宝前拒绝")
|
||||
}
|
||||
}
|
||||
|
||||
func (f *fakeInnerCodeWriter) DetailListByStock(context.Context, []int64) ([]syb.StockDetail, error) {
|
||||
f.readCount++
|
||||
if f.readErr != nil {
|
||||
|
||||
@@ -31,8 +31,8 @@ func DeleteInnerCodes(db *sql.DB, ids []int64, actorUserID string, now time.Time
|
||||
if len(unique) == 0 {
|
||||
return 0, fmt.Errorf("没有有效的档口入库码记录")
|
||||
}
|
||||
if len(unique) > PageSize {
|
||||
return 0, fmt.Errorf("一次最多删除当前页 %d 条记录", PageSize)
|
||||
if len(unique) > InnerCodeDeleteBatchLimit {
|
||||
return 0, fmt.Errorf("一次最多删除当前页 %d 条记录", InnerCodeDeleteBatchLimit)
|
||||
}
|
||||
return repository.SoftDeleteInnerCodeRecords(db, unique, actorUserID, now.UTC().Format(model.TimeLayout))
|
||||
}
|
||||
|
||||
@@ -27,6 +27,13 @@ var innerCodeStatusOptions = []InnerCodeStatusOption{
|
||||
{string(model.InnerCodeNeedsCheck), "需核对"},
|
||||
}
|
||||
|
||||
// 档口入库码的批量上限是业务安全限制,不能再复用界面每页条数。
|
||||
// 删除只改本地数据库,可覆盖最大单页;匹配和回写会访问顺运宝,继续限制 20 条。
|
||||
const (
|
||||
InnerCodeDeleteBatchLimit = 100
|
||||
InnerCodeRemoteBatchLimit = 20
|
||||
)
|
||||
|
||||
// InnerCodeRowView 是正式页面的一行。
|
||||
type InnerCodeRowView struct {
|
||||
ID int64
|
||||
@@ -66,8 +73,14 @@ func InnerCodeStatusOptions() []InnerCodeStatusOption {
|
||||
return append([]InnerCodeStatusOption(nil), innerCodeStatusOptions...)
|
||||
}
|
||||
|
||||
// ListInnerCodePage 校验筛选并返回统一 20 条 SQL 分页结果。
|
||||
// ListInnerCodePage 使用默认页容量返回档口入库码列表。
|
||||
func ListInnerCodePage(db *sql.DB, businessDate, status, keyword string, requestedPage int) (*InnerCodeListPage, error) {
|
||||
return ListInnerCodePageWithPageSize(db, businessDate, status, keyword, requestedPage, DefaultPageSize)
|
||||
}
|
||||
|
||||
// ListInnerCodePageWithPageSize 校验筛选并按白名单页容量执行 SQL 分页。
|
||||
func ListInnerCodePageWithPageSize(db *sql.DB, businessDate, status, keyword string, requestedPage, pageSize int) (*InnerCodeListPage, error) {
|
||||
pageSize = NormalizePageSize(pageSize)
|
||||
status = strings.TrimSpace(status)
|
||||
if !validInnerCodeStatusFilter(status) {
|
||||
status = ""
|
||||
@@ -78,9 +91,9 @@ func ListInnerCodePage(db *sql.DB, businessDate, status, keyword string, request
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
totalPages := TotalPages(total)
|
||||
totalPages := TotalPages(total, pageSize)
|
||||
page := ClampPage(requestedPage, totalPages)
|
||||
records, err := repository.ListInnerCodeRecords(db, filter, PageSize, (page-1)*PageSize)
|
||||
records, err := repository.ListInnerCodeRecords(db, filter, pageSize, (page-1)*pageSize)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
+90
-23
@@ -6,15 +6,19 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// PageSize 是全项目统一的每页条数。
|
||||
// DefaultPageSize 是主列表没有指定 page_size 时使用的每页条数。
|
||||
//
|
||||
// `[必须]` 固定 20:见 docs/admin/05-ui-specification.md §3.2——
|
||||
// 20 行在 1366×768 上正好一屏不用滚动。
|
||||
const PageSize = 20
|
||||
// 20 行在 1366×768 上通常不需要滚动,因此继续作为默认值;实际页面请求必须
|
||||
// 通过 ParsePageSize 白名单解析。
|
||||
const DefaultPageSize = 20
|
||||
|
||||
var allowedPageSizes = [...]int{20, 50, 100}
|
||||
|
||||
// ParsePage 解析 URL 上的 page 查询参数。
|
||||
//
|
||||
@@ -29,15 +33,36 @@ func ParsePage(s string) int {
|
||||
return n
|
||||
}
|
||||
|
||||
// TotalPages 按 PageSize 把总数换算成总页数。
|
||||
// ParsePageSize 解析 URL 上的 page_size,只接受界面提供的三个白名单值。
|
||||
// 非法值回退为 20,避免有人手改 URL 造成一次读取过多数据。
|
||||
func ParsePageSize(s string) int {
|
||||
n, err := strconv.Atoi(strings.TrimSpace(s))
|
||||
if err != nil {
|
||||
return DefaultPageSize
|
||||
}
|
||||
return NormalizePageSize(n)
|
||||
}
|
||||
|
||||
// NormalizePageSize 校验代码内部传入的页容量,规则和 URL 参数完全一致。
|
||||
func NormalizePageSize(n int) int {
|
||||
for _, allowed := range allowedPageSizes {
|
||||
if n == allowed {
|
||||
return n
|
||||
}
|
||||
}
|
||||
return DefaultPageSize
|
||||
}
|
||||
|
||||
// TotalPages 按实际 pageSize 把总数换算成总页数。
|
||||
//
|
||||
// `[必须]` 总数为 0 时返回 1,不是 0——页面要显示"第 1/1 页",
|
||||
// 不能出现"第 1/0 页"(工单 #43)。
|
||||
func TotalPages(total int) int {
|
||||
func TotalPages(total, pageSize int) int {
|
||||
if total <= 0 {
|
||||
return 1
|
||||
}
|
||||
return (total + PageSize - 1) / PageSize
|
||||
pageSize = NormalizePageSize(pageSize)
|
||||
return (total + pageSize - 1) / pageSize
|
||||
}
|
||||
|
||||
// ClampPage 把请求的页码限制在 [1, totalPages] 范围内。
|
||||
@@ -63,39 +88,81 @@ func ClampPage(page, totalPages int) int {
|
||||
// 会把动态内容当成单个参数值整体转义,问号和等号会被转成 %3F/%3D,
|
||||
// 链接直接失效——这是本工单实测踩到的一个坑,见 pagination_test.go。
|
||||
type PaginationView struct {
|
||||
Page int
|
||||
TotalPages int
|
||||
HasPrev bool
|
||||
HasNext bool
|
||||
FirstURL string
|
||||
PrevURL string
|
||||
NextURL string
|
||||
LastURL string
|
||||
Page int
|
||||
PageSize int
|
||||
TotalPages int
|
||||
HasPrev bool
|
||||
HasNext bool
|
||||
FirstURL string
|
||||
PrevURL string
|
||||
NextURL string
|
||||
LastURL string
|
||||
PageSizeOptions []PageSizeOption
|
||||
FilterFields []PaginationFilterField
|
||||
}
|
||||
|
||||
// PageSizeOption 是“每页条数”下拉框的一项。
|
||||
type PageSizeOption struct {
|
||||
Value int
|
||||
Selected bool
|
||||
}
|
||||
|
||||
// PaginationFilterField 是切换每页条数时需要保留的一个 GET 筛选参数。
|
||||
// page 和 page_size 不放在这里:切换容量必须从第 1 页重新开始。
|
||||
type PaginationFilterField struct {
|
||||
Name string
|
||||
Value string
|
||||
}
|
||||
|
||||
// NewPaginationView 根据当前页、总页数和筛选查询串组装分页控件视图。
|
||||
//
|
||||
// baseQuery 是不含 page 的查询串(例如 url.Values{"status": {"no_link"}}.Encode()
|
||||
// 的结果),由调用方(handler)负责把当前筛选和关键词编码进去——
|
||||
// 这样翻页时筛选和关键词才不会丢,见工单 #43。
|
||||
func NewPaginationView(page, totalPages int, baseQuery string) PaginationView {
|
||||
// baseQuery 是当前筛选查询串,由调用方(handler)负责把有效筛选编码进去;
|
||||
// 本函数会覆盖 page_size 并移除 page,避免旧参数污染生成的链接。
|
||||
func NewPaginationView(page, pageSize, totalPages int, baseQuery string) PaginationView {
|
||||
pageSize = NormalizePageSize(pageSize)
|
||||
v := PaginationView{
|
||||
Page: page,
|
||||
PageSize: pageSize,
|
||||
TotalPages: totalPages,
|
||||
HasPrev: page > 1,
|
||||
HasNext: page < totalPages,
|
||||
FirstURL: PaginationURL(baseQuery, 1),
|
||||
LastURL: PaginationURL(baseQuery, totalPages),
|
||||
}
|
||||
query, err := url.ParseQuery(baseQuery)
|
||||
if err != nil {
|
||||
query = url.Values{}
|
||||
}
|
||||
query.Del("page")
|
||||
query.Set("page_size", strconv.Itoa(pageSize))
|
||||
encodedQuery := query.Encode()
|
||||
v.FirstURL = PaginationURL(encodedQuery, 1)
|
||||
v.LastURL = PaginationURL(encodedQuery, totalPages)
|
||||
if v.HasPrev {
|
||||
v.PrevURL = PaginationURL(baseQuery, page-1)
|
||||
v.PrevURL = PaginationURL(encodedQuery, page-1)
|
||||
}
|
||||
if v.HasNext {
|
||||
v.NextURL = PaginationURL(baseQuery, page+1)
|
||||
v.NextURL = PaginationURL(encodedQuery, page+1)
|
||||
}
|
||||
for _, size := range allowedPageSizes {
|
||||
v.PageSizeOptions = append(v.PageSizeOptions, PageSizeOption{Value: size, Selected: size == pageSize})
|
||||
}
|
||||
query.Del("page_size")
|
||||
for _, name := range sortedQueryKeys(query) {
|
||||
for _, value := range query[name] {
|
||||
v.FilterFields = append(v.FilterFields, PaginationFilterField{Name: name, Value: value})
|
||||
}
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
func sortedQueryKeys(values url.Values) []string {
|
||||
keys := make([]string, 0, len(values))
|
||||
for key := range values {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
slices.Sort(keys)
|
||||
return keys
|
||||
}
|
||||
|
||||
// PaginationURL 把筛选查询串和目标页码拼成一个完整的相对链接("?...")。
|
||||
func PaginationURL(baseQuery string, page int) string {
|
||||
q := "page=" + strconv.Itoa(page)
|
||||
|
||||
@@ -20,27 +20,50 @@ func TestParsePage_合法值原样返回(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestParsePageSize_只接受白名单(t *testing.T) {
|
||||
for _, in := range []string{"20", "50", "100", " 50 "} {
|
||||
want := map[string]int{"20": 20, "50": 50, "100": 100, " 50 ": 50}[in]
|
||||
if got := ParsePageSize(in); got != want {
|
||||
t.Errorf("ParsePageSize(%q) = %d,想要 %d", in, got, want)
|
||||
}
|
||||
}
|
||||
for _, in := range []string{"", "0", "21", "200", "abc"} {
|
||||
if got := ParsePageSize(in); got != DefaultPageSize {
|
||||
t.Errorf("ParsePageSize(%q) = %d,非法值应回退 %d", in, got, DefaultPageSize)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTotalPages_总数为0返回1不是0(t *testing.T) {
|
||||
if got := TotalPages(0); got != 1 {
|
||||
if got := TotalPages(0, 100); got != 1 {
|
||||
t.Errorf("TotalPages(0) = %d,想要 1(不能出现『第 1/0 页』)", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTotalPages_按PageSize向上取整(t *testing.T) {
|
||||
cases := map[int]int{
|
||||
1: 1,
|
||||
PageSize: 1,
|
||||
PageSize + 1: 2,
|
||||
PageSize * 2: 2,
|
||||
5195: 260, // 工单 #43 实测样本量:5195 条 / 20 条一页 = 260 页
|
||||
1: 1,
|
||||
DefaultPageSize: 1,
|
||||
DefaultPageSize + 1: 2,
|
||||
DefaultPageSize * 2: 2,
|
||||
5195: 260, // 工单 #43 实测样本量:5195 条 / 20 条一页 = 260 页
|
||||
}
|
||||
for total, want := range cases {
|
||||
if got := TotalPages(total); got != want {
|
||||
if got := TotalPages(total, DefaultPageSize); got != want {
|
||||
t.Errorf("TotalPages(%d) = %d,想要 %d", total, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTotalPages_按选择的每页条数计算(t *testing.T) {
|
||||
if got := TotalPages(101, 50); got != 3 {
|
||||
t.Errorf("TotalPages(101, 50) = %d,想要 3", got)
|
||||
}
|
||||
if got := TotalPages(101, 100); got != 2 {
|
||||
t.Errorf("TotalPages(101, 100) = %d,想要 2", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClampPage_越界兜到最后一页(t *testing.T) {
|
||||
if got := ClampPage(9999, 260); got != 260 {
|
||||
t.Errorf("ClampPage(9999, 260) = %d,想要 260(兜到最后一页,不是空表格)", got)
|
||||
@@ -62,7 +85,7 @@ func TestClampPage_合法范围原样返回(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestNewPaginationView_首页禁用上一页和首页(t *testing.T) {
|
||||
v := NewPaginationView(1, 260, "")
|
||||
v := NewPaginationView(1, 20, 260, "")
|
||||
if v.HasPrev {
|
||||
t.Error("第 1 页时 HasPrev 应为 false(首页/上一页要禁用)")
|
||||
}
|
||||
@@ -72,7 +95,7 @@ func TestNewPaginationView_首页禁用上一页和首页(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestNewPaginationView_末页禁用下一页和末页(t *testing.T) {
|
||||
v := NewPaginationView(260, 260, "")
|
||||
v := NewPaginationView(260, 20, 260, "")
|
||||
if v.HasPrev != true {
|
||||
t.Error("最后一页时 HasPrev 应为 true(前面还有页)")
|
||||
}
|
||||
@@ -82,25 +105,31 @@ func TestNewPaginationView_末页禁用下一页和末页(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestNewPaginationView_总数为0时首尾都禁用(t *testing.T) {
|
||||
v := NewPaginationView(1, 1, "")
|
||||
v := NewPaginationView(1, 20, 1, "")
|
||||
if v.HasPrev || v.HasNext {
|
||||
t.Errorf("只有 1 页时前后都应禁用,HasPrev=%v HasNext=%v", v.HasPrev, v.HasNext)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewPaginationView_URL带上筛选条件(t *testing.T) {
|
||||
v := NewPaginationView(2, 260, "status=no_link")
|
||||
if v.PrevURL != "?status=no_link&page=1" {
|
||||
t.Errorf("PrevURL = %q,想要 ?status=no_link&page=1", v.PrevURL)
|
||||
v := NewPaginationView(2, 50, 260, "status=no_link&page_size=50")
|
||||
if v.PrevURL != "?page_size=50&status=no_link&page=1" {
|
||||
t.Errorf("PrevURL = %q,想要保留筛选和 page_size", v.PrevURL)
|
||||
}
|
||||
if v.NextURL != "?status=no_link&page=3" {
|
||||
t.Errorf("NextURL = %q,想要 ?status=no_link&page=3", v.NextURL)
|
||||
if v.NextURL != "?page_size=50&status=no_link&page=3" {
|
||||
t.Errorf("NextURL = %q,想要保留筛选和 page_size", v.NextURL)
|
||||
}
|
||||
if v.FirstURL != "?status=no_link&page=1" {
|
||||
t.Errorf("FirstURL = %q,想要 ?status=no_link&page=1", v.FirstURL)
|
||||
if v.FirstURL != "?page_size=50&status=no_link&page=1" {
|
||||
t.Errorf("FirstURL = %q,想要保留筛选和 page_size", v.FirstURL)
|
||||
}
|
||||
if v.LastURL != "?status=no_link&page=260" {
|
||||
t.Errorf("LastURL = %q,想要 ?status=no_link&page=260", v.LastURL)
|
||||
if v.LastURL != "?page_size=50&status=no_link&page=260" {
|
||||
t.Errorf("LastURL = %q,想要保留筛选和 page_size", v.LastURL)
|
||||
}
|
||||
if v.PageSize != 50 || !v.PageSizeOptions[1].Selected {
|
||||
t.Fatalf("当前页容量选择不正确: %+v", v)
|
||||
}
|
||||
if len(v.FilterFields) != 1 || v.FilterFields[0].Name != "status" {
|
||||
t.Fatalf("切换容量应只保留筛选且不保留 page/page_size: %+v", v.FilterFields)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -249,19 +249,25 @@ type PddListResult struct {
|
||||
|
||||
// ListPddProducts 查列表并把每一行翻成界面文字。
|
||||
func ListPddProducts(db *sql.DB, keyword string, status model.CollectStatus, requestedPage int) (*PddListResult, error) {
|
||||
return ListPddProductsWithPageSize(db, keyword, status, requestedPage, DefaultPageSize)
|
||||
}
|
||||
|
||||
// ListPddProductsWithPageSize 按白名单页容量返回 PDD 商品列表。
|
||||
func ListPddProductsWithPageSize(db *sql.DB, keyword string, status model.CollectStatus, requestedPage, pageSize int) (*PddListResult, error) {
|
||||
pageSize = NormalizePageSize(pageSize)
|
||||
page := requestedPage
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
rows, filteredTotal, err := repository.ListPddProducts(db, keyword, status, PageSize, (page-1)*PageSize)
|
||||
rows, filteredTotal, err := repository.ListPddProducts(db, keyword, status, pageSize, (page-1)*pageSize)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
totalPages := TotalPages(filteredTotal)
|
||||
totalPages := TotalPages(filteredTotal, pageSize)
|
||||
clampedPage := ClampPage(page, totalPages)
|
||||
if clampedPage != page {
|
||||
page = clampedPage
|
||||
rows, _, err = repository.ListPddProducts(db, keyword, status, PageSize, (page-1)*PageSize)
|
||||
rows, _, err = repository.ListPddProducts(db, keyword, status, pageSize, (page-1)*pageSize)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -40,6 +40,32 @@ func TestListPddProducts_统一每页20条并收敛越界页(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestListPddProducts_可选每页50条(t *testing.T) {
|
||||
db := newTestDB(t)
|
||||
for i := 0; i < 55; i++ {
|
||||
goodsID := fmt.Sprintf("73711654%04d", i)
|
||||
if _, err := CreatePddProduct(db,
|
||||
"https://mobile.yangkeduo.com/goods.html?goods_id="+goodsID); err != nil {
|
||||
t.Fatalf("创建第 %d 个商品失败: %v", i, err)
|
||||
}
|
||||
}
|
||||
|
||||
first, err := ListPddProductsWithPageSize(db, "", "", 1, 50)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(first.Rows) != 50 || first.TotalPages != 2 {
|
||||
t.Fatalf("50 条分页第 1 页不对: rows=%d pages=%d", len(first.Rows), first.TotalPages)
|
||||
}
|
||||
second, err := ListPddProductsWithPageSize(db, "", "", 2, 50)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(second.Rows) != 5 || second.Page != 2 {
|
||||
t.Fatalf("50 条分页第 2 页不对: rows=%d page=%d", len(second.Rows), second.Page)
|
||||
}
|
||||
}
|
||||
|
||||
// ── 链接解析 ───────────────────────────────────────────
|
||||
|
||||
func TestParsePddGoodsID_认得的写法(t *testing.T) {
|
||||
|
||||
@@ -166,6 +166,12 @@ type ClientListResult struct {
|
||||
|
||||
// ListClientPageForUser 按网页登录身份、搜索词和统一页大小返回客户端列表。
|
||||
func ListClientPageForUser(db *sql.DB, actor *model.User, keyword string, threshold time.Duration, requestedPage int) (*ClientListResult, error) {
|
||||
return ListClientPageForUserWithPageSize(db, actor, keyword, threshold, requestedPage, DefaultPageSize)
|
||||
}
|
||||
|
||||
// ListClientPageForUserWithPageSize 按白名单页容量返回当前账号可见客户端。
|
||||
func ListClientPageForUserWithPageSize(db *sql.DB, actor *model.User, keyword string, threshold time.Duration, requestedPage, pageSize int) (*ClientListResult, error) {
|
||||
pageSize = NormalizePageSize(pageSize)
|
||||
visibleUserID, err := visibleClientUserID(actor)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -175,15 +181,15 @@ func ListClientPageForUser(db *sql.DB, actor *model.User, keyword string, thresh
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
rows, total, err := repository.ListClientsForUserPage(db, keyword, visibleUserID, PageSize, (page-1)*PageSize)
|
||||
rows, total, err := repository.ListClientsForUserPage(db, keyword, visibleUserID, pageSize, (page-1)*pageSize)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
totalPages := TotalPages(total)
|
||||
totalPages := TotalPages(total, pageSize)
|
||||
clampedPage := ClampPage(page, totalPages)
|
||||
if clampedPage != page {
|
||||
page = clampedPage
|
||||
rows, _, err = repository.ListClientsForUserPage(db, keyword, visibleUserID, PageSize, (page-1)*PageSize)
|
||||
rows, _, err = repository.ListClientsForUserPage(db, keyword, visibleUserID, pageSize, (page-1)*pageSize)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -81,15 +81,21 @@ type ShopeeListResult struct {
|
||||
// (repository.CountShopeeProductsFiltered),分页页数据此算出,
|
||||
// page 越界时兜到最后一页,见 service/pagination.go(工单 #43)。
|
||||
func ListShopeeProducts(db *sql.DB, filter repository.ShopeeFilter, page int) (*ShopeeListResult, error) {
|
||||
return ListShopeeProductsWithPageSize(db, filter, page, DefaultPageSize)
|
||||
}
|
||||
|
||||
// ListShopeeProductsWithPageSize 按白名单页容量返回蝦皮商品列表。
|
||||
func ListShopeeProductsWithPageSize(db *sql.DB, filter repository.ShopeeFilter, page, pageSize int) (*ShopeeListResult, error) {
|
||||
pageSize = NormalizePageSize(pageSize)
|
||||
total, err := repository.CountShopeeProductsFiltered(db, filter)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
totalPages := TotalPages(total)
|
||||
totalPages := TotalPages(total, pageSize)
|
||||
page = ClampPage(page, totalPages)
|
||||
offset := (page - 1) * PageSize
|
||||
offset := (page - 1) * pageSize
|
||||
|
||||
rows, err := repository.ListShopeeProducts(db, filter, PageSize, offset)
|
||||
rows, err := repository.ListShopeeProducts(db, filter, pageSize, offset)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -105,7 +111,7 @@ func ListShopeeProducts(db *sql.DB, filter repository.ShopeeFilter, page int) (*
|
||||
HasAnyProducts: hasAny > 0,
|
||||
IsFiltered: strings.TrimSpace(filter.Keyword) != "" || filter.Status != "" || filter.Deleted || filter.Unlinked,
|
||||
Page: page,
|
||||
PageSize: PageSize,
|
||||
PageSize: pageSize,
|
||||
TotalPages: totalPages,
|
||||
}
|
||||
for _, r := range rows {
|
||||
|
||||
+14
-8
@@ -1246,6 +1246,12 @@ func ParseSybOrderNos(raw string) ([]string, error) {
|
||||
// `[必须]` 处理阶段由顺运宝规格键、PDD 关联、采集状态和规格映射实时推导,
|
||||
// 不额外保存一份容易过期的状态字段。
|
||||
func ListSybOrdersView(db *sql.DB, keyword, shop, stage string, page int) (*SybListResult, error) {
|
||||
return ListSybOrdersViewWithPageSize(db, keyword, shop, stage, page, DefaultPageSize)
|
||||
}
|
||||
|
||||
// ListSybOrdersViewWithPageSize 按白名单页容量查询顺运宝明细。
|
||||
func ListSybOrdersViewWithPageSize(db *sql.DB, keyword, shop, stage string, page, pageSize int) (*SybListResult, error) {
|
||||
pageSize = NormalizePageSize(pageSize)
|
||||
shop = strings.TrimSpace(shop)
|
||||
stage = ParseSybStage(stage)
|
||||
orderNos, err := ParseSybOrderNos(keyword)
|
||||
@@ -1264,18 +1270,18 @@ func ListSybOrdersView(db *sql.DB, keyword, shop, stage string, page int) (*SybL
|
||||
stage == SybStageAIMatched ||
|
||||
stage == SybStageTaskCreated || stage == SybStagePurchaseCompleted ||
|
||||
stage == SybStagePurchaseReview || stage == SybStagePurchaseBlocked {
|
||||
return listAdvancedSybStage(db, orderNos, shop, stage, page)
|
||||
return listAdvancedSybStage(db, orderNos, shop, stage, page, pageSize)
|
||||
}
|
||||
filter := repository.SybOrderFilter{OrderNos: orderNos, Shop: shop, Stage: stage}
|
||||
total, err := repository.CountSybOrders(db, filter)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
totalPages := TotalPages(total)
|
||||
totalPages := TotalPages(total, pageSize)
|
||||
page = ClampPage(page, totalPages)
|
||||
offset := (page - 1) * PageSize
|
||||
offset := (page - 1) * pageSize
|
||||
|
||||
rows, err := repository.ListSybOrderContexts(db, filter, PageSize, offset)
|
||||
rows, err := repository.ListSybOrderContexts(db, filter, pageSize, offset)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -1311,7 +1317,7 @@ func ListSybOrdersView(db *sql.DB, keyword, shop, stage string, page int) (*SybL
|
||||
// 这些阶段不能只靠 SQL 判断:同一个 option key 是否仍存在,需要走唯一的
|
||||
// OptionKey 规范化逻辑。当前同步量是百到千级,先保证采购判断正确;普通列表
|
||||
// 和其余阶段仍在数据库分页。
|
||||
func listAdvancedSybStage(db *sql.DB, orderNos []string, shop, stage string, page int) (*SybListResult, error) {
|
||||
func listAdvancedSybStage(db *sql.DB, orderNos []string, shop, stage string, page, pageSize int) (*SybListResult, error) {
|
||||
contexts, err := repository.ListSybOrderContexts(db,
|
||||
repository.SybOrderFilter{OrderNos: orderNos, Shop: shop}, -1, 0)
|
||||
if err != nil {
|
||||
@@ -1329,13 +1335,13 @@ func listAdvancedSybStage(db *sql.DB, orderNos []string, shop, stage string, pag
|
||||
}
|
||||
}
|
||||
total := len(filtered)
|
||||
totalPages := TotalPages(total)
|
||||
totalPages := TotalPages(total, pageSize)
|
||||
page = ClampPage(page, totalPages)
|
||||
start := (page - 1) * PageSize
|
||||
start := (page - 1) * pageSize
|
||||
if start > total {
|
||||
start = total
|
||||
}
|
||||
end := start + PageSize
|
||||
end := start + pageSize
|
||||
if end > total {
|
||||
end = total
|
||||
}
|
||||
|
||||
+14
-3
@@ -322,6 +322,12 @@ type TaskListResult struct {
|
||||
// `[必须]` 底部统计要跟随当前筛选,所以 Counts 和 Rows 用的是**同一个**
|
||||
// filter,见 #19。
|
||||
func ListTasksView(db *sql.DB, filter repository.TaskFilter, requestedPage int) (*TaskListResult, error) {
|
||||
return ListTasksViewWithPageSize(db, filter, requestedPage, DefaultPageSize)
|
||||
}
|
||||
|
||||
// ListTasksViewWithPageSize 按白名单页容量返回任务列表。
|
||||
func ListTasksViewWithPageSize(db *sql.DB, filter repository.TaskFilter, requestedPage, pageSize int) (*TaskListResult, error) {
|
||||
pageSize = NormalizePageSize(pageSize)
|
||||
counts, err := repository.CountTasksByStatus(db, filter)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -330,9 +336,9 @@ func ListTasksView(db *sql.DB, filter repository.TaskFilter, requestedPage int)
|
||||
for _, n := range counts {
|
||||
total += n
|
||||
}
|
||||
totalPages := TotalPages(total)
|
||||
totalPages := TotalPages(total, pageSize)
|
||||
page := ClampPage(requestedPage, totalPages)
|
||||
rows, err := repository.ListTasks(db, filter, PageSize, (page-1)*PageSize)
|
||||
rows, err := repository.ListTasks(db, filter, pageSize, (page-1)*pageSize)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -371,6 +377,11 @@ func taskCreatorText(username string) string {
|
||||
|
||||
// ListTasksViewForUser 在服务端固定当前账号范围,忽略采购员伪造的创建人筛选。
|
||||
func ListTasksViewForUser(db *sql.DB, actor *model.User, filter repository.TaskFilter, creatorValue string, requestedPage int) (*TaskListResult, error) {
|
||||
return ListTasksViewForUserWithPageSize(db, actor, filter, creatorValue, requestedPage, DefaultPageSize)
|
||||
}
|
||||
|
||||
// ListTasksViewForUserWithPageSize 固定账号可见范围后按白名单页容量返回任务。
|
||||
func ListTasksViewForUserWithPageSize(db *sql.DB, actor *model.User, filter repository.TaskFilter, creatorValue string, requestedPage, pageSize int) (*TaskListResult, error) {
|
||||
visibleUserID, err := visibleClientUserID(actor)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -386,7 +397,7 @@ func ListTasksViewForUser(db *sql.DB, actor *model.User, filter repository.TaskF
|
||||
filter.CreatorUserID = creatorValue
|
||||
}
|
||||
}
|
||||
return ListTasksView(db, filter, requestedPage)
|
||||
return ListTasksViewWithPageSize(db, filter, requestedPage, pageSize)
|
||||
}
|
||||
|
||||
// StatusLine 拼底部状态条,形如:
|
||||
|
||||
+10
-4
@@ -23,8 +23,14 @@ type UserListResult struct {
|
||||
TotalPages int
|
||||
}
|
||||
|
||||
// ListUsers 搜索并分页。用户页沿用全站统一的每页 20 条规则。
|
||||
// ListUsers 使用默认页容量搜索并分页。
|
||||
func ListUsers(db *sql.DB, actor *model.User, keyword string, status model.UserStatus, requestedPage int) (*UserListResult, error) {
|
||||
return ListUsersWithPageSize(db, actor, keyword, status, requestedPage, DefaultPageSize)
|
||||
}
|
||||
|
||||
// ListUsersWithPageSize 按白名单页容量搜索并分页。
|
||||
func ListUsersWithPageSize(db *sql.DB, actor *model.User, keyword string, status model.UserStatus, requestedPage, pageSize int) (*UserListResult, error) {
|
||||
pageSize = NormalizePageSize(pageSize)
|
||||
if actor == nil || !actor.IsAdmin() {
|
||||
return nil, ErrAdminRequired
|
||||
}
|
||||
@@ -36,15 +42,15 @@ func ListUsers(db *sql.DB, actor *model.User, keyword string, status model.UserS
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
rows, total, err := repository.ListUsers(db, keyword, status, PageSize, (page-1)*PageSize)
|
||||
rows, total, err := repository.ListUsers(db, keyword, status, pageSize, (page-1)*pageSize)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
totalPages := TotalPages(total)
|
||||
totalPages := TotalPages(total, pageSize)
|
||||
clampedPage := ClampPage(page, totalPages)
|
||||
if clampedPage != page {
|
||||
page = clampedPage
|
||||
rows, _, err = repository.ListUsers(db, keyword, status, PageSize, (page-1)*PageSize)
|
||||
rows, _, err = repository.ListUsers(db, keyword, status, pageSize, (page-1)*pageSize)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user