diff --git a/app/Http/Controllers/Api/LicenseController.php b/app/Http/Controllers/Api/LicenseController.php index c5bf05024..42bd12464 100644 --- a/app/Http/Controllers/Api/LicenseController.php +++ b/app/Http/Controllers/Api/LicenseController.php @@ -13,6 +13,7 @@ use Request; * 动态路由(routes/web.php): * api/license/email/send -> email__send() * api/license/login -> login() + * api/license/login/confirm -> login__confirm() * api/license/trial -> trial() * api/license/status -> status() * api/license/refresh -> refresh() @@ -49,6 +50,25 @@ class LicenseController extends AbstractController return Base::retSuccess('授权成功', $data); } + /** + * 多条可用授权时,用户选定后确认签发(复用验证码) + */ + public function login__confirm() + { + User::auth('admin'); + $email = trim(Request::input('email')); + $code = trim(Request::input('code')); + $entitlementId = (int)Request::input('entitlement_id'); + if ($email === '' || $code === '') { + return Base::retError('请输入邮箱和验证码'); + } + if ($entitlementId <= 0) { + return Base::retError('请选择要使用的授权'); + } + $data = OnlineLicense::loginConfirm($email, $code, $entitlementId); + return Base::retSuccess('授权成功', $data); + } + /** * 邮箱 + 验证码申请试用并签发 */ diff --git a/app/Http/Controllers/Api/SystemController.php b/app/Http/Controllers/Api/SystemController.php index dcac260c3..804bc36d5 100755 --- a/app/Http/Controllers/Api/SystemController.php +++ b/app/Http/Controllers/Api/SystemController.php @@ -861,6 +861,14 @@ class SystemController extends AbstractController $type = trim(Request::input('type')); if ($type == 'save') { $license = Request::input('license'); + // 解密失败(sn 为空)视为无效 license + $decoded = Doo::licenseDecode($license); + if ((string)($decoded['sn'] ?? '') === '') { + return Base::retError('LICENSE 格式错误'); + } + if ($err = Doo::licenseBindingError($decoded)) { + return Base::retError($err); + } Doo::licenseSave($license); // 离线/在线互斥:保存离线 license 即退出在线模式(尽力释放座位+清在线标志,不删除刚写入的文件) OnlineLicense::switchToOffline(); @@ -875,8 +883,8 @@ class SystemController extends AbstractController 'user_count' => User::whereBot(0)->whereNull('disable_at')->count(), 'error' => [] ]; - if ($data['info']['people'] > 3) { - // 小于3人的License不检查 + if ($data['info']['people'] == 0 || $data['info']['people'] > 3) { + // 付费档才检查 SN/MAC if ($data['info']['sn'] != $data['doo_sn']) { $data['error'][] = '终端SN与License不匹配'; } diff --git a/app/Models/User.php b/app/Models/User.php index a3ce47bfe..6edf5cb76 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -394,6 +394,10 @@ class User extends AbstractModel } // 密码 self::passwordPolicy($password); + // license + if ($err = Doo::licenseBindingError(Doo::license())) { + throw new ApiException($err); + } // 开始注册 $user = Doo::userCreate($email, $password); if ($other) { diff --git a/app/Module/Doo.php b/app/Module/Doo.php index 599abc17d..b3753726a 100644 --- a/app/Module/Doo.php +++ b/app/Module/Doo.php @@ -81,6 +81,39 @@ class Doo self::load()->licenseSave($license); } + /** + * 解析License取字段 + * @param $license + * @return array + */ + public static function licenseDecode($license): array + { + return self::load()->licenseDecode($license); + } + + /** + * 校验 license 的 SN/MAC 是否与本机匹配。通过返回 null,否则返回错误文案。 + * @param array $info license 信息,含 people/sn/mac(mac 兼容数组或逗号串) + * @return string|null + */ + public static function licenseBindingError(array $info): ?string + { + $people = (int)($info['people'] ?? 0); + if (!($people === 0 || $people > 3)) { + return null; + } + if ((string)($info['sn'] ?? '') !== self::dooSN()) { + return '终端SN与License不匹配'; + } + $mac = $info['mac'] ?? []; + $licenseMacs = array_filter(array_map('trim', is_array($mac) ? $mac : explode(',', (string)$mac))); + $curMacs = self::macs(); + if ($licenseMacs && $curMacs && !array_intersect($licenseMacs, $curMacs)) { + return '终端MAC与License不匹配'; + } + return null; + } + /** * 当前会员ID(来自请求的token) * @return int diff --git a/app/Module/OnlineLicense.php b/app/Module/OnlineLicense.php index 96a9d8f71..b3ff20bfe 100644 --- a/app/Module/OnlineLicense.php +++ b/app/Module/OnlineLicense.php @@ -176,6 +176,8 @@ class OnlineLicense /** * 邮箱 + 验证码登录并签发。失败抛 ApiException。 + * 本机有多条可用授权时,appstore 返回 select_required + candidates, + * 此处不签发、原样返回候选,由前端选定后走 loginConfirm()。 */ public static function login(string $email, string $code): array { @@ -183,6 +185,35 @@ class OnlineLicense if (!$r['ok']) { throw new ApiException($r['message']); } + $d = $r['data']; + if (($d['status'] ?? '') === 'select_required') { + return [ + 'select_required' => true, + 'candidates' => $d['candidates'] ?? [], + ]; + } + $status = self::applyIssue($email, $d); + if (!in_array($status, ['issued', 'renewed'], true)) { + throw new ApiException(self::statusHint($status)); + } + return self::status(); + } + + /** + * 多条可用授权时,用户选定 $entitlementId 后确认签发(复用同一验证码)。失败抛 ApiException。 + */ + public static function loginConfirm(string $email, string $code, int $entitlementId): array + { + $payload = array_merge([ + 'email' => $email, + 'code' => $code, + 'entitlement_id' => $entitlementId, + 'lang' => self::lang(), + ], self::fingerprint()); + $r = self::call('login/confirm', $payload); + if (!$r['ok']) { + throw new ApiException($r['message']); + } $status = self::applyIssue($email, $r['data']); if (!in_array($status, ['issued', 'renewed'], true)) { throw new ApiException(self::statusHint($status)); diff --git a/language/original-web.txt b/language/original-web.txt index 65d86b261..a3092a8cf 100644 --- a/language/original-web.txt +++ b/language/original-web.txt @@ -2516,9 +2516,12 @@ AI任务分析 请保持联网,系统会自动为你续期。 重新登录授权 匹配 -与本机不一致 +不匹配 收起 更多信息 提示 将释放当前设备占用的授权座位并回到登录,确定继续? 已过期 +选择要使用的授权 +当前设备使用中 +确定授权 diff --git a/resources/ai-kb/zh/howto/license/online.md b/resources/ai-kb/zh/howto/license/online.md index a1a6b7346..cd194ba7f 100644 --- a/resources/ai-kb/zh/howto/license/online.md +++ b/resources/ai-kb/zh/howto/license/online.md @@ -19,6 +19,9 @@ aliases: - 在线授权冻结 - 在线授权被吊销 - 换机 deactivate + - 选择要使用的授权 + - 一个账号多条授权 + - 登录时选择授权 - SN 与 License 不匹配 - MAC 与 License 不匹配 - 终端SN与License不匹配 @@ -60,6 +63,11 @@ DooTask 的 License 页提供两种授权方式,可在页面顶部 Tab 切换 - 终端把自身指纹(doo_sn、网卡 MAC、版本)上报授权中心,签发一张租约 License 并落地 - 成功后页面显示账号、套餐、使用人数、授权有效期、SN/MAC(与本机是否匹配)、当前状态 +#### 账号名下有多条可用授权时 +- 若该账号名下有**两条及以上**本机可用的授权(座位空闲、或本机已在用的),登录时会弹出「选择要使用的授权」窗口,逐条列出套餐、使用人数、授权有效期,本机已在用的会标注「当前设备使用中」 +- 选中一条后点击「确定授权」即对该条签发(复用刚才的验证码,无需重新发码) +- 只有一条可用时不弹窗,直接签发;没有可用授权则提示申请试用或购买;名下授权都被其它实例占用时提示需先在原实例释放(换机) + ### 没有正式授权 → 申请试用 - 填好邮箱与验证码后点击「申请试用」即开通试用授权并签发 - 试用默认 14 天 / 不限人数(具体以 App Store 管理员配置为准,时长硬上限 60 天),每个账号仅能申请一次 @@ -83,7 +91,7 @@ DooTask 的 License 页提供两种授权方式,可在页面顶部 Tab 切换 ## SN / MAC 与本机匹配 在线授权签发的 License 内嵌了签发时的终端 SN 与网卡 MAC。正常情况下二者与本机一致,「在线授权」Tab 保持极简,只显示账号、套餐、使用人数、有效期与状态(带颜色圆点),不常驻展示 SN/MAC 这类技术细节。 -一旦 SN 或 MAC 与本机不一致,页面顶部会浮出告警卡,并展开「诊断详情」逐项列出「授权 SN / 当前 SN / 授权 MAC / 当前 MAC」及匹配标记(✓/✕);同一批告警也会出现在仪表盘顶部横幅。「离线授权」Tab(手动粘贴场景)则始终以列表展示 SN/MAC,用行尾标签直陈匹配与否、不一致时整行标红,IP/域名/创建时间等低频字段收纳在「更多信息」里。 +一旦 SN 或 MAC 与本机不匹配,页面顶部会浮出告警卡,并展开「诊断详情」逐项列出「授权 SN / 当前 SN / 授权 MAC / 当前 MAC」及匹配标记(✓/✕);同一批告警也会出现在仪表盘顶部横幅。「离线授权」Tab(手动粘贴场景)则始终以列表展示 SN/MAC,用行尾标签直陈匹配与否、不匹配时整行标红,IP/域名/创建时间等低频字段收纳在「更多信息」里。 授权成功后若终端的 SN 或 MAC 发生变化,行为不同: diff --git a/resources/assets/js/pages/manage/setting/license.vue b/resources/assets/js/pages/manage/setting/license.vue index f2c4b5056..af5990f09 100644 --- a/resources/assets/js/pages/manage/setting/license.vue +++ b/resources/assets/js/pages/manage/setting/license.vue @@ -5,7 +5,7 @@
-
+
{{$L('加载中...')}}
  • {{$L('账号')}}:
  • @@ -16,7 +16,7 @@