2025-11-15 12:08:26 +08:00
|
|
|
package main
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"encoding/json"
|
|
|
|
|
"flag"
|
|
|
|
|
"fmt"
|
|
|
|
|
"log"
|
|
|
|
|
"math/rand"
|
|
|
|
|
"strings"
|
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
"github.com/google/uuid"
|
|
|
|
|
"github.com/nats-io/nats.go"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
var (
|
|
|
|
|
priorityIndicators = []string{"FF", "GG", "QU"}
|
|
|
|
|
primaryAddresses = []string{"ZBTJZPZX", "KSFOZPZX", "KLAXZPZX", "EDDFZPZX"}
|
|
|
|
|
originators = []string{"ZBTJYOYX", "KSFOYOYX", "SELOZKE"}
|
|
|
|
|
originatorLines = []string{"141604 ZBACZQZX", "150551 ZBTJUOBK", "210930 ZGGGZQZX"}
|
|
|
|
|
airports = []string{"ZBTJ", "ZGGG", "KSFO", "KLAX", "EDDF", "RJTT", "EGLL", "ZSPD"}
|
|
|
|
|
statusValues = []string{"parsed", "header_error", "body_error", "publish_error", "repository_error"}
|
|
|
|
|
bodyCategories = []string{"ARR", "DEP", "CNL", "DLA", "FPL"}
|
|
|
|
|
)
|
|
|
|
|
|
2025-11-16 17:24:16 +08:00
|
|
|
// SeedConfig controls how telegrams are generated and dispatched.
|
|
|
|
|
type SeedConfig struct {
|
|
|
|
|
Count int
|
|
|
|
|
Mode string
|
|
|
|
|
IntervalMin time.Duration
|
|
|
|
|
IntervalMax time.Duration
|
|
|
|
|
Duration time.Duration
|
|
|
|
|
CategoryFlag string
|
|
|
|
|
StatusFlag string
|
|
|
|
|
ErrorReason string
|
|
|
|
|
HeaderFormat string
|
|
|
|
|
DryRun bool
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// publishFunc abstracts the publish side-effect so that it can be swapped in tests.
|
|
|
|
|
type publishFunc func(*telegram) error
|
|
|
|
|
|
|
|
|
|
// sleepFunc is used instead of time.Sleep so tests can stub out real sleeping.
|
|
|
|
|
var sleepFunc = time.Sleep
|
|
|
|
|
|
2025-11-15 12:08:26 +08:00
|
|
|
func main() {
|
|
|
|
|
natsURL := flag.String("nats-url", "nats://127.0.0.1:4222", "NATS server URL (empty skips publish)")
|
|
|
|
|
subject := flag.String("subject", "telegram.raw", "Subject to publish telegrams to")
|
|
|
|
|
noNATS := flag.Bool("no-nats", false, "Skip NATS publish even if --nats-url is provided")
|
|
|
|
|
count := flag.Int("count", 10, "Number of telegrams to publish")
|
|
|
|
|
category := flag.String("category", "mixed", "ARR|DEP|CNL|DLA|FPL|mixed")
|
|
|
|
|
status := flag.String("status", "body_error", "parsed|header_error|body_error|publish_error|repository_error|random")
|
|
|
|
|
errorReason := flag.String("error-reason", "synthetic test payload", "Metadata header describing why message is in raw state")
|
|
|
|
|
dryRun := flag.Bool("dry-run", false, "Print telegrams instead of publishing to NATS")
|
|
|
|
|
useJetStream := flag.Bool("jetstream", false, "Publish via JetStream")
|
|
|
|
|
jsStream := flag.String("stream", "", "JetStream stream (optional when --jetstream)")
|
|
|
|
|
jsSubject := flag.String("js-subject", "", "Override subject for JetStream publish (defaults to --subject)")
|
|
|
|
|
headerFormat := flag.String("header-format", "json", "Metadata header encoding: json|none")
|
2025-11-16 17:24:16 +08:00
|
|
|
mode := flag.String("mode", "burst", "Seed mode: burst|interval|mixed")
|
|
|
|
|
intervalMin := flag.Duration("interval-min", time.Second, "Minimum interval between messages in interval/mixed modes")
|
|
|
|
|
intervalMax := flag.Duration("interval-max", 2*time.Second, "Maximum interval between messages in interval/mixed modes")
|
|
|
|
|
duration := flag.Duration("duration", 0, "Total duration for interval/mixed modes (0 = rely on --count only)")
|
2025-11-15 12:08:26 +08:00
|
|
|
flag.Parse()
|
|
|
|
|
|
|
|
|
|
rand.Seed(time.Now().UnixNano())
|
|
|
|
|
|
|
|
|
|
var nc *nats.Conn
|
|
|
|
|
var js nats.JetStreamContext
|
|
|
|
|
var err error
|
|
|
|
|
|
|
|
|
|
if !*dryRun && *natsURL != "" && !*noNATS {
|
|
|
|
|
nc, err = nats.Connect(*natsURL)
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Fatalf("connect nats: %v", err)
|
|
|
|
|
}
|
|
|
|
|
defer nc.Drain()
|
|
|
|
|
|
|
|
|
|
if *useJetStream {
|
|
|
|
|
opts := []nats.JSOpt{}
|
|
|
|
|
if *jsStream != "" {
|
|
|
|
|
opts = append(opts, nats.PublishAsyncMaxPending(256))
|
|
|
|
|
}
|
|
|
|
|
js, err = nc.JetStream()
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Fatalf("init jetstream: %v", err)
|
|
|
|
|
}
|
|
|
|
|
_ = opts
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-16 17:24:16 +08:00
|
|
|
// Normalize and prepare configuration.
|
|
|
|
|
if *intervalMax < *intervalMin {
|
|
|
|
|
*intervalMax = *intervalMin
|
2025-11-15 12:08:26 +08:00
|
|
|
}
|
|
|
|
|
|
2025-11-16 17:24:16 +08:00
|
|
|
cfg := SeedConfig{
|
|
|
|
|
Count: *count,
|
|
|
|
|
Mode: strings.ToLower(*mode),
|
|
|
|
|
IntervalMin: *intervalMin,
|
|
|
|
|
IntervalMax: *intervalMax,
|
|
|
|
|
Duration: *duration,
|
|
|
|
|
CategoryFlag: *category,
|
|
|
|
|
StatusFlag: *status,
|
|
|
|
|
ErrorReason: *errorReason,
|
|
|
|
|
HeaderFormat: *headerFormat,
|
|
|
|
|
DryRun: *dryRun,
|
2025-11-15 12:08:26 +08:00
|
|
|
}
|
|
|
|
|
|
2025-11-16 17:24:16 +08:00
|
|
|
categories := buildCategories(cfg.CategoryFlag, bodyCategories)
|
|
|
|
|
statuses := buildStatuses(cfg.StatusFlag, statusValues)
|
2025-11-16 10:18:40 +08:00
|
|
|
|
2025-11-16 17:24:16 +08:00
|
|
|
var publisher publishFunc
|
|
|
|
|
if nc != nil && !*noNATS {
|
|
|
|
|
publisher = func(payload *telegram) error {
|
2025-11-15 12:08:26 +08:00
|
|
|
data := []byte(payload.Content)
|
|
|
|
|
msg := &nats.Msg{Subject: *subject, Data: data, Header: nats.Header{}}
|
|
|
|
|
msg.Header.Set("Nats-Msg-Id", payload.UUID)
|
2025-11-16 17:24:16 +08:00
|
|
|
if strings.ToLower(cfg.HeaderFormat) == "json" {
|
2025-11-15 12:08:26 +08:00
|
|
|
headerJSON, _ := json.Marshal(payload.Metadata)
|
|
|
|
|
msg.Header.Set("x-telegram-meta", string(headerJSON))
|
|
|
|
|
}
|
|
|
|
|
msg.Header.Set("x-telegram-uuid", payload.UUID)
|
|
|
|
|
msg.Header.Set("x-telegram-status", payload.Status)
|
|
|
|
|
msg.Header.Set("x-telegram-error", payload.ErrorReason)
|
|
|
|
|
|
|
|
|
|
if js != nil {
|
|
|
|
|
pubSubject := *jsSubject
|
|
|
|
|
if pubSubject == "" {
|
|
|
|
|
pubSubject = *subject
|
|
|
|
|
}
|
|
|
|
|
msg.Subject = pubSubject
|
|
|
|
|
if _, err := js.PublishMsg(msg); err != nil {
|
2025-11-16 17:24:16 +08:00
|
|
|
return fmt.Errorf("jetstream publish: %w", err)
|
2025-11-15 12:08:26 +08:00
|
|
|
}
|
2025-11-16 17:24:16 +08:00
|
|
|
return nil
|
2025-11-15 12:08:26 +08:00
|
|
|
}
|
2025-11-16 17:24:16 +08:00
|
|
|
|
|
|
|
|
if err := nc.PublishMsg(msg); err != nil {
|
|
|
|
|
return fmt.Errorf("nats publish: %w", err)
|
|
|
|
|
}
|
|
|
|
|
return nil
|
2025-11-15 12:08:26 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-16 17:24:16 +08:00
|
|
|
if err := RunSeed(cfg, categories, statuses, publisher); err != nil {
|
|
|
|
|
log.Fatalf("run seed: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-15 12:08:26 +08:00
|
|
|
if !*dryRun {
|
|
|
|
|
if nc != nil && !*noNATS {
|
|
|
|
|
log.Printf("Published %d telegram(s) to %s", *count, *subject)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-16 17:24:16 +08:00
|
|
|
// RunSeed drives telegram generation according to the configured mode.
|
|
|
|
|
// It delegates message construction to buildTelegram and side-effects to the provided publisher.
|
|
|
|
|
func RunSeed(cfg SeedConfig, categories []string, statuses []string, publisher publishFunc) error {
|
|
|
|
|
if len(categories) == 0 {
|
|
|
|
|
return fmt.Errorf("no categories available")
|
|
|
|
|
}
|
|
|
|
|
if len(statuses) == 0 {
|
|
|
|
|
return fmt.Errorf("no statuses available")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
switch cfg.Mode {
|
|
|
|
|
case "interval":
|
|
|
|
|
return runIntervalMode(cfg, categories, statuses, publisher)
|
|
|
|
|
case "mixed":
|
|
|
|
|
return runMixedMode(cfg, categories, statuses, publisher)
|
|
|
|
|
default:
|
|
|
|
|
// Default to burst semantics.
|
|
|
|
|
return runBurstMode(cfg, categories, statuses, publisher)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func runBurstMode(cfg SeedConfig, categories []string, statuses []string, publisher publishFunc) error {
|
|
|
|
|
count := cfg.Count
|
|
|
|
|
if count <= 0 {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
for i := 0; i < count; i++ {
|
|
|
|
|
if err := sendTelegram(i, cfg, categories, statuses, publisher); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func runIntervalMode(cfg SeedConfig, categories []string, statuses []string, publisher publishFunc) error {
|
|
|
|
|
start := time.Now()
|
|
|
|
|
sent := 0
|
|
|
|
|
|
|
|
|
|
for {
|
|
|
|
|
if cfg.Count > 0 && sent >= cfg.Count {
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
if cfg.Duration > 0 && time.Since(start) >= cfg.Duration {
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if err := sendTelegram(sent, cfg, categories, statuses, publisher); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
sent++
|
|
|
|
|
|
|
|
|
|
// Compute next sleep duration within [IntervalMin, IntervalMax].
|
|
|
|
|
sleepDur := cfg.IntervalMin
|
|
|
|
|
if cfg.IntervalMax > cfg.IntervalMin {
|
|
|
|
|
delta := cfg.IntervalMax - cfg.IntervalMin
|
|
|
|
|
sleepDur = cfg.IntervalMin + time.Duration(rand.Int63n(int64(delta)+1))
|
|
|
|
|
}
|
|
|
|
|
if sleepDur > 0 && !cfg.DryRun {
|
|
|
|
|
sleepFunc(sleepDur)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// mixed 模式:前半段使用 interval 模式,后半段使用 burst。
|
|
|
|
|
func runMixedMode(cfg SeedConfig, categories []string, statuses []string, publisher publishFunc) error {
|
|
|
|
|
// 简单策略:如果 Count>0,前半部分 interval,后半部分 burst;否则退化为 interval。
|
|
|
|
|
if cfg.Count <= 0 {
|
|
|
|
|
return runIntervalMode(cfg, categories, statuses, publisher)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
half := cfg.Count / 2
|
|
|
|
|
if half == 0 {
|
|
|
|
|
// Count==1 时直接按 burst 处理。
|
|
|
|
|
return runBurstMode(cfg, categories, statuses, publisher)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
intervalCfg := cfg
|
|
|
|
|
intervalCfg.Count = half
|
|
|
|
|
if err := runIntervalMode(intervalCfg, categories, statuses, publisher); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
burstCfg := cfg
|
|
|
|
|
burstCfg.Count = cfg.Count - half
|
|
|
|
|
return runBurstMode(burstCfg, categories, statuses, publisher)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// sendTelegram builds a single telegram, assigns status/metadata, and either prints or publishes it.
|
|
|
|
|
func sendTelegram(iteration int, cfg SeedConfig, categories []string, statuses []string, publisher publishFunc) error {
|
|
|
|
|
cat := categories[rand.Intn(len(categories))]
|
|
|
|
|
payload, intentionallyInvalid := buildTelegram(cat)
|
|
|
|
|
|
|
|
|
|
payload.Status = chooseStatus(intentionallyInvalid, cfg.StatusFlag, statuses)
|
|
|
|
|
payload.ErrorReason = cfg.ErrorReason
|
|
|
|
|
payload.Metadata = map[string]string{
|
|
|
|
|
"message_id": payload.MessageID,
|
|
|
|
|
"category": payload.Category,
|
|
|
|
|
"comments": fmt.Sprintf("seeded iteration=%d", iteration),
|
|
|
|
|
"status": payload.Status,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if cfg.DryRun {
|
|
|
|
|
blob, _ := json.MarshalIndent(payload, "", " ")
|
|
|
|
|
fmt.Println(string(blob))
|
|
|
|
|
fmt.Println("---")
|
2025-11-17 11:47:23 +08:00
|
|
|
// Still call publisher in DryRun mode for test purposes
|
|
|
|
|
if publisher != nil {
|
|
|
|
|
return publisher(payload)
|
|
|
|
|
}
|
2025-11-16 17:24:16 +08:00
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if publisher != nil {
|
|
|
|
|
return publisher(payload)
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// buildCategories returns the effective categories list based on the CLI flag.
|
|
|
|
|
func buildCategories(flagValue string, all []string) []string {
|
|
|
|
|
if strings.ToLower(flagValue) == "mixed" || flagValue == "" {
|
|
|
|
|
return all
|
|
|
|
|
}
|
|
|
|
|
return []string{strings.ToUpper(flagValue)}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// buildStatuses returns the effective statuses list based on the CLI flag.
|
|
|
|
|
func buildStatuses(flagValue string, all []string) []string {
|
|
|
|
|
if strings.ToLower(flagValue) == "random" || flagValue == "" {
|
|
|
|
|
return all
|
|
|
|
|
}
|
|
|
|
|
return []string{strings.ToLower(flagValue)}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// chooseStatus encapsulates the status selection logic, including the special \"random\" behaviour.
|
|
|
|
|
func chooseStatus(intentionallyInvalid bool, statusFlag string, statuses []string) string {
|
|
|
|
|
if strings.ToLower(statusFlag) != "random" {
|
|
|
|
|
return statuses[rand.Intn(len(statuses))]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// random 模式:根据报文是否合法,对 parsed/body_error 做倾向性选择。
|
|
|
|
|
if intentionallyInvalid {
|
|
|
|
|
// 非法报文:大概率 body_error。
|
|
|
|
|
if rand.Intn(100) < 80 {
|
|
|
|
|
return "body_error"
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// 合法报文:大概率 parsed。
|
|
|
|
|
if rand.Intn(100) < 70 {
|
|
|
|
|
return "parsed"
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return statuses[rand.Intn(len(statuses))]
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-15 12:08:26 +08:00
|
|
|
type telegram struct {
|
|
|
|
|
UUID string `json:"uuid"`
|
|
|
|
|
MessageID string `json:"message_id"`
|
|
|
|
|
Category string `json:"category"`
|
|
|
|
|
Status string `json:"status"`
|
|
|
|
|
ErrorReason string `json:"error_reason"`
|
|
|
|
|
Content string `json:"content"`
|
|
|
|
|
ReceivedAt time.Time `json:"received_at"`
|
|
|
|
|
Metadata map[string]string `json:"metadata"`
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-16 10:18:40 +08:00
|
|
|
func buildTelegram(category string) (*telegram, bool) {
|
2025-11-15 12:08:26 +08:00
|
|
|
now := time.Now().UTC()
|
|
|
|
|
messageID := fmt.Sprintf("%s%04d", category, rand.Intn(9000)+1000)
|
|
|
|
|
headerTime := now.Format("020304")
|
|
|
|
|
priority := priorityIndicators[rand.Intn(len(priorityIndicators))]
|
|
|
|
|
primary := primaryAddresses[rand.Intn(len(primaryAddresses))]
|
|
|
|
|
originLine := originatorLines[rand.Intn(len(originatorLines))]
|
|
|
|
|
originator := originators[rand.Intn(len(originators))]
|
|
|
|
|
|
2025-11-16 10:18:40 +08:00
|
|
|
body, intentionallyInvalid := buildBody(category)
|
2025-11-15 12:08:26 +08:00
|
|
|
|
|
|
|
|
content := strings.Join([]string{
|
|
|
|
|
fmt.Sprintf("ZCZC %s %s", messageID, headerTime),
|
|
|
|
|
fmt.Sprintf("%s %s", priority, primary),
|
|
|
|
|
originLine,
|
|
|
|
|
originator,
|
|
|
|
|
body,
|
|
|
|
|
"NNNN",
|
|
|
|
|
}, "\n")
|
|
|
|
|
|
|
|
|
|
return &telegram{
|
|
|
|
|
UUID: uuid.NewString(),
|
|
|
|
|
MessageID: messageID,
|
|
|
|
|
Category: category,
|
|
|
|
|
Content: content,
|
|
|
|
|
ReceivedAt: now,
|
2025-11-16 10:18:40 +08:00
|
|
|
}, intentionallyInvalid
|
2025-11-15 12:08:26 +08:00
|
|
|
}
|
|
|
|
|
|
2025-11-16 10:18:40 +08:00
|
|
|
func buildBody(category string) (string, bool) {
|
2025-11-15 12:08:26 +08:00
|
|
|
flight := fmt.Sprintf("%s%04d", []string{"CCA", "SWA", "DLH", "AAL", "JAE"}[rand.Intn(5)], rand.Intn(9000)+1000)
|
|
|
|
|
dep := airports[rand.Intn(len(airports))]
|
|
|
|
|
arr := airports[rand.Intn(len(airports))]
|
|
|
|
|
depTime := time.Now().UTC().Add(time.Duration(rand.Intn(240)) * time.Minute).Format("1504")
|
|
|
|
|
arrTime := time.Now().UTC().Add(time.Duration(rand.Intn(360)) * time.Minute).Format("1504")
|
|
|
|
|
|
|
|
|
|
switch category {
|
|
|
|
|
case "ARR":
|
|
|
|
|
if rand.Intn(2) == 0 {
|
2025-11-16 10:18:40 +08:00
|
|
|
return fmt.Sprintf("(ARR-%s-%s-%s%s)", flight, dep, arr, arrTime), false
|
2025-11-15 12:08:26 +08:00
|
|
|
}
|
2025-11-16 10:18:40 +08:00
|
|
|
// 70% 使用简单 SSR(合法),30% 使用复杂 SSR(当前正则下非法)
|
|
|
|
|
if rand.Intn(100) < 30 {
|
|
|
|
|
return fmt.Sprintf("(ARR-%s/%s-%s-%s%s)", flight, randomComplexSSR(), dep, arr, arrTime), true
|
|
|
|
|
}
|
|
|
|
|
return fmt.Sprintf("(ARR-%s/%s-%s-%s%s)", flight, randomSimpleSSR(), dep, arr, arrTime), false
|
2025-11-15 12:08:26 +08:00
|
|
|
case "DEP":
|
2025-11-16 10:18:40 +08:00
|
|
|
// 70% 使用简单 SSR(合法),30% 使用复杂 SSR(当前正则下非法)
|
|
|
|
|
if rand.Intn(100) < 30 {
|
|
|
|
|
return fmt.Sprintf("(DEP-%s/%s-%s%s-%s)", flight, randomComplexSSR(), dep, depTime, arr), true
|
|
|
|
|
}
|
|
|
|
|
return fmt.Sprintf("(DEP-%s/%s-%s%s-%s)", flight, randomSimpleSSR(), dep, depTime, arr), false
|
2025-11-15 12:08:26 +08:00
|
|
|
case "CNL":
|
2025-11-16 10:18:40 +08:00
|
|
|
return fmt.Sprintf("(CNL-%s-%s-%s)", flight, dep, arr), false
|
2025-11-15 12:08:26 +08:00
|
|
|
case "DLA":
|
2025-11-16 10:18:40 +08:00
|
|
|
return fmt.Sprintf("(DLA-%s-%s%s-%s)", flight, dep, depTime, arr), false
|
2025-11-15 12:08:26 +08:00
|
|
|
default: // FPL
|
|
|
|
|
return fmt.Sprintf(`(FPL-%s-IS
|
|
|
|
|
-%s/H
|
|
|
|
|
-%s
|
|
|
|
|
-%s%s
|
|
|
|
|
-K%04dS%04d %s
|
|
|
|
|
-%s%s %s
|
|
|
|
|
-%s)`,
|
|
|
|
|
flight,
|
|
|
|
|
randomAircraft(),
|
|
|
|
|
randomSSR(),
|
|
|
|
|
dep,
|
|
|
|
|
depTime,
|
|
|
|
|
rand.Intn(9000)+500,
|
|
|
|
|
rand.Intn(8000)+400,
|
|
|
|
|
randomRoute(),
|
|
|
|
|
arr,
|
|
|
|
|
arrTime,
|
|
|
|
|
randomAirportPair(),
|
|
|
|
|
randomOtherInfo(),
|
2025-11-16 10:18:40 +08:00
|
|
|
), false
|
2025-11-15 12:08:26 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func randomSSR() string {
|
|
|
|
|
return []string{"A0132", "A5633", "SXIRPZJWY/LB101", "SHID/C"}[rand.Intn(4)]
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-16 10:18:40 +08:00
|
|
|
// 与当前 ARR/DEP 正则匹配的简单 SSR
|
|
|
|
|
func randomSimpleSSR() string {
|
|
|
|
|
return []string{"A0132", "A5633"}[rand.Intn(2)]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 故意构造为当前 ARR/DEP 正则无法解析的复杂 SSR
|
|
|
|
|
func randomComplexSSR() string {
|
|
|
|
|
return []string{"SXIRPZJWY/LB101", "SHID/C"}[rand.Intn(2)]
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-15 12:08:26 +08:00
|
|
|
func randomAircraft() string {
|
|
|
|
|
return []string{"A332", "B788", "MA60", "A359"}[rand.Intn(4)]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func randomRoute() string {
|
|
|
|
|
routes := []string{
|
|
|
|
|
"PIAKS G330 PIMOL A539 BTO W82 DOGAR",
|
|
|
|
|
"CG J1 FZ",
|
|
|
|
|
"EBAYY Q11 BSR J65 BCE Q135 KICNE",
|
|
|
|
|
}
|
|
|
|
|
return routes[rand.Intn(len(routes))]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func randomAirportPair() string {
|
|
|
|
|
return fmt.Sprintf("%s %s", airports[rand.Intn(len(airports))], airports[rand.Intn(len(airports))])
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func randomOtherInfo() string {
|
|
|
|
|
return []string{
|
|
|
|
|
"PBN/A1B2B3B4B5D1L1 NAV/ABAS REG/B6513 EET/ZBPE0112 SEL/KMAL PER/C RIF/FRT N640 ZBYN RMK/TCAS EQUIPPED",
|
|
|
|
|
"REG/B3710 SEL/ RMK/TCAS",
|
|
|
|
|
"NAV/RNAV1 RNAV5 RNP4 RMK/AGCS EQUIPPED",
|
|
|
|
|
}[rand.Intn(3)]
|
|
|
|
|
}
|