|
@@ -279,7 +279,8 @@ func TestAddSubordinateDepthLimit(t *testing.T) {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-// TestAddLogBFSThreeLevels 上行链 BFS:本人 + 两级上线各生成一条明细并累加 total_income。
|
|
|
|
|
|
|
+// TestAddLogBFSThreeLevels 上行链 BFS:**从上级起算**(dump `:763744` 子查询口径),
|
|
|
|
|
+// 本人不参与分成 —— c 报课,只有 b(直接上级)与 a(根)各得一条明细。
|
|
|
func TestAddLogBFSThreeLevels(t *testing.T) {
|
|
func TestAddLogBFSThreeLevels(t *testing.T) {
|
|
|
db := newDivideServiceDB(t)
|
|
db := newDivideServiceDB(t)
|
|
|
svc := NewDivideService(db, nil)
|
|
svc := NewDivideService(db, nil)
|
|
@@ -308,10 +309,10 @@ func TestAddLogBFSThreeLevels(t *testing.T) {
|
|
|
if err := db.Find(&rows).Error; err != nil {
|
|
if err := db.Find(&rows).Error; err != nil {
|
|
|
t.Fatalf("读明细失败: %v", err)
|
|
t.Fatalf("读明细失败: %v", err)
|
|
|
}
|
|
}
|
|
|
- if len(rows) != 3 {
|
|
|
|
|
- t.Fatalf("三级链应生成 3 条明细,实得 %d", len(rows))
|
|
|
|
|
|
|
+ if len(rows) != 2 {
|
|
|
|
|
+ t.Fatalf("上行链应生成 2 条明细(本人不参与),实得 %d", len(rows))
|
|
|
}
|
|
}
|
|
|
- want := map[string]int64{c: 200, b: 500, a: 1000}
|
|
|
|
|
|
|
+ want := map[string]int64{b: 500, a: 1000}
|
|
|
for i := range rows {
|
|
for i := range rows {
|
|
|
expect, ok := want[rows[i].UserID]
|
|
expect, ok := want[rows[i].UserID]
|
|
|
if !ok {
|
|
if !ok {
|
|
@@ -336,9 +337,20 @@ func TestAddLogBFSThreeLevels(t *testing.T) {
|
|
|
t.Fatalf("%s.total_income 应为 %d 分,实得 %d", user, expect, got)
|
|
t.Fatalf("%s.total_income 应为 %d 分,实得 %d", user, expect, got)
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
+ // D1 红线:本人(c)不参与分成,任何列都不得被写入
|
|
|
|
|
+ if got := cents(t, loadDivideNode(t, db, c).TotalIncome); got != 0 {
|
|
|
|
|
+ t.Fatalf("本人 %s 不应获得分成(total_income 应 0),实得 %d 分", c, got)
|
|
|
|
|
+ }
|
|
|
|
|
+ for i := range rows {
|
|
|
|
|
+ if rows[i].UserID == c {
|
|
|
|
|
+ t.Fatal("本人不得生成分成明细(dump 起点是 findUserid 的上级)")
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// TestAddLogTwoLevelIdempotency 两级幂等:type=1 忽略订单;type=2 含订单。
|
|
// TestAddLogTwoLevelIdempotency 两级幂等:type=1 忽略订单;type=2 含订单。
|
|
|
|
|
+//
|
|
|
|
|
+// 结构:root(拿分成的一方)+ child(报课/被推荐人)—— **分成只入 root**(本人不参与,dump 口径)。
|
|
|
func TestAddLogTwoLevelIdempotency(t *testing.T) {
|
|
func TestAddLogTwoLevelIdempotency(t *testing.T) {
|
|
|
ctx := context.Background()
|
|
ctx := context.Background()
|
|
|
stu := nodeID(901)
|
|
stu := nodeID(901)
|
|
@@ -346,13 +358,16 @@ func TestAddLogTwoLevelIdempotency(t *testing.T) {
|
|
|
t.Run("type=1 一次付清(同 user+student 只一条)", func(t *testing.T) {
|
|
t.Run("type=1 一次付清(同 user+student 只一条)", func(t *testing.T) {
|
|
|
db := newDivideServiceDB(t)
|
|
db := newDivideServiceDB(t)
|
|
|
svc := NewDivideService(db, nil)
|
|
svc := NewDivideService(db, nil)
|
|
|
- u := nodeID(0)
|
|
|
|
|
|
|
+ u, child := nodeID(0), nodeID(1)
|
|
|
seedRoot(t, db, u, model.DivideTypeOneOff, "10.00")
|
|
seedRoot(t, db, u, model.DivideTypeOneOff, "10.00")
|
|
|
|
|
+ if err := svc.AddSubordinate(ctx, u, child); err != nil {
|
|
|
|
|
+ t.Fatalf("建链失败: %v", err)
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- if err := svc.AddLog(ctx, u, stu, 777, ""); err != nil {
|
|
|
|
|
|
|
+ if err := svc.AddLog(ctx, child, stu, 777, ""); err != nil {
|
|
|
t.Fatalf("AddLog 失败: %v", err)
|
|
t.Fatalf("AddLog 失败: %v", err)
|
|
|
}
|
|
}
|
|
|
- if err := svc.AddLog(ctx, u, stu, 888, ""); err != nil { // 不同订单:应与 type=1 无关
|
|
|
|
|
|
|
+ if err := svc.AddLog(ctx, child, stu, 888, ""); err != nil { // 不同订单:应与 type=1 无关
|
|
|
t.Fatalf("AddLog(第二次) 失败: %v", err)
|
|
t.Fatalf("AddLog(第二次) 失败: %v", err)
|
|
|
}
|
|
}
|
|
|
var count int64
|
|
var count int64
|
|
@@ -370,18 +385,21 @@ func TestAddLogTwoLevelIdempotency(t *testing.T) {
|
|
|
t.Run("type=2 按课次(同 user+student+order 只一条)", func(t *testing.T) {
|
|
t.Run("type=2 按课次(同 user+student+order 只一条)", func(t *testing.T) {
|
|
|
db := newDivideServiceDB(t)
|
|
db := newDivideServiceDB(t)
|
|
|
svc := NewDivideService(db, nil)
|
|
svc := NewDivideService(db, nil)
|
|
|
- u := nodeID(0)
|
|
|
|
|
|
|
+ u, child := nodeID(0), nodeID(1)
|
|
|
seedRoot(t, db, u, model.DivideTypePerLesson, "10.00")
|
|
seedRoot(t, db, u, model.DivideTypePerLesson, "10.00")
|
|
|
|
|
+ if err := svc.AddSubordinate(ctx, u, child); err != nil {
|
|
|
|
|
+ t.Fatalf("建链失败: %v", err)
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
for i := 0; i < 2; i++ { // 同一订单重复请求
|
|
for i := 0; i < 2; i++ { // 同一订单重复请求
|
|
|
- if err := svc.AddLog(ctx, u, stu, 777, ""); err != nil {
|
|
|
|
|
|
|
+ if err := svc.AddLog(ctx, child, stu, 777, ""); err != nil {
|
|
|
t.Fatalf("AddLog(重复) 失败: %v", err)
|
|
t.Fatalf("AddLog(重复) 失败: %v", err)
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
if count := countLogs(t, db); count != 1 {
|
|
if count := countLogs(t, db); count != 1 {
|
|
|
t.Fatalf("同订单重复请求应幂等(1 条),实得 %d", count)
|
|
t.Fatalf("同订单重复请求应幂等(1 条),实得 %d", count)
|
|
|
}
|
|
}
|
|
|
- if err := svc.AddLog(ctx, u, stu, 888, ""); err != nil { // 新订单:应新增
|
|
|
|
|
|
|
+ if err := svc.AddLog(ctx, child, stu, 888, ""); err != nil { // 新订单:应新增
|
|
|
t.Fatalf("AddLog(新订单) 失败: %v", err)
|
|
t.Fatalf("AddLog(新订单) 失败: %v", err)
|
|
|
}
|
|
}
|
|
|
if count := countLogs(t, db); count != 2 {
|
|
if count := countLogs(t, db); count != 2 {
|
|
@@ -403,6 +421,96 @@ func countLogs(t *testing.T, db *gorm.DB) int64 {
|
|
|
return count
|
|
return count
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+// TestAddLogStartFromParentDumpSemantics 审计 P0-2(dump `yuansiwei_finance.sql:763744`):
|
|
|
|
|
+// 上行分成的起点是 **findUserid 的上级**,本人不参与;findUserid 是根 → 无明细(幂等空操作)。
|
|
|
|
|
+//
|
|
|
|
|
+// 修复前从本人开始 → 本人也记一份,整整多算一代(金额错发)。
|
|
|
|
|
+func TestAddLogStartFromParentDumpSemantics(t *testing.T) {
|
|
|
|
|
+ ctx := context.Background()
|
|
|
|
|
+ stu := nodeID(901)
|
|
|
|
|
+
|
|
|
|
|
+ t.Run("根用户无明细(子查询为 NULL)", func(t *testing.T) {
|
|
|
|
|
+ db := newDivideServiceDB(t)
|
|
|
|
|
+ svc := NewDivideService(db, nil)
|
|
|
|
|
+ root := nodeID(0)
|
|
|
|
|
+ seedRoot(t, db, root, model.DivideTypePerLesson, "10.00")
|
|
|
|
|
+
|
|
|
|
|
+ if err := svc.AddLog(ctx, root, stu, 777, ""); err != nil {
|
|
|
|
|
+ t.Fatalf("根用户报课应静默不入账(不报错),实得 %v", err)
|
|
|
|
|
+ }
|
|
|
|
|
+ if count := countLogs(t, db); count != 0 {
|
|
|
|
|
+ t.Fatalf("根用户无上级 → 不应生成明细,实得 %d", count)
|
|
|
|
|
+ }
|
|
|
|
|
+ if got := cents(t, loadDivideNode(t, db, root).TotalIncome); got != 0 {
|
|
|
|
|
+ t.Fatalf("根用户 total_income 应保持 0,实得 %d 分", got)
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ t.Run("只给上级及更高层分成、本人无明细", func(t *testing.T) {
|
|
|
|
|
+ db := newDivideServiceDB(t)
|
|
|
|
|
+ svc := NewDivideService(db, nil)
|
|
|
|
|
+ a, b, c := nodeID(0), nodeID(1), nodeID(2)
|
|
|
|
|
+ seedRoot(t, db, a, model.DivideTypePerLesson, "10.00")
|
|
|
|
|
+ if err := svc.AddSubordinate(ctx, a, b); err != nil {
|
|
|
|
|
+ t.Fatalf("建链失败: %v", err)
|
|
|
|
|
+ }
|
|
|
|
|
+ if err := svc.AddSubordinate(ctx, b, c); err != nil {
|
|
|
|
|
+ t.Fatalf("建链失败: %v", err)
|
|
|
|
|
+ }
|
|
|
|
|
+ if err := svc.UpdateQuota(ctx, b, money.FromCents(500)); err != nil {
|
|
|
|
|
+ t.Fatalf("设额度 B 失败: %v", err)
|
|
|
|
|
+ }
|
|
|
|
|
+ if err := svc.UpdateQuota(ctx, c, money.FromCents(200)); err != nil {
|
|
|
|
|
+ t.Fatalf("设额度 C 失败: %v", err)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if err := svc.AddLog(ctx, c, stu, 777, ""); err != nil {
|
|
|
|
|
+ t.Fatalf("AddLog 失败: %v", err)
|
|
|
|
|
+ }
|
|
|
|
|
+ var rows []model.UserDivideIntoLog
|
|
|
|
|
+ if err := db.Find(&rows).Error; err != nil {
|
|
|
|
|
+ t.Fatalf("读明细失败: %v", err)
|
|
|
|
|
+ }
|
|
|
|
|
+ got := make(map[string]int64, len(rows))
|
|
|
|
|
+ for i := range rows {
|
|
|
|
|
+ got[rows[i].UserID] = cents(t, rows[i].Money)
|
|
|
|
|
+ }
|
|
|
|
|
+ if len(got) != 2 {
|
|
|
|
|
+ t.Fatalf("应只有上级 b 与根 a 两条明细,实得 %d:%v", len(got), got)
|
|
|
|
|
+ }
|
|
|
|
|
+ if _, ok := got[c]; ok {
|
|
|
|
|
+ t.Fatal("本人 c 不得出现在分成明细中(多算一代 = 金额错发)")
|
|
|
|
|
+ }
|
|
|
|
|
+ if got[b] != 500 || got[a] != 1000 {
|
|
|
|
|
+ t.Fatalf("分成金额不符:b=%d a=%d(期望 500/1000)", got[b], got[a])
|
|
|
|
|
+ }
|
|
|
|
|
+ if cents(t, loadDivideNode(t, db, c).TotalIncome) != 0 {
|
|
|
|
|
+ t.Fatal("本人 c 的 total_income 必须保持 0")
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ t.Run("上级已断链(不在树中)→ 无明细", func(t *testing.T) {
|
|
|
|
|
+ db := newDivideServiceDB(t)
|
|
|
|
|
+ svc := NewDivideService(db, nil)
|
|
|
|
|
+ a, b := nodeID(0), nodeID(1)
|
|
|
|
|
+ seedRoot(t, db, a, model.DivideTypePerLesson, "10.00")
|
|
|
|
|
+ if err := svc.AddSubordinate(ctx, a, b); err != nil {
|
|
|
|
|
+ t.Fatalf("建链失败: %v", err)
|
|
|
|
|
+ }
|
|
|
|
|
+ // 人为把 b 的上级指向一个不存在的节点(脏数据 / 上级已退网)
|
|
|
|
|
+ if err := db.Model(&model.UserDivideInto{}).Where("user_id = ?", b).
|
|
|
|
|
+ UpdateColumn("parent_user_id", nodeID(777)).Error; err != nil {
|
|
|
|
|
+ t.Fatalf("造断链失败: %v", err)
|
|
|
|
|
+ }
|
|
|
|
|
+ if err := svc.AddLog(ctx, b, stu, 777, ""); err != nil {
|
|
|
|
|
+ t.Fatalf("断链应静默不入账,实得 %v", err)
|
|
|
|
|
+ }
|
|
|
|
|
+ if count := countLogs(t, db); count != 0 {
|
|
|
|
|
+ t.Fatalf("断链不应生成明细,实得 %d", count)
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
// TestAddLogNonTreeNodeNoop 非分账树节点 → 静默不入账(不报错)。
|
|
// TestAddLogNonTreeNodeNoop 非分账树节点 → 静默不入账(不报错)。
|
|
|
func TestAddLogNonTreeNodeNoop(t *testing.T) {
|
|
func TestAddLogNonTreeNodeNoop(t *testing.T) {
|
|
|
db := newDivideServiceDB(t)
|
|
db := newDivideServiceDB(t)
|
|
@@ -467,8 +575,9 @@ func TestDeleteLogRollbackBranch(t *testing.T) {
|
|
|
if err := svc.AddLog(ctx, b, stu, 777, ""); err != nil {
|
|
if err := svc.AddLog(ctx, b, stu, 777, ""); err != nil {
|
|
|
t.Fatalf("AddLog 失败: %v", err)
|
|
t.Fatalf("AddLog 失败: %v", err)
|
|
|
}
|
|
}
|
|
|
- if count := countLogs(t, db); count != 2 {
|
|
|
|
|
- t.Fatalf("应为 2 条明细,实得 %d", count)
|
|
|
|
|
|
|
+ // b 报课 → 只有上级 a 得一条(本人不参与分成,dump 口径)
|
|
|
|
|
+ if count := countLogs(t, db); count != 1 {
|
|
|
|
|
+ t.Fatalf("应为 1 条明细(仅上级),实得 %d", count)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
if err := svc.DeleteLog(ctx, stu, 777); err != nil {
|
|
if err := svc.DeleteLog(ctx, stu, 777); err != nil {
|
|
@@ -495,10 +604,13 @@ func TestDeleteLogRebindBranch(t *testing.T) {
|
|
|
port := &fakeFirstCharged{recordID: 888, found: true}
|
|
port := &fakeFirstCharged{recordID: 888, found: true}
|
|
|
svc := NewDivideService(db, port)
|
|
svc := NewDivideService(db, port)
|
|
|
ctx := context.Background()
|
|
ctx := context.Background()
|
|
|
- a := nodeID(0)
|
|
|
|
|
|
|
+ a, b := nodeID(0), nodeID(1)
|
|
|
seedRoot(t, db, a, model.DivideTypePerLesson, "10.00")
|
|
seedRoot(t, db, a, model.DivideTypePerLesson, "10.00")
|
|
|
|
|
+ if err := svc.AddSubordinate(ctx, a, b); err != nil {
|
|
|
|
|
+ t.Fatalf("建链失败: %v", err)
|
|
|
|
|
+ }
|
|
|
stu := nodeID(901)
|
|
stu := nodeID(901)
|
|
|
- if err := svc.AddLog(ctx, a, stu, 777, ""); err != nil {
|
|
|
|
|
|
|
+ if err := svc.AddLog(ctx, b, stu, 777, ""); err != nil {
|
|
|
t.Fatalf("AddLog 失败: %v", err)
|
|
t.Fatalf("AddLog 失败: %v", err)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -532,10 +644,13 @@ func TestDeleteLogFirstEqualsOrder(t *testing.T) {
|
|
|
port := &fakeFirstCharged{recordID: 777, found: true}
|
|
port := &fakeFirstCharged{recordID: 777, found: true}
|
|
|
svc := NewDivideService(db, port)
|
|
svc := NewDivideService(db, port)
|
|
|
ctx := context.Background()
|
|
ctx := context.Background()
|
|
|
- a := nodeID(0)
|
|
|
|
|
|
|
+ a, b := nodeID(0), nodeID(1)
|
|
|
seedRoot(t, db, a, model.DivideTypePerLesson, "10.00")
|
|
seedRoot(t, db, a, model.DivideTypePerLesson, "10.00")
|
|
|
|
|
+ if err := svc.AddSubordinate(ctx, a, b); err != nil {
|
|
|
|
|
+ t.Fatalf("建链失败: %v", err)
|
|
|
|
|
+ }
|
|
|
stu := nodeID(901)
|
|
stu := nodeID(901)
|
|
|
- if err := svc.AddLog(ctx, a, stu, 777, ""); err != nil {
|
|
|
|
|
|
|
+ if err := svc.AddLog(ctx, b, stu, 777, ""); err != nil {
|
|
|
t.Fatalf("AddLog 失败: %v", err)
|
|
t.Fatalf("AddLog 失败: %v", err)
|
|
|
}
|
|
}
|
|
|
if err := svc.DeleteLog(ctx, stu, 777); err != nil {
|
|
if err := svc.DeleteLog(ctx, stu, 777); err != nil {
|
|
@@ -624,8 +739,8 @@ func TestDivideServiceNestedTxPassThrough(t *testing.T) {
|
|
|
if cerr := tx.Model(&model.UserDivideIntoLog{}).Count(&inside).Error; cerr != nil {
|
|
if cerr := tx.Model(&model.UserDivideIntoLog{}).Count(&inside).Error; cerr != nil {
|
|
|
return cerr
|
|
return cerr
|
|
|
}
|
|
}
|
|
|
- if inside != 2 {
|
|
|
|
|
- t.Fatalf("外层事务内应可见 2 条明细,实得 %d", inside)
|
|
|
|
|
|
|
+ if inside != 1 {
|
|
|
|
|
+ t.Fatalf("外层事务内应可见 1 条明细(仅上级,本人不参与),实得 %d", inside)
|
|
|
}
|
|
}
|
|
|
return boom
|
|
return boom
|
|
|
})
|
|
})
|
|
@@ -640,8 +755,10 @@ func TestDivideServiceNestedTxPassThrough(t *testing.T) {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-// TestAddLogBFSDepthBoundary 100 层链(layer 1..100)恰好在上限内:BFS 必须完整走完 100 级
|
|
|
|
|
|
|
+// TestAddLogBFSDepthBoundary 100 层链(layer 1..100)恰好在上限内:BFS 必须完整走完到根
|
|
|
// (101 级才触发 3111,见 TestAddLogCycleGuard)。同时验证"迭代不递归"在大深度下不栈溢出。
|
|
// (101 级才触发 3111,见 TestAddLogCycleGuard)。同时验证"迭代不递归"在大深度下不栈溢出。
|
|
|
|
|
+//
|
|
|
|
|
+// 明细条数 = 99(链尾本人不参与分成,dump 起点是其上级,故比全链少 1 条)。
|
|
|
func TestAddLogBFSDepthBoundary(t *testing.T) {
|
|
func TestAddLogBFSDepthBoundary(t *testing.T) {
|
|
|
db := newDivideServiceDB(t)
|
|
db := newDivideServiceDB(t)
|
|
|
svc := NewDivideService(db, nil)
|
|
svc := NewDivideService(db, nil)
|
|
@@ -665,8 +782,9 @@ func TestAddLogBFSDepthBoundary(t *testing.T) {
|
|
|
if err := svc.AddLog(ctx, tail, nodeID(901), 777, ""); err != nil {
|
|
if err := svc.AddLog(ctx, tail, nodeID(901), 777, ""); err != nil {
|
|
|
t.Fatalf("%d 层链的 BFS 入账应在限内成功,实得 %v", MaxDivideDepth, err)
|
|
t.Fatalf("%d 层链的 BFS 入账应在限内成功,实得 %v", MaxDivideDepth, err)
|
|
|
}
|
|
}
|
|
|
- if count := countLogs(t, db); count != int64(MaxDivideDepth) {
|
|
|
|
|
- t.Fatalf("%d 层链应生成 %d 条明细,实得 %d", MaxDivideDepth, MaxDivideDepth, count)
|
|
|
|
|
|
|
+ // 链尾本人不参与 → 明细 = 层数 - 1
|
|
|
|
|
+ if count := countLogs(t, db); count != int64(MaxDivideDepth-1) {
|
|
|
|
|
+ t.Fatalf("%d 层链应生成 %d 条明细(本人不参与),实得 %d", MaxDivideDepth, MaxDivideDepth-1, count)
|
|
|
}
|
|
}
|
|
|
// 根节点收入应恰好为一次 quota
|
|
// 根节点收入应恰好为一次 quota
|
|
|
if got := cents(t, loadDivideNode(t, db, root).TotalIncome); got != 100 {
|
|
if got := cents(t, loadDivideNode(t, db, root).TotalIncome); got != 100 {
|