refactor(api): /v1/me 收敛 expire_at → 只发 expires_at(无 web 用户中心)

之前后端同时发 expire_at + expires_at(分别给 app + web 用户中心)。现已无 web 端,
双发是纯冗余,统一只用 expires_at:
- account.go:meResponse 删 ExpireAt 字段 + GetMe handler 不再赋值
- client me.dart:fromJson 删 (expires_at ?? expire_at) 别名回退
- 两端契约快照同步:Go meResponse 冻结集去 expire_at(12→11 key)、删 Dart 别名测试
- 验证:go test httpapi 绿、flutter test contract(14)绿、analyze EXIT 0、e2e 全链路绿

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
wangjia
2026-06-25 10:08:50 +08:00
parent dbd183ee00
commit 64f96fe018
4 changed files with 5 additions and 17 deletions
+1 -1
View File
@@ -43,7 +43,7 @@ class Me {
bool get isFree => plan == 'free';
factory Me.fromJson(Map<String, dynamic> m) {
final exp = (m['expires_at'] ?? m['expire_at']) as String?;
final exp = m['expires_at'] as String?;
return Me(
email: m['email'] as String? ?? '',
plan: m['plan'] as String? ?? 'free',
@@ -87,14 +87,6 @@ void main() {
expect(me.weeklyGb, hasLength(7));
expect(me.totpEnabled, isTrue);
});
test('expires_at 接受别名 expire_at(后端历史兼容)', () {
final me = Me.fromJson({
..._meSample()..remove('expires_at'),
'expire_at': '2027-01-01T00:00:00Z',
});
expect(me.expiresAt, DateTime.tryParse('2027-01-01T00:00:00Z'));
});
});
group('/v1/usage 契约快照(UsagePoint)', () {
+1 -4
View File
@@ -26,9 +26,7 @@ type meResponse struct {
Email string `json:"email"`
DpUUID string `json:"dp_uuid"`
Plan string `json:"plan"` // "free" | "pro" | "team"
// expire_at kept for the existing app; expires_at is the same value for the
// web user-center (both RFC3339 UTC, null = no active sub).
ExpireAt *string `json:"expire_at"`
// expires_at: RFC3339 UTC, null = no active subscription.
ExpiresAt *string `json:"expires_at"`
// Web user-center fields.
DevicesUsed int `json:"devices_used"`
@@ -120,7 +118,6 @@ func (a *AccountAPI) GetMe(w http.ResponseWriter, r *http.Request) {
}
if expiresAt.Valid {
s := expiresAt.Time.UTC().Format(time.RFC3339)
resp.ExpireAt = &s
resp.ExpiresAt = &s
}
// quota_today_min: null when the plan is unlimited, else remaining minutes.
+3 -4
View File
@@ -6,8 +6,8 @@ package httpapi
// 同步客户端 model + 文档,避免控制面与客户端悄悄分叉。
//
// ⚠️ 与客户端冻结集对账:client/test/contract/api_contract_test.dart 的 _frozenMeKeys
// (9 个)。后端这里多发 uuid / dp_uuid,且同时发 expire_at + expires_at(别名):客户端
// 故意忽略 uuid/dp_uuid、把 expire_at 当 expires_at 的回退。改任一端都要回看另一端。
// (9 个)。后端这里多发 uuid / dp_uuid(客户端故意忽略);expire_at 已废弃移除——
// 无 web 用户中心后,两端统一只用 expires_at。改任一端都要回看另一端。
import (
"reflect"
@@ -54,8 +54,7 @@ func assertFrozenKeys(t *testing.T, name string, got map[string]struct{}, want .
func TestContractMeResponse(t *testing.T) {
assertFrozenKeys(t, "/v1/me", jsonTagSet(meResponse{}),
"uuid", "email", "dp_uuid", "plan",
"expire_at", "expires_at",
"uuid", "email", "dp_uuid", "plan", "expires_at",
"devices_used", "devices_max",
"quota_today_min", "data_today_gb", "weekly_gb", "totp_enabled",
)