From 42f4d1477dc791520a874aed56035abcbc8c433b Mon Sep 17 00:00:00 2001 From: xiaojunnuo Date: Mon, 14 Jul 2025 23:02:47 +0800 Subject: [PATCH] =?UTF-8?q?perf:=20OpenAPI=E6=94=AF=E6=8C=81autoApply?= =?UTF-8?q?=E5=8F=82=E6=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../libs/lib-server/src/basic/constants.ts | 8 + .../src/basic/exception/common-exception.ts | 6 +- .../src/plugin/cert-plugin/index.ts | 6 + .../src/views/certd/monitor/site/crud.tsx | 22 + .../src/views/certd/monitor/site/index.vue | 16 - .../views/certd/pipeline/certd-form/use.tsx | 3 +- .../controller/openapi/v1/cert-controller.ts | 25 +- .../monitor/facade/cert-info-facade.ts | 96 +++++ .../monitor/service/cert-info-service.ts | 41 +- .../pipeline/service/pipeline-service.ts | 402 +++++++++++------- 10 files changed, 414 insertions(+), 211 deletions(-) create mode 100644 packages/ui/certd-server/src/modules/monitor/facade/cert-info-facade.ts diff --git a/packages/libs/lib-server/src/basic/constants.ts b/packages/libs/lib-server/src/basic/constants.ts index 8fdc4424..f7afaa75 100644 --- a/packages/libs/lib-server/src/basic/constants.ts +++ b/packages/libs/lib-server/src/basic/constants.ts @@ -107,5 +107,13 @@ export const Constants = { code: 20012, message: '证书还未生成', }, + openCertApplying: { + code: 20013, + message: '证书正在申请中,请稍后重新获取(需要事先在“域名管理”页面配置好校验方式)', + }, + openEmailNotFound: { + code: 20021, + message: '用户邮箱还未配置', + }, }, }; diff --git a/packages/libs/lib-server/src/basic/exception/common-exception.ts b/packages/libs/lib-server/src/basic/exception/common-exception.ts index 5e97743a..97f6e15f 100644 --- a/packages/libs/lib-server/src/basic/exception/common-exception.ts +++ b/packages/libs/lib-server/src/basic/exception/common-exception.ts @@ -11,13 +11,13 @@ export class CommonException extends BaseException { } export class CodeException extends BaseException { - constructor(res: { code: number; message: string }) { - super("CodeException", res.code, res.message); + constructor(res: { code: number; message: string; data?: any }) { + super("CodeException", res.code, res.message, res.data); } } export class TextException extends BaseException { - constructor(name, code,message, data?) { + constructor(name, code, message, data?) { super(name, code, message, data); } } diff --git a/packages/plugins/plugin-cert/src/plugin/cert-plugin/index.ts b/packages/plugins/plugin-cert/src/plugin/cert-plugin/index.ts index f41afe97..6c654cf1 100644 --- a/packages/plugins/plugin-cert/src/plugin/cert-plugin/index.ts +++ b/packages/plugins/plugin-cert/src/plugin/cert-plugin/index.ts @@ -470,6 +470,9 @@ export class CertApplyPlugin extends CertApplyBasePlugin { const domain = fullDomain.replaceAll("*.", ""); const mainDomain = await domainParser.parse(domain); const planSetting: DomainVerifyPlanInput = verifyPlanSetting[mainDomain]; + if (planSetting == null) { + throw new Error(`没有找到域名(${domain})的校验计划`); + } if (planSetting.type === "dns") { plan[domain] = await this.createDnsDomainVerifyPlan(planSetting, domain, mainDomain); } else if (planSetting.type === "cname") { @@ -498,6 +501,9 @@ export class CertApplyPlugin extends CertApplyBasePlugin { for (const domain in verifiers) { const verifier = verifiers[domain]; + if (verifier == null) { + throw new Error(`没有找到与该域名(${domain})匹配的校验方式,请先到‘域名管理’页面添加校验方式`); + } if (verifier.type === "dns") { plan[domain] = await this.createDnsDomainVerifyPlan(verifier.dns, domain, verifier.mainDomain); } else if (verifier.type === "cname") { diff --git a/packages/ui/certd-client/src/views/certd/monitor/site/crud.tsx b/packages/ui/certd-client/src/views/certd/monitor/site/crud.tsx index b5599e8f..c53a3214 100644 --- a/packages/ui/certd-client/src/views/certd/monitor/site/crud.tsx +++ b/packages/ui/certd-client/src/views/certd/monitor/site/crud.tsx @@ -46,6 +46,20 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat const { openSiteIpMonitorDialog } = useSiteIpMonitor(); const { openSiteImportDialog } = useSiteImport(); + + function checkAll() { + Modal.confirm({ + title: t("certd.monitor.confirmTitle"), // "确认" + content: t("certd.monitor.confirmContent"), // "确认触发检查全部站点证书吗?" + onOk: async () => { + await siteInfoApi.CheckAll(); + notification.success({ + message: t("certd.monitor.checkSubmitted"), // "检查任务已提交" + description: t("certd.monitor.pleaseRefresh"), // "请稍后刷新页面查看结果" + }); + }, + }); + } return { id: "siteMonitorCrud", crudOptions: { @@ -114,6 +128,14 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat }); }, }, + checkAll: { + show: true, + text: t("certd.monitor.checkAll"), + type: "primary", + click() { + checkAll(); + }, + }, }, }, rowHandle: { diff --git a/packages/ui/certd-client/src/views/certd/monitor/site/index.vue b/packages/ui/certd-client/src/views/certd/monitor/site/index.vue index 78227e94..436620c4 100644 --- a/packages/ui/certd-client/src/views/certd/monitor/site/index.vue +++ b/packages/ui/certd-client/src/views/certd/monitor/site/index.vue @@ -14,9 +14,6 @@ -
- {{ t("certd.monitor.checkAll") }} -
@@ -35,19 +32,6 @@ defineOptions({ name: "SiteCertMonitor", }); const { crudBinding, crudRef, crudExpose } = useFs({ createCrudOptions, context: {} }); -function checkAll() { - Modal.confirm({ - title: t("certd.monitor.confirmTitle"), // "确认" - content: t("certd.monitor.confirmContent"), // "确认触发检查全部站点证书吗?" - onOk: async () => { - await siteInfoApi.CheckAll(); - notification.success({ - message: t("certd.monitor.checkSubmitted"), // "检查任务已提交" - description: t("certd.monitor.pleaseRefresh"), // "请稍后刷新页面查看结果" - }); - }, - }); -} // 页面打开后获取列表数据 onMounted(() => { diff --git a/packages/ui/certd-client/src/views/certd/pipeline/certd-form/use.tsx b/packages/ui/certd-client/src/views/certd/pipeline/certd-form/use.tsx index f1edd6c4..74c89195 100644 --- a/packages/ui/certd-client/src/views/certd/pipeline/certd-form/use.tsx +++ b/packages/ui/certd-client/src/views/certd/pipeline/certd-form/use.tsx @@ -125,6 +125,7 @@ export function useCertPipelineCreator() { const pluginStore = usePluginStore(); const randomHour = Math.floor(Math.random() * 6); const randomMin = Math.floor(Math.random() * 60); + const randomCron = `0 ${randomMin} ${randomHour} * * *`; const groupDictRef = dict({ url: "/pi/pipeline/group/all", @@ -193,7 +194,7 @@ export function useCertPipelineCreator() { title: t("certd.pipelineForm.triggerCronTitle"), type: "text", form: { - value: `0 ${randomMin} ${randomHour} * * *`, + value: randomCron, component: { name: "cron-editor", vModel: "modelValue", diff --git a/packages/ui/certd-server/src/controller/openapi/v1/cert-controller.ts b/packages/ui/certd-server/src/controller/openapi/v1/cert-controller.ts index 0b02fabd..da78766d 100644 --- a/packages/ui/certd-server/src/controller/openapi/v1/cert-controller.ts +++ b/packages/ui/certd-server/src/controller/openapi/v1/cert-controller.ts @@ -1,13 +1,15 @@ -import { ALL, Body, Controller, Get, Inject, Post, Provide, Query } from '@midwayjs/core'; -import { CodeException, Constants, EncryptService } from '@certd/lib-server'; -import { CertInfoService } from '../../../modules/monitor/service/cert-info-service.js'; -import { CertInfo } from '@certd/plugin-cert'; -import { OpenKey } from '../../../modules/open/service/open-key-service.js'; -import { BaseOpenController } from '../base-open-controller.js'; +import { ALL, Body, Controller, Get, Inject, Post, Provide, Query } from "@midwayjs/core"; +import { CodeException, Constants, EncryptService } from "@certd/lib-server"; +import { CertInfo } from "@certd/plugin-cert"; +import { OpenKey } from "../../../modules/open/service/open-key-service.js"; +import { BaseOpenController } from "../base-open-controller.js"; +import { CertInfoFacade } from "../../../modules/monitor/facade/cert-info-facade.js"; +import { merge } from "lodash-es"; export type CertGetReq = { domains?: string; certId: number; + autoApply?:boolean; }; /** @@ -16,7 +18,7 @@ export type CertGetReq = { @Controller('/api/v1/cert') export class OpenCertController extends BaseOpenController { @Inject() - certInfoService: CertInfoService; + certInfoFacade: CertInfoFacade; @Inject() encryptService: EncryptService; @@ -29,10 +31,13 @@ export class OpenCertController extends BaseOpenController { throw new CodeException(Constants.res.openKeyError); } - const res: CertInfo = await this.certInfoService.getCertInfo({ + const req = merge({}, bean, query) + + const res: CertInfo = await this.certInfoFacade.getCertInfo({ userId, - domains: bean.domains || query.domains, - certId: bean.certId || query.certId, + domains: req.domains, + certId: req.certId, + autoApply: req.autoApply??false, }); return this.ok(res); } diff --git a/packages/ui/certd-server/src/modules/monitor/facade/cert-info-facade.ts b/packages/ui/certd-server/src/modules/monitor/facade/cert-info-facade.ts new file mode 100644 index 00000000..88249deb --- /dev/null +++ b/packages/ui/certd-server/src/modules/monitor/facade/cert-info-facade.ts @@ -0,0 +1,96 @@ +import { Inject, Provide, Scope, ScopeEnum } from "@midwayjs/core"; +import { CodeException, Constants } from "@certd/lib-server"; +import { CertInfoEntity } from "../entity/cert-info.js"; +import { utils } from "@certd/basic"; +import { PipelineService } from "../../pipeline/service/pipeline-service.js"; +import { UserSettingsService } from "../../mine/service/user-settings-service.js"; +import { UserEmailSetting } from "../../mine/service/models.js"; +import { PipelineEntity } from "../../pipeline/entity/pipeline.js"; +import { CertInfoService } from "../service/cert-info-service.js"; + + +@Provide("CertInfoFacade") +@Scope(ScopeEnum.Request, { allowDowngrade: true }) +export class CertInfoFacade { + + @Inject() + pipelineService: PipelineService; + + @Inject() + certInfoService: CertInfoService ; + + @Inject() + userSettingsService : UserSettingsService + + async getCertInfo(req: { domains?: string; certId?: number; userId: number,autoApply?:boolean }) { + const { domains, certId, userId } = req; + if (certId) { + return await this.certInfoService.getCertInfoById({ id: certId, userId }); + } + const domainArr = domains.split(','); + + const matchedList = await this.certInfoService.getMatchCertList({domains:domainArr,userId}) + let matched: CertInfoEntity = null + if (matchedList.length === 0 ) { + if(req.autoApply === true){ + //自动申请,先创建自动申请流水线 + const pipeline:PipelineEntity = await this.createAutoPipeline({domains:domainArr,userId}) + await this.triggerApplyPipeline({pipelineId:pipeline.id}) + }else{ + throw new CodeException(Constants.res.openCertNotFound); + } + } + matched = null; + for (const item of matchedList) { + if (item.expiresTime>0 && item.expiresTime < new Date().getTime()) { + matched = item; + break + } + } + if (!matched) { + if(req.autoApply === true){ + //如果没有找到有效期内的证书,则自动触发一次申请 + const first = matchedList[0] + await this.triggerApplyPipeline({pipelineId:first.pipelineId}) + return + }else{ + throw new CodeException(Constants.res.openCertNotFound); + } + } + + return await this.certInfoService.getCertInfoById({ id: matched.id, userId: userId }); + } + + async createAutoPipeline(req:{domains:string[],userId:number}){ + const userEmailSetting = await this.userSettingsService.getSetting(req.userId,UserEmailSetting) + if(!userEmailSetting.list){ + throw new CodeException(Constants.res.openEmailNotFound) + } + const email = userEmailSetting.list[0] + + return await this.pipelineService.createAutoPipeline({ + domains: req.domains, + email, + userId: req.userId, + from:"OpenAPI" + }) + + } + + async triggerApplyPipeline(req:{pipelineId:number}){ + //查询流水线状态 + const status = await this.pipelineService.getStatus(req.pipelineId) + if (status != 'running') { + await this.pipelineService.trigger(req.pipelineId) + await utils.sleep(1000) + } + throw new CodeException({ + ...Constants.res.openCertApplying, + data:{ + pipelineId:req.pipelineId + } + }); + } + + +} diff --git a/packages/ui/certd-server/src/modules/monitor/service/cert-info-service.ts b/packages/ui/certd-server/src/modules/monitor/service/cert-info-service.ts index 7a4cce25..a72bef65 100644 --- a/packages/ui/certd-server/src/modules/monitor/service/cert-info-service.ts +++ b/packages/ui/certd-server/src/modules/monitor/service/cert-info-service.ts @@ -1,10 +1,10 @@ -import {Provide, Scope, ScopeEnum} from "@midwayjs/core"; -import {BaseService, CodeException, Constants, PageReq} from "@certd/lib-server"; -import {InjectEntityModel} from "@midwayjs/typeorm"; -import {MoreThan, Repository} from "typeorm"; -import {CertInfoEntity} from "../entity/cert-info.js"; -import {utils} from "@certd/basic"; -import {CertInfo, CertReader} from "@certd/plugin-cert"; +import { Provide, Scope, ScopeEnum } from "@midwayjs/core"; +import { BaseService, CodeException, Constants, PageReq } from "@certd/lib-server"; +import { InjectEntityModel } from "@midwayjs/typeorm"; +import { Repository } from "typeorm"; +import { CertInfoEntity } from "../entity/cert-info.js"; +import { utils } from "@certd/basic"; +import { CertInfo, CertReader } from "@certd/plugin-cert"; export type UploadCertReq = { id?: number; @@ -71,6 +71,7 @@ export class CertInfoService extends BaseService { } await this.addOrUpdate(bean); + return bean.id } async deleteByPipelineId(id: number) { @@ -82,44 +83,28 @@ export class CertInfoService extends BaseService { }); } - async getCertInfo(params: { domains?: string; certId?: number; userId: number }) { - const { domains, certId, userId } = params; - if (certId) { - return await this.getCertInfoById({ id: certId, userId }); - } - return await this.getCertInfoByDomains({ - domains, - userId, - }); - } - - private async getCertInfoByDomains(params: { domains: string; userId: number }) { + async getMatchCertList(params: { domains: string[]; userId: number }) { const { domains, userId } = params; if (!domains) { throw new CodeException(Constants.res.openCertNotFound); } - const domainArr = domains.split(','); const list = await this.find({ select: { id: true, domains: true, + expiresTime:true, + pipelineId:true, }, where: { userId, - expiresTime: MoreThan(new Date().getTime()) }, }); //遍历查找 - const matched = list.find(item => { + return list.filter(item => { const itemDomains = item.domains.split(','); - return utils.domain.match(domainArr, itemDomains); + return utils.domain.match(domains, itemDomains); }); - if (!matched) { - throw new CodeException(Constants.res.openCertNotFound); - } - - return await this.getCertInfoById({ id: matched.id, userId: userId }); } async getCertInfoById(req: { id: number; userId: number }) { diff --git a/packages/ui/certd-server/src/modules/pipeline/service/pipeline-service.ts b/packages/ui/certd-server/src/modules/pipeline/service/pipeline-service.ts index b52c6151..31cd0479 100644 --- a/packages/ui/certd-server/src/modules/pipeline/service/pipeline-service.ts +++ b/packages/ui/certd-server/src/modules/pipeline/service/pipeline-service.ts @@ -1,6 +1,6 @@ -import {Config, Inject, Provide, Scope, ScopeEnum, sleep} from "@midwayjs/core"; -import {InjectEntityModel} from "@midwayjs/typeorm"; -import {In, MoreThan, Repository} from "typeorm"; +import { Config, Inject, Provide, Scope, ScopeEnum, sleep } from "@midwayjs/core"; +import { InjectEntityModel } from "@midwayjs/typeorm"; +import { In, MoreThan, Repository } from "typeorm"; import { AccessService, BaseService, @@ -11,8 +11,8 @@ import { SysSettingsService, SysSiteInfo } from "@certd/lib-server"; -import {PipelineEntity} from "../entity/pipeline.js"; -import {PipelineDetail} from "../entity/vo/pipeline-detail.js"; +import { PipelineEntity } from "../entity/pipeline.js"; +import { PipelineDetail } from "../entity/vo/pipeline-detail.js"; import { Executor, IAccessService, @@ -25,33 +25,32 @@ import { SysInfo, UserInfo } from "@certd/pipeline"; -import {DbStorage} from "./db-storage.js"; -import {StorageService} from "./storage-service.js"; -import {Cron} from "../../cron/cron.js"; -import {HistoryService} from "./history-service.js"; -import {HistoryEntity} from "../entity/history.js"; -import {HistoryLogEntity} from "../entity/history-log.js"; -import {HistoryLogService} from "./history-log-service.js"; -import {EmailService} from "../../basic/service/email-service.js"; -import {UserService} from "../../sys/authority/service/user-service.js"; -import {CnameRecordService} from "../../cname/service/cname-record-service.js"; -import {PluginConfigGetter} from "../../plugin/service/plugin-config-getter.js"; +import { DbStorage } from "./db-storage.js"; +import { StorageService } from "./storage-service.js"; +import { Cron } from "../../cron/cron.js"; +import { HistoryService } from "./history-service.js"; +import { HistoryEntity } from "../entity/history.js"; +import { HistoryLogEntity } from "../entity/history-log.js"; +import { HistoryLogService } from "./history-log-service.js"; +import { EmailService } from "../../basic/service/email-service.js"; +import { UserService } from "../../sys/authority/service/user-service.js"; +import { CnameRecordService } from "../../cname/service/cname-record-service.js"; +import { PluginConfigGetter } from "../../plugin/service/plugin-config-getter.js"; import dayjs from "dayjs"; -import {DbAdapter} from "../../db/index.js"; -import {isComm, isPlus} from "@certd/plus-core"; -import {logger} from "@certd/basic"; -import {UrlService} from "./url-service.js"; -import {NotificationService} from "./notification-service.js"; -import {UserSuiteEntity, UserSuiteService} from "@certd/commercial-core"; -import {CertInfoService} from "../../monitor/service/cert-info-service.js"; -import {TaskServiceBuilder} from "./getter/task-service-getter.js"; -import {nanoid} from "nanoid"; -import {set} from "lodash-es"; +import { DbAdapter } from "../../db/index.js"; +import { isComm, isPlus } from "@certd/plus-core"; +import { logger } from "@certd/basic"; +import { UrlService } from "./url-service.js"; +import { NotificationService } from "./notification-service.js"; +import { UserSuiteEntity, UserSuiteService } from "@certd/commercial-core"; +import { CertInfoService } from "../../monitor/service/cert-info-service.js"; +import { TaskServiceBuilder } from "./getter/task-service-getter.js"; +import { nanoid } from "nanoid"; +import { set } from "lodash-es"; const runningTasks: Map = new Map(); - /** * 证书申请 */ @@ -91,7 +90,7 @@ export class PipelineService extends BaseService { @Inject() cron: Cron; - @Config('certd') + @Config("certd") private certdConfig: any; @Inject() @@ -112,31 +111,31 @@ export class PipelineService extends BaseService { } async add(bean: PipelineEntity) { - bean.status = ResultType.none + bean.status = ResultType.none; await this.save(bean); return bean; } async page(pageReq: PageReq) { //模版流水线不要被查询出来 - set(pageReq,"query.isTemplate",false) + set(pageReq, "query.isTemplate", false); const result = await super.page(pageReq); await this.fillLastVars(result.records); for (const item of result.records) { - if (!item.content){ - continue + if (!item.content) { + continue; } const pipeline = JSON.parse(item.content); - let stepCount = 0 + let stepCount = 0; RunnableCollection.each(pipeline.stages, (runnable: any) => { - stepCount++ - }) + stepCount++; + }); // @ts-ignore - item.stepCount = stepCount + item.stepCount = stepCount; // @ts-ignore - item.triggerCount = pipeline.triggers.length - delete item.content + item.triggerCount = pipeline.triggers.length; + delete item.content; } return result; @@ -148,7 +147,7 @@ export class PipelineService extends BaseService { for (const record of records) { pipelineIds.push(record.id); recordMap[record.id] = record; - record.title = record.title + ''; + record.title = record.title + ""; } if (pipelineIds?.length > 0) { const vars = await this.storageService.findPipelineVars(pipelineIds); @@ -169,17 +168,17 @@ export class PipelineService extends BaseService { const info = await this.info(pipelineId); if (info && !info.disabled) { const pipeline = JSON.parse(info.content); - this.registerTriggers(pipeline,false); + this.registerTriggers(pipeline, false); } } - public async registerTrigger(info:PipelineEntity) { + public async registerTrigger(info: PipelineEntity) { if (info == null) { return; } if (info && !info.disabled) { const pipeline = JSON.parse(info.content); - this.registerTriggers(pipeline,false); + this.registerTriggers(pipeline, false); } } @@ -206,12 +205,12 @@ export class PipelineService extends BaseService { const isUpdate = bean.id > 0 && old != null; - const pipeline = JSON.parse(bean.content || '{}'); + const pipeline = JSON.parse(bean.content || "{}"); RunnableCollection.initPipelineRunnableType(pipeline); let domains = []; if (pipeline.stages) { RunnableCollection.each(pipeline.stages, (runnable: any) => { - if (runnable.runnableType === 'step' && runnable.type.indexOf('CertApply')>=0) { + if (runnable.runnableType === "step" && runnable.type.indexOf("CertApply") >= 0) { domains = runnable.input.domains || []; } }); @@ -222,7 +221,7 @@ export class PipelineService extends BaseService { await this.checkMaxPipelineCount(bean, pipeline, domains); } - if (!bean.status ){ + if (!bean.status) { bean.status = ResultType.none; } if (!isUpdate) { @@ -233,9 +232,11 @@ export class PipelineService extends BaseService { await this.doUpdatePipelineJson(bean, pipeline); //保存域名信息到certInfo表 - let fromType = 'pipeline'; - if (bean.type === 'cert_upload') { - fromType = 'upload'; + let fromType = "pipeline"; + if (bean.type === "cert_upload") { + fromType = "upload"; + }else if (bean.type === "cert_auto") { + fromType = "auto"; } await this.certInfoService.updateDomains(pipeline.id, pipeline.userId || bean.userId, domains, fromType); return bean; @@ -246,7 +247,7 @@ export class PipelineService extends BaseService { * @param bean * @param pipeline */ - async doUpdatePipelineJson(bean: PipelineEntity, pipeline:Pipeline) { + async doUpdatePipelineJson(bean: PipelineEntity, pipeline: Pipeline) { await this.clearTriggers(bean); if (pipeline.title) { bean.title = pipeline.title; @@ -277,7 +278,7 @@ export class PipelineService extends BaseService { throw new NeedSuiteException(`对不起,您最多只能添加${userSuite.domainCount.max}个域名,请购买或升级套餐`); } } - }else{ + } else { //非商业版校验用户最大流水线数量 const userId = bean.userId; const userIsAdmin = await this.userService.isAdmin(userId); @@ -296,12 +297,12 @@ export class PipelineService extends BaseService { async foreachPipeline(callback: (pipeline: PipelineEntity) => void) { const idEntityList = await this.repository.find({ select: { - id: true, + id: true }, where: { disabled: false, - templateId: 0, - }, + templateId: 0 + } }); const ids = idEntityList.map(item => { return item.id; @@ -321,7 +322,7 @@ export class PipelineService extends BaseService { //分段加载记录 for (const idArr of idsSpan) { const list = await this.repository.findBy({ - id: In(idArr), + id: In(idArr) }); for (const entity of list) { @@ -346,14 +347,14 @@ export class PipelineService extends BaseService { if (onlyAdminUser && entity.userId !== 1) { return; } - const pipeline = JSON.parse(entity.content ?? '{}'); + const pipeline = JSON.parse(entity.content ?? "{}"); try { await this.registerTriggers(pipeline, immediateTriggerOnce); } catch (e) { - logger.error('加载定时trigger失败:', e); + logger.error("加载定时trigger失败:", e); } }); - logger.info('定时器数量:', this.cron.getTaskSize()); + logger.info("定时器数量:", this.cron.getTaskSize()); } async registerTriggers(pipeline?: Pipeline, immediateTriggerOnce = false) { @@ -375,18 +376,18 @@ export class PipelineService extends BaseService { if (isComm()) { await this.checkHasDeployCount(id, entity.userId); } - await this.checkUserStatus(entity.userId) + await this.checkUserStatus(entity.userId); this.cron.register({ name: `pipeline.${id}.trigger.once`, cron: null, job: async () => { - logger.info('用户手动启动job'); + logger.info("用户手动启动job"); try { await this.doRun(entity, null, stepId); } catch (e) { - logger.error('手动job执行失败:', e); + logger.error("手动job执行失败:", e); } - }, + } }); } @@ -398,7 +399,7 @@ export class PipelineService extends BaseService { logger.error(e.message); await this.update({ id: pipelineId, - status: 'no_deploy_count', + status: "no_deploy_count" }); } throw e; @@ -406,7 +407,7 @@ export class PipelineService extends BaseService { } //@ts-ignore - async delete(id:any) { + async delete(id: any) { await this.clearTriggers(id); //TODO 删除storage // const storage = new DbStorage(pipeline.userId, this.storageService); @@ -421,11 +422,11 @@ export class PipelineService extends BaseService { if (id == null) { return; } - let pipeline:PipelineEntity = null - if (typeof id === 'number') { + let pipeline: PipelineEntity = null; + if (typeof id === "number") { pipeline = await this.info(id); - }else{ - pipeline = id + } else { + pipeline = id; } if (!pipeline) { return; @@ -445,7 +446,7 @@ export class PipelineService extends BaseService { registerCron(pipelineId, trigger) { if (pipelineId == null) { - logger.warn('pipelineId为空,无法注册定时任务'); + logger.warn("pipelineId为空,无法注册定时任务"); return; } @@ -454,11 +455,11 @@ export class PipelineService extends BaseService { return; } cron = cron.trim(); - if (cron.startsWith('* *')) { - cron = cron.replace('* *', '0 0'); + if (cron.startsWith("* *")) { + cron = cron.replace("* *", "0 0"); } - if (cron.startsWith('*')) { - cron = cron.replace('*', '0'); + if (cron.startsWith("*")) { + cron = cron.replace("*", "0"); } const triggerId = trigger.id; const name = this.buildCronKey(pipelineId, triggerId); @@ -467,19 +468,19 @@ export class PipelineService extends BaseService { name, cron, job: async () => { - logger.info('定时任务触发:', pipelineId, triggerId); + logger.info("定时任务触发:", pipelineId, triggerId); if (pipelineId == null) { - logger.warn('pipelineId为空,无法执行'); + logger.warn("pipelineId为空,无法执行"); return; } try { await this.run(pipelineId, triggerId); } catch (e) { - logger.error('定时job执行失败:', e); + logger.error("定时job执行失败:", e); } - }, + } }); - logger.info('当前定时器数量:', this.cron.getTaskSize()); + logger.info("当前定时器数量:", this.cron.getTaskSize()); } /** @@ -499,11 +500,11 @@ export class PipelineService extends BaseService { if (isComm()) { suite = await this.checkHasDeployCount(id, entity.userId); } - try{ - await this.checkUserStatus(entity.userId) - }catch (e) { - logger.info(e.message) - return + try { + await this.checkUserStatus(entity.userId); + } catch (e) { + logger.info(e.message); + return; } @@ -521,7 +522,7 @@ export class PipelineService extends BaseService { return; } - if (triggerType === 'timer') { + if (triggerType === "timer") { if (entity.disabled) { return; } @@ -530,25 +531,25 @@ export class PipelineService extends BaseService { const onChanged = async (history: RunHistory) => { //保存执行历史 try { - logger.info('保存执行历史:', history.id); + logger.info("保存执行历史:", history.id); await this.saveHistory(history); } catch (e) { const pipelineEntity = new PipelineEntity(); pipelineEntity.id = id; - pipelineEntity.status = 'error'; + pipelineEntity.status = "error"; pipelineEntity.lastHistoryTime = history.pipeline.status.startTime; await this.update(pipelineEntity); - logger.error('保存执行历史失败:', e); + logger.error("保存执行历史失败:", e); throw e; } }; const userId = entity.userId; - const historyId = await this.historyService.start(entity,triggerType); + const historyId = await this.historyService.start(entity, triggerType); const userIsAdmin = await this.userService.isAdmin(userId); const user: UserInfo = { id: userId, - role: userIsAdmin ? 'admin' : 'user', + role: userIsAdmin ? "admin" : "user" }; @@ -559,11 +560,11 @@ export class PipelineService extends BaseService { } const taskServiceGetter = this.taskServiceBuilder.create({ - userId, - }) - const accessGetter = await taskServiceGetter.get("accessService") - const notificationGetter =await taskServiceGetter.get("notificationService") - const cnameProxyService =await taskServiceGetter.get("cnameProxyService") + userId + }); + const accessGetter = await taskServiceGetter.get("accessService"); + const notificationGetter = await taskServiceGetter.get("notificationService"); + const cnameProxyService = await taskServiceGetter.get("cnameProxyService"); const executor = new Executor({ user, pipeline, @@ -577,7 +578,7 @@ export class PipelineService extends BaseService { notificationService: notificationGetter, fileRootDir: this.certdConfig.fileRootDir, sysInfo, - serviceGetter:taskServiceGetter + serviceGetter: taskServiceGetter }); try { runningTasks.set(historyId, executor); @@ -595,7 +596,7 @@ export class PipelineService extends BaseService { } } } catch (e) { - logger.error('执行失败:', e); + logger.error("执行失败:", e); // throw e; } finally { runningTasks.delete(historyId); @@ -619,7 +620,7 @@ export class PipelineService extends BaseService { } private getTriggerType(triggerId, pipeline) { - let triggerType = 'user'; + let triggerType = "user"; if (triggerId != null) { //如果不是手动触发 //查找trigger @@ -629,8 +630,8 @@ export class PipelineService extends BaseService { this.cron.remove(this.buildCronKey(pipeline.id, triggerId)); triggerType = null; } else { - logger.info('timer trigger:' + found.id, found.title, found.cron); - triggerType = 'timer'; + logger.info("timer trigger:" + found.id, found.title, found.cron); + triggerType = "timer"; } } return triggerType; @@ -653,7 +654,7 @@ export class PipelineService extends BaseService { //修改pipeline状态 const pipelineEntity = new PipelineEntity(); pipelineEntity.id = parseInt(history.pipeline.id); - pipelineEntity.status = history.pipeline.status.result + ''; + pipelineEntity.status = history.pipeline.status.result + ""; pipelineEntity.lastHistoryTime = history.pipeline.status.startTime; await this.update(pipelineEntity); @@ -677,8 +678,8 @@ export class PipelineService extends BaseService { async count(param: { userId?: any }) { const count = await this.repository.count({ where: { - userId: param.userId, - }, + userId: param.userId + } }); return count; } @@ -686,12 +687,12 @@ export class PipelineService extends BaseService { async statusCount(param: { userId?: any } = {}) { const statusCount = await this.repository .createQueryBuilder() - .select('status') - .addSelect('count(1)', 'count') + .select("status") + .addSelect("count(1)", "count") .where({ - userId: param.userId, + userId: param.userId }) - .groupBy('status') + .groupBy("status") .getRawMany(); return statusCount; } @@ -701,11 +702,11 @@ export class PipelineService extends BaseService { select: { id: true, title: true, - status: true, + status: true }, where: { - userId, - }, + userId + } }); await this.fillLastVars(list); list = list.filter(item => { @@ -719,16 +720,16 @@ export class PipelineService extends BaseService { } async createCountPerDay(param: { days: number } = { days: 7 }) { - const todayEnd = dayjs().endOf('day'); + const todayEnd = dayjs().endOf("day"); const result = await this.getRepository() - .createQueryBuilder('main') - .select(`${this.dbAdapter.date('main.createTime')} AS date`) // 将UNIX时间戳转换为日期 - .addSelect('COUNT(1) AS count') + .createQueryBuilder("main") + .select(`${this.dbAdapter.date("main.createTime")} AS date`) // 将UNIX时间戳转换为日期 + .addSelect("COUNT(1) AS count") .where({ // 0点 - createTime: MoreThan(todayEnd.add(-param.days, 'day').toDate()), + createTime: MoreThan(todayEnd.add(-param.days, "day").toDate()) }) - .groupBy('date') + .groupBy("date") .getRawMany(); return result; @@ -745,48 +746,48 @@ export class PipelineService extends BaseService { await this.repository.update( { id: In(ids), - userId, + userId }, { groupId } ); } - async batchUpdateTrigger(ids: number[], trigger: any, userId: any){ + async batchUpdateTrigger(ids: number[], trigger: any, userId: any) { const list = await this.find({ - where:{ + where: { id: In(ids), userId } - }) + }); for (const item of list) { const pipeline = JSON.parse(item.content); pipeline.triggers = [{ id: nanoid(), - title: '定时触发', + title: "定时触发", ...trigger - }] - await this.doUpdatePipelineJson(item,pipeline) + }]; + await this.doUpdatePipelineJson(item, pipeline); } } - async batchUpdateNotifications(ids: number[], notification: Notification, userId: any){ + async batchUpdateNotifications(ids: number[], notification: Notification, userId: any) { const list = await this.find({ - where:{ + where: { id: In(ids), userId } - }) + }); for (const item of list) { const pipeline = JSON.parse(item.content); pipeline.notifications = [{ id: nanoid(), - title: '通知', + title: "通知", /** * type: NotificationType; * when: NotificationWhen[]; @@ -797,44 +798,44 @@ export class PipelineService extends BaseService { */ type: "other", ...notification - }] - await this.doUpdatePipelineJson(item,pipeline) + }]; + await this.doUpdatePipelineJson(item, pipeline); } } async batchRerun(ids: number[], userId: any) { - if (!isPlus()){ - throw new NeedVIPException("此功能需要升级专业版") + if (!isPlus()) { + throw new NeedVIPException("此功能需要升级专业版"); } if (!userId || ids.length === 0) { return; } const list = await this.repository.find({ - select:{ - id:true + select: { + id: true }, - where:{ + where: { id: In(ids), userId } - }) + }); - ids = list.map(item=>item.id) + ids = list.map(item => item.id); //异步执行 - this.startBatchRerun(ids) + this.startBatchRerun(ids); } - async startBatchRerun(ids: number[]){ + async startBatchRerun(ids: number[]) { //20条一批 const batchSize = 20; for (let i = 0; i < ids.length; i += batchSize) { const batchIds = ids.slice(i, i + batchSize); - const batchPromises = batchIds.map(async (id)=>{ - await this.run(id,null,"ALL") + const batchPromises = batchIds.map(async (id) => { + await this.run(id, null, "ALL"); }); - await Promise.all(batchPromises) + await Promise.all(batchPromises); } } @@ -847,35 +848,130 @@ export class PipelineService extends BaseService { return await this.repository.find({ select: { id: true, - title: true, + title: true }, where: { id: In(pipelineIds), - userId, - }, + userId + } }); } private async checkUserStatus(userId: number) { const userEntity = await this.userService.info(userId); - if(userEntity == null){ - throw new Error('用户不存在'); + if (userEntity == null) { + throw new Error("用户不存在"); } - if(userEntity.status === 0){ - const message = `账户${userId}已被禁用,禁止运行流水线` - throw new Error(message) + if (userEntity.status === 0) { + const message = `账户${userId}已被禁用,禁止运行流水线`; + throw new Error(message); } - const sysPublic = await this.sysSettingsService.getPublicSettings() - if(isPlus() && sysPublic.userValidTimeEnabled === true){ + const sysPublic = await this.sysSettingsService.getPublicSettings(); + if (isPlus() && sysPublic.userValidTimeEnabled === true) { //校验用户有效期是否设置 - if(userEntity.validTime!= null && userEntity.validTime > 0){ - if(userEntity.validTime < new Date().getTime()){ + if (userEntity.validTime != null && userEntity.validTime > 0) { + if (userEntity.validTime < new Date().getTime()) { //用户已过期 - const message = `账户${userId}已过有效期,禁止运行流水线` - throw new Error(message) + const message = `账户${userId}已过有效期,禁止运行流水线`; + throw new Error(message); } } } } + + async createAutoPipeline(req: { domains: string[]; email: string; userId: number ,from:string}) { + + const randomHour = Math.floor(Math.random() * 6); + const randomMin = Math.floor(Math.random() * 60); + const randomCron = `0 ${randomMin} ${randomHour} * * *`; + + let pipeline: any = { + title: req.domains[0] + `证书自动申请【${req.from??"OpenAPI"}】`, + runnableType: "pipeline", + triggers: [ + { + id: nanoid(), + title: "定时触发", + cron: randomCron, + type: "cron" + } + ], + notifications: [ + { + id: nanoid(), + type: "custom", + when: ["error", "turnToSuccess", "success"], + notificationId: 0, + title: "默认通知", + } + ], + stages: [ + { + id: nanoid(), + title: "证书申请阶段", + maxTaskCount: 1, + runnableType: "stage", + tasks: [ + { + id: nanoid(), + title: "证书申请任务", + runnableType: "task", + steps: [ + { + id: nanoid(), + title: "申请证书", + runnableType: "step", + input: { + renewDays: 35, + domains: req.domains, + email: req.email, + "challengeType": "auto", + "sslProvider": "letsencrypt", + "privateKeyType": "rsa_2048", + "certProfile": "classic", + "useProxy": false, + "skipLocalVerify": false, + "maxCheckRetryCount": 20, + "waitDnsDiffuseTime": 30, + "pfxArgs": "-macalg SHA1 -keypbe PBE-SHA1-3DES -certpbe PBE-SHA1-3DES", + "successNotify": true + }, + strategy: { + runStrategy: 0 // 正常执行 + }, + type: "CertApply" + } + ] + } + ] + } + ] + }; + + const bean = new PipelineEntity(); + bean.title = pipeline.title; + bean.content = JSON.stringify(pipeline); + bean.userId = req.userId; + bean.status = "none"; + bean.type = "cert_auto"; + bean.disabled = false + bean.keepHistoryCount = 30 + await this.save(bean) + + + return bean; + } + + async getStatus(pipelineId: number) { + const res = await this.repository.findOne({ + select: { + status: true + }, + where: { + id: pipelineId + } + }); + return res?.status; + } }