diff --git a/app/Http/Controllers/Api/LicenseController.php b/app/Http/Controllers/Api/LicenseController.php index 3c87748a4..c5bf05024 100644 --- a/app/Http/Controllers/Api/LicenseController.php +++ b/app/Http/Controllers/Api/LicenseController.php @@ -11,8 +11,8 @@ use Request; * 在线授权客户端(与 SystemController::license 的离线粘贴并存)。 * * 动态路由(routes/web.php): + * api/license/email/send -> email__send() * api/license/login -> login() - * api/license/trial/send -> trial__send() * api/license/trial -> trial() * api/license/status -> status() * api/license/refresh -> refresh() @@ -21,48 +21,46 @@ use Request; class LicenseController extends AbstractController { /** - * 账号登录并签发在线授权 + * 发送邮箱验证码(登录与试用共用) + */ + public function email__send() + { + User::auth('admin'); + $email = trim(Request::input('email')); + if ($email === '') { + return Base::retError('请输入邮箱'); + } + $masked = OnlineLicense::emailSend($email); + return Base::retSuccess('验证码已发送', ['email' => $masked]); + } + + /** + * 邮箱 + 验证码登录并签发在线授权 */ public function login() { User::auth('admin'); - $account = trim(Request::input('account')); - $password = trim(Request::input('password')); - if ($account === '' || $password === '') { - return Base::retError('请输入账号和密码'); + $email = trim(Request::input('email')); + $code = trim(Request::input('code')); + if ($email === '' || $code === '') { + return Base::retError('请输入邮箱和验证码'); } - $data = OnlineLicense::login($account, $password); + $data = OnlineLicense::login($email, $code); return Base::retSuccess('授权成功', $data); } /** - * 发送试用验证码 - */ - public function trial__send() - { - User::auth('admin'); - $account = trim(Request::input('account')); - $password = trim(Request::input('password')); - if ($account === '' || $password === '') { - return Base::retError('请输入账号和密码'); - } - $email = OnlineLicense::trialSend($account, $password); - return Base::retSuccess('验证码已发送', ['email' => $email]); - } - - /** - * 申请试用并签发 + * 邮箱 + 验证码申请试用并签发 */ public function trial() { User::auth('admin'); - $account = trim(Request::input('account')); - $password = trim(Request::input('password')); + $email = trim(Request::input('email')); $code = trim(Request::input('code')); - if ($account === '' || $password === '' || $code === '') { - return Base::retError('请输入账号、密码和验证码'); + if ($email === '' || $code === '') { + return Base::retError('请输入邮箱和验证码'); } - $data = OnlineLicense::trial($account, $password, $code); + $data = OnlineLicense::trial($email, $code); return Base::retSuccess('试用已开通', $data); } diff --git a/app/Module/OnlineLicense.php b/app/Module/OnlineLicense.php index 9e4eb9fcf..b47cfb201 100644 --- a/app/Module/OnlineLicense.php +++ b/app/Module/OnlineLicense.php @@ -154,28 +154,11 @@ class OnlineLicense // ---- 对外动作 ---- /** - * 账号登录并签发。失败抛 ApiException。 + * 发送邮箱验证码(登录与试用共用),返回脱敏邮箱。 */ - public static function login(string $account, string $password): array + public static function emailSend(string $email): string { - $r = self::call('login', array_merge(['account' => $account, 'password' => $password], self::fingerprint())); - if (!$r['ok']) { - throw new ApiException($r['message']); - } - $status = self::applyIssue($account, $r['data']); - if (!in_array($status, ['issued', 'renewed'], true)) { - throw new ApiException(self::statusHint($status)); - } - return self::status(); - } - - /** - * 发送试用验证码,返回脱敏邮箱。 - */ - public static function trialSend(string $account, string $password): string - { - // 带上实例指纹(sn/macs),让 appstore 在发送验证码前即可做试用资格校验 - $r = self::call('trial/send', array_merge(['account' => $account, 'password' => $password], self::fingerprint())); + $r = self::call('email/send', ['email' => $email]); if (!$r['ok']) { throw new ApiException($r['message']); } @@ -183,16 +166,32 @@ class OnlineLicense } /** - * 申请试用并签发。 + * 邮箱 + 验证码登录并签发。失败抛 ApiException。 */ - public static function trial(string $account, string $password, string $code): array + public static function login(string $email, string $code): array { - $payload = array_merge(['account' => $account, 'password' => $password, 'code' => $code], self::fingerprint()); + $r = self::call('login', array_merge(['email' => $email, 'code' => $code], self::fingerprint())); + 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)); + } + return self::status(); + } + + /** + * 邮箱 + 验证码申请试用并签发。 + */ + public static function trial(string $email, string $code): array + { + $payload = array_merge(['email' => $email, 'code' => $code], self::fingerprint()); $r = self::call('trial', $payload); if (!$r['ok']) { throw new ApiException($r['message']); } - $status = self::applyIssue($account, $r['data']); + $status = self::applyIssue($email, $r['data']); if (!in_array($status, ['issued', 'renewed'], true)) { throw new ApiException(self::statusHint($status)); } diff --git a/language/original-api.txt b/language/original-api.txt index 4adf5fb1b..41da57f9f 100644 --- a/language/original-api.txt +++ b/language/original-api.txt @@ -1018,3 +1018,5 @@ AI 助手 在线授权即将到期,请保持联网续期 在线授权已过期,新增用户受限,请尽快续期 在线授权已失效,已回落到基础版 +请输入邮箱 +请输入邮箱和验证码 diff --git a/language/original-web.txt b/language/original-web.txt index fe92cde26..7c4df7306 100644 --- a/language/original-web.txt +++ b/language/original-web.txt @@ -2501,3 +2501,6 @@ App Store账号 当前已绑定在线授权,绑定离线后将替换当前授权并释放在线座位,是否继续? 当前已绑定离线授权,绑定在线后将替换当前授权,是否继续? 请输入License +验证码已发送至(*) +(*)秒后重发 +请输入邮箱和验证码 diff --git a/resources/ai-kb/zh/howto/license/online.md b/resources/ai-kb/zh/howto/license/online.md index 373e323be..81410369a 100644 --- a/resources/ai-kb/zh/howto/license/online.md +++ b/resources/ai-kb/zh/howto/license/online.md @@ -1,19 +1,20 @@ --- id: license.online.howto -title: 在线授权(账号登录 / 申请试用 / 自动续期) +title: 在线授权(邮箱验证码登录 / 申请试用 / 自动续期) type: howto feature: license scope: super-admin locale: zh aliases: - 在线授权 - - 账号授权 + - 邮箱授权 + - 邮箱验证码授权 - 用 appstore 账号授权 - 申请试用 - 试用授权 - 在线 License - 自动续期 - - 授权账号登录 + - 授权邮箱登录 - 在线授权到期 - 在线授权冻结 - 在线授权被吊销 @@ -22,7 +23,7 @@ related_tools: [] related_pages: [] prerequisites: - 需要进入「系统设置」→「License」页面(仅管理员可见),切到「在线授权」Tab - - 需要一个在 App Store 申请的账号(开发者账号),登录后可在 App Store 门户「我的授权」查看 + - 需要一个在 App Store 注册的邮箱账号,登录后可在 App Store 门户「我的授权」查看 - 终端需能联网访问 App Store 授权中心 negative: - 在线授权与离线授权互斥:同一时刻只有一张生效 License;切到在线并登录后会接管 License 文件 @@ -32,29 +33,31 @@ negative: last_verified: v1.7.91 --- -# 在线授权(账号登录 / 申请试用 / 自动续期) +# 在线授权(邮箱验证码登录 / 申请试用 / 自动续期) DooTask 的 License 页提供两种授权方式,可在页面顶部 Tab 切换: - **离线授权**:手动粘贴 License 原文(见 [[license.howto]]),无自动续期。 -- **在线授权**:用 App Store 账号登录自助签发,终端自动定时续期(类似 JetBrains 账号激活)。 +- **在线授权**:用 App Store 注册邮箱 + 邮箱验证码登录自助签发,终端自动定时续期(类似 JetBrains 账号激活)。 ## 入口 桌面端:左上角头像 →「系统设置」→「License」→ 顶部「在线授权」Tab(仅管理员可见)。 ## 怎么用 -### 已有账号 + 已有授权 -1. 在「在线授权」Tab 输入 App Store 账号、密码 -2. 点击「登录授权」 -3. 终端把自身指纹(doo_sn、网卡 MAC、版本)上报授权中心,签发一张租约 License 并落地 -4. 成功后页面显示套餐、使用人数、租约到期、当前状态 +登录授权与申请试用共用同一套「邮箱 + 验证码」输入,先发码再选择动作: +1. 在「在线授权」Tab 输入 App Store 注册邮箱 +2. 点击「发送验证码」→ 系统向该邮箱发码(按钮进入 60 秒倒计时,发码成功后提示验证码已发送至脱敏邮箱) +3. 填入收到的验证码 -### 申请试用(没有正式授权时) -1. 在「在线授权」Tab 输入账号、密码 -2. 点击「申请试用」→ 系统向账号注册邮箱发送验证码 -3. 填入验证码 → 点击「确定试用」即开通试用授权并签发 -4. 试用默认 14 天 / 不限人数(具体以 App Store 管理员配置为准,时长硬上限 60 天),每个账号仅能申请一次 +### 已有授权 → 登录授权 +- 填好邮箱与验证码后点击「登录授权」 +- 终端把自身指纹(doo_sn、网卡 MAC、版本)上报授权中心,签发一张租约 License 并落地 +- 成功后页面显示套餐、使用人数、租约到期、当前状态 + +### 没有正式授权 → 申请试用 +- 填好邮箱与验证码后点击「申请试用」即开通试用授权并签发 +- 试用默认 14 天 / 不限人数(具体以 App Store 管理员配置为准,时长硬上限 60 天),每个账号仅能申请一次 ### 日常维护 - **自动续期**:终端每小时检查,租约将尽时自动向授权中心续期,无需人工干预(需保持联网) diff --git a/resources/assets/js/pages/manage/setting/license.vue b/resources/assets/js/pages/manage/setting/license.vue index 1c1957cc7..5a3bcf3c0 100644 --- a/resources/assets/js/pages/manage/setting/license.vue +++ b/resources/assets/js/pages/manage/setting/license.vue @@ -21,7 +21,7 @@
@@ -103,7 +103,7 @@ @@ -125,21 +125,21 @@ @@ -222,17 +222,21 @@ export default { offlineRebindLicense: '', onlineIng: 0, onlineForm: { - account: '', - password: '', + email: '', code: '', }, - trialStep: 0, - trialEmail: '', + codeSent: false, // 是否已发码(登录与试用共用同一套邮箱+验证码) + maskedEmail: '', // 发码成功后服务端返回的脱敏邮箱 + codeCountdown: 0, // 重发倒计时(秒) + codeTimer: null, } }, mounted() { this.onlineRefresh(); }, + beforeDestroy() { + this.clearCodeTimer(); + }, computed: { ...mapState(['userInfo', 'formOptions']), @@ -418,15 +422,33 @@ export default { } }, + // 发送邮箱验证码(登录与试用共用),成功后开启 60s 倒计时并展示脱敏邮箱 + emailSend() { + if (this.codeCountdown > 0) { + return; + } + if (!this.onlineForm.email) { + $A.messageError('请输入邮箱'); + return; + } + this.onlineCall('license/email/send', { + email: this.onlineForm.email, + }).then(({data}) => { + this.codeSent = true; + this.maskedEmail = data?.email || ''; + this.startCodeCountdown(); + }); + }, + onlineLogin() { - if (!this.onlineForm.account || !this.onlineForm.password) { - $A.messageError('请输入账号和密码'); + if (!this.onlineForm.email || !this.onlineForm.code) { + $A.messageError('请输入邮箱和验证码'); return; } this.confirmReplaceOffline(() => { this.onlineCall('license/login', { - account: this.onlineForm.account, - password: this.onlineForm.password, + email: this.onlineForm.email, + code: this.onlineForm.code, }, '授权成功').then(_ => { this.resetOnlineForm(); this.systemSetting(); @@ -434,29 +456,14 @@ export default { }); }, - trialSend() { - if (!this.onlineForm.account || !this.onlineForm.password) { - $A.messageError('请输入账号和密码'); - return; - } - this.onlineCall('license/trial/send', { - account: this.onlineForm.account, - password: this.onlineForm.password, - }).then(({data}) => { - this.trialStep = 1; - this.trialEmail = data?.email || ''; - }); - }, - trialSubmit() { - if (!this.onlineForm.code) { - $A.messageError('请输入验证码'); + if (!this.onlineForm.email || !this.onlineForm.code) { + $A.messageError('请输入邮箱和验证码'); return; } this.confirmReplaceOffline(() => { this.onlineCall('license/trial', { - account: this.onlineForm.account, - password: this.onlineForm.password, + email: this.onlineForm.email, code: this.onlineForm.code, }, '试用已开通').then(_ => { this.resetOnlineForm(); @@ -476,10 +483,30 @@ export default { }); }, + startCodeCountdown() { + this.clearCodeTimer(); + this.codeCountdown = 60; + this.codeTimer = setInterval(() => { + this.codeCountdown--; + if (this.codeCountdown <= 0) { + this.clearCodeTimer(); + } + }, 1000); + }, + + clearCodeTimer() { + if (this.codeTimer) { + clearInterval(this.codeTimer); + this.codeTimer = null; + } + this.codeCountdown = 0; + }, + resetOnlineForm() { - this.onlineForm = {account: '', password: '', code: ''}; - this.trialStep = 0; - this.trialEmail = ''; + this.onlineForm = {email: '', code: ''}; + this.codeSent = false; + this.maskedEmail = ''; + this.clearCodeTimer(); }, } } diff --git a/routes/api-map.md b/routes/api-map.md index b66aabea4..f100a84fb 100644 --- a/routes/api-map.md +++ b/routes/api-map.md @@ -200,8 +200,8 @@ API 使用动态路由(见 `routes/web.php`),URL 段映射为控制器方 | URL | 方法名 | HTTP | 说明 | | --- | --- | --- | --- | +| api/license/email/send | email__send() | any | | | api/license/login | login() | any | | -| api/license/trial/send | trial__send() | any | | | api/license/trial | trial() | any | | | api/license/status | status() | any | | | api/license/refresh | refresh() | any | |