package pickup

import (
	"context"
	crand "crypto/rand"
	"encoding/json"
	"errors"
	"fmt"
	"math"
	"strconv"
	"strings"
	"time"

	"hfb_sys/backend/internal/auditlog"
	"hfb_sys/backend/internal/model"
	"hfb_sys/backend/internal/modules/notification"
	"hfb_sys/backend/internal/modules/wallet"
	"hfb_sys/backend/internal/timeutil"
	"hfb_sys/backend/pkg/money"

	"gorm.io/datatypes"
	"gorm.io/gorm"
	"gorm.io/gorm/clause"
)

type Repository struct {
	db *gorm.DB
}

func NewRepository(db *gorm.DB) *Repository {
	return &Repository{db: db}
}

// Create 创建提号订单（状态：提号中）。
// 锁定 listing.in_transaction，与正常订单互斥；不结算、不动钱包。
func (r *Repository) Create(ctx context.Context, req CreateRequest, adminID uint64, meta auditlog.Meta) (*PickupDTO, error) {
	if r == nil || r.db == nil {
		return nil, ErrDependencyUnavailable
	}
	var createdID uint64
	err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
		var listing model.RentalListing
		if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&listing, req.ListingID).Error; err != nil {
			if errors.Is(err, gorm.ErrRecordNotFound) {
				return ErrListingUnavailable
			}
			return err
		}
		if listing.Status != "published" || listing.ReviewStatus != "approved" || listing.InTransaction {
			return ErrListingUnavailable
		}

		// 同一 listing 不能有进行中的提号
		var cnt int64
		if err := tx.Model(&model.AdminPickup{}).
			Where("listing_id = ? AND status = ?", listing.ID, StatusPickingUp).
			Count(&cnt).Error; err != nil {
			return err
		}
		if cnt > 0 {
			return ErrDuplicatePickup
		}

		var account model.GameAccount
		if err := tx.First(&account, listing.AccountID).Error; err != nil {
			return err
		}
		priceSnapshot := buildPickupPriceSnapshot(listing, account)
		accountSnapshot, err := makePickupAccountSnapshot(account, listing)
		if err != nil {
			return err
		}

		pickupNo, err := newPickupNo()
		if err != nil {
			return err
		}
		pickup := model.AdminPickup{
			PickupNo:          pickupNo,
			ListingID:         listing.ID,
			AccountID:         listing.AccountID,
			OwnerID:           listing.OwnerID,
			AdminID:           adminID,
			Platform:          strings.TrimSpace(req.Platform),
			ListingPriceCent:  priceSnapshot.ListingPriceCent,
			OwnerPriceCent:    priceSnapshot.OwnerPriceCent,
			WebsiteProfitCent: priceSnapshot.WebsiteProfitCent,
			ProfitAmountCent:  req.ProfitAmountCent,
			SellerRatio:       priceSnapshot.SellerRatio,
			BuyerRatio:        priceSnapshot.BuyerRatio,
			AccountSnapshot:   accountSnapshot,
			Status:            StatusPickingUp,
			Remark:            strings.TrimSpace(req.Remark),
		}
		if err := tx.Create(&pickup).Error; err != nil {
			return err
		}
		createdID = pickup.ID

		listing.InTransaction = true
		if err := tx.Save(&listing).Error; err != nil {
			return err
		}

		bid := pickup.ID
		if err := notification.Append(tx, notification.Entry{
			UserID:  listing.OwnerID,
			Type:    "pickup",
			Title:   "账号被管理员提号",
			Content: fmt.Sprintf("您的账号「%s」已被管理员提号（线下交易），等待完成结算。", account.Title),
			BizType: "pickup",
			BizID:   &bid,
		}); err != nil {
			return err
		}

		if err := auditlog.Append(tx, auditlog.Entry{
			ActorType: "admin",
			ActorID:   adminID,
			Action:    "pickup_create",
			BizType:   "admin_pickup",
			BizID:     &bid,
			Meta:      meta,
			Detail: map[string]any{
				"pickup_no":   pickupNo,
				"listing_id":  listing.ID,
				"platform":    pickup.Platform,
				"profit_cent": pickup.ProfitAmountCent,
			},
		}); err != nil {
			return err
		}
		return nil
	})
	if err != nil {
		return nil, err
	}
	return r.FindByID(ctx, createdID)
}

// Complete 完成提号：给卖家加可用余额，listing 置 completed（售出终态，不可再上架）。
func (r *Repository) Complete(ctx context.Context, pickupID uint64, req CompleteRequest, adminID uint64, meta auditlog.Meta) (*PickupDTO, error) {
	if r == nil || r.db == nil {
		return nil, ErrDependencyUnavailable
	}
	if req.SettleAmountCent <= 0 {
		return nil, ErrInvalidAmount
	}
	err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
		var pickup model.AdminPickup
		if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&pickup, pickupID).Error; err != nil {
			if errors.Is(err, gorm.ErrRecordNotFound) {
				return ErrPickupNotFound
			}
			return err
		}
		if pickup.Status != StatusPickingUp {
			return ErrPickupNotPickingUp
		}

		var listing model.RentalListing
		if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&listing, pickup.ListingID).Error; err != nil {
			return err
		}
		var account model.GameAccount
		_ = tx.First(&account, pickup.AccountID).Error

		now := time.Now()
		pickup.SettleAmountCent = req.SettleAmountCent
		if req.ProfitAmountCent != nil {
			pickup.ProfitAmountCent = *req.ProfitAmountCent
		}
		pickup.Status = StatusCompleted
		pickup.CompleteRemark = strings.TrimSpace(req.CompleteRemark)
		pickup.CompletedAt = &now
		if err := tx.Save(&pickup).Error; err != nil {
			return err
		}

		// listing 置 completed：listingLockedForOwnerMutation 已拦截此状态再上架。
		listing.InTransaction = true
		listing.Status = "completed"
		if err := tx.Save(&listing).Error; err != nil {
			return err
		}

		if err := wallet.AppendEntries(tx, wallet.Entry{
			UserID:      pickup.OwnerID,
			Direction:   "in",
			AmountCent:  req.SettleAmountCent,
			BalanceType: "available",
			BizType:     "admin_pickup_settle",
			BizNo:       pickup.PickupNo,
			Remark:      fmt.Sprintf("管理员提号结算：%s", pickup.PickupNo),
		}); err != nil {
			return err
		}

		bid := pickup.ID
		if err := notification.Append(tx, notification.Entry{
			UserID:  pickup.OwnerID,
			Type:    "pickup",
			Title:   "提号已完成，已结算到账",
			Content: fmt.Sprintf("账号「%s」提号完成，已结算 %s 到您的钱包。", account.Title, money.FormatWithSymbol(req.SettleAmountCent)),
			BizType: "pickup",
			BizID:   &bid,
		}); err != nil {
			return err
		}

		if err := auditlog.Append(tx, auditlog.Entry{
			ActorType: "admin",
			ActorID:   adminID,
			Action:    "pickup_complete",
			BizType:   "admin_pickup",
			BizID:     &bid,
			Meta:      meta,
			Detail: map[string]any{
				"pickup_no":          pickup.PickupNo,
				"settle_amount_cent": req.SettleAmountCent,
				"profit_amount_cent": pickup.ProfitAmountCent,
			},
		}); err != nil {
			return err
		}
		return nil
	})
	if err != nil {
		return nil, err
	}
	return r.FindByID(ctx, pickupID)
}

// Cancel 取消提号：解锁 listing.in_transaction，账号恢复可租。
func (r *Repository) Cancel(ctx context.Context, pickupID uint64, reason string, adminID uint64, meta auditlog.Meta) error {
	if r == nil || r.db == nil {
		return ErrDependencyUnavailable
	}
	return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
		var pickup model.AdminPickup
		if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&pickup, pickupID).Error; err != nil {
			if errors.Is(err, gorm.ErrRecordNotFound) {
				return ErrPickupNotFound
			}
			return err
		}
		if pickup.Status != StatusPickingUp {
			return ErrPickupNotPickingUp
		}
		var listing model.RentalListing
		if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&listing, pickup.ListingID).Error; err != nil {
			return err
		}
		now := time.Now()
		pickup.Status = StatusCancelled
		pickup.CancelledAt = &now
		if err := tx.Save(&pickup).Error; err != nil {
			return err
		}
		listing.InTransaction = false
		if err := tx.Save(&listing).Error; err != nil {
			return err
		}
		bid := pickup.ID
		_ = notification.Append(tx, notification.Entry{
			UserID:  pickup.OwnerID,
			Type:    "pickup",
			Title:   "提号已取消",
			Content: fmt.Sprintf("提号订单 %s 已取消，原因：%s", pickup.PickupNo, reason),
			BizType: "pickup",
			BizID:   &bid,
		})
		_ = auditlog.Append(tx, auditlog.Entry{
			ActorType: "admin",
			ActorID:   adminID,
			Action:    "pickup_cancel",
			BizType:   "admin_pickup",
			BizID:     &bid,
			Meta:      meta,
			Detail:    map[string]any{"pickup_no": pickup.PickupNo, "reason": reason},
		})
		return nil
	})
}

func (r *Repository) FindByID(ctx context.Context, id uint64) (*PickupDTO, error) {
	if r == nil || r.db == nil {
		return nil, ErrDependencyUnavailable
	}
	var row pickupRow
	if err := r.baseQuery(ctx).Where("p.id = ?", id).First(&row).Error; err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return nil, ErrPickupNotFound
		}
		return nil, err
	}
	dto := row.toDTO()
	if isEmptyPickupAccountSnapshot(dto.AccountSnapshot) {
		if snapshot, err := r.fallbackAccountSnapshot(ctx, row.ListingID, row.AccountID); err == nil {
			dto.AccountSnapshot = snapshot
		}
	}
	return &dto, nil
}

func (r *Repository) ListAdmin(ctx context.Context, query AdminPickupQuery) (*PaginatedResult, error) {
	if r == nil || r.db == nil {
		return nil, ErrDependencyUnavailable
	}
	db := r.baseQuery(ctx)
	if status := strings.TrimSpace(query.Status); status != "" {
		db = db.Where("p.status = ?", status)
	}
	if kw := strings.TrimSpace(query.Keyword); kw != "" {
		like := "%" + kw + "%"
		db = db.Where("p.pickup_no LIKE ? OR l.listing_no LIKE ? OR a.title LIKE ?", like, like, like)
	}
	return r.paginatePickups(db, query.Page, query.PageSize, false)
}

func (r *Repository) ListForSeller(ctx context.Context, ownerID uint64, query SellerPickupQuery) (*PaginatedResult, error) {
	if r == nil || r.db == nil {
		return nil, ErrDependencyUnavailable
	}
	db := r.baseQuery(ctx).Where("p.owner_id = ?", ownerID)
	if status := strings.TrimSpace(query.Status); status != "" {
		db = db.Where("p.status = ?", status)
	}
	return r.paginatePickups(db, query.Page, query.PageSize, true)
}

func (r *Repository) paginatePickups(db *gorm.DB, page, pageSize int, sellerView bool) (*PaginatedResult, error) {
	page, pageSize = normalizePaging(page, pageSize)
	var total int64
	if err := db.Count(&total).Error; err != nil {
		return nil, err
	}
	var rows []pickupRow
	if err := db.Order("p.id DESC").Offset((page - 1) * pageSize).Limit(pageSize).Scan(&rows).Error; err != nil {
		return nil, err
	}
	items := make([]PickupDTO, 0, len(rows))
	for _, row := range rows {
		item := row.toDTO()
		item.AccountSnapshot = nil
		if sellerView {
			item.WebsiteProfitCent = 0
			item.ProfitAmountCent = 0
			item.BuyerRatio = 0
		}
		items = append(items, item)
	}
	return &PaginatedResult{Items: items, Total: total, Page: page, PageSize: pageSize}, nil
}

// ListAvailableListings 可提号的 listing：已发布、已审核、未在交易中。
func (r *Repository) ListAvailableListings(ctx context.Context, keyword string, page, pageSize int) (*AvailableListingsResult, error) {
	if r == nil || r.db == nil {
		return nil, ErrDependencyUnavailable
	}
	db := r.db.WithContext(ctx).Table("rental_listings AS l").
		Select("l.id, l.listing_no, l.account_id, l.owner_id, l.price_cent AS listing_price_cent, a.title AS account_title, a.server_region, a.login_platform, a.haf_coin_amount, a.asset_summary, owner.phone AS owner_phone").
		Joins("JOIN game_accounts AS a ON a.id = l.account_id").
		Joins("JOIN users AS owner ON owner.id = l.owner_id").
		Where("l.status = ? AND l.review_status = ? AND l.in_transaction = ?", "published", "approved", false)
	if kw := strings.TrimSpace(keyword); kw != "" {
		like := "%" + kw + "%"
		db = db.Where("l.listing_no LIKE ? OR a.title LIKE ?", like, like)
	}

	var total int64
	if err := db.Count(&total).Error; err != nil {
		return nil, err
	}
	page, pageSize = normalizePaging(page, pageSize)
	var rows []availableListingRow
	if err := db.Order("l.id DESC").Offset((page - 1) * pageSize).Limit(pageSize).Scan(&rows).Error; err != nil {
		return nil, err
	}
	items := make([]AvailableListingDTO, 0, len(rows))
	for _, row := range rows {
		items = append(items, row.toDTO())
	}
	return &AvailableListingsResult{Items: items, Total: total, Page: page, PageSize: pageSize}, nil
}

func (r *Repository) baseQuery(ctx context.Context) *gorm.DB {
	return r.db.WithContext(ctx).Table("admin_pickups AS p").
		Select("p.*, l.listing_no, a.title AS account_title, a.server_region, a.login_platform, owner.phone AS owner_phone").
		Joins("JOIN rental_listings AS l ON l.id = p.listing_id").
		Joins("JOIN game_accounts AS a ON a.id = p.account_id").
		Joins("JOIN users AS owner ON owner.id = p.owner_id")
}

type pickupRow struct {
	model.AdminPickup
	ListingNo     string
	AccountTitle  string
	ServerRegion  string
	LoginPlatform string
	OwnerPhone    string
}

func (row pickupRow) toDTO() PickupDTO {
	return PickupDTO{
		ID:                row.ID,
		PickupNo:          row.PickupNo,
		ListingID:         row.ListingID,
		ListingNo:         row.ListingNo,
		AccountID:         row.AccountID,
		AccountTitle:      row.AccountTitle,
		ServerRegion:      row.ServerRegion,
		LoginPlatform:     row.LoginPlatform,
		OwnerID:           row.OwnerID,
		OwnerPhone:        row.OwnerPhone,
		AdminID:           row.AdminID,
		Platform:          row.Platform,
		ListingPriceCent:  row.ListingPriceCent,
		OwnerPriceCent:    row.OwnerPriceCent,
		WebsiteProfitCent: row.WebsiteProfitCent,
		ProfitAmountCent:  row.ProfitAmountCent,
		SellerRatio:       row.SellerRatio,
		BuyerRatio:        row.BuyerRatio,
		AccountSnapshot:   row.AccountSnapshot,
		SettleAmountCent:  row.SettleAmountCent,
		Status:            row.Status,
		Remark:            row.Remark,
		CompleteRemark:    row.CompleteRemark,
		CreatedAt:         row.CreatedAt,
		CompletedAt:       row.CompletedAt,
		CancelledAt:       row.CancelledAt,
	}
}

type availableListingRow struct {
	ID               uint64
	ListingNo        string
	AccountID        uint64
	AccountTitle     string
	ServerRegion     string
	LoginPlatform    string
	OwnerID          uint64
	OwnerPhone       string
	ListingPriceCent int64
	HafCoinAmount    int64
	AssetSummary     datatypes.JSON
}

func (row availableListingRow) toDTO() AvailableListingDTO {
	snapshot := buildPickupPriceSnapshot(
		model.RentalListing{PriceCent: row.ListingPriceCent},
		model.GameAccount{HafCoinAmount: row.HafCoinAmount, AssetSummary: row.AssetSummary},
	)
	return AvailableListingDTO{
		ID:                      row.ID,
		ListingNo:               row.ListingNo,
		AccountID:               row.AccountID,
		AccountTitle:            row.AccountTitle,
		ServerRegion:            row.ServerRegion,
		LoginPlatform:           row.LoginPlatform,
		OwnerID:                 row.OwnerID,
		OwnerPhone:              row.OwnerPhone,
		ListingPriceCent:        snapshot.ListingPriceCent,
		OwnerPriceCent:          snapshot.OwnerPriceCent,
		OwnerPurePriceCent:      snapshot.OwnerPurePriceCent,
		OwnerExtraItemPriceCent: snapshot.OwnerExtraItemPriceCent,
		OwnerTotalPriceCent:     snapshot.OwnerTotalPriceCent,
		WebsiteProfitCent:       snapshot.WebsiteProfitCent,
		SellerRatio:             snapshot.SellerRatio,
		BuyerRatio:              snapshot.BuyerRatio,
	}
}

func makePickupAccountSnapshot(account model.GameAccount, listing model.RentalListing) (datatypes.JSON, error) {
	payload := map[string]any{
		"listing_id":       listing.ID,
		"listing_no":       listing.ListingNo,
		"account_id":       account.ID,
		"title":            account.Title,
		"game_name":        account.GameName,
		"server_region":    account.ServerRegion,
		"login_platform":   account.LoginPlatform,
		"rank_level":       account.RankLevel,
		"haf_coin_amount":  account.HafCoinAmount,
		"asset_summary":    account.AssetSummary,
		"season_tags":      account.SeasonTags,
		"screenshot_urls":  account.ScreenshotURLS,
		"snapshot_version": 1,
	}
	raw, err := json.Marshal(payload)
	return datatypes.JSON(raw), err
}

func (r *Repository) fallbackAccountSnapshot(ctx context.Context, listingID uint64, accountID uint64) (datatypes.JSON, error) {
	var listing model.RentalListing
	if err := r.db.WithContext(ctx).First(&listing, listingID).Error; err != nil {
		return nil, err
	}
	var account model.GameAccount
	if err := r.db.WithContext(ctx).First(&account, accountID).Error; err != nil {
		return nil, err
	}
	return makePickupAccountSnapshot(account, listing)
}

func isEmptyPickupAccountSnapshot(raw datatypes.JSON) bool {
	trimmed := strings.TrimSpace(string(raw))
	return trimmed == "" || trimmed == "null" || trimmed == "{}"
}

type pickupPriceSnapshot struct {
	ListingPriceCent        int64
	OwnerPriceCent          int64
	OwnerPurePriceCent      int64
	OwnerExtraItemPriceCent int64
	OwnerTotalPriceCent     int64
	WebsiteProfitCent       int64
	SellerRatio             float64
	BuyerRatio              float64
}

func buildPickupPriceSnapshot(listing model.RentalListing, account model.GameAccount) pickupPriceSnapshot {
	summary := decodePickupAssetSummary(account.AssetSummary)
	breakdown := pickupPriceBreakdown(summary)
	listingPriceCent := maxPickupCent(listing.PriceCent, 0)
	ownerPurePriceCent := yuanToPickupCent(readPickupNumber(breakdown["seller_coin_base_price"]))
	ownerExtraItemPriceCent := yuanToPickupCent(readPickupNumber(breakdown["consumable_price"]))
	ownerTotalPriceCent := yuanToPickupCent(readPickupNumber(breakdown["seller_total_price"]))
	if ownerTotalPriceCent <= 0 && (ownerPurePriceCent > 0 || ownerExtraItemPriceCent > 0) {
		ownerTotalPriceCent = ownerPurePriceCent + ownerExtraItemPriceCent
	}
	if ownerTotalPriceCent <= 0 || (listingPriceCent > 0 && ownerTotalPriceCent > listingPriceCent) {
		ownerTotalPriceCent = listingPriceCent
	}
	ownerPurePriceCent, ownerExtraItemPriceCent = normalizePickupOwnerPriceSplit(
		ownerTotalPriceCent,
		ownerPurePriceCent,
		ownerExtraItemPriceCent,
	)
	websiteProfitCent := yuanToPickupCent(readPickupNumber(breakdown["platform_markup_amount"]))
	if websiteProfitCent <= 0 {
		websiteProfitCent = listingPriceCent - ownerTotalPriceCent
	}
	if websiteProfitCent < 0 {
		websiteProfitCent = 0
	}
	sellerRatio := readPickupNumber(breakdown["seller_ratio"])
	if sellerRatio <= 0 {
		sellerRatio = ratioFromCoin(account.HafCoinAmount, readPickupNumber(breakdown["seller_coin_base_price"]))
	}
	buyerRatio := readPickupNumber(breakdown["buyer_ratio"])
	if buyerRatio <= 0 {
		buyerRatio = readPickupNumber(summary["publish_ratio"])
	}
	if buyerRatio <= 0 {
		buyerRatio = ratioFromCoin(account.HafCoinAmount, readPickupNumber(breakdown["buyer_coin_base_price"]))
	}
	return pickupPriceSnapshot{
		ListingPriceCent:        listingPriceCent,
		OwnerPriceCent:          ownerTotalPriceCent,
		OwnerPurePriceCent:      ownerPurePriceCent,
		OwnerExtraItemPriceCent: ownerExtraItemPriceCent,
		OwnerTotalPriceCent:     ownerTotalPriceCent,
		WebsiteProfitCent:       websiteProfitCent,
		SellerRatio:             roundPickupRatio(sellerRatio),
		BuyerRatio:              roundPickupRatio(buyerRatio),
	}
}

func normalizePickupOwnerPriceSplit(totalCent, pureCent, extraCent int64) (int64, int64) {
	if totalCent <= 0 {
		return 0, 0
	}
	pureCent = maxPickupCent(pureCent, 0)
	extraCent = maxPickupCent(extraCent, 0)
	switch {
	case pureCent == 0 && extraCent == 0:
		pureCent = totalCent
	case pureCent == 0:
		pureCent = maxPickupCent(totalCent-extraCent, 0)
	case extraCent == 0:
		extraCent = maxPickupCent(totalCent-pureCent, 0)
	case pureCent+extraCent != totalCent:
		if pureCent > totalCent {
			pureCent = totalCent
			extraCent = 0
		} else {
			extraCent = totalCent - pureCent
		}
	}
	return pureCent, extraCent
}

func decodePickupAssetSummary(raw datatypes.JSON) map[string]any {
	if len(raw) == 0 {
		return map[string]any{}
	}
	var summary map[string]any
	if err := json.Unmarshal(raw, &summary); err != nil {
		return map[string]any{}
	}
	return summary
}

func pickupPriceBreakdown(summary map[string]any) map[string]any {
	if summary == nil {
		return map[string]any{}
	}
	if breakdown, ok := summary["price_breakdown"].(map[string]any); ok {
		return breakdown
	}
	if raw, ok := summary["price_breakdown"].(string); ok && strings.TrimSpace(raw) != "" {
		var breakdown map[string]any
		if err := json.Unmarshal([]byte(raw), &breakdown); err == nil {
			return breakdown
		}
	}
	return map[string]any{}
}

func readPickupNumber(value any) float64 {
	switch typed := value.(type) {
	case float64:
		return typed
	case float32:
		return float64(typed)
	case int:
		return float64(typed)
	case int64:
		return float64(typed)
	case json.Number:
		number, err := typed.Float64()
		if err != nil {
			return 0
		}
		return number
	case string:
		number, err := strconv.ParseFloat(strings.TrimSpace(typed), 64)
		if err != nil {
			return 0
		}
		return number
	default:
		return 0
	}
}

func yuanToPickupCent(value float64) int64 {
	if value <= 0 || math.IsNaN(value) || math.IsInf(value, 0) {
		return 0
	}
	return int64(math.Round(value * 100))
}

func ratioFromCoin(coinAmount int64, basePrice float64) float64 {
	if coinAmount <= 0 || basePrice <= 0 {
		return 0
	}
	return float64(coinAmount) / 10000 / basePrice
}

func roundPickupRatio(value float64) float64 {
	if value <= 0 || math.IsNaN(value) || math.IsInf(value, 0) {
		return 0
	}
	return math.Round(value*100) / 100
}

func maxPickupCent(a, b int64) int64 {
	if a > b {
		return a
	}
	return b
}

func normalizePaging(page, pageSize int) (int, int) {
	if page < 1 {
		page = 1
	}
	if pageSize < 1 {
		pageSize = 20
	}
	if pageSize > 100 {
		pageSize = 100
	}
	return page, pageSize
}

// newPickupNo 生成提号编号：PK + YYYYMMDDHHMMSS + 3位随机。
func newPickupNo() (string, error) {
	now := timeutil.ShanghaiNow()
	timeStr := now.Format("20060102150405")
	buf := make([]byte, 2)
	if _, err := crand.Read(buf); err != nil {
		return "", err
	}
	randomNum := (int(buf[0])<<8 | int(buf[1])) % 1000
	return "PK" + timeStr + strconv.Itoa(1000 + randomNum)[1:], nil
}
