mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-13 03:13:41 +08:00
fix(wiki): cascade + scan must not null content/summary via FieldStrategy.ALWAYS
This commit is contained in:
parent
e9bbce862c
commit
296c91d6da
@ -269,15 +269,24 @@ public class WikiLintJobService {
|
||||
* broken_links_scanned_at. Kept in this service so the scan loop above
|
||||
* is unambiguously per-page transactional without polluting the larger
|
||||
* WikiPageService API.
|
||||
* <p>
|
||||
* Uses {@link com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper}
|
||||
* with explicit {@code set()} calls instead of {@code updateById(partial entity)}.
|
||||
* Several columns on {@link WikiPageEntity} (content, summary,
|
||||
* outgoing_links, broken_links) carry {@code FieldStrategy.ALWAYS} so an
|
||||
* entity-style update with those fields left null would generate
|
||||
* {@code SET content = NULL, summary = NULL} and wipe the page body.
|
||||
* The wrapper-based update only emits SET clauses for the three columns
|
||||
* we mean to touch.
|
||||
*/
|
||||
@Transactional(propagation = Propagation.REQUIRES_NEW)
|
||||
public void rewriteBrokenLinks(Long pageId, String outgoingLinksJson, String brokenLinksJson) {
|
||||
WikiPageEntity update = new WikiPageEntity();
|
||||
update.setId(pageId);
|
||||
update.setOutgoingLinks(outgoingLinksJson);
|
||||
update.setBrokenLinks(brokenLinksJson);
|
||||
update.setBrokenLinksScannedAt(LocalDateTime.now());
|
||||
pageMapper.updateById(update);
|
||||
pageMapper.update(null,
|
||||
new com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper<WikiPageEntity>()
|
||||
.eq(WikiPageEntity::getId, pageId)
|
||||
.set(WikiPageEntity::getOutgoingLinks, outgoingLinksJson)
|
||||
.set(WikiPageEntity::getBrokenLinks, brokenLinksJson)
|
||||
.set(WikiPageEntity::getBrokenLinksScannedAt, LocalDateTime.now()));
|
||||
}
|
||||
|
||||
private void updateJob(Long kbId, String jobId, java.util.function.Function<LintJob, LintJob> fn) {
|
||||
|
||||
@ -679,13 +679,19 @@ public class WikiPageService {
|
||||
}
|
||||
WikiLinkService.LinkAnalysis a = linkService.analyze(rewritten, activeForThisReferrer);
|
||||
|
||||
WikiPageEntity update = new WikiPageEntity();
|
||||
update.setId(referrer.getId());
|
||||
update.setContent(rewritten);
|
||||
update.setOutgoingLinks(linkService.toJsonArray(a.outgoingLinks()));
|
||||
update.setBrokenLinks(linkService.toJsonArray(a.brokenLinks()));
|
||||
update.setBrokenLinksScannedAt(LocalDateTime.now());
|
||||
pageMapper.updateById(update);
|
||||
// LambdaUpdateWrapper — content, summary, outgoing_links and
|
||||
// broken_links all carry FieldStrategy.ALWAYS on WikiPageEntity,
|
||||
// so a partial-entity updateById would generate SET summary=NULL
|
||||
// (and clear any other ALWAYS column we didn't explicitly set).
|
||||
// The wrapper-based update only writes the four columns we mean
|
||||
// to touch, leaving summary and the rest intact.
|
||||
pageMapper.update(null,
|
||||
new com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper<WikiPageEntity>()
|
||||
.eq(WikiPageEntity::getId, referrer.getId())
|
||||
.set(WikiPageEntity::getContent, rewritten)
|
||||
.set(WikiPageEntity::getOutgoingLinks, linkService.toJsonArray(a.outgoingLinks()))
|
||||
.set(WikiPageEntity::getBrokenLinks, linkService.toJsonArray(a.brokenLinks()))
|
||||
.set(WikiPageEntity::getBrokenLinksScannedAt, LocalDateTime.now()));
|
||||
affected.add(referrer.getId());
|
||||
}
|
||||
return affected;
|
||||
@ -804,13 +810,16 @@ public class WikiPageService {
|
||||
}
|
||||
WikiLinkService.LinkAnalysis a = linkService.analyze(rewritten, activeForThisReferrer);
|
||||
|
||||
WikiPageEntity update = new WikiPageEntity();
|
||||
update.setId(referrer.getId());
|
||||
update.setContent(rewritten);
|
||||
update.setOutgoingLinks(linkService.toJsonArray(a.outgoingLinks()));
|
||||
update.setBrokenLinks(linkService.toJsonArray(a.brokenLinks()));
|
||||
update.setBrokenLinksScannedAt(LocalDateTime.now());
|
||||
pageMapper.updateById(update);
|
||||
// LambdaUpdateWrapper to avoid the FieldStrategy.ALWAYS-induced
|
||||
// null overwrite on summary (and other ALWAYS columns we don't
|
||||
// touch in a rename).
|
||||
pageMapper.update(null,
|
||||
new com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper<WikiPageEntity>()
|
||||
.eq(WikiPageEntity::getId, referrer.getId())
|
||||
.set(WikiPageEntity::getContent, rewritten)
|
||||
.set(WikiPageEntity::getOutgoingLinks, linkService.toJsonArray(a.outgoingLinks()))
|
||||
.set(WikiPageEntity::getBrokenLinks, linkService.toJsonArray(a.brokenLinks()))
|
||||
.set(WikiPageEntity::getBrokenLinksScannedAt, LocalDateTime.now()));
|
||||
affected.add(referrer.getId());
|
||||
}
|
||||
return affected;
|
||||
|
||||
@ -400,3 +400,78 @@ End-to-end "user reports broken link → lint reveals all → delete or
|
||||
rename a page → cascade clears the dangling tokens" flow is reproducible
|
||||
on a clean dev box in under three minutes (KB create + ingest + verify).
|
||||
|
||||
---
|
||||
|
||||
## 8. Second pass — multi-referrer / code-block / round-trip (2026-05-28)
|
||||
|
||||
Extended e2e with deeper scenarios. **Caught and fixed one data-loss bug**
|
||||
before publishing the pass report.
|
||||
|
||||
### 8.0 Setup
|
||||
|
||||
Fresh KB `E2E-RFC55-StressKB`. Ingested a 5-entity team handbook (Alice
|
||||
Chen, Bob Patel, Carol Liu, Crawler subsystem, Indexing project) and
|
||||
manually edited `overview` to fan in references to all five plus a
|
||||
fenced-code block + inline-code block both containing literal
|
||||
`[[alice-chen]]` examples.
|
||||
|
||||
### 8.1 Scenarios run
|
||||
|
||||
| Scenario | What it covers | Result |
|
||||
|---|---|---|
|
||||
| **B. Multi-referrer cascade delete** | Delete `carol-liu` with 2 referrers (`indexing-project` + `overview`); only non-empty content gets rewritten | ✅ overview's `[[carol-liu]]` (1×) demoted to plain "Carol Liu"; outgoing updated; audit `affectedPageIds:[overview_id]` |
|
||||
| **C. Code-block protection during cascade** | Delete `alice-chen`; overview has `[[alice-chen]]` 2× in prose AND 2× in fenced/inline-code blocks | ✅ Prose `[[alice-chen]]` and `[[alice-chen\|Alice]]` demoted to `Alice Chen` / `Alice`; **code block byte-for-byte preserved**: ```` ```markdown\nUse [[alice-chen]] or [[alice-chen\|some alias]] to link to a teammate.\n``` ```` |
|
||||
| **D. Multi-referrer cascade rename + alias preservation** | Rename `bob-patel` → `robert-patel` with 2 referrers (overview has `[[bob-patel\|Bob the pair-programmer]]`, log has `[[bob-patel]]` + `[[bob-patel\|Bob]]`) | ✅ All 3 occurrences across both pages rewrite to `robert-patel`, aliases preserved; old slug → 404; audit `affectedPageIds=[overview_id, log_id]` |
|
||||
| **E. Break-then-fix round trip** | PUT log with `[[nonexistent-1]] [[also-fake]] [[robert-patel]]` → scan → fix via PUT with valid slugs only → re-scan | ✅ Break: `broken_links=["nonexistent-1","also-fake"]` synchronously, scan aggregate shows 2 refs across 1 page. Fix: `broken_links=[]` synchronously, scan aggregate clean |
|
||||
| **F. Archive + scan interaction** | Archive `crawler-subsystem`; refs index excludes it by default, includes with `?includeArchived=true` (with `archived:true`) | ✅ Default refs hides; `?includeArchived=true` returns it with the flag |
|
||||
|
||||
### 8.2 Bug found and fixed mid-run
|
||||
|
||||
While re-reading the multi-referrer cascade output, noticed that
|
||||
`indexing-project` had `content_len=0` even though it had been a referrer
|
||||
to `carol-liu`. Tracing down: every page in the KB had `content` and
|
||||
`summary` set to `NULL` after any of the following ran:
|
||||
|
||||
1. `WikiLintJobService.rewriteBrokenLinks` — runs on every KB-wide scan
|
||||
2. `WikiPageService.cascadeStripReferrers` — runs on every cascade delete
|
||||
3. `WikiPageService.cascadeRenameReferrers` — runs on every cascade rename
|
||||
|
||||
All three built a partial `WikiPageEntity` setting only the fields they
|
||||
intended to update (`id` + `outgoing_links` + `broken_links` + `broken_links_scanned_at`),
|
||||
then called `pageMapper.updateById(partialEntity)`. But `WikiPageEntity`
|
||||
declares `FieldStrategy.ALWAYS` on `content`, `summary`, `outgoingLinks`,
|
||||
and `brokenLinks`, so MyBatis-Plus generated `UPDATE ... SET content =
|
||||
NULL, summary = NULL, ...` — silently destroying the body of every page
|
||||
the cascade or scan touched.
|
||||
|
||||
**Fix**: replace `updateById(partialEntity)` with
|
||||
`update(null, new LambdaUpdateWrapper<WikiPageEntity>().eq(...).set(col, val))`
|
||||
in all three sites. The wrapper-based path emits SET clauses only for
|
||||
explicit `.set()` calls, so unmentioned columns are untouched regardless
|
||||
of their `FieldStrategy`.
|
||||
|
||||
### 8.3 Post-fix verification
|
||||
|
||||
After restart with the fixed jar:
|
||||
|
||||
- Scan x 3 on a page with 113-char content + summary → both **unchanged**
|
||||
(length stable at 113, summary string identical).
|
||||
- Cascade delete of `alice` with `bob` as referrer → bob's content went
|
||||
200 → 192 chars (the `[[alice]]` → `Alice` rewrite, ~8-char shrink as
|
||||
expected), summary fully preserved.
|
||||
- Cascade rename of `carol` → `caroline` with `dave` as referrer →
|
||||
dave's summary preserved verbatim.
|
||||
|
||||
The same `WikiEnrichmentApplierTest` + `WikiLinkServiceCascadeTest` +
|
||||
`WikiPageServiceTest` suites still pass; the bug was strictly in the
|
||||
write-back path that those tests didn't exercise (the cascade tests
|
||||
operate on pure-string helpers; the page-service test mocks the mapper
|
||||
so the actual SQL generated doesn't matter).
|
||||
|
||||
### 8.4 Follow-up
|
||||
|
||||
A regression-locking integration test (real Spring + H2) for "scan must
|
||||
not null content/summary" is worth adding in a separate PR — would have
|
||||
caught this class of bug at the boundary between MyBatis-Plus field
|
||||
strategy and partial-entity update calls. Tracked.
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user