mirror of
https://github.com/kuaifan/dootask.git
synced 2026-06-26 17:22:26 +00:00
feat(license): 新增在线授权(App Store 账号自助签发 + 自动续期)
- OnlineLicense 模块:登录/试用/续期/释放/状态机,离线↔在线互斥(last-write-wins) - LicenseController + 动态路由;容器内 supervisor 独立进程定时续期(不依赖 LARAVELS_TIMER) - license.vue 双 Tab:在线授权 + 离线绑定二次确认,已绑定在线时离线页提示+按需绑定 - 进入授权页静默刷新;同步 ai-kb 在线授权知识库 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
6a4f815d5a
commit
4ca7fc10d1
29
app/Console/Commands/OnlineLicenseRenew.php
Normal file
29
app/Console/Commands/OnlineLicenseRenew.php
Normal file
@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Module\OnlineLicense;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
/**
|
||||
* 在线授权续期(容器内独立进程按小时调用,无需 LARAVELS_TIMER、不经过 HTTP 转发)。
|
||||
*
|
||||
* 由 php 容器 supervisor 程序 [program:license] 循环调用:
|
||||
* while true; do php artisan online-license:renew; sleep 3600; done
|
||||
*/
|
||||
class OnlineLicenseRenew extends Command
|
||||
{
|
||||
protected $signature = 'online-license:renew';
|
||||
protected $description = '在线授权:本地状态机推进 + 租约将尽时自动续期';
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
if (!OnlineLicense::enabled()) {
|
||||
return 0;
|
||||
}
|
||||
OnlineLicense::cron();
|
||||
$status = OnlineLicense::status();
|
||||
$this->info('online-license: ' . ($status['status'] ?? 'offline') . ' lease=' . ($status['lease_expired_at'] ?? '-'));
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
97
app/Http/Controllers/Api/LicenseController.php
Normal file
97
app/Http/Controllers/Api/LicenseController.php
Normal file
@ -0,0 +1,97 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Module\Base;
|
||||
use App\Module\OnlineLicense;
|
||||
use Request;
|
||||
|
||||
/**
|
||||
* 在线授权客户端(与 SystemController::license 的离线粘贴并存)。
|
||||
*
|
||||
* 动态路由(routes/web.php):
|
||||
* api/license/login -> login()
|
||||
* api/license/trial/send -> trial__send()
|
||||
* api/license/trial -> trial()
|
||||
* api/license/status -> status()
|
||||
* api/license/refresh -> refresh()
|
||||
* api/license/logout -> logout()
|
||||
*/
|
||||
class LicenseController extends AbstractController
|
||||
{
|
||||
/**
|
||||
* 账号登录并签发在线授权
|
||||
*/
|
||||
public function login()
|
||||
{
|
||||
User::auth('admin');
|
||||
$account = trim(Request::input('account'));
|
||||
$password = trim(Request::input('password'));
|
||||
if ($account === '' || $password === '') {
|
||||
return Base::retError('请输入账号和密码');
|
||||
}
|
||||
$data = OnlineLicense::login($account, $password);
|
||||
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'));
|
||||
$code = trim(Request::input('code'));
|
||||
if ($account === '' || $password === '' || $code === '') {
|
||||
return Base::retError('请输入账号、密码和验证码');
|
||||
}
|
||||
$data = OnlineLicense::trial($account, $password, $code);
|
||||
return Base::retSuccess('试用已开通', $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 当前在线授权状态
|
||||
*/
|
||||
public function status()
|
||||
{
|
||||
User::auth('admin');
|
||||
return Base::retSuccess('success', OnlineLicense::status());
|
||||
}
|
||||
|
||||
/**
|
||||
* 进入授权页时的静默刷新:服务可达则更新授权数据,网络失败则不更新、不提示。
|
||||
*/
|
||||
public function refresh()
|
||||
{
|
||||
User::auth('admin');
|
||||
OnlineLicense::refresh();
|
||||
return Base::retSuccess('success', OnlineLicense::status());
|
||||
}
|
||||
|
||||
/**
|
||||
* 退出在线授权(释放座位 + 回落默认)
|
||||
*/
|
||||
public function logout()
|
||||
{
|
||||
User::auth('admin');
|
||||
OnlineLicense::logout();
|
||||
return Base::retSuccess('已退出在线授权');
|
||||
}
|
||||
}
|
||||
@ -13,6 +13,7 @@ use Carbon\Carbon;
|
||||
use App\Module\Doo;
|
||||
use App\Models\User;
|
||||
use App\Module\Base;
|
||||
use App\Module\OnlineLicense;
|
||||
use App\Module\Timer;
|
||||
use App\Models\Setting;
|
||||
use LdapRecord\Container;
|
||||
@ -857,6 +858,8 @@ class SystemController extends AbstractController
|
||||
if ($type == 'save') {
|
||||
$license = Request::input('license');
|
||||
Doo::licenseSave($license);
|
||||
// 离线/在线互斥:保存离线 license 即退出在线模式(尽力释放座位+清在线标志,不删除刚写入的文件)
|
||||
OnlineLicense::switchToOffline();
|
||||
}
|
||||
//
|
||||
$data = [
|
||||
@ -892,6 +895,11 @@ class SystemController extends AbstractController
|
||||
if ($data['info']['expired_at'] && strtotime($data['info']['expired_at']) <= Timer::time()) {
|
||||
$data['error'][] = '终端License已过期';
|
||||
}
|
||||
// 在线授权:把状态机提醒并入 error[](dashboard 警告条与本页错误展示自动复用),并附在线状态
|
||||
foreach (OnlineLicense::stageMessages() as $msg) {
|
||||
$data['error'][] = $msg;
|
||||
}
|
||||
$data['online'] = OnlineLicense::status();
|
||||
//
|
||||
if ($type === 'error') {
|
||||
$data = [
|
||||
|
||||
442
app/Module/OnlineLicense.php
Normal file
442
app/Module/OnlineLicense.php
Normal file
@ -0,0 +1,442 @@
|
||||
<?php
|
||||
|
||||
namespace App\Module;
|
||||
|
||||
use App\Exceptions\ApiException;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Support\Facades\Crypt;
|
||||
|
||||
/**
|
||||
* 在线授权客户端编排。
|
||||
*
|
||||
* 在线授权产出的仍是现有格式的离线 license blob,只是「获取方式」变成用 appstore 账号登录
|
||||
* 自助签发、并由本类定时续期。doo.so 本地校验、license 文件存储全部复用。绑定状态以单例
|
||||
* 形式存于 settings 表(name=onlineLicense),instance_token 用 Crypt 加密。
|
||||
*
|
||||
* 四级状态机(基于租约内嵌到期 lease_expired_at 与本地宽限,全程不依赖 appstore 可达):
|
||||
* active 续期正常
|
||||
* reminder 续期失败/租约剩余不足 warn_days(仅管理员可见提醒)
|
||||
* frozen 租约已过期(doo.so 既有行为:限制新增用户)
|
||||
* revoked 冻结超过 grace_days 或 appstore 明确吊销 → 回落默认 3 人版
|
||||
*/
|
||||
class OnlineLicense
|
||||
{
|
||||
const KEY = 'onlineLicense';
|
||||
|
||||
// ---- 配置 ----
|
||||
|
||||
protected static function appstoreUrl(): string
|
||||
{
|
||||
return rtrim((string)config('dootask.online_license_appstore_url'), '/');
|
||||
}
|
||||
|
||||
protected static function renewWithinDays(): int
|
||||
{
|
||||
return (int)config('dootask.online_license_renew_within_days', 20);
|
||||
}
|
||||
|
||||
protected static function warnDays(): int
|
||||
{
|
||||
return (int)config('dootask.online_license_warn_days', 7);
|
||||
}
|
||||
|
||||
protected static function graceDays(): int
|
||||
{
|
||||
return (int)config('dootask.online_license_grace_days', 14);
|
||||
}
|
||||
|
||||
// ---- 状态读写(单例 settings)----
|
||||
|
||||
public static function get(): array
|
||||
{
|
||||
return Base::setting(self::KEY) ?: [];
|
||||
}
|
||||
|
||||
protected static function set(array $patch): array
|
||||
{
|
||||
$next = array_merge(self::get(), $patch);
|
||||
Base::setting(self::KEY, $next);
|
||||
return $next;
|
||||
}
|
||||
|
||||
public static function enabled(): bool
|
||||
{
|
||||
$s = self::get();
|
||||
return !empty($s['enabled']) && ($s['mode'] ?? '') === 'online';
|
||||
}
|
||||
|
||||
protected static function token(): string
|
||||
{
|
||||
$enc = self::get()['instance_token'] ?? '';
|
||||
if (empty($enc)) {
|
||||
return '';
|
||||
}
|
||||
try {
|
||||
return Crypt::decryptString($enc);
|
||||
} catch (\Throwable) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
protected static function fingerprint(): array
|
||||
{
|
||||
return [
|
||||
'sn' => Doo::dooSN(),
|
||||
'macs' => implode(',', Doo::macs()),
|
||||
'url' => (string)config('app.url'),
|
||||
'version' => Doo::dooVersion(),
|
||||
];
|
||||
}
|
||||
|
||||
// ---- appstore 调用 ----
|
||||
|
||||
/**
|
||||
* 调 appstore license 接口。返回 ['ok'=>bool, 'data'=>array, 'message'=>string]。
|
||||
* $bearer 非空时带实例令牌(续期/释放)。
|
||||
*/
|
||||
protected static function call(string $path, array $payload, string $bearer = ''): array
|
||||
{
|
||||
$url = self::appstoreUrl() . '/api/v1/license/' . ltrim($path, '/');
|
||||
$headers = ['Content-Type' => 'application/json'];
|
||||
if ($bearer !== '') {
|
||||
$headers['Authorization'] = 'Bearer ' . $bearer;
|
||||
}
|
||||
$resp = Ihttp::ihttp_request($url, json_encode($payload, JSON_UNESCAPED_UNICODE), $headers, 15);
|
||||
if (Base::isError($resp)) {
|
||||
return ['ok' => false, 'data' => [], 'message' => $resp['msg'] ?: '无法连接授权服务'];
|
||||
}
|
||||
$body = Base::json2array($resp['data'] ?? '');
|
||||
if (($body['code'] ?? 0) !== 200) {
|
||||
return ['ok' => false, 'data' => [], 'message' => $body['message'] ?: '授权服务返回错误'];
|
||||
}
|
||||
return ['ok' => true, 'data' => $body['data'] ?? [], 'message' => ''];
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理签发结果:issued/renewed 则落地 license + 更新绑定状态;其它状态原样返回供上层决策。
|
||||
*/
|
||||
protected static function applyIssue(string $account, array $d): string
|
||||
{
|
||||
$status = $d['status'] ?? '';
|
||||
if (in_array($status, ['issued', 'renewed'], true)) {
|
||||
$blob = $d['license'] ?? '';
|
||||
if ($blob === '') {
|
||||
throw new ApiException('授权服务未返回 license');
|
||||
}
|
||||
Doo::licenseSave($blob); // 复用离线落地与 doo.so 校验
|
||||
$snap = $d['snapshot'] ?? [];
|
||||
$patch = [
|
||||
'enabled' => true,
|
||||
'mode' => 'online',
|
||||
'account' => $account,
|
||||
'plan' => $snap['plan'] ?? '',
|
||||
'people' => $snap['people'] ?? 0,
|
||||
'valid_until' => $snap['valid_until'] ?? null,
|
||||
'lease_expired_at' => $snap['lease_expired_at'] ?? null,
|
||||
'server_status' => $status,
|
||||
'error_count' => 0,
|
||||
'last_error' => '',
|
||||
'frozen_since' => null,
|
||||
'last_renewed_at' => Carbon::now()->toDateTimeString(),
|
||||
];
|
||||
if (!empty($d['instance_token'])) {
|
||||
$patch['instance_token'] = Crypt::encryptString($d['instance_token']);
|
||||
}
|
||||
self::set($patch);
|
||||
self::computeStage();
|
||||
}
|
||||
return $status;
|
||||
}
|
||||
|
||||
// ---- 对外动作 ----
|
||||
|
||||
/**
|
||||
* 账号登录并签发。失败抛 ApiException。
|
||||
*/
|
||||
public static function login(string $account, string $password): array
|
||||
{
|
||||
$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
|
||||
{
|
||||
$r = self::call('trial/send', ['account' => $account, 'password' => $password]);
|
||||
if (!$r['ok']) {
|
||||
throw new ApiException($r['message']);
|
||||
}
|
||||
return $r['data']['email'] ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
* 申请试用并签发。
|
||||
*/
|
||||
public static function trial(string $account, string $password, string $code): array
|
||||
{
|
||||
$payload = array_merge(['account' => $account, 'password' => $password, 'code' => $code], self::fingerprint());
|
||||
$r = self::call('trial', $payload);
|
||||
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 renew(): void
|
||||
{
|
||||
if (!self::enabled()) {
|
||||
return;
|
||||
}
|
||||
$token = self::token();
|
||||
if ($token === '') {
|
||||
return;
|
||||
}
|
||||
$r = self::call('renew', self::fingerprint(), $token);
|
||||
if (!$r['ok']) {
|
||||
$s = self::get();
|
||||
self::set([
|
||||
'error_count' => (int)($s['error_count'] ?? 0) + 1,
|
||||
'last_error' => $r['message'],
|
||||
]);
|
||||
self::computeStage();
|
||||
return;
|
||||
}
|
||||
$status = $r['data']['status'] ?? '';
|
||||
if (in_array($status, ['issued', 'renewed'], true)) {
|
||||
self::applyIssue(self::get()['account'] ?? '', $r['data']);
|
||||
return;
|
||||
}
|
||||
// 服务侧明确状态(revoked/suspended/no_entitlement):不延长租约,记录后交状态机
|
||||
self::set(['server_status' => $status, 'last_error' => self::statusHint($status)]);
|
||||
self::computeStage();
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否到了该续期的时间(租约剩余不足 renew_within_days)。
|
||||
*/
|
||||
public static function dueForRenew(): bool
|
||||
{
|
||||
$lease = self::get()['lease_expired_at'] ?? null;
|
||||
if (!$lease) {
|
||||
return true;
|
||||
}
|
||||
return Carbon::parse($lease)->lte(Carbon::now()->addDays(self::renewWithinDays()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 定时续期入口:由容器内独立进程的 artisan 命令(online-license:renew)按小时调用。
|
||||
* 先本地状态机推进(断网也能降级 frozen→revoked),再在租约将尽时续期。
|
||||
*/
|
||||
public static function cron(): void
|
||||
{
|
||||
if (!self::enabled()) {
|
||||
return;
|
||||
}
|
||||
self::computeStage();
|
||||
if (self::enabled() && self::dueForRenew()) {
|
||||
self::renew();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 进入授权页时的静默刷新:服务可达则按服务结果更新(成功续签 / 反映吊销冻结),
|
||||
* 网络失败则什么都不做、不提示、不降级(避免一次页面刷新失败就误报)。
|
||||
*/
|
||||
public static function refresh(): void
|
||||
{
|
||||
if (!self::enabled()) {
|
||||
return;
|
||||
}
|
||||
$token = self::token();
|
||||
if ($token === '') {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
$r = self::call('renew', self::fingerprint(), $token);
|
||||
if (!$r['ok']) {
|
||||
return; // 刷新失败:不更新、不提示
|
||||
}
|
||||
$status = $r['data']['status'] ?? '';
|
||||
if (in_array($status, ['issued', 'renewed'], true)) {
|
||||
self::applyIssue(self::get()['account'] ?? '', $r['data']);
|
||||
} elseif (in_array($status, ['revoked', 'suspended', 'no_entitlement'], true)) {
|
||||
// 服务侧明确结果(非网络失败):如实反映
|
||||
self::set(['server_status' => $status, 'last_error' => self::statusHint($status)]);
|
||||
self::computeStage();
|
||||
}
|
||||
} catch (\Throwable) {
|
||||
// 忽略,保持现状
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 退出在线授权:释放座位 + 回落默认。
|
||||
*/
|
||||
public static function logout(): void
|
||||
{
|
||||
$token = self::token();
|
||||
if ($token !== '') {
|
||||
self::call('deactivate', [], $token);
|
||||
}
|
||||
self::fallbackToDefault();
|
||||
Base::setting(self::KEY, ['enabled' => false, 'mode' => 'offline']);
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换到离线授权(互斥):保存离线 license 后调用。
|
||||
* 尽力释放在线座位 + 清在线标志,但「不」删除 license 文件(刚保存的离线 license 要保留)。
|
||||
*/
|
||||
public static function switchToOffline(): void
|
||||
{
|
||||
if (!self::enabled()) {
|
||||
return;
|
||||
}
|
||||
$token = self::token();
|
||||
if ($token !== '') {
|
||||
self::call('deactivate', [], $token); // 尽力释放座位,失败忽略
|
||||
}
|
||||
Base::setting(self::KEY, ['enabled' => false, 'mode' => 'offline']);
|
||||
}
|
||||
|
||||
// ---- 状态机 ----
|
||||
|
||||
/**
|
||||
* 据租约到期 + 宽限重新计算 status,并在 revoked 时执行降级。
|
||||
*/
|
||||
public static function computeStage(): string
|
||||
{
|
||||
$s = self::get();
|
||||
if (($s['mode'] ?? '') !== 'online' || empty($s['enabled'])) {
|
||||
return 'offline';
|
||||
}
|
||||
$now = Carbon::now();
|
||||
$server = $s['server_status'] ?? '';
|
||||
$lease = $s['lease_expired_at'] ?? null;
|
||||
|
||||
if ($server === 'revoked') {
|
||||
return self::transitionRevoked();
|
||||
}
|
||||
|
||||
if ($lease && Carbon::parse($lease)->lte($now)) {
|
||||
// 租约已过期 → 冻结;超过宽限 → 吊销
|
||||
$frozenSince = $s['frozen_since'] ?? null;
|
||||
if (!$frozenSince) {
|
||||
$frozenSince = $now->toDateTimeString();
|
||||
self::set(['frozen_since' => $frozenSince]);
|
||||
}
|
||||
if (Carbon::parse($frozenSince)->addDays(self::graceDays())->lte($now)) {
|
||||
return self::transitionRevoked();
|
||||
}
|
||||
self::set(['status' => 'frozen']);
|
||||
return 'frozen';
|
||||
}
|
||||
|
||||
// 租约有效
|
||||
$remindByLease = $lease && Carbon::parse($lease)->lte($now->copy()->addDays(self::warnDays()));
|
||||
$remindByError = (int)($s['error_count'] ?? 0) > 0 || $server === 'suspended' || $server === 'no_entitlement';
|
||||
$status = ($remindByLease || $remindByError) ? 'reminder' : 'active';
|
||||
self::set(['status' => $status, 'frozen_since' => null]);
|
||||
return $status;
|
||||
}
|
||||
|
||||
protected static function transitionRevoked(): string
|
||||
{
|
||||
self::fallbackToDefault();
|
||||
self::set(['status' => 'revoked', 'enabled' => false]);
|
||||
return 'revoked';
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除在线 license 文件,让 dooso 回落默认 3 人版(触发既有超员禁用)。
|
||||
*/
|
||||
protected static function fallbackToDefault(): void
|
||||
{
|
||||
foreach (['LICENSE', 'license'] as $name) {
|
||||
$path = config_path($name);
|
||||
if (is_file($path)) {
|
||||
@unlink($path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 提醒文案(注入 system/license 的 error[],复用 dashboard 警告条与 license 页)----
|
||||
|
||||
public static function stageMessages(): array
|
||||
{
|
||||
if (!self::enabled() && (self::get()['status'] ?? '') !== 'revoked') {
|
||||
return [];
|
||||
}
|
||||
$s = self::get();
|
||||
$status = $s['status'] ?? self::computeStage();
|
||||
$msgs = [];
|
||||
switch ($status) {
|
||||
case 'reminder':
|
||||
if (($s['server_status'] ?? '') === 'suspended') {
|
||||
$msgs[] = '在线授权已被冻结,请联系服务商';
|
||||
} elseif ((int)($s['error_count'] ?? 0) > 0) {
|
||||
$msgs[] = '在线授权续期失败,请检查网络';
|
||||
} else {
|
||||
$msgs[] = '在线授权即将到期,请保持联网续期';
|
||||
}
|
||||
break;
|
||||
case 'frozen':
|
||||
$msgs[] = '在线授权已过期,新增用户受限,请尽快续期';
|
||||
break;
|
||||
case 'revoked':
|
||||
$msgs[] = '在线授权已失效,已回落到基础版';
|
||||
break;
|
||||
}
|
||||
return $msgs;
|
||||
}
|
||||
|
||||
protected static function statusHint(string $status): string
|
||||
{
|
||||
return match ($status) {
|
||||
'no_entitlement' => '该账号暂无可用授权,请先申请试用或购买',
|
||||
'revoked' => '该授权已被吊销',
|
||||
'suspended' => '该授权已被冻结',
|
||||
'seat_taken' => '该授权已在另一台实例使用,请先在原实例释放(换机)',
|
||||
'entitlement_expired' => '该授权已到期',
|
||||
default => '签发失败(' . $status . ')',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 对外状态(前端在线 Tab / status 接口用,不含敏感 token)。
|
||||
*/
|
||||
public static function status(): array
|
||||
{
|
||||
$s = self::get();
|
||||
if (($s['mode'] ?? '') !== 'online' || empty($s['enabled'])) {
|
||||
return ['mode' => 'offline'];
|
||||
}
|
||||
return [
|
||||
'mode' => 'online',
|
||||
'account' => $s['account'] ?? '',
|
||||
'plan' => $s['plan'] ?? '',
|
||||
'people' => $s['people'] ?? 0,
|
||||
'valid_until' => $s['valid_until'] ?? null,
|
||||
'lease_expired_at' => $s['lease_expired_at'] ?? null,
|
||||
'last_renewed_at' => $s['last_renewed_at'] ?? null,
|
||||
'status' => $s['status'] ?? self::computeStage(),
|
||||
'error_count' => (int)($s['error_count'] ?? 0),
|
||||
'last_error' => $s['last_error'] ?? '',
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -32,4 +32,17 @@ return [
|
||||
// 临时文件自动清理天数(DeleteTmpTask)
|
||||
'auto_empty_temp_file' => env('AUTO_EMPTY_TEMP_FILE', 30),
|
||||
|
||||
// 在线授权:appstore 授权中心地址(OnlineLicense;默认中央,测试可指向 dev appstore)
|
||||
// [调试中] 临时指向本地 dev appstore,发版前改回 'https://appstore.dootask.com'
|
||||
'online_license_appstore_url' => env('ONLINE_LICENSE_APPSTORE_URL', 'https://appstore.dootask.com'),
|
||||
|
||||
// 在线授权:租约剩余不足该天数时触发续期(OnlineLicense)
|
||||
'online_license_renew_within_days' => env('ONLINE_LICENSE_RENEW_WITHIN_DAYS', 20),
|
||||
|
||||
// 在线授权:租约剩余不足该天数时在提醒(OnlineLicense)
|
||||
'online_license_warn_days' => env('ONLINE_LICENSE_WARN_DAYS', 7),
|
||||
|
||||
// 在线授权:冻结(租约过期)后到吊销的宽限天数(OnlineLicense)
|
||||
'online_license_grace_days' => env('ONLINE_LICENSE_GRACE_DAYS', 14),
|
||||
|
||||
];
|
||||
|
||||
@ -198,6 +198,9 @@ return [
|
||||
'jobs' => [
|
||||
// Enable LaravelScheduleJob to run `php artisan schedule:run` every 1 minute, replace Linux Crontab
|
||||
// Hhxsv5\LaravelS\Illuminate\LaravelScheduleJob::class,
|
||||
|
||||
// 在线授权续期改由容器内独立进程跑(supervisor [program:license] + artisan online-license:renew),
|
||||
// 不再依赖 LARAVELS_TIMER;见 docker/php/license.conf
|
||||
],
|
||||
|
||||
// Max waiting time of reloading
|
||||
|
||||
@ -11,6 +11,7 @@ services:
|
||||
- shared_data:/usr/share/dootask
|
||||
- ./docker/crontab/crontab.conf:/etc/supervisor/conf.d/crontab.conf
|
||||
- ./docker/php/php.conf:/etc/supervisor/conf.d/php.conf
|
||||
- ./docker/php/license.conf:/etc/supervisor/conf.d/license.conf
|
||||
- ./docker/php/php.ini:/usr/local/etc/php/php.ini
|
||||
- ./docker/logs/supervisor:/var/log/supervisor
|
||||
- ./:/var/www
|
||||
|
||||
10
docker/php/license.conf
Normal file
10
docker/php/license.conf
Normal file
@ -0,0 +1,10 @@
|
||||
[program:license]
|
||||
directory=/var/www
|
||||
command=sh docker/php/license.sh
|
||||
numprocs=1
|
||||
autostart=true
|
||||
autorestart=true
|
||||
startretries=100
|
||||
user=root
|
||||
redirect_stderr=true
|
||||
stdout_logfile=/var/log/supervisor/%(program_name)s.log
|
||||
8
docker/php/license.sh
Normal file
8
docker/php/license.sh
Normal file
@ -0,0 +1,8 @@
|
||||
#!/bin/sh
|
||||
# 在线授权续期:php 容器内的独立系统进程(supervisor [program:license] 托管)。
|
||||
# 每小时直接跑一次 artisan CLI——不依赖 LARAVELS_TIMER、不经过 HTTP 转发。
|
||||
# artisan 失败(如启动时 DB 未就绪)不会中断本循环,下个周期自动重试。
|
||||
while true; do
|
||||
php /var/www/artisan online-license:renew
|
||||
sleep 3600
|
||||
done
|
||||
@ -999,3 +999,22 @@ AI 助手
|
||||
反馈类型错误
|
||||
会话不存在或无权限
|
||||
无权限
|
||||
请输入账号和密码
|
||||
请输入账号、密码和验证码
|
||||
授权成功
|
||||
验证码已发送
|
||||
试用已开通
|
||||
已退出在线授权
|
||||
该账号暂无可用授权,请先申请试用或购买
|
||||
该授权已被吊销
|
||||
该授权已被冻结
|
||||
该授权已在另一台实例使用,请先在原实例释放(换机)
|
||||
该授权已到期
|
||||
授权服务未返回 license
|
||||
无法连接授权服务
|
||||
授权服务返回错误
|
||||
在线授权已被冻结,请联系服务商
|
||||
在线授权续期失败,请检查网络
|
||||
在线授权即将到期,请保持联网续期
|
||||
在线授权已过期,新增用户受限,请尽快续期
|
||||
在线授权已失效,已回落到基础版
|
||||
|
||||
@ -2469,3 +2469,35 @@ AI任务分析
|
||||
操作引导启动失败
|
||||
最多只能添加(*)个
|
||||
该标签已存在
|
||||
离线授权
|
||||
在线授权
|
||||
账号
|
||||
套餐
|
||||
租约到期
|
||||
上次续期
|
||||
当前状态
|
||||
立即续期
|
||||
授权有效期
|
||||
退出在线授权
|
||||
App Store账号
|
||||
请输入App Store账号
|
||||
验证码已发送至
|
||||
登录授权
|
||||
申请试用
|
||||
确定试用
|
||||
生效中
|
||||
已冻结
|
||||
已吊销
|
||||
请输入账号和密码
|
||||
授权成功
|
||||
试用已开通
|
||||
在线授权续期成功
|
||||
已退出在线授权
|
||||
确定退出在线授权?
|
||||
已绑定在线授权
|
||||
绑定离线 License
|
||||
绑定离线授权
|
||||
绑定在线授权
|
||||
当前已绑定在线授权,绑定离线后将替换当前授权并释放在线座位,是否继续?
|
||||
当前已绑定离线授权,绑定在线后将替换当前授权,是否继续?
|
||||
请输入License
|
||||
|
||||
@ -581,13 +581,14 @@ features:
|
||||
scope: super-admin
|
||||
batch: B4
|
||||
priority: P1
|
||||
chunk_count_est: 3
|
||||
chunk_count_est: 4
|
||||
owner: ~
|
||||
status: drafted
|
||||
chunks:
|
||||
- license.concept
|
||||
- license.expire.faq
|
||||
- license.howto
|
||||
- license.online.howto
|
||||
|
||||
- id: ldap
|
||||
name: LDAP 集成
|
||||
|
||||
@ -22,7 +22,8 @@ negative:
|
||||
- License 失效不会立即锁死系统,但会持续在管理端报错
|
||||
- 修改 License 接口(save)仅超级管理员能调用,普通管理员只能看
|
||||
- 不支持自行重置 SN(重新部署会生成新 SN,需要重新签发 License)
|
||||
last_verified: v1.7.90
|
||||
- 若用的是在线授权,error 数组还可能出现「在线授权即将到期/已过期/已失效」等提醒,处理见 [[license.online.howto]]
|
||||
last_verified: v1.7.91
|
||||
---
|
||||
|
||||
# License 过期或失效怎么办
|
||||
|
||||
@ -23,13 +23,16 @@ negative:
|
||||
- 不能在终端外部直接编辑 License 文件,必须走管理端 API
|
||||
- 一份 License 仅对当前终端的 SN + MAC 有效,换机或换网卡需重新申请
|
||||
- 不支持把 License 拆给多个独立部署共享
|
||||
last_verified: v1.7.90
|
||||
last_verified: v1.7.91
|
||||
---
|
||||
|
||||
# 申请与录入 License
|
||||
|
||||
> 本文介绍**离线授权**(手动粘贴 License 原文)。License 页现有「离线授权 / 在线授权」两个 Tab,
|
||||
> 用 App Store 账号登录自助签发并自动续期的方式见 [[license.online.howto]]。
|
||||
|
||||
## 入口
|
||||
桌面端:左上角头像 →「系统设置」→「License」(仅管理员可见)。
|
||||
桌面端:左上角头像 →「系统设置」→「License」→「离线授权」Tab(仅管理员可见)。
|
||||
对应后端:`POST api/system/license`,`type=save` 写入。
|
||||
|
||||
## 操作步骤
|
||||
|
||||
83
resources/ai-kb/zh/howto/license/online.md
Normal file
83
resources/ai-kb/zh/howto/license/online.md
Normal file
@ -0,0 +1,83 @@
|
||||
---
|
||||
id: license.online.howto
|
||||
title: 在线授权(账号登录 / 申请试用 / 自动续期)
|
||||
type: howto
|
||||
feature: license
|
||||
scope: super-admin
|
||||
locale: zh
|
||||
aliases:
|
||||
- 在线授权
|
||||
- 账号授权
|
||||
- 用 appstore 账号授权
|
||||
- 申请试用
|
||||
- 试用授权
|
||||
- 在线 License
|
||||
- 自动续期
|
||||
- 授权账号登录
|
||||
- 在线授权到期
|
||||
- 在线授权冻结
|
||||
- 在线授权被吊销
|
||||
- 换机 deactivate
|
||||
related_tools: []
|
||||
related_pages: []
|
||||
prerequisites:
|
||||
- 需要进入「系统设置」→「License」页面(仅管理员可见),切到「在线授权」Tab
|
||||
- 需要一个在 App Store 申请的账号(开发者账号),登录后可在 App Store 门户「我的授权」查看
|
||||
- 终端需能联网访问 App Store 授权中心
|
||||
negative:
|
||||
- 在线授权与离线授权互斥:同一时刻只有一张生效 License;切到在线并登录后会接管 License 文件
|
||||
- 离线授权(粘贴 License 原文)完全不受影响,没有自动续期
|
||||
- 一个账号同一时刻只占用一个实例座位,换机需先在原实例「退出在线授权」释放
|
||||
- 试用每个账号仅一次,时长由 App Store 管理员配置且硬上限 60 天
|
||||
last_verified: v1.7.91
|
||||
---
|
||||
|
||||
# 在线授权(账号登录 / 申请试用 / 自动续期)
|
||||
|
||||
DooTask 的 License 页提供两种授权方式,可在页面顶部 Tab 切换:
|
||||
|
||||
- **离线授权**:手动粘贴 License 原文(见 [[license.howto]]),无自动续期。
|
||||
- **在线授权**:用 App Store 账号登录自助签发,终端自动定时续期(类似 JetBrains 账号激活)。
|
||||
|
||||
## 入口
|
||||
桌面端:左上角头像 →「系统设置」→「License」→ 顶部「在线授权」Tab(仅管理员可见)。
|
||||
|
||||
## 怎么用
|
||||
|
||||
### 已有账号 + 已有授权
|
||||
1. 在「在线授权」Tab 输入 App Store 账号、密码
|
||||
2. 点击「登录授权」
|
||||
3. 终端把自身指纹(doo_sn、网卡 MAC、版本)上报授权中心,签发一张租约 License 并落地
|
||||
4. 成功后页面显示套餐、使用人数、租约到期、当前状态
|
||||
|
||||
### 申请试用(没有正式授权时)
|
||||
1. 在「在线授权」Tab 输入账号、密码
|
||||
2. 点击「申请试用」→ 系统向账号注册邮箱发送验证码
|
||||
3. 填入验证码 → 点击「确定试用」即开通试用授权并签发
|
||||
4. 试用默认 14 天 / 不限人数(具体以 App Store 管理员配置为准,时长硬上限 60 天),每个账号仅能申请一次
|
||||
|
||||
### 日常维护
|
||||
- **自动续期**:终端每小时检查,租约将尽时自动向授权中心续期,无需人工干预(需保持联网)
|
||||
- **立即续期**:状态页「立即续期」按钮可手动触发一次
|
||||
- **换机 / 退出**:「退出在线授权」会释放该账号在授权中心占用的座位,并把终端回落到基础版;换机时先在原实例退出,再到新实例登录
|
||||
|
||||
## 状态与提醒(断网/欠费时的分级降级)
|
||||
在线授权按租约 + 本地宽限分四级,提醒会显示在「License」页与仪表盘顶部横幅(仅管理员可见):
|
||||
|
||||
| 状态 | 含义 | 影响 |
|
||||
| --- | --- | --- |
|
||||
| 生效中 | 续期正常 | 无 |
|
||||
| 即将到期 | 续期失败或租约临近 | 仅提醒 |
|
||||
| 已冻结 | 租约已过期 | 限制新增用户(沿用离线过期的既有行为) |
|
||||
| 已吊销 | 冻结超过宽限期或授权被收回 | 回落基础版(最多 3 人,超出的账号按既有规则禁用) |
|
||||
|
||||
只要在租约窗口内恢复联网并成功续期一次,即可回到「生效中」。
|
||||
|
||||
## 怎么自检
|
||||
- 接口 `POST api/license/status` 返回当前在线授权状态(mode/plan/people/lease_expired_at/status)
|
||||
- 提醒文案同样并入 `POST api/system/license` 的 `error` 数组,便于脚本巡检
|
||||
|
||||
## 不支持
|
||||
- 普通管理员能看授权信息但不能登录/签发;只有超级管理员(id=1)能操作
|
||||
- 在线授权依赖联网;完全离线/内网隔离部署请用离线授权(粘贴原文)
|
||||
- 在 App Store 内购买 License 为后续能力,当前在线授权面向已售出/试用账号
|
||||
@ -1,5 +1,31 @@
|
||||
<template>
|
||||
<div class="setting-item submit">
|
||||
<div class="setting-item submit license-setting">
|
||||
<Tabs v-model="mode">
|
||||
<TabPane :label="$L('离线授权')" name="offline">
|
||||
<template v-if="onlineActive">
|
||||
<div class="license-box">
|
||||
<ul class="online-info">
|
||||
<li><em>{{$L('当前状态')}}:</em><span class="online-link" @click="mode = 'online'">{{$L('已绑定在线授权')}}</span></li>
|
||||
<li><em>SN:</em><span>{{formData.doo_sn}}</span></li>
|
||||
<li><em>MAC:</em><span>{{infoJoin(formData.macs)}}</span></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div v-if="!offlineRebindShow" class="setting-footer">
|
||||
<Button type="primary" @click="offlineRebindShow = true">{{$L('绑定离线 License')}}</Button>
|
||||
</div>
|
||||
<template v-else>
|
||||
<Form :model="formData" v-bind="formOptions" @submit.native.prevent>
|
||||
<FormItem label="License">
|
||||
<Input v-model="offlineRebindLicense" type="textarea" :autosize="{minRows: 2,maxRows: 5}" :placeholder="$L('请输入License...')" />
|
||||
</FormItem>
|
||||
</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>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
<template v-else>
|
||||
<Form ref="formData" :model="formData" v-bind="formOptions" @submit.native.prevent>
|
||||
<FormItem label="License" prop="license">
|
||||
<Input v-model="formData.license" type="textarea" :autosize="{minRows: 2,maxRows: 5}" :placeholder="$L('请输入License...')" />
|
||||
@ -70,7 +96,7 @@
|
||||
<em>MAC:</em>
|
||||
<span>{{infoJoin(formData.macs)}}</span>
|
||||
</li>
|
||||
<li v-for="tip in formData.error" class="warning">{{tip}}</li>
|
||||
<li v-for="(tip, ti) in formData.error" :key="ti" class="warning">{{tip}}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</FormItem>
|
||||
@ -79,6 +105,45 @@
|
||||
<Button :loading="loadIng > 0" type="primary" @click="submitForm">{{$L('提交')}}</Button>
|
||||
<Button :loading="loadIng > 0" @click="resetForm" style="margin-left: 8px">{{$L('重置')}}</Button>
|
||||
</div>
|
||||
</template>
|
||||
</TabPane>
|
||||
<TabPane :label="$L('在线授权')" name="online">
|
||||
<div v-if="onlineActive" class="license-box">
|
||||
<ul class="online-info">
|
||||
<li><em>{{$L('账号')}}:</em><span>{{online.account}}</span></li>
|
||||
<li><em>{{$L('套餐')}}:</em><span>{{online.plan || '-'}}</span></li>
|
||||
<li><em>{{$L('使用人数')}}:</em><span>{{online.people || $L('无限制')}}</span></li>
|
||||
<li><em>{{$L('授权有效期')}}:</em><span>{{online.valid_until ? fmt(online.valid_until) : $L('永久')}}</span></li>
|
||||
<li>
|
||||
<em>{{$L('当前状态')}}:</em>
|
||||
<span :class="{warning: online.status !== 'active'}">{{stageText(online.status)}}</span>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="setting-footer">
|
||||
<Button :loading="onlineIng > 0" @click="onlineLogout">{{$L('退出在线授权')}}</Button>
|
||||
</div>
|
||||
</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>
|
||||
<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>
|
||||
</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>
|
||||
</div>
|
||||
</template>
|
||||
</TabPane>
|
||||
</Tabs>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -86,6 +151,16 @@
|
||||
.license-box {
|
||||
padding-top: 6px;
|
||||
> ul {
|
||||
&.online-info {
|
||||
padding-left: 24px;
|
||||
.online-link {
|
||||
cursor: pointer;
|
||||
color: #2d8cf0;
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
> li {
|
||||
list-style: none;
|
||||
font-size: 14px;
|
||||
@ -116,6 +191,12 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
.online-tip {
|
||||
font-size: 12px;
|
||||
line-height: 20px;
|
||||
margin-top: 4px;
|
||||
opacity: 0.6;
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
import {mapState} from "vuex";
|
||||
@ -131,15 +212,42 @@ export default {
|
||||
macs: [],
|
||||
doo_sn: '',
|
||||
user_count: 0,
|
||||
error: []
|
||||
error: [],
|
||||
online: {},
|
||||
},
|
||||
|
||||
mode: 'offline',
|
||||
tabInited: false,
|
||||
offlineRebindShow: false,
|
||||
offlineRebindLicense: '',
|
||||
onlineIng: 0,
|
||||
onlineForm: {
|
||||
account: '',
|
||||
password: '',
|
||||
code: '',
|
||||
},
|
||||
trialStep: 0,
|
||||
trialEmail: '',
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.systemSetting();
|
||||
this.onlineRefresh();
|
||||
},
|
||||
computed: {
|
||||
...mapState(['userInfo', 'formOptions']),
|
||||
|
||||
online() {
|
||||
return this.formData.online || {};
|
||||
},
|
||||
|
||||
onlineActive() {
|
||||
return this.online.mode === 'online';
|
||||
},
|
||||
|
||||
// 已绑定离线授权 = 存在已保存的 license 且当前非在线托管
|
||||
offlineBound() {
|
||||
return !this.onlineActive && !!String(this.formData.license || '').trim();
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
submitForm() {
|
||||
@ -154,6 +262,40 @@ export default {
|
||||
this.formData = $A.cloneJSON(this.formData_bak);
|
||||
},
|
||||
|
||||
offlineRebindCancel() {
|
||||
this.offlineRebindShow = false;
|
||||
this.offlineRebindLicense = '';
|
||||
},
|
||||
|
||||
// 已绑定在线时,从离线页面提交新的离线 License:二次确认 → 成功保存,失败仅提示不保存
|
||||
offlineRebindSubmit() {
|
||||
if (!String(this.offlineRebindLicense).trim()) {
|
||||
$A.messageError('请输入License');
|
||||
return;
|
||||
}
|
||||
$A.modalConfirm({
|
||||
title: '绑定离线授权',
|
||||
content: '当前已绑定在线授权,绑定离线后将替换当前授权并释放在线座位,是否继续?',
|
||||
onOk: () => {
|
||||
this.loadIng++;
|
||||
return this.$store.dispatch("call", {
|
||||
url: 'system/license',
|
||||
data: {type: 'save', license: this.offlineRebindLicense},
|
||||
method: 'post',
|
||||
}).then(({data}) => {
|
||||
$A.messageSuccess('修改成功');
|
||||
this.formData = data;
|
||||
this.formData_bak = $A.cloneJSON(this.formData);
|
||||
this.offlineRebindCancel();
|
||||
}).catch(({msg}) => {
|
||||
$A.modalError(msg); // 失败仅提示,不保存
|
||||
}).finally(_ => {
|
||||
this.loadIng--;
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
systemSetting(save) {
|
||||
this.loadIng++;
|
||||
this.$store.dispatch("call", {
|
||||
@ -168,6 +310,13 @@ export default {
|
||||
}
|
||||
this.formData = data;
|
||||
this.formData_bak = $A.cloneJSON(this.formData);
|
||||
// 首次加载:若已是在线授权则默认切到在线 Tab
|
||||
if (!this.tabInited) {
|
||||
this.tabInited = true;
|
||||
if (data.online && data.online.mode === 'online') {
|
||||
this.mode = 'online';
|
||||
}
|
||||
}
|
||||
}).catch(({msg}) => {
|
||||
if (save) {
|
||||
$A.modalError(msg);
|
||||
@ -177,6 +326,18 @@ export default {
|
||||
});
|
||||
},
|
||||
|
||||
onlineRefresh() {
|
||||
// 进入授权页静默刷新在线授权:成功后端会更新数据,失败不提示;无论结果都拉取最新展示
|
||||
this.$store.dispatch("call", {
|
||||
url: 'license/refresh',
|
||||
method: 'post',
|
||||
}).catch(() => {
|
||||
// 刷新失败:静默
|
||||
}).finally(() => {
|
||||
this.systemSetting();
|
||||
});
|
||||
},
|
||||
|
||||
infoJoin(val, def = null) {
|
||||
if ($A.isArray(val)) {
|
||||
val = val.join(",")
|
||||
@ -195,7 +356,116 @@ export default {
|
||||
val2 = [val2]
|
||||
}
|
||||
return val1.some(v => val2.includes(v))
|
||||
}
|
||||
},
|
||||
|
||||
fmt(dt) {
|
||||
return dt ? $A.dayjs(dt).format('YYYY-MM-DD HH:mm') : '-';
|
||||
},
|
||||
|
||||
stageText(status) {
|
||||
return {
|
||||
active: this.$L('生效中'),
|
||||
reminder: this.$L('即将到期'),
|
||||
frozen: this.$L('已冻结'),
|
||||
revoked: this.$L('已吊销'),
|
||||
}[status] || status || '-';
|
||||
},
|
||||
|
||||
onlineCall(url, data, successMsg) {
|
||||
this.onlineIng++;
|
||||
return this.$store.dispatch("call", {
|
||||
url,
|
||||
data,
|
||||
method: 'post',
|
||||
}).then((res) => {
|
||||
if (successMsg) {
|
||||
$A.messageSuccess(successMsg);
|
||||
}
|
||||
return res;
|
||||
}).catch(({msg}) => {
|
||||
$A.modalError(msg);
|
||||
return Promise.reject(msg);
|
||||
}).finally(_ => {
|
||||
this.onlineIng--;
|
||||
});
|
||||
},
|
||||
|
||||
// 已绑定离线时,绑定在线前二次确认
|
||||
confirmReplaceOffline(onOk) {
|
||||
if (this.offlineBound) {
|
||||
$A.modalConfirm({
|
||||
title: '绑定在线授权',
|
||||
content: '当前已绑定离线授权,绑定在线后将替换当前授权,是否继续?',
|
||||
onOk,
|
||||
});
|
||||
} else {
|
||||
onOk();
|
||||
}
|
||||
},
|
||||
|
||||
onlineLogin() {
|
||||
if (!this.onlineForm.account || !this.onlineForm.password) {
|
||||
$A.messageError('请输入账号和密码');
|
||||
return;
|
||||
}
|
||||
this.confirmReplaceOffline(() => {
|
||||
this.onlineCall('license/login', {
|
||||
account: this.onlineForm.account,
|
||||
password: this.onlineForm.password,
|
||||
}, '授权成功').then(_ => {
|
||||
this.resetOnlineForm();
|
||||
this.systemSetting();
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
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('请输入验证码');
|
||||
return;
|
||||
}
|
||||
this.confirmReplaceOffline(() => {
|
||||
this.onlineCall('license/trial', {
|
||||
account: this.onlineForm.account,
|
||||
password: this.onlineForm.password,
|
||||
code: this.onlineForm.code,
|
||||
}, '试用已开通').then(_ => {
|
||||
this.resetOnlineForm();
|
||||
this.systemSetting();
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
onlineLogout() {
|
||||
$A.modalConfirm({
|
||||
content: '确定退出在线授权?',
|
||||
onOk: () => {
|
||||
this.onlineCall('license/logout', {}, '已退出在线授权').then(_ => {
|
||||
this.systemSetting();
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
resetOnlineForm() {
|
||||
this.onlineForm = {account: '', password: '', code: ''};
|
||||
this.trialStep = 0;
|
||||
this.trialEmail = '';
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
|
||||
> 此文件由 `php artisan doc:api-map` 生成,勿手改。
|
||||
|
||||
接口总数:298
|
||||
接口总数:304
|
||||
|
||||
## 路由规则
|
||||
|
||||
@ -196,6 +196,17 @@ API 使用动态路由(见 `routes/web.php`),URL 段映射为控制器方
|
||||
| api/system/version | version() | get | 获取版本号 |
|
||||
| api/system/prefetch | prefetch() | get | 预加载的资源 |
|
||||
|
||||
## license(LicenseController)
|
||||
|
||||
| URL | 方法名 | HTTP | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| 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 | |
|
||||
| api/license/logout | logout() | any | |
|
||||
|
||||
## dialog(DialogController)
|
||||
|
||||
| URL | 方法名 | HTTP | 说明 |
|
||||
|
||||
@ -9,6 +9,7 @@ use App\Http\Controllers\Api\DialogController;
|
||||
use App\Http\Controllers\Api\PublicController;
|
||||
use App\Http\Controllers\Api\ReportController;
|
||||
use App\Http\Controllers\Api\SystemController;
|
||||
use App\Http\Controllers\Api\LicenseController;
|
||||
use App\Http\Controllers\Api\AssistantController;
|
||||
use App\Http\Controllers\Api\ProjectController;
|
||||
use App\Http\Controllers\Api\ComplaintController;
|
||||
@ -39,6 +40,9 @@ Route::prefix('api')->middleware(['webapi'])->group(function () {
|
||||
// 系统
|
||||
Route::any('system/{method}', SystemController::class);
|
||||
Route::any('system/{method}/{action}', SystemController::class);
|
||||
// 在线授权
|
||||
Route::any('license/{method}', LicenseController::class);
|
||||
Route::any('license/{method}/{action}', LicenseController::class);
|
||||
// 对话
|
||||
Route::any('dialog/{method}', DialogController::class);
|
||||
Route::any('dialog/{method}/{action}', DialogController::class);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user