From daa531f4ed9174d9c99f3a62263f0b8f0b5ef4cd Mon Sep 17 00:00:00 2001 From: opsx Date: Sun, 1 Nov 2026 04:06:56 +0800 Subject: [PATCH] =?UTF-8?q?fix(license):=20=E9=80=80=E5=87=BA=E6=8E=88?= =?UTF-8?q?=E6=9D=83=E5=9C=A8=E6=8B=BF=E4=B8=8D=E5=88=B0=E8=A7=A3=E7=BB=91?= =?UTF-8?q?=E5=87=AD=E8=AF=81=E6=97=B6=E9=99=8D=E7=BA=A7=E6=B8=85=E6=9C=AC?= =?UTF-8?q?=E6=9C=BA,=E4=B8=8D=E5=86=8D=E5=8D=A1=E6=AD=BB=E6=8A=A5?= =?UTF-8?q?=E9=94=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 根因(实测确认):授权服务 activate 响应不含 activation_token 字段,本机存证里 token 为空;deactivate 端要求该凭证 → 远端永远拒绝 → 用户点退出只会报 「远端解绑失败,本机授权未清除」且无法自愈。 修复:远端解绑失败时降级——清本机授权(回免费版,用户诉求达成), 返回 offlineLocalOnly 提示远端席位需服务端释放;服务端将来补发 token 后 此路径自动回到完整解绑。 --- backend/services/license.go | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/backend/services/license.go b/backend/services/license.go index 6df0f98..2fbf9bc 100644 --- a/backend/services/license.go +++ b/backend/services/license.go @@ -139,7 +139,7 @@ func ActivateLicense(key string) (*models.License, error) { lic := &models.License{ Plan: "pro", Source: "opsx", LicenseKey: key, InstanceID: instanceID, PlanType: claims.PlanType, MaxDevices: claims.MaxDevices, Instances: claims.MaxDevices, - Edition: editionFromPlanType(claims.PlanType, claims.MaxDevices), + Edition: editionFromPlanType(claims.PlanType, claims.MaxDevices), ExpiresAt: claims.Exp, IssuedAt: time.Unix(claims.Iat, 0), ActivatedAt: time.Now(), LastVerifiedAt: time.Now().Unix(), Customer: claims.Email, InvalidReason: "", } @@ -233,7 +233,13 @@ func DeactivateLicense() (*DeactivateResult, error) { if key != "" && isOpsxLicenseKey(key) { if err := remoteDeactivateLicence(key, instanceID, token); err != nil { - return nil, fmt.Errorf("远端解绑失败,本机授权未清除: %w", err) + // 授权服务不返回 activation_token(激活响应无该字段),本机拿不到解绑凭证时 + // 远端必然拒绝——不能把用户锁死在"永远解不了绑"里。降级为仅清本机: + // 本机回免费版(用户诉求达成),远端座位由服务端到期/管理员释放。 + clearStoredLicense() + result.OfflineLocalOnly = true + result.Message = "已退出授权,本机已回到免费版。远端席位未能自动释放(服务未下发解绑凭证),如需释放请到授权平台管理或联系运营。" + return result, nil } clearStoredLicense() result.Message = "已退出授权并释放本实例" @@ -317,7 +323,6 @@ func RefreshOpsxLicenseIfDue() { _ = remoteHeartbeatLicence(lic.LicenseKey, instanceID) } - // StoredLicense 库内原始许可(含已降级/过期记录);勿与 CurrentLicense 混淆。 func StoredLicense() *models.License { db := repository.GetDB()