package core import ( "encoding/json" "net/http" "net/http/httptest" "testing" "time" ) // 余额分档选号(照参考 buddy-balance-rank.ts / loomy-balance-rank.ts)。 // // 解决什么:一个号常同时持有多**种**资源包(如「14 天到期」+「8 年后扣费截止」), // 而**服务端扣哪个包不由我们决定** —— 插件能决定的是**用哪个号**: // 优先用「还有 15 天内到期积分」的号(那部分不用就作废),只剩永久积分的号排最后; // **锁定永久积分**时只剩永久积分的号等同于不可用。 // // 判据用假余额上游,逐条钉住分档规则(含三个易错点)。 func TestBalanceTierSelection(t *testing.T) { // 假 buddy 余额端点:返回三个包,覆盖三种情形 // pkg A: 有效 + 10 天内到期 + 剩余 100 → 临时桶 // pkg B: 有效 + 到期时间未知 + 剩余 50 → 永久桶(保守) // pkg C: **已失效** + 剩余 9999 → 必须跳过(否则虚增可用额度) future := nowMs() + 300*24*3600*1000 // 300 天后 soon := nowMs() + 10*24*3600*1000 // 10 天后 body := map[string]interface{}{ "data": map[string]interface{}{ "Response": map[string]interface{}{ "Data": map[string]interface{}{ "Accounts": []map[string]interface{}{ {"Status": 1, "CycleCapacityRemain": 100, "DeductionEndTime": soon}, {"Status": 1, "CycleCapacityRemain": 50, "DeductionEndTime": future}, {"Status": 2, "CycleCapacityRemain": 9999, "DeductionEndTime": future}, }, }, }, }, } raw, _ := json.Marshal(body) ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") _, _ = w.Write(raw) })) defer ts.Close() _, ps := poolCore(t) // 把一个 buddy 账号加进池,并把余额端点指到假上游 if _, err := ps.Call("channels", "set_endpoints", map[string]interface{}{ "platform": "buddy", "balance": map[string]interface{}{"url": ts.URL}, }); err != nil { t.Fatalf("set_endpoints: %v", err) } m, err := ps.Call("channels", "account_add", map[string]interface{}{ "platform": "buddy", "account_id": "tieracct", "credential": `{"access_token":"TOK"}`, }) if err != nil { t.Fatalf("account_add: %v", err) } w, _ := m.(map[string]interface{}) j, _ := w["json"].(map[string]interface{}) if j == nil || j["error"] != nil { t.Fatalf("account_add 失败: %#v", m) } // ① 分档:临时=100(只有 10 天内那个),永久=50(未知/远期),失效包被跳过 out, err := ps.Call("channels", "credits_tier_probe", map[string]interface{}{"platform": "buddy", "account_id": "tieracct"}) if err != nil { t.Fatalf("分档探针: %v", err) } tw, _ := out.(map[string]interface{}) tj, _ := tw["json"].(map[string]interface{}) if tj == nil { t.Fatalf("无 json: %#v", out) } if tj["error"] != nil { t.Fatalf("分档报错: %v", tj["error"]) } if got, _ := tj["expiring"].(float64); got != 100 { t.Errorf("临时余额应为 100(仅 10 天内到期的包),实得 %v", tj["expiring"]) } if got, _ := tj["permanent"].(float64); got != 50 { t.Errorf("永久余额应为 50(到期未知/远期),实得 %v(若为 10049 说明失效包没跳过)", tj["permanent"]) } // ② 档位判定:有临时积分 → "expiring"(最优先) if tj["tier"] != "expiring" { t.Errorf("有 15 天内到期积分时档位应为 expiring,实得 %v", tj["tier"]) } // ③ 锁定永久积分:该账号**仍可用**(因为有临时积分) if _, err := ps.Call("channels", "__set_lock_permanent_probe", map[string]interface{}{"lock": true}); err != nil { t.Fatalf("锁定探针: %v", err) } out2, _ := ps.Call("channels", "credits_tier_probe", map[string]interface{}{"platform": "buddy", "account_id": "tieracct"}) tj2, _ := out2.(map[string]interface{})["json"].(map[string]interface{}) if tj2["usable"] != true { t.Errorf("锁定永久积分时,有临时积分的号仍应可用,实得 usable=%v", tj2["usable"]) } // ④ 窗口可被显式覆盖:把窗口设成 0 ⇒ 没有"临时积分" ⇒ 只剩永久 ⇒ 锁定后不可用 if _, err := ps.Call("channels", "__set_expiring_window_probe", map[string]interface{}{"days": 0}); err != nil { t.Fatalf("窗口探针: %v", err) } out3, _ := ps.Call("channels", "credits_tier_probe", map[string]interface{}{"platform": "buddy", "account_id": "tieracct"}) tj3, _ := out3.(map[string]interface{})["json"].(map[string]interface{}) if got, _ := tj3["expiring"].(float64); got != 0 { t.Errorf("窗口=0 时临时桶应为 0,实得 %v", tj3["expiring"]) } if tj3["usable"] != false { t.Errorf("窗口=0 且锁定永久积分时该号应不可用,实得 usable=%v", tj3["usable"]) } } // nowMs 返回当前毫秒时间戳(与插件里 DeductionEndTime 的口径一致)。 func nowMs() int64 { return time.Now().UnixMilli() }