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

v2
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数据库
>
> 流水线数量现已调整为无限制,欢迎大家使用
>
## 二、在线体验
@ -203,12 +205,13 @@ https://afdian.com/a/greper
专业版特权对比
| 功能 | 基础版 | 专业版 |
|---------|-----------------|-------------------|
|------|-----------------|-------------------|
| 免费证书申请 | 免费无限制 | 无限制 |
| 域名数量 | 免费无限制 | 无限制 |
| 证书流水线条数 | 免费无限制 | 无限制 |
| 站点证书监控 | 1条 | 无限制 |
| 自动部署插件 | 阿里云、腾讯云、七牛云、SSH | 支持群晖、宝塔、1Panel等持续开发中 |
| 通知 | 邮件、webhook | server酱、企微、anpush、钉钉等 |
| 通知 | 邮件、webhook | server酱、企微、anpush等 |
************************

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -9,7 +9,7 @@
"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-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-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",

View File

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

View File

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

View File

@ -7,7 +7,7 @@ import { Cron } from '../cron/cron.js';
@Autoload()
@Scope(ScopeEnum.Request, { allowDowngrade: true })
export class AutoRegisterCron {
export class AutoCRegisterCron {
@Inject()
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() {
//监听https
this.startHttpsServer();
if (isDev()) {
this.startHeapLog();
}
const installInfo: SysInstallInfo = await this.sysSettingsService.getSetting(SysInstallInfo);
logger.info('=========================================');
logger.info('当前站点ID:', installInfo.siteId);
@ -36,9 +38,6 @@ export class AutoZPrint {
}
logger.info('Certd已启动');
logger.info('=========================================');
if (isDev()) {
this.startHeapLog();
}
}
startHeapLog() {
@ -50,7 +49,7 @@ export class AutoZPrint {
}, 60000);
}
async startHttpsServer() {
startHttpsServer() {
if (!this.httpsConfig.enabled) {
logger.info('Https server is not enabled');
return;

View File

@ -1,5 +1,5 @@
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 { Repository } from 'typeorm';
import { SiteInfoEntity } from '../entity/site-info.js';
@ -8,6 +8,8 @@ import dayjs from 'dayjs';
import { logger } from '@certd/basic';
import { PeerCertificate } from 'tls';
import { NotificationService } from '../../pipeline/service/notification-service.js';
import { isComm, isPlus } from '@certd/plus-core';
import { UserSuiteService } from '@certd/commercial-core';
@Provide()
export class SiteInfoService extends BaseService<SiteInfoEntity> {
@ -17,11 +19,41 @@ export class SiteInfoService extends BaseService<SiteInfoEntity> {
@Inject()
notificationService: NotificationService;
@Inject()
sysSettingsService: SysSettingsService;
@Inject()
userSuiteService: UserSuiteService;
//@ts-ignore
getRepository() {
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) {
if (!userId) {
throw new Error('userId is required');

View File

@ -205,11 +205,11 @@ export class PipelineService extends BaseService<PipelineEntity> {
if (isComm()) {
//校验pipelineCount
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}条流水线,请购买或升级套餐`);
}
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}个域名,请购买或升级套餐`);
}
}
@ -222,7 +222,7 @@ export class PipelineService extends BaseService<PipelineEntity> {
const sysPublic = await this.sysSettingsService.getSetting<SysPublicSettings>(SysPublicSettings);
const limitUserPipelineCount = sysPublic.limitUserPipelineCount;
if (limitUserPipelineCount && limitUserPipelineCount > 0 && count >= limitUserPipelineCount) {
throw new NeedVIPException(`最多只能创建${limitUserPipelineCount}条流水线`);
throw new NeedVIPException(`普通用户最多只能创建${limitUserPipelineCount}条流水线`);
}
}
}