feat: 配置列表分页与钉钉机器人分钟级排队限流
统一 sources/templates/channels/rules 列表为分页响应,避免配置增多时全量返回;按钉钉 access_token 限制每分钟发送并在超限时等待下一分钟,降低触发官方封禁风险。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -47,10 +47,16 @@ func (s *Store) GetChannelByName(ctx context.Context, name string) (*model.Chann
|
||||
return &ch, nil
|
||||
}
|
||||
|
||||
func (s *Store) ListChannels(ctx context.Context) ([]model.Channel, error) {
|
||||
rows, err := s.DB.QueryContext(ctx, `SELECT id, name, type, config, status, created_at, updated_at FROM notification_channel ORDER BY id`)
|
||||
func (s *Store) ListChannels(ctx context.Context, page PageFilter) ([]model.Channel, int, error) {
|
||||
var count int
|
||||
if err := s.DB.GetContext(ctx, &count, `SELECT COUNT(*) FROM notification_channel`); err != nil {
|
||||
return nil, 0, fmt.Errorf("count channels: %w", err)
|
||||
}
|
||||
|
||||
page.Normalize()
|
||||
rows, err := s.DB.QueryContext(ctx, `SELECT id, name, type, config, status, created_at, updated_at FROM notification_channel ORDER BY id LIMIT ? OFFSET ?`, page.PageSize, page.Offset())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list channels: %w", err)
|
||||
return nil, 0, fmt.Errorf("list channels: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
@@ -59,13 +65,13 @@ func (s *Store) ListChannels(ctx context.Context) ([]model.Channel, error) {
|
||||
var ch model.Channel
|
||||
var configBytes []byte
|
||||
if err := rows.Scan(&ch.ID, &ch.Name, &ch.Type, &configBytes, &ch.Status, &ch.CreatedAt, &ch.UpdatedAt); err != nil {
|
||||
return nil, fmt.Errorf("scan channel: %w", err)
|
||||
return nil, 0, fmt.Errorf("scan channel: %w", err)
|
||||
}
|
||||
raw := json.RawMessage(configBytes)
|
||||
ch.Config = &raw
|
||||
channels = append(channels, ch)
|
||||
}
|
||||
return channels, rows.Err()
|
||||
return channels, count, rows.Err()
|
||||
}
|
||||
|
||||
func (s *Store) UpdateChannel(ctx context.Context, id int, ch *model.Channel) error {
|
||||
|
||||
@@ -8,11 +8,10 @@ import (
|
||||
)
|
||||
|
||||
type MessageLogFilter struct {
|
||||
Source string `form:"source"`
|
||||
Event string `form:"event"`
|
||||
Status string `form:"status"`
|
||||
Page int `form:"page"`
|
||||
PageSize int `form:"page_size"`
|
||||
Source string `form:"source"`
|
||||
Event string `form:"event"`
|
||||
Status string `form:"status"`
|
||||
PageFilter
|
||||
}
|
||||
|
||||
func (s *Store) CreateMessageLog(ctx context.Context, ml *model.MessageLog) error {
|
||||
@@ -54,17 +53,11 @@ func (s *Store) ListMessageLogs(ctx context.Context, filter MessageLogFilter) ([
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
if filter.Page <= 0 {
|
||||
filter.Page = 1
|
||||
}
|
||||
if filter.PageSize <= 0 {
|
||||
filter.PageSize = 20
|
||||
}
|
||||
offset := (filter.Page - 1) * filter.PageSize
|
||||
filter.Normalize()
|
||||
|
||||
logs := make([]model.MessageLog, 0)
|
||||
query := "SELECT * FROM notification_message_log " + where + " ORDER BY id DESC LIMIT ? OFFSET ?"
|
||||
args = append(args, filter.PageSize, offset)
|
||||
args = append(args, filter.PageSize, filter.Offset())
|
||||
if err := s.DB.SelectContext(ctx, &logs, query, args...); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
package store
|
||||
|
||||
// PageFilter is the shared pagination query for list endpoints.
|
||||
type PageFilter struct {
|
||||
Page int `form:"page"`
|
||||
PageSize int `form:"page_size"`
|
||||
}
|
||||
|
||||
// Normalize applies defaults: page=1, page_size=20.
|
||||
func (p *PageFilter) Normalize() {
|
||||
if p.Page <= 0 {
|
||||
p.Page = 1
|
||||
}
|
||||
if p.PageSize <= 0 {
|
||||
p.PageSize = 20
|
||||
}
|
||||
}
|
||||
|
||||
// Offset returns the SQL OFFSET for the current page.
|
||||
func (p PageFilter) Offset() int {
|
||||
return (p.Page - 1) * p.PageSize
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package store
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestPageFilterNormalize(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
in PageFilter
|
||||
wantPage int
|
||||
wantPageSize int
|
||||
wantOffset int
|
||||
}{
|
||||
{name: "defaults", in: PageFilter{}, wantPage: 1, wantPageSize: 20, wantOffset: 0},
|
||||
{name: "negative", in: PageFilter{Page: -1, PageSize: -5}, wantPage: 1, wantPageSize: 20, wantOffset: 0},
|
||||
{name: "page2", in: PageFilter{Page: 2, PageSize: 10}, wantPage: 2, wantPageSize: 10, wantOffset: 10},
|
||||
{name: "zero page size", in: PageFilter{Page: 3, PageSize: 0}, wantPage: 3, wantPageSize: 20, wantOffset: 40},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
p := tt.in
|
||||
p.Normalize()
|
||||
if p.Page != tt.wantPage || p.PageSize != tt.wantPageSize {
|
||||
t.Fatalf("Normalize() = %+v, want page=%d page_size=%d", p, tt.wantPage, tt.wantPageSize)
|
||||
}
|
||||
if got := p.Offset(); got != tt.wantOffset {
|
||||
t.Fatalf("Offset() = %d, want %d", got, tt.wantOffset)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
+14
-4
@@ -66,13 +66,23 @@ func (s *Store) GetRuleBySourceEvent(ctx context.Context, sourceID int, event st
|
||||
return &r, nil
|
||||
}
|
||||
|
||||
func (s *Store) ListRules(ctx context.Context) ([]model.Rule, error) {
|
||||
rows, err := s.DB.QueryContext(ctx, `SELECT id, source_id, event, template_id, conditions, enabled, created_at, updated_at FROM notification_rule ORDER BY id`)
|
||||
func (s *Store) ListRules(ctx context.Context, page PageFilter) ([]model.Rule, int, error) {
|
||||
var count int
|
||||
if err := s.DB.GetContext(ctx, &count, `SELECT COUNT(*) FROM notification_rule`); err != nil {
|
||||
return nil, 0, fmt.Errorf("count rules: %w", err)
|
||||
}
|
||||
|
||||
page.Normalize()
|
||||
rows, err := s.DB.QueryContext(ctx, `SELECT id, source_id, event, template_id, conditions, enabled, created_at, updated_at FROM notification_rule ORDER BY id LIMIT ? OFFSET ?`, page.PageSize, page.Offset())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list rules: %w", err)
|
||||
return nil, 0, fmt.Errorf("list rules: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
return scanRules(rows)
|
||||
rules, err := scanRules(rows)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
return rules, count, nil
|
||||
}
|
||||
|
||||
func (s *Store) UpdateRule(ctx context.Context, id int, r *model.Rule, channelIDs []int) error {
|
||||
|
||||
@@ -54,13 +54,19 @@ func (s *Store) GetSourceByName(ctx context.Context, name string) (*model.Source
|
||||
return &src, nil
|
||||
}
|
||||
|
||||
func (s *Store) ListSources(ctx context.Context) ([]model.Source, error) {
|
||||
sources := make([]model.Source, 0)
|
||||
err := s.DB.SelectContext(ctx, &sources, `SELECT * FROM notification_source ORDER BY id`)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list sources: %w", err)
|
||||
func (s *Store) ListSources(ctx context.Context, page PageFilter) ([]model.Source, int, error) {
|
||||
var count int
|
||||
if err := s.DB.GetContext(ctx, &count, `SELECT COUNT(*) FROM notification_source`); err != nil {
|
||||
return nil, 0, fmt.Errorf("count sources: %w", err)
|
||||
}
|
||||
return sources, nil
|
||||
|
||||
page.Normalize()
|
||||
sources := make([]model.Source, 0)
|
||||
err := s.DB.SelectContext(ctx, &sources, `SELECT * FROM notification_source ORDER BY id LIMIT ? OFFSET ?`, page.PageSize, page.Offset())
|
||||
if err != nil {
|
||||
return nil, 0, fmt.Errorf("list sources: %w", err)
|
||||
}
|
||||
return sources, count, nil
|
||||
}
|
||||
|
||||
func (s *Store) UpdateSource(ctx context.Context, id int, src *model.Source) error {
|
||||
|
||||
@@ -36,13 +36,19 @@ func (s *Store) GetTemplateByName(ctx context.Context, name string) (*model.Temp
|
||||
return &t, nil
|
||||
}
|
||||
|
||||
func (s *Store) ListTemplates(ctx context.Context) ([]model.Template, error) {
|
||||
templates := make([]model.Template, 0)
|
||||
err := s.DB.SelectContext(ctx, &templates, `SELECT * FROM notification_template ORDER BY id`)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list templates: %w", err)
|
||||
func (s *Store) ListTemplates(ctx context.Context, page PageFilter) ([]model.Template, int, error) {
|
||||
var count int
|
||||
if err := s.DB.GetContext(ctx, &count, `SELECT COUNT(*) FROM notification_template`); err != nil {
|
||||
return nil, 0, fmt.Errorf("count templates: %w", err)
|
||||
}
|
||||
return templates, nil
|
||||
|
||||
page.Normalize()
|
||||
templates := make([]model.Template, 0)
|
||||
err := s.DB.SelectContext(ctx, &templates, `SELECT * FROM notification_template ORDER BY id LIMIT ? OFFSET ?`, page.PageSize, page.Offset())
|
||||
if err != nil {
|
||||
return nil, 0, fmt.Errorf("list templates: %w", err)
|
||||
}
|
||||
return templates, count, nil
|
||||
}
|
||||
|
||||
func (s *Store) UpdateTemplate(ctx context.Context, id int, t *model.Template) error {
|
||||
|
||||
Reference in New Issue
Block a user