9501751792
三轮审查修复(60+ 项),相对远端 main(b536672)的关键变更:
- 安全: 数据面 SSRF 拨号防护(防 DNS rebinding)/上游凭据剥离/登录防枚举
与锁定态统一/可信代理(X-Forwarded-For)限流加固/会话版本失效机制/
撤销即时传播/弱密钥拒绝启动/脱敏字节级重写(保签名契约)
- 业务逻辑: 裸 body 上传 panic/bootstrap 审计管线卡死/定价通配符优先级/
全局工具可见性/调度器停机补跑/TOTP 挑战令牌消费顺序/熔断探针语义/
>4MB 响应 token 计量/管理员重置密码作废会话 等
- 前端: 新 logo(语枢 AI 网关主题)/Provider 凭据异常警示/删除入口/
后端错误消息透传/localStorage 敏感数据收敛
- 部署: CREDENTIAL_MASTER_KEY 持久化与弱值拒绝/Provider DELETE 接口/
nginx 安全头/worker 内存限制
- 新增迁移 000029(key_hash 索引)/000030(usage_daily 币种维度)
132 lines
3.7 KiB
Go
132 lines
3.7 KiB
Go
package scheduler
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// Cron implements the standard five-field cron format: minute, hour,
|
|
// day-of-month, month, day-of-week. Lists, ranges and steps are supported.
|
|
type Cron struct {
|
|
minute, hour, day, month, weekday field
|
|
dayWildcard, weekdayWildcard bool
|
|
}
|
|
|
|
type field struct {
|
|
min, max int
|
|
values map[int]bool
|
|
}
|
|
|
|
func ParseCron(expression string) (Cron, error) {
|
|
parts := strings.Fields(expression)
|
|
if len(parts) != 5 {
|
|
return Cron{}, errors.New("cron 表达式必须包含 5 段: 分 时 日 月 周")
|
|
}
|
|
definitions := [5][2]int{{0, 59}, {0, 23}, {1, 31}, {1, 12}, {0, 7}}
|
|
fields := make([]field, 5)
|
|
for i, part := range parts {
|
|
parsed, err := parseField(part, definitions[i][0], definitions[i][1], i == 4)
|
|
if err != nil {
|
|
return Cron{}, fmt.Errorf("cron 第 %d 段无效: %w", i+1, err)
|
|
}
|
|
fields[i] = parsed
|
|
}
|
|
return Cron{minute: fields[0], hour: fields[1], day: fields[2], month: fields[3], weekday: fields[4], dayWildcard: parts[2] == "*", weekdayWildcard: parts[4] == "*"}, nil
|
|
}
|
|
|
|
func parseField(raw string, minimum, maximum int, weekday bool) (field, error) {
|
|
result := field{min: minimum, max: maximum, values: map[int]bool{}}
|
|
for _, item := range strings.Split(raw, ",") {
|
|
item = strings.TrimSpace(item)
|
|
if item == "" {
|
|
return field{}, errors.New("存在空列表项")
|
|
}
|
|
base, stepText, hasStep := strings.Cut(item, "/")
|
|
step := 1
|
|
if hasStep {
|
|
var err error
|
|
step, err = strconv.Atoi(stepText)
|
|
if err != nil || step < 1 || step > maximum-minimum+1 {
|
|
return field{}, errors.New("步长无效")
|
|
}
|
|
}
|
|
start, end := minimum, maximum
|
|
switch {
|
|
case base == "*":
|
|
case strings.Contains(base, "-"):
|
|
left, right, _ := strings.Cut(base, "-")
|
|
var err error
|
|
start, err = cronNumber(left, minimum, maximum, weekday)
|
|
if err != nil {
|
|
return field{}, err
|
|
}
|
|
end, err = cronNumber(right, minimum, maximum, weekday)
|
|
if err != nil || start > end {
|
|
return field{}, errors.New("范围无效")
|
|
}
|
|
default:
|
|
var err error
|
|
start, err = cronNumber(base, minimum, maximum, weekday)
|
|
if err != nil {
|
|
return field{}, err
|
|
}
|
|
end = start
|
|
if hasStep {
|
|
end = maximum
|
|
}
|
|
}
|
|
for value := start; value <= end; value += step {
|
|
if weekday && value == 7 {
|
|
value = 0
|
|
result.values[value] = true
|
|
break
|
|
}
|
|
result.values[value] = true
|
|
}
|
|
}
|
|
if len(result.values) == 0 {
|
|
return field{}, errors.New("没有可用取值")
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func cronNumber(raw string, minimum, maximum int, weekday bool) (int, error) {
|
|
value, err := strconv.Atoi(raw)
|
|
if err != nil || value < minimum || value > maximum {
|
|
return 0, fmt.Errorf("%q 超出 %d-%d", raw, minimum, maximum)
|
|
}
|
|
if weekday && value == 7 {
|
|
return 7, nil
|
|
}
|
|
return value, nil
|
|
}
|
|
|
|
func (c Cron) Matches(value time.Time) bool {
|
|
dayMatch := c.day.values[value.Day()]
|
|
weekdayMatch := c.weekday.values[int(value.Weekday())]
|
|
calendarMatch := dayMatch && weekdayMatch
|
|
// Vixie cron semantics: when both day fields are restricted, either may match.
|
|
if !c.dayWildcard && !c.weekdayWildcard {
|
|
calendarMatch = dayMatch || weekdayMatch
|
|
}
|
|
return c.minute.values[value.Minute()] && c.hour.values[value.Hour()] && c.month.values[int(value.Month())] && calendarMatch
|
|
}
|
|
|
|
func (c Cron) Next(after time.Time, location *time.Location) (time.Time, error) {
|
|
if location == nil {
|
|
location = time.UTC
|
|
}
|
|
candidate := after.UTC().Truncate(time.Minute).Add(time.Minute)
|
|
deadline := candidate.AddDate(5, 0, 0)
|
|
for candidate.Before(deadline) {
|
|
if c.Matches(candidate.In(location)) {
|
|
return candidate, nil
|
|
}
|
|
candidate = candidate.Add(time.Minute)
|
|
}
|
|
return time.Time{}, errors.New("未来 5 年内无匹配执行时间")
|
|
}
|