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
190 lines
5.9 KiB
Go
190 lines
5.9 KiB
Go
package config
|
|
|
|
import (
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
|
|
sdkconfig "github.com/go-admin-team/go-admin-core/sdk/config"
|
|
)
|
|
|
|
var ExtConfig Extend
|
|
|
|
// Extend 扩展配置
|
|
//
|
|
// extend:
|
|
// demo:
|
|
// name: demo-name
|
|
//
|
|
// 使用方法: config.ExtConfig......即可!!
|
|
type Extend struct {
|
|
AMap AMap // 这里配置对应配置文件的结构即可
|
|
SYB SYB
|
|
Yeeke Yeeke
|
|
}
|
|
|
|
type AMap struct {
|
|
Key string
|
|
}
|
|
|
|
// SYB holds the 顺云宝 ERP connection settings (#48).
|
|
//
|
|
// `[必须]` Username and Password are NOT read from settings.yml — they come
|
|
// from GOAUTO_SYB_USERNAME / GOAUTO_SYB_PASSWORD via ApplyEnvironment, so no
|
|
// credential ever lands in a tracked file. Everything else here is non-secret
|
|
// and belongs in settings.yml.
|
|
//
|
|
// See docs/12-syb-erp-interface.md §8.
|
|
type SYB struct {
|
|
BaseURL string
|
|
Username string
|
|
Password string
|
|
PageSize int
|
|
// MaxMatches caps how many shipment orders one sync may touch. Zero means
|
|
// the caller's own default applies.
|
|
MaxMatches int
|
|
// OcrURL is the captcha recognition service. Empty disables OCR and falls
|
|
// back to the manual-entry path — that is a supported configuration, not an
|
|
// error (§8.1).
|
|
//
|
|
// `[必须]` Captcha images are sent to this host. Re-evaluate before pointing
|
|
// it at a service operated by somebody else.
|
|
OcrURL string
|
|
OcrMaxAttempts int
|
|
}
|
|
|
|
// SYBDefaults are the values used when settings.yml leaves a field blank.
|
|
const (
|
|
DefaultSYBBaseURL = "https://www.shunyunbaoerp.com"
|
|
DefaultSYBPageSize = 50
|
|
DefaultSYBMaxMatches = 10000
|
|
DefaultSYBOcrMaxAttempts = 5
|
|
)
|
|
|
|
// Resolved returns the SYB settings with blanks replaced by defaults. It never
|
|
// defaults Username or Password: missing credentials must surface as an error
|
|
// at the call site, not as an attempt to log in as nobody.
|
|
func (s SYB) Resolved() SYB {
|
|
if strings.TrimSpace(s.BaseURL) == "" {
|
|
s.BaseURL = DefaultSYBBaseURL
|
|
}
|
|
if s.PageSize <= 0 {
|
|
s.PageSize = DefaultSYBPageSize
|
|
}
|
|
if s.MaxMatches <= 0 {
|
|
s.MaxMatches = DefaultSYBMaxMatches
|
|
}
|
|
if s.OcrMaxAttempts <= 0 {
|
|
s.OcrMaxAttempts = DefaultSYBOcrMaxAttempts
|
|
}
|
|
s.Username = strings.TrimSpace(s.Username)
|
|
s.BaseURL = strings.TrimRight(strings.TrimSpace(s.BaseURL), "/")
|
|
s.OcrURL = strings.TrimSpace(s.OcrURL)
|
|
return s
|
|
}
|
|
|
|
// HasCredentials reports whether both account fields were supplied. Callers
|
|
// check this before starting a sync so a missing environment variable fails
|
|
// with a clear message instead of a 未登录 error from SYB.
|
|
func (s SYB) HasCredentials() bool {
|
|
return strings.TrimSpace(s.Username) != "" && s.Password != ""
|
|
}
|
|
|
|
// Yeeke holds the mmt.yeeke.com 退货包裹只读对接 connection settings (#336).
|
|
//
|
|
// `[必须]` Username and Password are NOT read from settings.yml — same rule as
|
|
// SYB above — they come from GOAUTO_YEEKE_USERNAME / GOAUTO_YEEKE_PASSWORD (or
|
|
// config.yaml's yeeke: section) so no credential ever lands in a tracked file.
|
|
type Yeeke struct {
|
|
BaseURL string
|
|
Username string
|
|
Password string
|
|
PageSize int
|
|
MaxPages int
|
|
Retry int
|
|
// OcrURL is the captcha recognition service shared with SYB (#336, approved
|
|
// 2026-09-23). Empty disables OCR; there is no manual-entry fallback here
|
|
// because this is a server-side scheduled/triggered flow, not an interactive
|
|
// login form, so a disabled OCR simply makes sync fail with a clear error.
|
|
OcrURL string
|
|
OcrMaxAttempts int
|
|
}
|
|
|
|
// YeekeDefaults are the values used when settings.yml leaves a field blank.
|
|
const (
|
|
DefaultYeekeBaseURL = "https://mmt.yeeke.com"
|
|
DefaultYeekePageSize = 100
|
|
DefaultYeekeMaxPages = 10000
|
|
DefaultYeekeRetry = 2
|
|
DefaultYeekeOcrMaxAttempts = 5
|
|
)
|
|
|
|
// Resolved returns the Yeeke settings with blanks replaced by defaults. It
|
|
// never defaults Username or Password: missing credentials must surface as an
|
|
// error at the call site, not as an attempt to log in as nobody.
|
|
func (y Yeeke) Resolved() Yeeke {
|
|
if strings.TrimSpace(y.BaseURL) == "" {
|
|
y.BaseURL = DefaultYeekeBaseURL
|
|
}
|
|
if y.PageSize <= 0 {
|
|
y.PageSize = DefaultYeekePageSize
|
|
}
|
|
if y.MaxPages <= 0 {
|
|
y.MaxPages = DefaultYeekeMaxPages
|
|
}
|
|
if y.Retry < 0 {
|
|
y.Retry = DefaultYeekeRetry
|
|
}
|
|
if y.OcrMaxAttempts <= 0 {
|
|
y.OcrMaxAttempts = DefaultYeekeOcrMaxAttempts
|
|
}
|
|
y.Username = strings.TrimSpace(y.Username)
|
|
y.BaseURL = strings.TrimRight(strings.TrimSpace(y.BaseURL), "/")
|
|
y.OcrURL = strings.TrimSpace(y.OcrURL)
|
|
return y
|
|
}
|
|
|
|
// HasCredentials reports whether both account fields were supplied.
|
|
func (y Yeeke) HasCredentials() bool {
|
|
return strings.TrimSpace(y.Username) != "" && y.Password != ""
|
|
}
|
|
|
|
// ApplyEnvironment replaces tracked defaults with process-local runtime values.
|
|
// Credentials stay outside tracked configuration files. GOAUTO_DB_DRIVER
|
|
// defaults to mysql when GOAUTO_DB_DSN is present.
|
|
func ApplyEnvironment() {
|
|
if port, err := strconv.Atoi(strings.TrimSpace(os.Getenv("GOAUTO_SERVER_PORT"))); err == nil && port >= 1 && port <= 65535 {
|
|
sdkconfig.ApplicationConfig.Port = int64(port)
|
|
}
|
|
if username := strings.TrimSpace(os.Getenv("GOAUTO_SYB_USERNAME")); username != "" {
|
|
ExtConfig.SYB.Username = username
|
|
}
|
|
// `[必须]` The password is taken verbatim: trimming it would silently change
|
|
// a credential whose leading or trailing space is intentional.
|
|
if password := os.Getenv("GOAUTO_SYB_PASSWORD"); password != "" {
|
|
ExtConfig.SYB.Password = password
|
|
}
|
|
if username := strings.TrimSpace(os.Getenv("GOAUTO_YEEKE_USERNAME")); username != "" {
|
|
ExtConfig.Yeeke.Username = username
|
|
}
|
|
// `[必须]` Taken verbatim, same reasoning as GOAUTO_SYB_PASSWORD above.
|
|
if password := os.Getenv("GOAUTO_YEEKE_PASSWORD"); password != "" {
|
|
ExtConfig.Yeeke.Password = password
|
|
}
|
|
|
|
dsn := strings.TrimSpace(os.Getenv("GOAUTO_DB_DSN"))
|
|
if dsn == "" {
|
|
return
|
|
}
|
|
driver := strings.TrimSpace(os.Getenv("GOAUTO_DB_DRIVER"))
|
|
if driver == "" {
|
|
driver = "mysql"
|
|
}
|
|
sdkconfig.DatabaseConfig.Driver = driver
|
|
sdkconfig.DatabaseConfig.Source = dsn
|
|
for _, database := range sdkconfig.DatabasesConfig {
|
|
database.Driver = driver
|
|
database.Source = dsn
|
|
}
|
|
}
|