mirror of https://github.com/certd/certd
				
				
				
			feat: 基础版不再限制流水线数量
							parent
							
								
									bb4910f4e5
								
							
						
					
					
						commit
						cb27d4b490
					
				
							
								
								
									
										17
									
								
								README.md
								
								
								
								
							
							
						
						
									
										17
									
								
								README.md
								
								
								
								
							|  | @ -16,7 +16,9 @@ Certd 是一个免费全自动申请和自动部署更新SSL证书的管理系 | |||
| * 支持SQLite,PostgreSQL、MySQL数据库 | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| > | ||||
| > 流水线数量现已调整为无限制,欢迎大家使用 | ||||
| > | ||||
| 
 | ||||
| ## 二、在线体验 | ||||
| 
 | ||||
|  | @ -202,13 +204,14 @@ https://afdian.com/a/greper | |||
| 
 | ||||
| 专业版特权对比 | ||||
| 
 | ||||
| | 功能      | 基础版             | 专业版               | | ||||
| |---------|-----------------|-------------------| | ||||
| | 免费证书申请  | 免费无限制           | 无限制               | | ||||
| | 功能   | 基础版             | 专业版               | | ||||
| |------|-----------------|-------------------| | ||||
| | 免费证书申请 | 免费无限制           | 无限制               | | ||||
| | 域名数量 | 免费无限制           | 无限制               | | ||||
| | 证书流水线条数 | 免费无限制           | 无限制               | | ||||
| | 站点证书监控  | 1条              | 无限制               | | ||||
| | 自动部署插件  | 阿里云、腾讯云、七牛云、SSH | 支持群晖、宝塔、1Panel等,持续开发中 | | ||||
| | 通知      | 邮件、webhook      | server酱、企微、anpush、钉钉等 | | ||||
| | 站点证书监控 | 1条              | 无限制               | | ||||
| | 自动部署插件 | 阿里云、腾讯云、七牛云、SSH | 支持群晖、宝塔、1Panel等,持续开发中 | | ||||
| | 通知   | 邮件、webhook      | server酱、企微、anpush等 | | ||||
| 
 | ||||
| ************************ | ||||
| 
 | ||||
|  |  | |||
|  | @ -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; | ||||
| 
 | ||||
|  |  | |||
|  | @ -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 = ( | ||||
|             <span> | ||||
|               <a href="https://afdian.com/a/greper" target="_blank"> | ||||
|                 爱发电赞助“VIP会员(¥29.9)”后获取一年期专业版激活码 | ||||
|               </a> | ||||
|               <span> 商业版请直接联系作者</span> | ||||
|             </span> | ||||
|           ); | ||||
|         } else { | ||||
|           activationCodeGetWay = ( | ||||
|             <a href={agent.contactLink} target="_blank"> | ||||
|               {agent.contactText} | ||||
|             </a> | ||||
|           ); | ||||
|         } | ||||
|       } | ||||
|       let activationCodeGetWay = ( | ||||
|         <span> | ||||
|           <a href="https://afdian.com/a/greper" target="_blank"> | ||||
|             爱发电赞助“VIP会员”后获取一年期专业版激活码 | ||||
|           </a> | ||||
|           <span> 商业版请直接联系作者</span> | ||||
|         </span> | ||||
|       ); | ||||
|       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 { | ||||
|  |  | |||
|  | @ -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 | ||||
|         } | ||||
|  |  | |||
|  | @ -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; | ||||
|       } | ||||
| 
 | ||||
|  |  | |||
|  | @ -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 { | ||||
|  |  | |||
|  | @ -23,8 +23,10 @@ | |||
|                 </a-tag> | ||||
|               </a-badge> | ||||
|             </template> | ||||
|             <a-divider type="vertical" /> | ||||
|             <suite-card class="m-0"></suite-card> | ||||
|             <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; | ||||
|  |  | |||
|  | @ -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; | ||||
|  |  | |||
|  | @ -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", | ||||
|  |  | |||
|  | @ -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); | ||||
| 
 | ||||
|  |  | |||
|  | @ -7,7 +7,7 @@ import crypto from 'crypto'; | |||
| 
 | ||||
| @Autoload() | ||||
| @Scope(ScopeEnum.Request, { allowDowngrade: true }) | ||||
| export class AutoInitSite { | ||||
| export class AutoAInitSite { | ||||
|   @Inject() | ||||
|   userService: UserService; | ||||
| 
 | ||||
|  | @ -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; | ||||
| 
 | ||||
|  | @ -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); | ||||
|     }); | ||||
|   } | ||||
| } | ||||
|  | @ -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; | ||||
|  |  | |||
|  | @ -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'); | ||||
|  |  | |||
|  | @ -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}条流水线`); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue
	
	 xiaojunnuo
						xiaojunnuo