|
@@ -610,7 +610,23 @@ func TestRequestApproveTouchesFunds(t *testing.T) {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-// TestBillEndpointIdentity 端点 17:账单汇总恒等式(期末−期初 = 窗口内流水)+ 分页元信息。
|
|
|
|
|
|
|
+// TestBillEndpointIdentity 端点 17:账单恒等式 + **契约 v1.3 两个对账字段名** + 分页元信息。
|
|
|
|
|
+//
|
|
|
|
|
+// 契约口径(前端契约 v1.3 端点 17 注 ①,**字段名由契约定、后端照做**):
|
|
|
|
|
+// - `flowExclusiveEnd`:与「期末 − 期初」**对齐**的当期发生额(右开口径,不含 `to` 当天);
|
|
|
|
|
+// - `flowInclusiveEnd`:**含 `to` 当天**的当期发生额(dump 的 `BETWEEN` 口径)。
|
|
|
|
|
+// 二者之差 = 落在 `to` 当天的流水金额。
|
|
|
|
|
+//
|
|
|
|
|
+// 为什么两个都要下发(不能只留一个):只留一个就没法让前端**看见**不对称,
|
|
|
|
|
+// 而契约 v1.2 那句"期末余额与当期发生额都按 `< to`"的错句,根因正是"看不见"。
|
|
|
|
|
+//
|
|
|
|
|
+// 本用例因此**刻意造一笔落在 `to` 当天的流水**(300.00),断言:
|
|
|
|
|
+// 1. 期末不含它(700.00)、`flowExclusiveEnd` 不含它(200.00);
|
|
|
|
|
+// 2. `flowInclusiveEnd` 含它(500.00);
|
|
|
|
|
+// 3. 二者之差 **恰好等于** 该笔金额(300.00)—— 也就是契约说的"落在 to 当天的流水"。
|
|
|
|
|
+//
|
|
|
|
|
+// 顺带守字段名:若有人把 service.BillSummary 的 json tag 改名,本用例立刻红
|
|
|
|
|
+// (`flowExclusiveEnd` 解出来是零值 ⇒ 差值断言必失败)。
|
|
|
func TestBillEndpointIdentity(t *testing.T) {
|
|
func TestBillEndpointIdentity(t *testing.T) {
|
|
|
db := newHandlerTestDB(t)
|
|
db := newHandlerTestDB(t)
|
|
|
app := newTestApp(t, db)
|
|
app := newTestApp(t, db)
|
|
@@ -623,8 +639,9 @@ func TestBillEndpointIdentity(t *testing.T) {
|
|
|
t.Fatalf("seed 流水失败: %v", err)
|
|
t.Fatalf("seed 流水失败: %v", err)
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
- seedLog("500.00", time.Date(2026, 2, 20, 10, 0, 0, 0, time.UTC))
|
|
|
|
|
- seedLog("200.00", time.Date(2026, 3, 5, 10, 0, 0, 0, time.UTC))
|
|
|
|
|
|
|
+ seedLog("500.00", time.Date(2026, 2, 20, 10, 0, 0, 0, time.UTC)) // 期初
|
|
|
|
|
+ seedLog("200.00", time.Date(2026, 3, 5, 10, 0, 0, 0, time.UTC)) // 窗口内
|
|
|
|
|
+ seedLog("300.00", time.Date(2026, 4, 1, 0, 0, 0, 0, time.UTC)) // **恰好落在 to 当天**
|
|
|
|
|
|
|
|
code, msg, data := doJSON(t, app, http.MethodGet,
|
|
code, msg, data := doJSON(t, app, http.MethodGet,
|
|
|
"/student/bill?userId="+user+"&from=2026-03-01&to=2026-04-01", nil)
|
|
"/student/bill?userId="+user+"&from=2026-03-01&to=2026-04-01", nil)
|
|
@@ -636,6 +653,7 @@ func TestBillEndpointIdentity(t *testing.T) {
|
|
|
PrevBalance string `json:"prevBalance"`
|
|
PrevBalance string `json:"prevBalance"`
|
|
|
EndBalance string `json:"endBalance"`
|
|
EndBalance string `json:"endBalance"`
|
|
|
FlowExclusiveEnd string `json:"flowExclusiveEnd"`
|
|
FlowExclusiveEnd string `json:"flowExclusiveEnd"`
|
|
|
|
|
+ FlowInclusiveEnd string `json:"flowInclusiveEnd"`
|
|
|
CampusIDs []int64 `json:"campusIds"`
|
|
CampusIDs []int64 `json:"campusIds"`
|
|
|
} `json:"summary"`
|
|
} `json:"summary"`
|
|
|
List []map[string]any `json:"list"`
|
|
List []map[string]any `json:"list"`
|
|
@@ -648,16 +666,25 @@ func TestBillEndpointIdentity(t *testing.T) {
|
|
|
t.Fatalf("账单体解码失败: %v", err)
|
|
t.Fatalf("账单体解码失败: %v", err)
|
|
|
}
|
|
}
|
|
|
if bill.Summary.PrevBalance != "500.00" || bill.Summary.EndBalance != "700.00" {
|
|
if bill.Summary.PrevBalance != "500.00" || bill.Summary.EndBalance != "700.00" {
|
|
|
- t.Fatalf("期初/期末异常:%+v", bill.Summary)
|
|
|
|
|
|
|
+ t.Fatalf("期初/期末异常(期末必须**不含** to 当天那笔 300.00):%+v", bill.Summary)
|
|
|
}
|
|
}
|
|
|
- // 恒等式:期末 − 期初 == 窗口内流水
|
|
|
|
|
|
|
+ // 恒等式:期末 − 期初 == flowExclusiveEnd(右开口径)
|
|
|
prev, _ := money.Parse(bill.Summary.PrevBalance)
|
|
prev, _ := money.Parse(bill.Summary.PrevBalance)
|
|
|
end, _ := money.Parse(bill.Summary.EndBalance)
|
|
end, _ := money.Parse(bill.Summary.EndBalance)
|
|
|
- flow, _ := money.Parse(bill.Summary.FlowExclusiveEnd)
|
|
|
|
|
- if end.Sub(prev) != flow {
|
|
|
|
|
- t.Fatalf("恒等式破:期末−期初=%s,窗口流水=%s", end.Sub(prev).String(), flow.String())
|
|
|
|
|
|
|
+ exclusive, _ := money.Parse(bill.Summary.FlowExclusiveEnd)
|
|
|
|
|
+ inclusive, _ := money.Parse(bill.Summary.FlowInclusiveEnd)
|
|
|
|
|
+ if end.Sub(prev) != exclusive {
|
|
|
|
|
+ t.Fatalf("恒等式破:期末−期初=%s,flowExclusiveEnd=%s", end.Sub(prev).String(), exclusive.String())
|
|
|
|
|
+ }
|
|
|
|
|
+ // 契约 v1.3:两口径差值 == 落在 to 当天的流水
|
|
|
|
|
+ if got, want := inclusive.Sub(exclusive), int64Money(t, "300.00"); got != want {
|
|
|
|
|
+ t.Fatalf("flowInclusiveEnd−flowExclusiveEnd = %s,期望 %s(= 落在 to 当天的流水)",
|
|
|
|
|
+ got.String(), want.String())
|
|
|
|
|
+ }
|
|
|
|
|
+ if got, want := inclusive.String(), "500.00"; got != want {
|
|
|
|
|
+ t.Fatalf("flowInclusiveEnd = %s,期望 %s(含 to 当天那笔)", got, want)
|
|
|
}
|
|
}
|
|
|
- if bill.Total != 2 || bill.PageSize != 10 || bill.Pages != 1 || bill.List == nil {
|
|
|
|
|
|
|
+ if bill.Total != 3 || bill.PageSize != 10 || bill.Pages != 1 || bill.List == nil {
|
|
|
t.Fatalf("分页元信息异常:total=%d pageSize=%d pages=%d list=%v", bill.Total, bill.PageSize, bill.Pages, bill.List)
|
|
t.Fatalf("分页元信息异常:total=%d pageSize=%d pages=%d list=%v", bill.Total, bill.PageSize, bill.Pages, bill.List)
|
|
|
}
|
|
}
|
|
|
if bill.Summary.CampusIDs == nil {
|
|
if bill.Summary.CampusIDs == nil {
|
|
@@ -665,6 +692,16 @@ func TestBillEndpointIdentity(t *testing.T) {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+// int64Money 金额字面量解析(测试内断言用;解析失败直接 fail)。
|
|
|
|
|
+func int64Money(t *testing.T, s string) money.Amount {
|
|
|
|
|
+ t.Helper()
|
|
|
|
|
+ amount, err := money.Parse(s)
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ t.Fatalf("金额解析失败 %q: %v", s, err)
|
|
|
|
|
+ }
|
|
|
|
|
+ return amount
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
// TestDashboardEndpoint 端点 21:聚合校区账户累计列 + 明确告知"不支持按校区拆流水"。
|
|
// TestDashboardEndpoint 端点 21:聚合校区账户累计列 + 明确告知"不支持按校区拆流水"。
|
|
|
func TestDashboardEndpoint(t *testing.T) {
|
|
func TestDashboardEndpoint(t *testing.T) {
|
|
|
db := newHandlerTestDB(t)
|
|
db := newHandlerTestDB(t)
|