服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
152 lines
3.6 KiB
Go
152 lines
3.6 KiB
Go
package datatable
|
|
|
|
import (
|
|
"context"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
const DefaultPageLimit = 100
|
|
|
|
type PageOptions struct {
|
|
Offset int `json:"offset,omitempty"`
|
|
Limit int `json:"limit,omitempty"`
|
|
Cursor string `json:"cursor,omitempty"`
|
|
}
|
|
|
|
type Page[T any] struct {
|
|
Table Table `json:"table"`
|
|
Manifest Manifest `json:"manifest"`
|
|
Rows []T `json:"rows,omitempty"`
|
|
Offset int `json:"offset"`
|
|
Limit int `json:"limit"`
|
|
Total int `json:"total"`
|
|
NextCursor string `json:"next_cursor,omitempty"`
|
|
HasMore bool `json:"has_more"`
|
|
}
|
|
|
|
func LoadRowsPage[T any](ctx context.Context, source RowSource, name string, options PageOptions) (Page[T], error) {
|
|
if err := contextErr(ctx); err != nil {
|
|
return Page[T]{}, err
|
|
}
|
|
rows, err := LoadRows[T](source, name)
|
|
if err != nil {
|
|
return Page[T]{}, err
|
|
}
|
|
return PageRows(rows, options)
|
|
}
|
|
|
|
func PageRows[T any](set RowSet[T], options PageOptions) (Page[T], error) {
|
|
offset, limit, err := normalizePageOptions(options, len(set.Rows))
|
|
if err != nil {
|
|
return Page[T]{}, err
|
|
}
|
|
end := offset + limit
|
|
if end > len(set.Rows) {
|
|
end = len(set.Rows)
|
|
}
|
|
pageRows := append([]T(nil), set.Rows[offset:end]...)
|
|
next := ""
|
|
hasMore := end < len(set.Rows)
|
|
if hasMore {
|
|
next = EncodePageCursor(end)
|
|
}
|
|
return Page[T]{
|
|
Table: set.Table,
|
|
Manifest: set.Manifest,
|
|
Rows: pageRows,
|
|
Offset: offset,
|
|
Limit: limit,
|
|
Total: len(set.Rows),
|
|
NextCursor: next,
|
|
HasMore: hasMore,
|
|
}, nil
|
|
}
|
|
|
|
func (s RowSet[T]) Page(options PageOptions) (Page[T], error) {
|
|
return PageRows(s, options)
|
|
}
|
|
|
|
func (v *TableView[T]) Page(options PageOptions) (Page[T], error) {
|
|
if v == nil {
|
|
return Page[T]{}, ErrTableRequired
|
|
}
|
|
return v.Snapshot().Page(options)
|
|
}
|
|
|
|
func EncodePageCursor(offset int) string {
|
|
if offset <= 0 {
|
|
return ""
|
|
}
|
|
payload, _ := json.Marshal(map[string]int{"offset": offset})
|
|
return base64.RawURLEncoding.EncodeToString(payload)
|
|
}
|
|
|
|
func DecodePageCursor(cursor string) (int, error) {
|
|
cursor = strings.TrimSpace(cursor)
|
|
if cursor == "" {
|
|
return 0, nil
|
|
}
|
|
data, err := base64.RawURLEncoding.DecodeString(cursor)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("decode page cursor: %w", err)
|
|
}
|
|
var payload struct {
|
|
Offset int `json:"offset"`
|
|
}
|
|
if err := json.Unmarshal(data, &payload); err != nil {
|
|
return 0, fmt.Errorf("parse page cursor: %w", err)
|
|
}
|
|
if payload.Offset < 0 {
|
|
return 0, fmt.Errorf("page cursor offset must be non-negative")
|
|
}
|
|
return payload.Offset, nil
|
|
}
|
|
|
|
func normalizePageOptions(options PageOptions, total int) (int, int, error) {
|
|
offset := options.Offset
|
|
if strings.TrimSpace(options.Cursor) != "" {
|
|
decoded, err := DecodePageCursor(options.Cursor)
|
|
if err != nil {
|
|
return 0, 0, err
|
|
}
|
|
offset = decoded
|
|
}
|
|
if offset < 0 {
|
|
return 0, 0, fmt.Errorf("page offset must be non-negative")
|
|
}
|
|
if offset > total {
|
|
offset = total
|
|
}
|
|
limit := options.Limit
|
|
if limit < 0 {
|
|
return 0, 0, fmt.Errorf("page limit must be positive")
|
|
}
|
|
if limit == 0 {
|
|
limit = DefaultPageLimit
|
|
}
|
|
return offset, limit, nil
|
|
}
|
|
|
|
func ParsePageOptions(offsetText, limitText, cursor string) (PageOptions, error) {
|
|
options := PageOptions{Cursor: strings.TrimSpace(cursor)}
|
|
if strings.TrimSpace(offsetText) != "" {
|
|
offset, err := strconv.Atoi(strings.TrimSpace(offsetText))
|
|
if err != nil {
|
|
return PageOptions{}, fmt.Errorf("parse page offset: %w", err)
|
|
}
|
|
options.Offset = offset
|
|
}
|
|
if strings.TrimSpace(limitText) != "" {
|
|
limit, err := strconv.Atoi(strings.TrimSpace(limitText))
|
|
if err != nil {
|
|
return PageOptions{}, fmt.Errorf("parse page limit: %w", err)
|
|
}
|
|
options.Limit = limit
|
|
}
|
|
return options, nil
|
|
}
|