Stage B steps 3 and 4.
The filter runs after loadDailyList has proved the day's snapshot whole,
never before. Filtering first would let drift among other shops' orders
hide a hole in the ones we do want. OrderCount stays the unfiltered total
alongside AcceptedCount and ShopSkipped, so a shop missing from the
allow-list shows up as a number rather than as absent data.
An empty allow-list is a refusal, not "import everything" — the latter
looks exactly like a filter that works. Mutation-tested, along with the
detail-level shop re-check that catches a list and detail response
disagreeing about which shop an order belongs to.
ShopBreakdown counts orders per shop including skipped ones, under the
stored display name rather than SYB's spelling, so one shop cannot appear
under two spellings. #50 renders it.
Discovery lives in sybimport, not sybshop: it needs the SYB client, and
sybimport already depends on sybshop for the filter, so the reverse would
be an import cycle. It reads the list endpoint only and never adds a
shop — widening what gets imported stays an explicit action.
Read and write routes are registered separately. GoAuto inherits
go-admin's per-path permission model, so "配置仅管理员" is expressible but
not enforced in code; the grant is configured in 系统管理.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Stage B step 2 (service layer; HTTP handlers next).
Rename rewrites NormalizedName along with DisplayName. Leaving the key
stale would show the new name while still matching the old one, so the
archive would look correctly configured while importing a different shop.
Mutation-tested: updating only display_name makes the rename test fail.
Duplicates are reported against the name already stored, not the one just
submitted — the two can differ only in case or character width, and
echoing back what was typed reads as the system rejecting a name it does
not have.
Delete is a soft delete carrying the id into DeletedFlag, so the same
name can be added again afterwards while past sync records keep resolving
the old row.
EnabledNames returns an empty map without error. Empty is a legitimate
state that callers must turn into "refuse to sync", never "import
everything".
MarkSeen only updates shops already on the list. A sync must not grow the
allow-list as a side effect; discovery is a separate explicit action.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>