Stage B step 1. The shop list decides which of a SYB account's shops get
imported.
Matching is by name, so the normalized form is the real key: whitespace
trimmed, full-width ASCII folded, letters lowercased. " ABC店 ", "ABC店"
and "abc店" are one shop. DisplayName is only ever shown to people, and
the unique index sits on the normalized column so a caller that skips
normalization cannot create rows that look identical on screen.
Interior whitespace is deliberately not collapsed: two shops differing
only there are still two shops, and merging them silently would be worse
than the duplicate this prevents.
Soft delete uses the DeletedFlag sentinel rather than a nullable
deleted_at in the unique index, which is silently inert because unique
indexes treat every NULL as distinct. Mutation-tested: dropping the
sentinel from the index makes the recreate-after-delete case fail.
The version-local migration file is included this time — the model alone
would never reach an existing database.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Adding SYBSession to migrations.Migrate was not enough. Schema reaches an
existing database only through a version-local file; with the previous
version already recorded in sys_migration, Migrate never re-ran and the
table simply never appeared. The unit tests build a fresh database every
time, so they stayed green while the real database was missing a table —
which surfaced as "Error 1146: Table 'goauto.syb_session' doesn't exist"
on the first import attempt.
server/.gitignore was hiding these files. go-admin ignores version-local
because it is where generated local migrations land, but every GoAuto
migration belongs in version control; the existing ones had been forced
in with `git add -f`. Un-ignoring *.go there also recovers four migrations
that were never committed at all — 1786700000000 through 1786700300000,
covering the base schema, device registration, heartbeat and collection
execution. A fresh clone could not have built a working database.
Guard the class of mistake rather than just this instance:
- migrations.VerifyTables checks every model's table after migrating and
names what is missing along with the fix.
- The migrate command runs it, so the failure lands at migrate time
instead of at the first request that needs the table.
- initDB no longer discards migrateModel's error. Upstream had
`_ = migrateModel()` followed by an unconditional "初始化成功", so a
failed migration reported success and the launcher believed it.
Also records the two-step rule in Common-Changes: a new model needs both
the model registration and a new version file.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The startup output was dominated by two upstream behaviours. Migrate
printed a bare count for every already-applied migration — a lone "1"
with no version, no explanation — through the standard logger, hence
stderr, which the launcher's `2>&1 |` renders as a red PowerShell error
block that looks like a failed migration. And the version-existence check
echoed one SELECT count(*) per version. Both replaced: a Warn-level
session for the check, and one summary line on stdout.
Console encoding needed more than [Console]::OutputEncoding: PowerShell
5.1 decodes child-process output by the console code page, so chcp 65001
goes with it.
The regression test capture was wrong twice and mutation testing caught
both. It captured only stdout while the bug wrote to stderr, and it
compared lines to "1" when the logger prefixes a timestamp, so the
assertion could never fail. It now captures the standard logger too and
matches with a pattern that allows the prefix — verified by reinstating
the bug and watching the test fail.
Not verified: the PowerShell edits, which need a Windows run.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Diagnosing "credential not configured" required reading the source: the
local config layer was a silent no-op when it found no file, and the
import error named only environment variables even though config.yaml is
now the normal place to configure this — sending operators to look in a
file that was never the problem.
Startup now logs which config.yaml was loaded, or that none was found and
where it looked, followed by whether SYB credentials resolved and from
which layer. Presence only, never values: server logs get pasted into
tickets.
The import error now names the file that was actually consulted and
distinguishes "no config.yaml found" from "found it, but it has no syb
credentials" — two problems with different fixes that previously produced
identical text.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The repo already layered file defaults under environment overrides, on
both the backend (settings.yml < GOAUTO_*) and the frontend
(.env.production < process.env). What was missing was a translator for
production: config.yaml only ever existed for the PowerShell launchers,
so a packaged binary read none of it and had no database credentials
either — SYB was inheriting an existing gap, not creating one.
The server now reads config.yaml itself, between settings.yml and the
environment. Lookup is GOAUTO_CONFIG, then ./config.yaml, then beside the
executable, so a packaged binary works wherever it is started. An absent
file is not an error: containers supply everything through the
environment. Scalars are read by YAML type and coerced, so an unquoted
all-digit password cannot take startup down over a quoting detail.
This removed the need for a Read-SybConfig in PowerShell: the launcher
just hands over the path it already knows, rather than reimplementing a
YAML parser.
The server also serves the built frontend when dist is present, which is
what .env.production's empty VUE_APP_BASE_API already assumes. The
history fallback is restricted to non-API GETs, and is not installed at
all without dist, so development 404s stay 404s.
Precedence is mutation-tested: applying the local file after the
environment instead of before makes the layering test fail.
Not verified: the PowerShell change and any Windows deployment — both
need a run on the Windows side.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>