Skip to content

Commit 0261645

Browse files
LuoDi-Nateclaude
andcommitted
feat(onboarding) · v0.7.2 · 系统内首次引导 + 修首登500 + README/FAQ易用性
从「第一次刷到 repo 的开发者」视角补易用性(PRD §9 / tech-design §九): - 修首登 500(关键):/ 无脑 redirect dashboard,而 dashboard 零周期 anchorPeriod 抛异常 + deploy.sh 清 period/account → 全新部署首次登录直接 500(砸开源新用户,beta/prod有数据未暴露) - HomeController / 改智能路由:未初始化(零周期 或 零账户)→ onboarding/index 引导页 (一句话流程 + 加账户/开周期/填余额 3 步直达 + 状态打勾 + 顺手改名链),否则 redirect dashboard - DashboardController 加零周期兜底 redirect:/;/entry 顶部加「周期流程」一句话(FR-135) - README:能力列表去版本号噪音、加最低系统要求(512M会OOM)、抽路径无关「第一次怎么用」 section(讲清周期生命周期+6步)、新增 docs/faq.md(远程访问/备份恢复/忘记密码/多家庭…) - OnboardingRoutingTest 4 例 + qa-run v07-ONB-1/2;纯增量0schema;mvn test 248 全绿 - 已部署 beta 验无回归(有数据→仍跳 dashboard;/entry 周期流程已渲染) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 7bb6bad commit 0261645

9 files changed

Lines changed: 277 additions & 2 deletions

File tree

CHANGELOG.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,24 @@
22

33
[Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/) 风格记录。每个版本详细需求见对应 [`prd/v0.X.md`](prd/),技术设计见 [`tech-design/v0.X.md`](tech-design/),QA case 见 [`docs/qa-cases.md`](docs/qa-cases.md)
44

5+
## [v0.7.2] · 待发(beta 验证中)
6+
7+
易用性第三批:从「第一次刷到 repo 的开发者」视角补「说不清 / 不够易用」。详 [`prd/v0.7.md`](prd/v0.7.md) §9 + [`tech-design/v0.7.md`](tech-design/v0.7.md) §九。
8+
9+
### Fixed
10+
11+
- **修首登 500(关键)**:`/` 旧实现无脑 `redirect:/dashboard`,而 dashboard 零周期时 `anchorPeriod()` 抛异常 + `deploy.sh` 清数据会清空 `period`/`account`**全新部署首次登录直接 500**(砸在开源新用户身上,beta/prod 有数据未暴露)。
12+
13+
### Added
14+
15+
- **系统内首次引导**(FR-133~135):`/` 改智能路由——未初始化(零周期 或 零账户)→ 渲染引导页 `onboarding/index`(一句话讲清「开周期→填余额→关周期→出报告」+ 3 步直达按钮 + 状态打勾 + 顺手改名链),否则 → `redirect:/dashboard`;`/dashboard` 加零周期兜底 `redirect:/``/entry` 顶部加「周期流程」一句话。
16+
- **README / 文档易用性**:主要能力列表去 `(v0.x)` 版本号噪音(改干净能力总览);加「最低系统要求」(1G 内存,512M 会 OOM);抽出路径无关「部署好了:第一次怎么用」section(讲清周期生命周期 + 6 步,Docker/直装通用);新增 `docs/faq.md`(远程访问 / 备份恢复 / 忘记密码 / 多家庭 / 改 env 不生效 等)。
17+
18+
### 兼容 / 红线
19+
20+
- 纯增量 0 schema;`/dashboard` 只加一行零周期兜底;存量(有数据)家庭 `/` 行为不变(仍直达 dashboard)。
21+
- 无 emoji 用 inline SVG;文案不用技术黑话。`OnboardingRoutingTest` 4 例 + qa-run v07-ONB-1/2 守护;mvn test 248 全绿。
22+
523
## [v0.7.1] · 2026-06-16
624

725
开源用户「跑起来之后怎么接外部服务」的易用性改造 —— 配置引导。详 [`prd/v0.7.md`](prd/v0.7.md) §8 + [`tech-design/v0.7.md`](tech-design/v0.7.md) §八。

docs/qa-cases.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1294,6 +1294,26 @@ Docker 化部署 + systemd/macOS 存量零丢迁移。**真机冒烟(docker buil
12941294
| 私密 | 审计 `/admin/audit` 测试事件只记 vendor + 成功/失败归类,无 key 明文;`PrivacyIsolationTest` 绿 |
12951295
| 文档入口 | README「文档」「配置项」「首次登录」三处都能跳到 `docs/configuration.md` |
12961296

1297+
### v0.7 第三批 · 系统内首次引导(2026-06-16)
1298+
1299+
**黑盒 · qa-run(v07-ONB-* · 静态)**
1300+
1301+
| Case | 校验 |
1302+
|---|---|
1303+
| v07-ONB-1 | `HomeController` `/` 智能路由(零周期/零账户→`onboarding/index`,有数据→`redirect:/dashboard`)+ `DashboardController` 零周期兜底 `redirect:/`(修首登 500);`onboarding/index.html` 存在 |
1304+
| v07-ONB-2 | 引导页含「加账户 / 开本期周期」起步步骤;`/entry` 顶部有「周期流程」说明;`OnboardingRoutingTest`|
1305+
1306+
**单元 · OnboardingRoutingTest**:零周期/零账户→onboarding;有账户无周期→onboarding;有周期无账户→onboarding;两者齐→redirect dashboard。
1307+
1308+
**人工 · 真机验收(全新装)**
1309+
1310+
| 场景 | 校验 |
1311+
|---|---|
1312+
| 首登不崩 | 全新部署(零周期零账户)首次登录 → **不再 500**,落到引导页 |
1313+
| 引导可用 | 引导页见「开→填→关→出报告」一句话流程 + 3 步直达按钮;加账户/开周期后对应步骤打勾 |
1314+
| 完成即隐 | 加好账户 + 开好周期后,`/` 自动 redirect `/dashboard` |
1315+
| entry 说明 | `/entry` 顶部见「周期流程:开→填(本页)→关→出报告」 |
1316+
12971317
**backward-compat 红线**
12981318
-`deploy.sh`(systemd 直装/迭代)路径不动,存量(含 prod/beta)零破坏
12991319
- 迁移前强制 mysqldump、全程不删旧部署、可回滚;共用 schema_history 防重放

scripts/qa-run.sh

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2637,6 +2637,27 @@ grep -q 'aliyun-sms-setup.md' "$RD/src/main/resources/templates/admin/notificati
26372637
&& log_ok "v07-CFG-5 短信页有「配置指南」文档链" \
26382638
|| log_bad "v07-CFG-5 短信页缺文档链" "see notification.html"
26392639

2640+
section "v0.7 第三批 · 系统内首次引导(静态守护)"
2641+
HC="$RD/src/main/java/com/family/finance/common/HomeController.java"
2642+
DC="$RD/src/main/java/com/family/finance/web/dashboard/DashboardController.java"
2643+
2644+
# v07-ONB-1 落地页智能路由 + onboarding 模板 + 首登500兜底
2645+
{ [[ -f "$RD/src/main/resources/templates/onboarding/index.html" ]] \
2646+
&& grep -q 'onboarding/index' "$HC" \
2647+
&& grep -q 'redirect:/dashboard' "$HC" \
2648+
&& grep -q 'countByFamily' "$HC" \
2649+
&& grep -q 'redirect:/' "$DC"; } \
2650+
&& log_ok "v07-ONB-1 / 智能路由(onboarding/dashboard)+ dashboard 零周期兜底 redirect" \
2651+
|| log_bad "v07-ONB-1 首次引导路由/兜底缺失" "see HomeController/DashboardController"
2652+
2653+
# v07-ONB-2 引导页起步步骤 + /entry 周期流程说明
2654+
{ grep -q '加账户' "$RD/src/main/resources/templates/onboarding/index.html" \
2655+
&& grep -q '开本期周期' "$RD/src/main/resources/templates/onboarding/index.html" \
2656+
&& grep -q '周期流程' "$RD/src/main/resources/templates/entry/index.html" \
2657+
&& [[ -f "$RD/src/test/java/com/family/finance/web/OnboardingRoutingTest.java" ]]; } \
2658+
&& log_ok "v07-ONB-2 引导 3 步 + entry 周期流程说明 + 路由单测在" \
2659+
|| log_bad "v07-ONB-2 引导内容/单测缺失" "see onboarding/entry"
2660+
26402661
echo
26412662
echo "═══════════════════════════════════════"
26422663
echo " 总结: PASS=$PASS FAIL=$FAIL SKIP=$SKIP"
Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,38 @@
11
package com.family.finance.common;
22

3+
import com.family.finance.auth.MemberPrincipal;
4+
import com.family.finance.repository.AccountMapper;
5+
import com.family.finance.repository.PeriodMapper;
6+
import lombok.RequiredArgsConstructor;
7+
import org.springframework.security.core.annotation.AuthenticationPrincipal;
38
import org.springframework.stereotype.Controller;
9+
import org.springframework.ui.Model;
410
import org.springframework.web.bind.annotation.GetMapping;
511

12+
/**
13+
* 落地页路由。v0.7 第三批(FR-133):
14+
* <p>未初始化(零周期 或 零账户)的家庭 → 渲染首次引导页 {@code onboarding/index};
15+
* 否则 → redirect /dashboard(原行为)。
16+
* <p>这同时修了一个首登崩溃:旧实现无脑转 /dashboard,而 dashboard 在零周期时
17+
* {@code anchorPeriod()} 抛异常 → 全新部署首次登录 500(见 tech-design §九)。
18+
*/
619
@Controller
20+
@RequiredArgsConstructor
721
public class HomeController {
822

23+
private final PeriodMapper periodMapper;
24+
private final AccountMapper accountMapper;
25+
926
@GetMapping("/")
10-
public String home() {
11-
return "redirect:/dashboard";
27+
public String home(@AuthenticationPrincipal MemberPrincipal me, Model model) {
28+
long fid = me.getFamilyId();
29+
boolean hasPeriod = periodMapper.countByFamily(fid) > 0;
30+
boolean hasAccount = !accountMapper.findActiveByFamily(fid).isEmpty();
31+
if (hasPeriod && hasAccount) {
32+
return "redirect:/dashboard";
33+
}
34+
model.addAttribute("hasAccount", hasAccount);
35+
model.addAttribute("hasPeriod", hasPeriod);
36+
return "onboarding/index";
1237
}
1338
}

src/main/java/com/family/finance/web/dashboard/DashboardController.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,10 @@ public String dashboard(@AuthenticationPrincipal MemberPrincipal me,
6565
@RequestParam(required = false) String currency,
6666
@RequestHeader(value = "HX-Request", required = false) String htmx,
6767
Model model) {
68+
// v0.7 FR-133 兜底:零周期(全新部署)→ 回首页引导,避免 anchorPeriod() 抛异常 500
69+
if (periodMapper.countByFamily(me.getFamilyId()) == 0) {
70+
return "redirect:/";
71+
}
6872
String accountsCsv = accounts == null || accounts.isEmpty()
6973
? null
7074
: accounts.stream().map(String::valueOf).collect(java.util.stream.Collectors.joining(","));

src/main/resources/templates/entry/index.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ <h1 class="font-display text-3xl sm:text-5xl font-medium tracking-tight">
1717
th:text="${#temporals.format(period.periodStart, 'yyyy.MM.dd')} + ' - ' + ${#temporals.format(period.periodEnd, 'yyyy.MM.dd')}">2026.05.01 - 2026.05.31</span>
1818
</h1>
1919
<p class="text-ink-soft text-sm mt-2">余额驱动填报:保存期末余额后,系统会按已登记的收入/支出/账户间划转实时轧差。</p>
20+
<p class="font-mono text-[10px] text-ink-subtle mt-1 tracking-wider">周期流程:开周期 → <b class="text-ink">填本期余额(本页)</b> → 关周期 → 出净资产 / 收益报告</p>
2021
</div>
2122
<div class="flex items-center gap-3">
2223
<a th:href="@{/entry(mine=${mineOnly})}" class="btn-paper no-underline">本期</a>
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
<!DOCTYPE html>
2+
<html lang="zh-CN" xmlns:th="http://www.thymeleaf.org">
3+
<head th:replace="~{fragments/layout :: head('开始使用 · 家庭账房')}"></head>
4+
<body class="min-h-screen relative">
5+
<div class="fixed top-0 left-0 right-0 h-1 bg-ink z-40"></div>
6+
<header th:replace="~{fragments/nav :: nav('dashboard', ${me}, ${nav})}"></header>
7+
8+
<main class="max-w-[820px] mx-auto px-6 lg:px-10 py-12 relative z-10">
9+
10+
<div class="eyebrow mb-2">开 · 始 · 使 · 用</div>
11+
<h1 class="font-display text-3xl sm:text-4xl font-medium tracking-tight">三步,开始用家庭账房</h1>
12+
<p class="text-ink-soft text-sm mt-3 leading-relaxed max-w-2xl">
13+
本工具按「<b>周期</b>」记账 —— 一个周期 = 一次月度快照。流程是
14+
<span class="font-mono text-[12px] bg-card-soft border border-rule-soft px-2 py-0.5">开周期 → 填本期余额 → 关周期 → 出净资产 / 收益报告</span>
15+
每月花 10 分钟填一次,不用逐笔记。下面三步走完就能看到你家的资产全局图。
16+
</p>
17+
18+
<div class="mt-8 space-y-4">
19+
20+
<!-- ① 加账户 -->
21+
<div class="bracket-card flex items-center gap-4" th:classappend="${hasAccount} ? ' opacity-70' : ' ring-1 ring-brass'">
22+
<span th:if="${hasAccount}" class="shrink-0 w-8 h-8 rounded-full border-2 border-forest text-forest flex items-center justify-center">
23+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3"><path d="M5 13l4 4L19 7"/></svg>
24+
</span>
25+
<span th:unless="${hasAccount}" class="shrink-0 w-8 h-8 rounded-full border-2 border-ink text-ink font-display flex items-center justify-center">1</span>
26+
<div class="flex-1 min-w-0">
27+
<div class="font-display text-lg flex items-center gap-2">加账户
28+
<span th:if="${hasAccount}" class="font-mono text-[10px] text-forest border border-forest px-1.5">已完成</span></div>
29+
<div class="text-ink-soft text-[13px] leading-relaxed">把银行卡 / 支付宝 / 券商 / 房 / 房贷加进来 —— 向导式,6 类账户、13 个内置模板。</div>
30+
</div>
31+
<a th:href="@{/accounts/new}" class="btn-ink no-underline shrink-0" th:text="${hasAccount} ? '再加一个' : '去加账户'">去加账户</a>
32+
</div>
33+
34+
<!-- ② 开周期 -->
35+
<div class="bracket-card flex items-center gap-4" th:classappend="${hasPeriod ? ' opacity-70' : (hasAccount ? ' ring-1 ring-brass' : '')}">
36+
<span th:if="${hasPeriod}" class="shrink-0 w-8 h-8 rounded-full border-2 border-forest text-forest flex items-center justify-center">
37+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3"><path d="M5 13l4 4L19 7"/></svg>
38+
</span>
39+
<span th:unless="${hasPeriod}" class="shrink-0 w-8 h-8 rounded-full border-2 border-ink text-ink font-display flex items-center justify-center">2</span>
40+
<div class="flex-1 min-w-0">
41+
<div class="font-display text-lg flex items-center gap-2">开本期周期
42+
<span th:if="${hasPeriod}" class="font-mono text-[10px] text-forest border border-forest px-1.5">已完成</span></div>
43+
<div class="text-ink-soft text-[13px] leading-relaxed">在周期管理里点「立即开下一周期」,开一个本月的记账周期。</div>
44+
</div>
45+
<a th:href="@{/admin/periods}" class="btn-ink no-underline shrink-0" th:text="${hasPeriod} ? '去管理' : '去开周期'">去开周期</a>
46+
</div>
47+
48+
<!-- ③ 填余额 -->
49+
<div class="bracket-card flex items-center gap-4 opacity-70">
50+
<span class="shrink-0 w-8 h-8 rounded-full border-2 border-ink text-ink font-display flex items-center justify-center">3</span>
51+
<div class="flex-1 min-w-0">
52+
<div class="font-display text-lg">填本期余额</div>
53+
<div class="text-ink-soft text-[13px] leading-relaxed">把各账户月末余额填一下(再记几笔大额收支),保存后就能在仪表盘看到净资产和真实收益。</div>
54+
</div>
55+
<a th:href="@{/entry}" class="btn-paper no-underline shrink-0">去填报</a>
56+
</div>
57+
58+
</div>
59+
60+
<div class="paper-card p-5 mt-8">
61+
<div class="eyebrow mb-2">顺 · 手(可选)</div>
62+
<div class="text-[13px] flex flex-wrap gap-x-5 gap-y-2">
63+
<a th:href="@{/admin/family}" class="text-brass-deep underline">改家庭名 / 图标</a>
64+
<a th:href="@{/admin/members}" class="text-brass-deep underline">把成员名改成你和家人</a>
65+
<a href="https://github.com/LuoDi-Nate/financial-management/blob/master/docs/configuration.md" target="_blank" rel="noreferrer" class="text-brass-deep underline">接 AI 体检 / 短信提醒(全部可选)</a>
66+
</div>
67+
</div>
68+
69+
<p class="font-mono text-[10px] text-ink-subtle mt-6 tracking-wider">加好账户 + 开好周期后,这个页面会自动换成仪表盘。</p>
70+
71+
</main>
72+
</body>
73+
</html>
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package com.family.finance.web;
2+
3+
import com.family.finance.auth.MemberPrincipal;
4+
import com.family.finance.common.HomeController;
5+
import com.family.finance.domain.account.Account;
6+
import com.family.finance.domain.member.Member;
7+
import com.family.finance.repository.AccountMapper;
8+
import com.family.finance.repository.PeriodMapper;
9+
import org.junit.jupiter.api.Test;
10+
import org.springframework.ui.ExtendedModelMap;
11+
import org.springframework.ui.Model;
12+
13+
import java.util.List;
14+
15+
import static org.assertj.core.api.Assertions.assertThat;
16+
import static org.mockito.Mockito.mock;
17+
import static org.mockito.Mockito.when;
18+
19+
/**
20+
* v0.7 FR-133/134 · 落地页智能路由:
21+
* 未初始化(零周期 或 零账户)→ onboarding;有数据 → redirect dashboard。
22+
* 同时是首登 500 修复的回归保护(零周期不再无脑转 dashboard)。
23+
*/
24+
class OnboardingRoutingTest {
25+
26+
private final PeriodMapper periodMapper = mock(PeriodMapper.class);
27+
private final AccountMapper accountMapper = mock(AccountMapper.class);
28+
private final HomeController controller = new HomeController(periodMapper, accountMapper);
29+
30+
private MemberPrincipal principal() {
31+
return new MemberPrincipal(Member.builder().id(2L).familyId(1L).username("diwa").build());
32+
}
33+
34+
@Test
35+
void zeroPeriodAndZeroAccount_goesToOnboarding() {
36+
when(periodMapper.countByFamily(1L)).thenReturn(0);
37+
when(accountMapper.findActiveByFamily(1L)).thenReturn(List.of());
38+
Model model = new ExtendedModelMap();
39+
assertThat(controller.home(principal(), model)).isEqualTo("onboarding/index");
40+
assertThat(model.getAttribute("hasAccount")).isEqualTo(false);
41+
assertThat(model.getAttribute("hasPeriod")).isEqualTo(false);
42+
}
43+
44+
@Test
45+
void hasAccountButZeroPeriod_stillOnboarding() {
46+
when(periodMapper.countByFamily(1L)).thenReturn(0);
47+
when(accountMapper.findActiveByFamily(1L)).thenReturn(List.of(Account.builder().id(1L).build()));
48+
Model model = new ExtendedModelMap();
49+
assertThat(controller.home(principal(), model)).isEqualTo("onboarding/index");
50+
assertThat(model.getAttribute("hasAccount")).isEqualTo(true);
51+
assertThat(model.getAttribute("hasPeriod")).isEqualTo(false);
52+
}
53+
54+
@Test
55+
void hasPeriodButZeroAccount_stillOnboarding() {
56+
when(periodMapper.countByFamily(1L)).thenReturn(2);
57+
when(accountMapper.findActiveByFamily(1L)).thenReturn(List.of());
58+
assertThat(controller.home(principal(), new ExtendedModelMap())).isEqualTo("onboarding/index");
59+
}
60+
61+
@Test
62+
void hasPeriodAndAccount_redirectsToDashboard() {
63+
when(periodMapper.countByFamily(1L)).thenReturn(3);
64+
when(accountMapper.findActiveByFamily(1L)).thenReturn(List.of(Account.builder().id(1L).build()));
65+
assertThat(controller.home(principal(), new ExtendedModelMap())).isEqualTo("redirect:/dashboard");
66+
}
67+
}

0 commit comments

Comments
 (0)