feat: 支持主列表选择每页条数 (#242)

This commit is contained in:
chengma
2026-08-15 15:15:38 +08:00
parent 8c09023434
commit b4558537d5
37 changed files with 479 additions and 160 deletions
+8 -2
View File
@@ -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
}
+2 -2
View File
@@ -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 {
+10
View File
@@ -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 {
+2 -2
View File
@@ -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))
}
+16 -3
View File
@@ -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
View File
@@ -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)
+48 -19
View File
@@ -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)
}
}
+9 -3
View File
@@ -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
}
+26
View File
@@ -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) {
+9 -3
View File
@@ -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
}
+10 -4
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
}