feat: 基础版不再限制流水线数量

pull/330/head
xiaojunnuo 2024-12-23 23:33:13 +08:00
parent bb4910f4e5
commit cb27d4b490
16 changed files with 158 additions and 62 deletions

View File

@ -16,7 +16,9 @@ Certd 是一个免费全自动申请和自动部署更新SSL证书的管理系
* 支持SQLitePostgreSQL、MySQL数据库 * 支持SQLitePostgreSQL、MySQL数据库
>
> 流水线数量现已调整为无限制,欢迎大家使用
>
## 二、在线体验 ## 二、在线体验
@ -202,13 +204,14 @@ https://afdian.com/a/greper
专业版特权对比 专业版特权对比
| 功能 | 基础版 | 专业版 | | 功能 | 基础版 | 专业版 |
|---------|-----------------|-------------------| |------|-----------------|-------------------|
| 免费证书申请 | 免费无限制 | 无限制 | | 免费证书申请 | 免费无限制 | 无限制 |
| 域名数量 | 免费无限制 | 无限制 |
| 证书流水线条数 | 免费无限制 | 无限制 | | 证书流水线条数 | 免费无限制 | 无限制 |
| 站点证书监控 | 1条 | 无限制 | | 站点证书监控 | 1条 | 无限制 |
| 自动部署插件 | 阿里云、腾讯云、七牛云、SSH | 支持群晖、宝塔、1Panel等持续开发中 | | 自动部署插件 | 阿里云、腾讯云、七牛云、SSH | 支持群晖、宝塔、1Panel等持续开发中 |
| 通知 | 邮件、webhook | server酱、企微、anpush、钉钉等 | | 通知 | 邮件、webhook | server酱、企微、anpush等 |
************************ ************************

View File

@ -6,7 +6,7 @@ import { SysPrivateSettings, SysSettingsService } from '../../../system/index.js
* *
*/ */
@Provide() @Provide()
@Scope(ScopeEnum.Request, { allowDowngrade: true }) @Scope(ScopeEnum.Singleton)
export class EncryptService { export class EncryptService {
secretKey: Buffer; secretKey: Buffer;

View File

@ -229,27 +229,41 @@ function openUpgrade() {
const vipTypeDefine = { const vipTypeDefine = {
free: { free: {
title: "基础版", title: "基础版",
desc: "免费使用", desc: "社区免费版",
type: "free", type: "free",
privilege: ["证书申请功能无限制", "证书流水线数量无限制", "常用的主机、云平台、cdn等部署插件"] privilege: ["证书申请无限制", "域名数量无限制", "证书流水线数量无限制", "常用的主机、云平台、cdn等部署插件", "邮件、webhook通知方式"]
}, },
plus: { plus: {
title: "专业版", title: "专业版",
desc: "功能增强,适用于个人企业内部使用", desc: "开源需要您的赞助支持",
type: "plus", type: "plus",
privilege: ["可加VIP群需求优先实现", "宝塔、群晖、1Panel、易盾等部署插件", "站点证书监控", "更多通知种类"], privilege: ["可加VIP群您需求将优先实现", "站点证书监控无限制", "更多通知方式", "更多强大部署插件宝塔、群晖、1Panel等"],
trial: { trial: {
title: "7天试用", title: "点击获取7天试用",
click: () => { click: () => {
openStarModal(); openStarModal();
} }
},
price: 29.9,
get() {
return (
<a-tooltip title="爱发电赞助“VIP会员”后获取一年期专业版激活码开源需要您的支持">
<a-button size="small" type="primary" href="https://afdian.com/a/greper" target="_blank">
爱发电赞助后获取
</a-button>
</a-tooltip>
);
} }
}, },
comm: { comm: {
title: "商业版", title: "商业版",
desc: "商业授权,可对外运营", desc: "商业授权,可对外运营",
type: "comm", type: "comm",
privilege: ["拥有专业版所有特权", "允许商用可修改logo、标题", "数据统计", "插件管理", "多用户无限制", "支持用户支付(敬请期待)"] privilege: ["拥有专业版所有特权", "允许商用可修改logo、标题", "数据统计", "插件管理", "多用户无限制", "支持用户支付"],
price: 399,
get() {
return <a-button size="small">请联系作者获取</a-button>;
}
} }
}; };
@ -260,28 +274,16 @@ function openUpgrade() {
}, },
maskClosable: true, maskClosable: true,
okText: "激活", okText: "激活",
width: 900, width: 1000,
content: () => { content: () => {
let activationCodeGetWay: any = null; let activationCodeGetWay = (
if (settingStore.siteEnv.agent.enabled != null) { <span>
const agent = settingStore.siteEnv.agent; <a href="https://afdian.com/a/greper" target="_blank">
if (agent.enabled === false) { 爱发电赞助VIP会员后获取一年期专业版激活码
activationCodeGetWay = ( </a>
<span> <span> 商业版请直接联系作者</span>
<a href="https://afdian.com/a/greper" target="_blank"> </span>
爱发电赞助VIP会员¥29.9后获取一年期专业版激活码 );
</a>
<span> 商业版请直接联系作者</span>
</span>
);
} else {
activationCodeGetWay = (
<a href={agent.contactLink} target="_blank">
{agent.contactText}
</a>
);
}
}
const vipLabel = settingStore.vipLabel; const vipLabel = settingStore.vipLabel;
const slots = []; const slots = [];
for (const key in vipTypeDefine) { for (const key in vipTypeDefine) {
@ -301,15 +303,31 @@ function openUpgrade() {
</span> </span>
)} )}
</h3> </h3>
<div>{item.desc}</div> <div style="color:green">{item.desc}</div>
<ul> <ul class="flex-1">
{item.privilege.map((p: string) => ( {item.privilege.map((p: string) => (
<li> <li class="flex-baseline">
<fs-icon class="color-green" icon="ion:checkmark-sharp" /> <fs-icon class="color-green" icon="ion:checkmark-sharp" />
{p} {p}
</li> </li>
))} ))}
</ul> </ul>
<div class="footer flex-between flex-vc">
<div class="price-show">
{item.price && (
<span>
<span class="price-text">¥{item.price}</span>
/
</span>
)}
{!item.price && (
<span>
<span class="price-text">免费</span>
</span>
)}
</div>
<div class="get-show">{item.get && <div>{item.get()}</div>}</div>
</div>
</div> </div>
</a-col> </a-col>
); );
@ -372,10 +390,12 @@ onMounted(() => {
.vip-active-modal { .vip-active-modal {
.vip-block { .vip-block {
display: flex;
flex-direction: column;
padding: 10px; padding: 10px;
border: 1px solid #eee; border: 1px solid #eee;
border-radius: 5px; border-radius: 5px;
height: 195px; height: 250px;
//background-color: rgba(250, 237, 167, 0.79); //background-color: rgba(250, 237, 167, 0.79);
&.current { &.current {
border-color: green; border-color: green;
@ -389,6 +409,16 @@ onMounted(() => {
font-wight: 400; font-wight: 400;
} }
} }
.footer {
padding-top: 5px;
margin-top: 0px;
border-top: 1px solid #eee;
.price-text {
font-size: 18px;
color: red;
}
}
} }
ul { ul {

View File

@ -46,10 +46,6 @@ export const certdResources = [
path: "/certd/monitor/site", path: "/certd/monitor/site",
component: "/certd/monitor/site/index.vue", component: "/certd/monitor/site/index.vue",
meta: { meta: {
show: () => {
const settingStore = useSettingStore();
return settingStore.isPlus;
},
icon: "ion:videocam-outline", icon: "ion:videocam-outline",
auth: true auth: true
} }

View File

@ -160,7 +160,7 @@ export const useSettingStore = defineStore({
async checkUrlBound() { async checkUrlBound() {
const userStore = useUserStore(); const userStore = useUserStore();
const settingStore = useSettingStore(); const settingStore = useSettingStore();
if (!userStore.isAdmin || !settingStore.isPlus) { if (!userStore.isAdmin) {
return; return;
} }

View File

@ -54,15 +54,25 @@ h1, h2, h3, h4, h5, h6 {
justify-content: center; justify-content: center;
align-items: center; align-items: center;
} }
.flex-vc{
align-items: center;
}
.flex-vb{
align-items: baseline;
}
.flex-o { .flex-o {
display: flex !important; display: flex !important;
align-items: center; align-items: center;
} }
.flex-baseline{
display: flex !important;
align-items: baseline;
}
.flex-between { .flex-between {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: baseline;
} }
.flex { .flex {

View File

@ -23,8 +23,10 @@
</a-tag> </a-tag>
</a-badge> </a-badge>
</template> </template>
<a-divider type="vertical" /> <template v-if="settingsStore.isComm">
<suite-card class="m-0"></suite-card> <a-divider type="vertical" />
<suite-card class="m-0"></suite-card>
</template>
</div> </div>
</div> </div>
</div> </div>
@ -92,7 +94,7 @@
<a-row :gutter="10"> <a-row :gutter="10">
<a-col v-for="item of pluginGroups.groups.all.plugins" :key="item.name" class="plugin-item-col" :span="4"> <a-col v-for="item of pluginGroups.groups.all.plugins" :key="item.name" class="plugin-item-col" :span="4">
<a-card> <a-card>
<a-tooltip :title="item.desc"> <a-tooltip :title="item.desc" class="flex-between">
<div class="plugin-item pointer"> <div class="plugin-item pointer">
<div class="icon"> <div class="icon">
<fs-icon :icon="item.icon" class="font-size-16 color-blue" /> <fs-icon :icon="item.icon" class="font-size-16 color-blue" />
@ -101,6 +103,7 @@
<div class="title">{{ item.title }}</div> <div class="title">{{ item.title }}</div>
</div> </div>
</div> </div>
<div class="flex-o"><vip-button v-if="item.needPlus" mode="icon" class="" /></div>
</a-tooltip> </a-tooltip>
</a-card> </a-card>
</a-col> </a-col>
@ -158,7 +161,7 @@ const settingStore = useSettingStore();
const siteInfo: Ref<SiteInfo> = computed(() => { const siteInfo: Ref<SiteInfo> = computed(() => {
return settingStore.siteInfo; return settingStore.siteInfo;
}); });
const settingsStore = useSettingStore();
const userStore = useUserStore(); const userStore = useUserStore();
const userInfo: ComputedRef<UserInfoRes> = computed(() => { const userInfo: ComputedRef<UserInfoRes> = computed(() => {
return userStore.getUserInfo; return userStore.getUserInfo;

View File

@ -1,5 +1,5 @@
<template> <template>
<div class="my-suite-card"> <div v-if="detail.enabled" class="my-suite-card">
<div class="flex-o"> <div class="flex-o">
<a-popover> <a-popover>
<template #content> <template #content>
@ -52,6 +52,7 @@ type SuiteValue = {
used: number; used: number;
}; };
type SuiteDetail = { type SuiteDetail = {
enabled?: boolean;
suites?: any[]; suites?: any[];
expiresTime?: number; expiresTime?: number;
pipelineCount?: SuiteValue; pipelineCount?: SuiteValue;

View File

@ -9,7 +9,7 @@
"dev": "cross-env NODE_ENV=local mwtsc --watch --run @midwayjs/mock/app", "dev": "cross-env NODE_ENV=local mwtsc --watch --run @midwayjs/mock/app",
"dev-commlocal": "cross-env NODE_ENV=dev-commlocal mwtsc --watch --run @midwayjs/mock/app", "dev-commlocal": "cross-env NODE_ENV=dev-commlocal mwtsc --watch --run @midwayjs/mock/app",
"dev-commpro": "cross-env NODE_ENV=dev-commpro mwtsc --watch --run @midwayjs/mock/app", "dev-commpro": "cross-env NODE_ENV=dev-commpro mwtsc --watch --run @midwayjs/mock/app",
"dev-pgd": "cross-env NODE_ENV=dev-pgd mwtsc --watch --run @midwayjs/mock/app", "dev-pg": "cross-env NODE_ENV=dev-pg mwtsc --watch --run @midwayjs/mock/app",
"dev-mysql": "cross-env NODE_ENV=dev-mysql mwtsc --watch --run @midwayjs/mock/app", "dev-mysql": "cross-env NODE_ENV=dev-mysql mwtsc --watch --run @midwayjs/mock/app",
"dev-localplus": "cross-env NODE_ENV=dev-localplus mwtsc --watch --run @midwayjs/mock/app", "dev-localplus": "cross-env NODE_ENV=dev-localplus mwtsc --watch --run @midwayjs/mock/app",
"dev-pgpl": "cross-env NODE_ENV=dev-pgpl mwtsc --watch --run @midwayjs/mock/app", "dev-pgpl": "cross-env NODE_ENV=dev-pgpl mwtsc --watch --run @midwayjs/mock/app",

View File

@ -24,6 +24,7 @@ export class SysPlusController extends BaseController {
async bindUrl(@Body(ALL) body: { url: string }) { async bindUrl(@Body(ALL) body: { url: string }) {
const { url } = body; const { url } = body;
await this.plusService.register();
const installInfo: SysInstallInfo = await this.sysSettingsService.getSetting(SysInstallInfo); const installInfo: SysInstallInfo = await this.sysSettingsService.getSetting(SysInstallInfo);
await this.plusService.bindUrl(url); await this.plusService.bindUrl(url);

View File

@ -7,7 +7,7 @@ import crypto from 'crypto';
@Autoload() @Autoload()
@Scope(ScopeEnum.Request, { allowDowngrade: true }) @Scope(ScopeEnum.Request, { allowDowngrade: true })
export class AutoInitSite { export class AutoAInitSite {
@Inject() @Inject()
userService: UserService; userService: UserService;

View File

@ -7,7 +7,7 @@ import { Cron } from '../cron/cron.js';
@Autoload() @Autoload()
@Scope(ScopeEnum.Request, { allowDowngrade: true }) @Scope(ScopeEnum.Request, { allowDowngrade: true })
export class AutoRegisterCron { export class AutoCRegisterCron {
@Inject() @Inject()
pipelineService: PipelineService; pipelineService: PipelineService;

View File

@ -0,0 +1,21 @@
import { logger, utils } from '@certd/basic';
import { UserSuiteService } from '@certd/commercial-core';
import { Autoload, Init, Inject, Scope, ScopeEnum } from '@midwayjs/core';
@Autoload()
@Scope(ScopeEnum.Request, { allowDowngrade: true })
export class AutoDMitterRegister {
@Inject()
userSuiteService: UserSuiteService;
@Init()
async init() {
await this.registerOnNewUser();
}
async registerOnNewUser() {
utils.mitter.on('register', async (req: { userId: number }) => {
logger.info('register event', req.userId);
await this.userSuiteService.presentGiftSuite(req.userId);
});
}
}

View File

@ -24,7 +24,9 @@ export class AutoZPrint {
async init() { async init() {
//监听https //监听https
this.startHttpsServer(); this.startHttpsServer();
if (isDev()) {
this.startHeapLog();
}
const installInfo: SysInstallInfo = await this.sysSettingsService.getSetting(SysInstallInfo); const installInfo: SysInstallInfo = await this.sysSettingsService.getSetting(SysInstallInfo);
logger.info('========================================='); logger.info('=========================================');
logger.info('当前站点ID:', installInfo.siteId); logger.info('当前站点ID:', installInfo.siteId);
@ -36,9 +38,6 @@ export class AutoZPrint {
} }
logger.info('Certd已启动'); logger.info('Certd已启动');
logger.info('========================================='); logger.info('=========================================');
if (isDev()) {
this.startHeapLog();
}
} }
startHeapLog() { startHeapLog() {
@ -50,7 +49,7 @@ export class AutoZPrint {
}, 60000); }, 60000);
} }
async startHttpsServer() { startHttpsServer() {
if (!this.httpsConfig.enabled) { if (!this.httpsConfig.enabled) {
logger.info('Https server is not enabled'); logger.info('Https server is not enabled');
return; return;

View File

@ -1,5 +1,5 @@
import { Inject, Provide } from '@midwayjs/core'; import { Inject, Provide } from '@midwayjs/core';
import { BaseService } from '@certd/lib-server'; import { BaseService, NeedSuiteException, NeedVIPException, SysSettingsService, SysSuiteSetting } from '@certd/lib-server';
import { InjectEntityModel } from '@midwayjs/typeorm'; import { InjectEntityModel } from '@midwayjs/typeorm';
import { Repository } from 'typeorm'; import { Repository } from 'typeorm';
import { SiteInfoEntity } from '../entity/site-info.js'; import { SiteInfoEntity } from '../entity/site-info.js';
@ -8,6 +8,8 @@ import dayjs from 'dayjs';
import { logger } from '@certd/basic'; import { logger } from '@certd/basic';
import { PeerCertificate } from 'tls'; import { PeerCertificate } from 'tls';
import { NotificationService } from '../../pipeline/service/notification-service.js'; import { NotificationService } from '../../pipeline/service/notification-service.js';
import { isComm, isPlus } from '@certd/plus-core';
import { UserSuiteService } from '@certd/commercial-core';
@Provide() @Provide()
export class SiteInfoService extends BaseService<SiteInfoEntity> { export class SiteInfoService extends BaseService<SiteInfoEntity> {
@ -17,11 +19,41 @@ export class SiteInfoService extends BaseService<SiteInfoEntity> {
@Inject() @Inject()
notificationService: NotificationService; notificationService: NotificationService;
@Inject()
sysSettingsService: SysSettingsService;
@Inject()
userSuiteService: UserSuiteService;
//@ts-ignore //@ts-ignore
getRepository() { getRepository() {
return this.repository; return this.repository;
} }
async add(data: SiteInfoEntity) {
if (!data.userId) {
throw new Error('userId is required');
}
if (!isPlus()) {
const count = await this.getUserMonitorCount(data.userId);
if (count >= 1) {
throw new NeedVIPException('站点监控数量已达上限,请升级专业版');
}
}
if (isComm()) {
const suiteSetting = await this.sysSettingsService.getSetting<SysSuiteSetting>(SysSuiteSetting);
if (suiteSetting.enabled) {
const userSuite = await this.userSuiteService.getMySuiteDetail(data.userId);
if (userSuite.monitorCount.max != -1 && userSuite.monitorCount.max <= userSuite.monitorCount.used) {
throw new NeedSuiteException('站点监控数量已达上限,请购买或升级套餐');
}
}
}
return await this.repository.save(data);
}
async getUserMonitorCount(userId: number) { async getUserMonitorCount(userId: number) {
if (!userId) { if (!userId) {
throw new Error('userId is required'); throw new Error('userId is required');

View File

@ -205,11 +205,11 @@ export class PipelineService extends BaseService<PipelineEntity> {
if (isComm()) { if (isComm()) {
//校验pipelineCount //校验pipelineCount
const userSuite = await this.userSuiteService.getMySuiteDetail(bean.userId); const userSuite = await this.userSuiteService.getMySuiteDetail(bean.userId);
if (userSuite?.pipelineCount.used + 1 > userSuite?.pipelineCount.max) { if (userSuite?.pipelineCount.max != -1 && userSuite?.pipelineCount.used + 1 > userSuite?.pipelineCount.max) {
throw new NeedSuiteException(`对不起,您最多只能创建${userSuite?.pipelineCount.max}条流水线,请购买或升级套餐`); throw new NeedSuiteException(`对不起,您最多只能创建${userSuite?.pipelineCount.max}条流水线,请购买或升级套餐`);
} }
if (userSuite.domainCount.used + domains.length > userSuite.domainCount.max) { if (userSuite.domainCount.max != -1 && userSuite.domainCount.used + domains.length > userSuite.domainCount.max) {
throw new NeedSuiteException(`对不起,您最多只能添加${userSuite.domainCount.max}个域名,请购买或升级套餐`); throw new NeedSuiteException(`对不起,您最多只能添加${userSuite.domainCount.max}个域名,请购买或升级套餐`);
} }
} }
@ -222,7 +222,7 @@ export class PipelineService extends BaseService<PipelineEntity> {
const sysPublic = await this.sysSettingsService.getSetting<SysPublicSettings>(SysPublicSettings); const sysPublic = await this.sysSettingsService.getSetting<SysPublicSettings>(SysPublicSettings);
const limitUserPipelineCount = sysPublic.limitUserPipelineCount; const limitUserPipelineCount = sysPublic.limitUserPipelineCount;
if (limitUserPipelineCount && limitUserPipelineCount > 0 && count >= limitUserPipelineCount) { if (limitUserPipelineCount && limitUserPipelineCount > 0 && count >= limitUserPipelineCount) {
throw new NeedVIPException(`最多只能创建${limitUserPipelineCount}条流水线`); throw new NeedVIPException(`普通用户最多只能创建${limitUserPipelineCount}条流水线`);
} }
} }
} }