Builds on cf70021, which added the read-only yeeke client/session/sync core
but left it unreachable and unconfigurable. This commit:
- Wires config.ExtConfig.Yeeke (settings.yml + config.yaml + GOAUTO_YEEKE_*
env vars), mirroring the existing SYB credential pattern exactly, with a
dedicated OcrURL and shared OCR client from sybclient.
- Adds yeeke.StartSync as the single entry point for both a manual admin
trigger and the scheduled job, sharing one in-memory gate plus the existing
DB-level unique active_slot lease so they can never run concurrently.
- Fixes sync.go bugs found in review: Service.Sync always returned a nil
error even when the run failed (start/resume semantics were untestable),
item upserts on ctx-less s.db calls, and no error_message/last_success_at
was ever recorded on the run row.
- Adds status_unrecognized to yeeke_return_package: an unknown claim status
is preserved verbatim and flagged rather than silently bucketed.
- Adds the admin read-only surface (GET .../sync-runs, GET
.../sync-runs/:runId, POST .../sync) under /api/admin/v1/yeeke-returns,
visible to admin and purchaser per the #336 review comment, registered as
a GoAuto access module/menu group and purchaser API.
- Registers GoAutoYeekeReturnSync in the existing job/lease framework
(app/jobs), seeded disabled (Status 2) by a new version-local migration,
following 1786701600000_syb_hourly_sync_job.go's pattern exactly.
- Expands tests: session reuse/bounded re-login/timeout-preserves-cache in
yeekeclient; paging robustness (total changing mid-run, duplicate page,
empty page, timeout, simulated restart/resume), idempotent upserts,
unrecognized-status flagging, active_slot lease contention, StartSync gate
contention, and a credential/captcha redaction check in yeeke; settings.yml
binding and env var precedence in config.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NTDbDcwbDw1TSAcE6wfh2F
107 lines
3.3 KiB
Go
107 lines
3.3 KiB
Go
package config
|
|
|
|
import (
|
|
"os"
|
|
"testing"
|
|
|
|
"gopkg.in/yaml.v2"
|
|
)
|
|
|
|
// settings.yml 里的 extend.syb 必须真的能绑进 ExtConfig.SYB。
|
|
//
|
|
// yaml.v2 默认把字段名整体小写当作键名(BaseURL -> baseurl),大小写或拼写
|
|
// 一错,配置会静默地保持零值——服务照常启动,直到真去调顺云宝才报一个
|
|
// 看不出原因的错。这里直接拿仓库里真实的 settings.yml 校验绑定。
|
|
func TestSYBSettingsInRepoBindToExtendStruct(t *testing.T) {
|
|
raw, err := os.ReadFile("settings.yml")
|
|
if err != nil {
|
|
t.Fatalf("读取 settings.yml 失败: %v", err)
|
|
}
|
|
var file struct {
|
|
Settings struct {
|
|
Extend Extend `yaml:"extend"`
|
|
} `yaml:"settings"`
|
|
}
|
|
if err := yaml.Unmarshal(raw, &file); err != nil {
|
|
t.Fatalf("解析 settings.yml 失败: %v", err)
|
|
}
|
|
|
|
syb := file.Settings.Extend.SYB
|
|
if syb.BaseURL != "https://www.shunyunbaoerp.com" {
|
|
t.Fatalf("baseurl 没有绑定成功: %q", syb.BaseURL)
|
|
}
|
|
if syb.PageSize != 50 {
|
|
t.Fatalf("pagesize 没有绑定成功: %d", syb.PageSize)
|
|
}
|
|
if syb.MaxMatches != 10000 {
|
|
t.Fatalf("maxmatches 没有绑定成功: %d", syb.MaxMatches)
|
|
}
|
|
if syb.OcrURL == "" {
|
|
t.Fatal("ocrurl 没有绑定成功")
|
|
}
|
|
if syb.OcrMaxAttempts != 5 {
|
|
t.Fatalf("ocrmaxattempts 没有绑定成功: %d", syb.OcrMaxAttempts)
|
|
}
|
|
}
|
|
|
|
// `[必须]` 被 Git 跟踪的配置文件里不得出现顺云宝账号密码字段。
|
|
func TestSettingsFilesCarryNoSYBCredentials(t *testing.T) {
|
|
for _, name := range []string{"settings.yml", "settings.full.yml", "settings.demo.yml", "settings.sqlite.yml"} {
|
|
raw, err := os.ReadFile(name)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
var file struct {
|
|
Settings struct {
|
|
Extend Extend `yaml:"extend"`
|
|
} `yaml:"settings"`
|
|
}
|
|
if err := yaml.Unmarshal(raw, &file); err != nil {
|
|
t.Fatalf("%s 解析失败: %v", name, err)
|
|
}
|
|
if file.Settings.Extend.SYB.Username != "" || file.Settings.Extend.SYB.Password != "" {
|
|
t.Fatalf("%s 里出现了顺云宝凭据,凭据必须走环境变量", name)
|
|
}
|
|
if file.Settings.Extend.Yeeke.Username != "" || file.Settings.Extend.Yeeke.Password != "" {
|
|
t.Fatalf("%s 里出现了 yeeke 凭据,凭据必须走环境变量 (#336)", name)
|
|
}
|
|
}
|
|
}
|
|
|
|
// settings.yml 里的 extend.yeeke 必须真的能绑进 ExtConfig.Yeeke,同样的道理
|
|
// 见上面 TestSYBSettingsInRepoBindToExtendStruct 的注释 (#336)。
|
|
func TestYeekeSettingsInRepoBindToExtendStruct(t *testing.T) {
|
|
raw, err := os.ReadFile("settings.yml")
|
|
if err != nil {
|
|
t.Fatalf("读取 settings.yml 失败: %v", err)
|
|
}
|
|
var file struct {
|
|
Settings struct {
|
|
Extend Extend `yaml:"extend"`
|
|
} `yaml:"settings"`
|
|
}
|
|
if err := yaml.Unmarshal(raw, &file); err != nil {
|
|
t.Fatalf("解析 settings.yml 失败: %v", err)
|
|
}
|
|
|
|
yeeke := file.Settings.Extend.Yeeke
|
|
if yeeke.BaseURL != "https://mmt.yeeke.com" {
|
|
t.Fatalf("baseurl 没有绑定成功: %q", yeeke.BaseURL)
|
|
}
|
|
if yeeke.PageSize != 100 {
|
|
t.Fatalf("pagesize 没有绑定成功: %d", yeeke.PageSize)
|
|
}
|
|
if yeeke.MaxPages != 10000 {
|
|
t.Fatalf("maxpages 没有绑定成功: %d", yeeke.MaxPages)
|
|
}
|
|
if yeeke.Retry != 2 {
|
|
t.Fatalf("retry 没有绑定成功: %d", yeeke.Retry)
|
|
}
|
|
if yeeke.OcrURL == "" {
|
|
t.Fatal("ocrurl 没有绑定成功")
|
|
}
|
|
if yeeke.OcrMaxAttempts != 5 {
|
|
t.Fatalf("ocrmaxattempts 没有绑定成功: %d", yeeke.OcrMaxAttempts)
|
|
}
|
|
}
|