diff --git a/.gitignore b/.gitignore index 2103e04..5ccf434 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ dist/ pdf/ ../log/ +/log/ diff --git a/README.md b/README.md index 4805405..6e3af2c 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ cp .env.example .env ## 运行 -**运营推荐**:直接双击或运行 `transit.exe`(无参数),按中文菜单选择「单次执行」或「定时执行」。 +**运营推荐**:直接双击或运行 `transit.exe`(无参数)。交互菜单 **10 秒内无操作** 将自动进入「定时执行(常驻)」;也可用菜单手动选「单次执行」或「定时执行」。超时秒数:`MENU_AUTO_SERVE_TIMEOUT_SEC`(默认 10)。 **完整命令说明**(单次 / 定时 / 计划任务 / 排错):见 **[docs/COMMANDS.md](docs/COMMANDS.md)**。 diff --git a/cmd/transit/main.go b/cmd/transit/main.go index 1ae35c0..3ec1ca2 100644 --- a/cmd/transit/main.go +++ b/cmd/transit/main.go @@ -79,6 +79,7 @@ func isInteractiveTerminal() bool { // runInteractiveMenu 无参数时展示中文菜单,供运营选择单次或定时。 func runInteractiveMenu() { reader := bufio.NewReader(os.Stdin) + timeout := menuAutoServeTimeout() for { fmt.Println() fmt.Println("======== 互联网医院监管中转 ========") @@ -86,9 +87,13 @@ func runInteractiveMenu() { fmt.Println(" 2) 定时执行(常驻,按 .env 中 CRON 每天跑)") fmt.Println(" 3) 查看日志(Web 控制台,不跑定时)") fmt.Println(" 0) 退出") + fmt.Printf("%v 内未选择将自动启动「2) 定时执行(常驻)」\n", timeout.Round(time.Second)) fmt.Print("请选择 [0-3]: ") - choice, _ := reader.ReadString('\n') - choice = strings.TrimSpace(choice) + choice, timedOut := readLineTimeout(reader, timeout) + if timedOut { + choice = "2" + fmt.Println("已超时,自动选择: 2) 定时执行(常驻)") + } switch choice { case "1": runInteractiveSync(reader) @@ -145,10 +150,14 @@ func runInteractiveServe(reader *bufio.Reader) { fmt.Printf(" 默认 cron: %s(每天 22:00)\n", expr) } fmt.Println("启动后进程将常驻,到点自动执行 step=all。") + timeout := menuAutoServeTimeout() + fmt.Printf("%v 内未确认将自动启动定时服务\n", timeout.Round(time.Second)) fmt.Print("确认启动定时服务? [Y/n]: ") - confirm, _ := reader.ReadString('\n') - confirm = strings.TrimSpace(strings.ToLower(confirm)) - if confirm == "n" || confirm == "no" { + confirm, timedOut := readLineTimeout(reader, timeout) + if timedOut { + fmt.Println("已超时,自动确认启动。") + } + if !serveConfirmYes(confirm, timedOut) { fmt.Println("已取消。") return } diff --git a/cmd/transit/menu_io.go b/cmd/transit/menu_io.go new file mode 100644 index 0000000..56e50ba --- /dev/null +++ b/cmd/transit/menu_io.go @@ -0,0 +1,49 @@ +package main + +import ( + "bufio" + "os" + "strconv" + "strings" + "time" +) + +const defaultMenuAutoServeTimeout = 10 * time.Second + +// menuAutoServeTimeout 交互菜单自动进入常驻的超时;可由 MENU_AUTO_SERVE_TIMEOUT_SEC 覆盖。 +func menuAutoServeTimeout() time.Duration { + v := strings.TrimSpace(os.Getenv("MENU_AUTO_SERVE_TIMEOUT_SEC")) + if v == "" { + return defaultMenuAutoServeTimeout + } + sec, err := strconv.Atoi(v) + if err != nil || sec <= 0 { + return defaultMenuAutoServeTimeout + } + return time.Duration(sec) * time.Second +} + +// readLineTimeout 在 timeout 内等待一行;超时返回 timedOut=true。 +// 注意:Windows 控制台无法在超时后取消阻塞读,后台 goroutine 仍会等到用户按 Enter。 +func readLineTimeout(r *bufio.Reader, timeout time.Duration) (line string, timedOut bool) { + ch := make(chan string, 1) + go func() { + s, _ := r.ReadString('\n') + ch <- s + }() + select { + case s := <-ch: + return strings.TrimSpace(s), false + case <-time.After(timeout): + return "", true + } +} + +// serveConfirmYes 是否启动定时常驻(超时视为确认)。 +func serveConfirmYes(confirm string, timedOut bool) bool { + if timedOut { + return true + } + c := strings.TrimSpace(strings.ToLower(confirm)) + return c != "n" && c != "no" +} diff --git a/cmd/transit/menu_io_test.go b/cmd/transit/menu_io_test.go new file mode 100644 index 0000000..89c8f4e --- /dev/null +++ b/cmd/transit/menu_io_test.go @@ -0,0 +1,61 @@ +package main + +import ( + "bufio" + "bytes" + "io" + "testing" + "time" +) + +func TestReadLineTimeout_returnsBeforeDeadline(t *testing.T) { + r := bufio.NewReader(bytes.NewBufferString("hello\n")) + line, timedOut := readLineTimeout(r, 2*time.Second) + if timedOut { + t.Fatal("expected input, got timeout") + } + if line != "hello" { + t.Fatalf("line=%q", line) + } +} + +func TestReadLineTimeout_timesOut(t *testing.T) { + pr, _ := io.Pipe() + r := bufio.NewReader(pr) + _, timedOut := readLineTimeout(r, 50*time.Millisecond) + if !timedOut { + t.Fatal("expected timeout") + } +} + +func TestServeConfirmYes(t *testing.T) { + if !serveConfirmYes("", false) { + t.Fatal("empty should confirm") + } + if !serveConfirmYes("y", false) { + t.Fatal("y should confirm") + } + if !serveConfirmYes("", true) { + t.Fatal("timeout should confirm") + } + if serveConfirmYes("n", false) { + t.Fatal("n should cancel") + } + if serveConfirmYes("no", false) { + t.Fatal("no should cancel") + } +} + +func TestMenuAutoServeTimeout_default(t *testing.T) { + t.Setenv("MENU_AUTO_SERVE_TIMEOUT_SEC", "") + if menuAutoServeTimeout() != defaultMenuAutoServeTimeout { + t.Fatalf("got %v", menuAutoServeTimeout()) + } +} + +func TestMenuAutoServeTimeout_env(t *testing.T) { + t.Setenv("MENU_AUTO_SERVE_TIMEOUT_SEC", "5") + if menuAutoServeTimeout() != 5*time.Second { + t.Fatalf("got %v", menuAutoServeTimeout()) + } +} diff --git a/docs/COMMANDS.md b/docs/COMMANDS.md index 0fe9444..193ef98 100644 --- a/docs/COMMANDS.md +++ b/docs/COMMANDS.md @@ -31,10 +31,12 @@ go run ./cmd/transit 2) 定时执行(常驻,按 .env 中 CRON 每天跑) 3) 查看日志(Web 控制台,不跑定时) 0) 退出 +10s 内未选择将自动启动「2) 定时执行(常驻)」 请选择 [0-3]: ``` -- 选 **1**:再输入 `step`(默认 all)、`date`(回车=按 ANCHOR_OFFSET_DAYS 推算,默认昨日)。 +- **10 秒无输入**:自动选择 **2** 并进入确认步骤;确认步骤同样 **10 秒无输入** 则自动启动常驻(适合双击 `transit.exe` 无人值守)。超时秒数可用环境变量 `MENU_AUTO_SERVE_TIMEOUT_SEC`(正整数,默认 10)调整。 +- 选 **1**:再输入 `step`(默认 all)、`date`(回车=按 ANCHOR_OFFSET_DAYS 推算,默认昨日);该路径仍须手动输入,无自动超时。 - 选 **2**:显示当前 cron 配置,确认后进程常驻,到点自动 `step=all`,并启动 Web 控制台(默认 `http://127.0.0.1:8765/`)。 - 选 **3**:仅启动 Web,可浏览文件日志与本机同步流水(测试同步需选 2 用 serve)。 - 选 **0**:退出。