mirror of
https://github.com/kuaifan/dootask.git
synced 2026-07-02 04:15:05 +00:00
feat(license): 在线授权登录与试用改为邮箱+验证码
配合 appstore 账号体系统一,在线授权去除 App Store 账号+密码: - OnlineLicense 新增 emailSend,login/trial 改 (email, code),call payload 去 account/password、加 email/code(fingerprint 续传) - LicenseController 新增 email__send,login/trial 读 email/code - license.vue 在线 Tab 改邮箱+发码+验证码(与试用复用状态防串台), 离线 Tab 与互斥链路不动 - 同步 i18n 文案与 ai-kb license/online、api-map
This commit is contained in:
parent
7c6dfe8a25
commit
7c6b8ce6f4
@ -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);
|
||||
}
|
||||
|
||||
|
||||
@ -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));
|
||||
}
|
||||
|
||||
@ -1018,3 +1018,5 @@ AI 助手
|
||||
在线授权即将到期,请保持联网续期
|
||||
在线授权已过期,新增用户受限,请尽快续期
|
||||
在线授权已失效,已回落到基础版
|
||||
请输入邮箱
|
||||
请输入邮箱和验证码
|
||||
|
||||
@ -2501,3 +2501,6 @@ App Store账号
|
||||
当前已绑定在线授权,绑定离线后将替换当前授权并释放在线座位,是否继续?
|
||||
当前已绑定离线授权,绑定在线后将替换当前授权,是否继续?
|
||||
请输入License
|
||||
验证码已发送至(*)
|
||||
(*)秒后重发
|
||||
请输入邮箱和验证码
|
||||
|
||||
@ -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 天),每个账号仅能申请一次
|
||||
|
||||
### 日常维护
|
||||
- **自动续期**:终端每小时检查,租约将尽时自动向授权中心续期,无需人工干预(需保持联网)
|
||||
|
||||
@ -21,7 +21,7 @@
|
||||
</Form>
|
||||
<div class="setting-footer">
|
||||
<Button :loading="loadIng > 0" type="primary" @click="offlineRebindSubmit">{{$L('提交')}}</Button>
|
||||
<Button :loading="loadIng > 0" @click="offlineRebindCancel" style="margin-left: 8px">{{$L('取消')}}</Button>
|
||||
<Button :loading="loadIng > 0" @click="offlineRebindCancel">{{$L('取消')}}</Button>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
@ -103,7 +103,7 @@
|
||||
</Form>
|
||||
<div class="setting-footer">
|
||||
<Button :loading="loadIng > 0" type="primary" @click="submitForm">{{$L('提交')}}</Button>
|
||||
<Button :loading="loadIng > 0" @click="resetForm" style="margin-left: 8px">{{$L('重置')}}</Button>
|
||||
<Button :loading="loadIng > 0" @click="resetForm">{{$L('重置')}}</Button>
|
||||
</div>
|
||||
</template>
|
||||
</TabPane>
|
||||
@ -125,21 +125,21 @@
|
||||
</div>
|
||||
<template v-else>
|
||||
<Form :model="onlineForm" v-bind="formOptions" @submit.native.prevent>
|
||||
<FormItem :label="$L('App Store账号')">
|
||||
<Input v-model="onlineForm.account" :placeholder="$L('请输入App Store账号')" />
|
||||
<FormItem :label="$L('邮箱')">
|
||||
<Input v-model="onlineForm.email"
|
||||
:class="codeCountdown > 0 ? 'setting-send-input' : 'setting-input'"
|
||||
search @on-search="emailSend"
|
||||
:enter-button="codeCountdown > 0 ? $L('(*)秒后重发', codeCountdown) : $L('发送验证码')"
|
||||
:placeholder="$L('请输入邮箱')"/>
|
||||
</FormItem>
|
||||
<FormItem :label="$L('密码')">
|
||||
<Input v-model="onlineForm.password" type="password" :placeholder="$L('请输入密码')" />
|
||||
</FormItem>
|
||||
<FormItem v-if="trialStep === 1" :label="$L('邮箱验证码')">
|
||||
<Input v-model="onlineForm.code" :placeholder="$L('请输入验证码')" />
|
||||
<div class="online-tip">{{$L('验证码已发送至')}} {{trialEmail}}</div>
|
||||
<FormItem v-if="codeSent" :label="$L('邮箱验证码')">
|
||||
<Input v-model="onlineForm.code" class="setting-input" :placeholder="$L('请输入验证码')"/>
|
||||
<div class="online-tip">{{$L('验证码已发送至(*)', maskedEmail)}}</div>
|
||||
</FormItem>
|
||||
</Form>
|
||||
<div class="setting-footer">
|
||||
<Button :loading="onlineIng > 0" type="primary" @click="onlineLogin">{{$L('登录授权')}}</Button>
|
||||
<Button v-if="trialStep === 0" :loading="onlineIng > 0" @click="trialSend" style="margin-left: 8px">{{$L('申请试用')}}</Button>
|
||||
<Button v-else :loading="onlineIng > 0" type="success" @click="trialSubmit" style="margin-left: 8px">{{$L('确定试用')}}</Button>
|
||||
<Button :loading="onlineIng > 0" type="success" @click="trialSubmit">{{$L('申请试用')}}</Button>
|
||||
</div>
|
||||
</template>
|
||||
</TabPane>
|
||||
@ -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();
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 | |
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user