perf: OpenAPI支持autoApply参数

v2-dev-order
xiaojunnuo 2025-07-14 23:02:47 +08:00
parent 609ac9c9a2
commit 42f4d1477d
10 changed files with 414 additions and 211 deletions

View File

@ -107,5 +107,13 @@ export const Constants = {
code: 20012, code: 20012,
message: '证书还未生成', message: '证书还未生成',
}, },
openCertApplying: {
code: 20013,
message: '证书正在申请中,请稍后重新获取(需要事先在“域名管理”页面配置好校验方式)',
},
openEmailNotFound: {
code: 20021,
message: '用户邮箱还未配置',
},
}, },
}; };

View File

@ -11,13 +11,13 @@ export class CommonException extends BaseException {
} }
export class CodeException extends BaseException { export class CodeException extends BaseException {
constructor(res: { code: number; message: string }) { constructor(res: { code: number; message: string; data?: any }) {
super("CodeException", res.code, res.message); super("CodeException", res.code, res.message, res.data);
} }
} }
export class TextException extends BaseException { export class TextException extends BaseException {
constructor(name, code,message, data?) { constructor(name, code, message, data?) {
super(name, code, message, data); super(name, code, message, data);
} }
} }

View File

@ -470,6 +470,9 @@ export class CertApplyPlugin extends CertApplyBasePlugin {
const domain = fullDomain.replaceAll("*.", ""); const domain = fullDomain.replaceAll("*.", "");
const mainDomain = await domainParser.parse(domain); const mainDomain = await domainParser.parse(domain);
const planSetting: DomainVerifyPlanInput = verifyPlanSetting[mainDomain]; const planSetting: DomainVerifyPlanInput = verifyPlanSetting[mainDomain];
if (planSetting == null) {
throw new Error(`没有找到域名(${domain})的校验计划`);
}
if (planSetting.type === "dns") { if (planSetting.type === "dns") {
plan[domain] = await this.createDnsDomainVerifyPlan(planSetting, domain, mainDomain); plan[domain] = await this.createDnsDomainVerifyPlan(planSetting, domain, mainDomain);
} else if (planSetting.type === "cname") { } else if (planSetting.type === "cname") {
@ -498,6 +501,9 @@ export class CertApplyPlugin extends CertApplyBasePlugin {
for (const domain in verifiers) { for (const domain in verifiers) {
const verifier = verifiers[domain]; const verifier = verifiers[domain];
if (verifier == null) {
throw new Error(`没有找到与该域名(${domain})匹配的校验方式,请先到‘域名管理’页面添加校验方式`);
}
if (verifier.type === "dns") { if (verifier.type === "dns") {
plan[domain] = await this.createDnsDomainVerifyPlan(verifier.dns, domain, verifier.mainDomain); plan[domain] = await this.createDnsDomainVerifyPlan(verifier.dns, domain, verifier.mainDomain);
} else if (verifier.type === "cname") { } else if (verifier.type === "cname") {

View File

@ -46,6 +46,20 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
const { openSiteIpMonitorDialog } = useSiteIpMonitor(); const { openSiteIpMonitorDialog } = useSiteIpMonitor();
const { openSiteImportDialog } = useSiteImport(); 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 { return {
id: "siteMonitorCrud", id: "siteMonitorCrud",
crudOptions: { 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: { rowHandle: {

View File

@ -14,9 +14,6 @@
</div> </div>
</div> </div>
</div> </div>
<div class="more">
<a-button type="primary" @click="checkAll">{{ t("certd.monitor.checkAll") }}</a-button>
</div>
</template> </template>
<fs-crud ref="crudRef" v-bind="crudBinding"> </fs-crud> <fs-crud ref="crudRef" v-bind="crudBinding"> </fs-crud>
</fs-page> </fs-page>
@ -35,19 +32,6 @@ defineOptions({
name: "SiteCertMonitor", name: "SiteCertMonitor",
}); });
const { crudBinding, crudRef, crudExpose } = useFs({ createCrudOptions, context: {} }); 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(() => { onMounted(() => {

View File

@ -125,6 +125,7 @@ export function useCertPipelineCreator() {
const pluginStore = usePluginStore(); const pluginStore = usePluginStore();
const randomHour = Math.floor(Math.random() * 6); const randomHour = Math.floor(Math.random() * 6);
const randomMin = Math.floor(Math.random() * 60); const randomMin = Math.floor(Math.random() * 60);
const randomCron = `0 ${randomMin} ${randomHour} * * *`;
const groupDictRef = dict({ const groupDictRef = dict({
url: "/pi/pipeline/group/all", url: "/pi/pipeline/group/all",
@ -193,7 +194,7 @@ export function useCertPipelineCreator() {
title: t("certd.pipelineForm.triggerCronTitle"), title: t("certd.pipelineForm.triggerCronTitle"),
type: "text", type: "text",
form: { form: {
value: `0 ${randomMin} ${randomHour} * * *`, value: randomCron,
component: { component: {
name: "cron-editor", name: "cron-editor",
vModel: "modelValue", vModel: "modelValue",

View File

@ -1,13 +1,15 @@
import { ALL, Body, Controller, Get, Inject, Post, Provide, Query } from '@midwayjs/core'; import { ALL, Body, Controller, Get, Inject, Post, Provide, Query } from "@midwayjs/core";
import { CodeException, Constants, EncryptService } from '@certd/lib-server'; 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 { CertInfo } from '@certd/plugin-cert'; import { OpenKey } from "../../../modules/open/service/open-key-service.js";
import { OpenKey } from '../../../modules/open/service/open-key-service.js'; import { BaseOpenController } from "../base-open-controller.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 = { export type CertGetReq = {
domains?: string; domains?: string;
certId: number; certId: number;
autoApply?:boolean;
}; };
/** /**
@ -16,7 +18,7 @@ export type CertGetReq = {
@Controller('/api/v1/cert') @Controller('/api/v1/cert')
export class OpenCertController extends BaseOpenController { export class OpenCertController extends BaseOpenController {
@Inject() @Inject()
certInfoService: CertInfoService; certInfoFacade: CertInfoFacade;
@Inject() @Inject()
encryptService: EncryptService; encryptService: EncryptService;
@ -29,10 +31,13 @@ export class OpenCertController extends BaseOpenController {
throw new CodeException(Constants.res.openKeyError); 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, userId,
domains: bean.domains || query.domains, domains: req.domains,
certId: bean.certId || query.certId, certId: req.certId,
autoApply: req.autoApply??false,
}); });
return this.ok(res); return this.ok(res);
} }

View File

@ -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<UserEmailSetting>(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
}
});
}
}

View File

@ -1,10 +1,10 @@
import {Provide, Scope, ScopeEnum} from "@midwayjs/core"; import { Provide, Scope, ScopeEnum } from "@midwayjs/core";
import {BaseService, CodeException, Constants, PageReq} from "@certd/lib-server"; import { BaseService, CodeException, Constants, PageReq } from "@certd/lib-server";
import {InjectEntityModel} from "@midwayjs/typeorm"; import { InjectEntityModel } from "@midwayjs/typeorm";
import {MoreThan, Repository} from "typeorm"; import { Repository } from "typeorm";
import {CertInfoEntity} from "../entity/cert-info.js"; import { CertInfoEntity } from "../entity/cert-info.js";
import {utils} from "@certd/basic"; import { utils } from "@certd/basic";
import {CertInfo, CertReader} from "@certd/plugin-cert"; import { CertInfo, CertReader } from "@certd/plugin-cert";
export type UploadCertReq = { export type UploadCertReq = {
id?: number; id?: number;
@ -71,6 +71,7 @@ export class CertInfoService extends BaseService<CertInfoEntity> {
} }
await this.addOrUpdate(bean); await this.addOrUpdate(bean);
return bean.id
} }
async deleteByPipelineId(id: number) { async deleteByPipelineId(id: number) {
@ -82,44 +83,28 @@ export class CertInfoService extends BaseService<CertInfoEntity> {
}); });
} }
async getCertInfo(params: { domains?: string; certId?: number; userId: number }) { async getMatchCertList(params: { domains: string[]; 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 }) {
const { domains, userId } = params; const { domains, userId } = params;
if (!domains) { if (!domains) {
throw new CodeException(Constants.res.openCertNotFound); throw new CodeException(Constants.res.openCertNotFound);
} }
const domainArr = domains.split(',');
const list = await this.find({ const list = await this.find({
select: { select: {
id: true, id: true,
domains: true, domains: true,
expiresTime:true,
pipelineId:true,
}, },
where: { where: {
userId, userId,
expiresTime: MoreThan(new Date().getTime())
}, },
}); });
//遍历查找 //遍历查找
const matched = list.find(item => { return list.filter(item => {
const itemDomains = item.domains.split(','); 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 }) { async getCertInfoById(req: { id: number; userId: number }) {

View File

@ -1,6 +1,6 @@
import {Config, Inject, Provide, Scope, ScopeEnum, sleep} from "@midwayjs/core"; import { Config, Inject, Provide, Scope, ScopeEnum, sleep } from "@midwayjs/core";
import {InjectEntityModel} from "@midwayjs/typeorm"; import { InjectEntityModel } from "@midwayjs/typeorm";
import {In, MoreThan, Repository} from "typeorm"; import { In, MoreThan, Repository } from "typeorm";
import { import {
AccessService, AccessService,
BaseService, BaseService,
@ -11,8 +11,8 @@ import {
SysSettingsService, SysSettingsService,
SysSiteInfo SysSiteInfo
} from "@certd/lib-server"; } from "@certd/lib-server";
import {PipelineEntity} from "../entity/pipeline.js"; import { PipelineEntity } from "../entity/pipeline.js";
import {PipelineDetail} from "../entity/vo/pipeline-detail.js"; import { PipelineDetail } from "../entity/vo/pipeline-detail.js";
import { import {
Executor, Executor,
IAccessService, IAccessService,
@ -25,33 +25,32 @@ import {
SysInfo, SysInfo,
UserInfo UserInfo
} from "@certd/pipeline"; } from "@certd/pipeline";
import {DbStorage} from "./db-storage.js"; import { DbStorage } from "./db-storage.js";
import {StorageService} from "./storage-service.js"; import { StorageService } from "./storage-service.js";
import {Cron} from "../../cron/cron.js"; import { Cron } from "../../cron/cron.js";
import {HistoryService} from "./history-service.js"; import { HistoryService } from "./history-service.js";
import {HistoryEntity} from "../entity/history.js"; import { HistoryEntity } from "../entity/history.js";
import {HistoryLogEntity} from "../entity/history-log.js"; import { HistoryLogEntity } from "../entity/history-log.js";
import {HistoryLogService} from "./history-log-service.js"; import { HistoryLogService } from "./history-log-service.js";
import {EmailService} from "../../basic/service/email-service.js"; import { EmailService } from "../../basic/service/email-service.js";
import {UserService} from "../../sys/authority/service/user-service.js"; import { UserService } from "../../sys/authority/service/user-service.js";
import {CnameRecordService} from "../../cname/service/cname-record-service.js"; import { CnameRecordService } from "../../cname/service/cname-record-service.js";
import {PluginConfigGetter} from "../../plugin/service/plugin-config-getter.js"; import { PluginConfigGetter } from "../../plugin/service/plugin-config-getter.js";
import dayjs from "dayjs"; import dayjs from "dayjs";
import {DbAdapter} from "../../db/index.js"; import { DbAdapter } from "../../db/index.js";
import {isComm, isPlus} from "@certd/plus-core"; import { isComm, isPlus } from "@certd/plus-core";
import {logger} from "@certd/basic"; import { logger } from "@certd/basic";
import {UrlService} from "./url-service.js"; import { UrlService } from "./url-service.js";
import {NotificationService} from "./notification-service.js"; import { NotificationService } from "./notification-service.js";
import {UserSuiteEntity, UserSuiteService} from "@certd/commercial-core"; import { UserSuiteEntity, UserSuiteService } from "@certd/commercial-core";
import {CertInfoService} from "../../monitor/service/cert-info-service.js"; import { CertInfoService } from "../../monitor/service/cert-info-service.js";
import {TaskServiceBuilder} from "./getter/task-service-getter.js"; import { TaskServiceBuilder } from "./getter/task-service-getter.js";
import {nanoid} from "nanoid"; import { nanoid } from "nanoid";
import {set} from "lodash-es"; import { set } from "lodash-es";
const runningTasks: Map<string | number, Executor> = new Map(); const runningTasks: Map<string | number, Executor> = new Map();
/** /**
* *
*/ */
@ -91,7 +90,7 @@ export class PipelineService extends BaseService<PipelineEntity> {
@Inject() @Inject()
cron: Cron; cron: Cron;
@Config('certd') @Config("certd")
private certdConfig: any; private certdConfig: any;
@Inject() @Inject()
@ -112,31 +111,31 @@ export class PipelineService extends BaseService<PipelineEntity> {
} }
async add(bean: PipelineEntity) { async add(bean: PipelineEntity) {
bean.status = ResultType.none bean.status = ResultType.none;
await this.save(bean); await this.save(bean);
return bean; return bean;
} }
async page(pageReq: PageReq<PipelineEntity>) { async page(pageReq: PageReq<PipelineEntity>) {
//模版流水线不要被查询出来 //模版流水线不要被查询出来
set(pageReq,"query.isTemplate",false) set(pageReq, "query.isTemplate", false);
const result = await super.page(pageReq); const result = await super.page(pageReq);
await this.fillLastVars(result.records); await this.fillLastVars(result.records);
for (const item of result.records) { for (const item of result.records) {
if (!item.content){ if (!item.content) {
continue continue;
} }
const pipeline = JSON.parse(item.content); const pipeline = JSON.parse(item.content);
let stepCount = 0 let stepCount = 0;
RunnableCollection.each(pipeline.stages, (runnable: any) => { RunnableCollection.each(pipeline.stages, (runnable: any) => {
stepCount++ stepCount++;
}) });
// @ts-ignore // @ts-ignore
item.stepCount = stepCount item.stepCount = stepCount;
// @ts-ignore // @ts-ignore
item.triggerCount = pipeline.triggers.length item.triggerCount = pipeline.triggers.length;
delete item.content delete item.content;
} }
return result; return result;
@ -148,7 +147,7 @@ export class PipelineService extends BaseService<PipelineEntity> {
for (const record of records) { for (const record of records) {
pipelineIds.push(record.id); pipelineIds.push(record.id);
recordMap[record.id] = record; recordMap[record.id] = record;
record.title = record.title + ''; record.title = record.title + "";
} }
if (pipelineIds?.length > 0) { if (pipelineIds?.length > 0) {
const vars = await this.storageService.findPipelineVars(pipelineIds); const vars = await this.storageService.findPipelineVars(pipelineIds);
@ -169,17 +168,17 @@ export class PipelineService extends BaseService<PipelineEntity> {
const info = await this.info(pipelineId); const info = await this.info(pipelineId);
if (info && !info.disabled) { if (info && !info.disabled) {
const pipeline = JSON.parse(info.content); 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) { if (info == null) {
return; return;
} }
if (info && !info.disabled) { if (info && !info.disabled) {
const pipeline = JSON.parse(info.content); const pipeline = JSON.parse(info.content);
this.registerTriggers(pipeline,false); this.registerTriggers(pipeline, false);
} }
} }
@ -206,12 +205,12 @@ export class PipelineService extends BaseService<PipelineEntity> {
const isUpdate = bean.id > 0 && old != null; const isUpdate = bean.id > 0 && old != null;
const pipeline = JSON.parse(bean.content || '{}'); const pipeline = JSON.parse(bean.content || "{}");
RunnableCollection.initPipelineRunnableType(pipeline); RunnableCollection.initPipelineRunnableType(pipeline);
let domains = []; let domains = [];
if (pipeline.stages) { if (pipeline.stages) {
RunnableCollection.each(pipeline.stages, (runnable: any) => { 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 || []; domains = runnable.input.domains || [];
} }
}); });
@ -222,7 +221,7 @@ export class PipelineService extends BaseService<PipelineEntity> {
await this.checkMaxPipelineCount(bean, pipeline, domains); await this.checkMaxPipelineCount(bean, pipeline, domains);
} }
if (!bean.status ){ if (!bean.status) {
bean.status = ResultType.none; bean.status = ResultType.none;
} }
if (!isUpdate) { if (!isUpdate) {
@ -233,9 +232,11 @@ export class PipelineService extends BaseService<PipelineEntity> {
await this.doUpdatePipelineJson(bean, pipeline); await this.doUpdatePipelineJson(bean, pipeline);
//保存域名信息到certInfo表 //保存域名信息到certInfo表
let fromType = 'pipeline'; let fromType = "pipeline";
if (bean.type === 'cert_upload') { if (bean.type === "cert_upload") {
fromType = 'upload'; fromType = "upload";
}else if (bean.type === "cert_auto") {
fromType = "auto";
} }
await this.certInfoService.updateDomains(pipeline.id, pipeline.userId || bean.userId, domains, fromType); await this.certInfoService.updateDomains(pipeline.id, pipeline.userId || bean.userId, domains, fromType);
return bean; return bean;
@ -246,7 +247,7 @@ export class PipelineService extends BaseService<PipelineEntity> {
* @param bean * @param bean
* @param pipeline * @param pipeline
*/ */
async doUpdatePipelineJson(bean: PipelineEntity, pipeline:Pipeline) { async doUpdatePipelineJson(bean: PipelineEntity, pipeline: Pipeline) {
await this.clearTriggers(bean); await this.clearTriggers(bean);
if (pipeline.title) { if (pipeline.title) {
bean.title = pipeline.title; bean.title = pipeline.title;
@ -277,7 +278,7 @@ export class PipelineService extends BaseService<PipelineEntity> {
throw new NeedSuiteException(`对不起,您最多只能添加${userSuite.domainCount.max}个域名,请购买或升级套餐`); throw new NeedSuiteException(`对不起,您最多只能添加${userSuite.domainCount.max}个域名,请购买或升级套餐`);
} }
} }
}else{ } else {
//非商业版校验用户最大流水线数量 //非商业版校验用户最大流水线数量
const userId = bean.userId; const userId = bean.userId;
const userIsAdmin = await this.userService.isAdmin(userId); const userIsAdmin = await this.userService.isAdmin(userId);
@ -296,12 +297,12 @@ export class PipelineService extends BaseService<PipelineEntity> {
async foreachPipeline(callback: (pipeline: PipelineEntity) => void) { async foreachPipeline(callback: (pipeline: PipelineEntity) => void) {
const idEntityList = await this.repository.find({ const idEntityList = await this.repository.find({
select: { select: {
id: true, id: true
}, },
where: { where: {
disabled: false, disabled: false,
templateId: 0, templateId: 0
}, }
}); });
const ids = idEntityList.map(item => { const ids = idEntityList.map(item => {
return item.id; return item.id;
@ -321,7 +322,7 @@ export class PipelineService extends BaseService<PipelineEntity> {
//分段加载记录 //分段加载记录
for (const idArr of idsSpan) { for (const idArr of idsSpan) {
const list = await this.repository.findBy({ const list = await this.repository.findBy({
id: In(idArr), id: In(idArr)
}); });
for (const entity of list) { for (const entity of list) {
@ -346,14 +347,14 @@ export class PipelineService extends BaseService<PipelineEntity> {
if (onlyAdminUser && entity.userId !== 1) { if (onlyAdminUser && entity.userId !== 1) {
return; return;
} }
const pipeline = JSON.parse(entity.content ?? '{}'); const pipeline = JSON.parse(entity.content ?? "{}");
try { try {
await this.registerTriggers(pipeline, immediateTriggerOnce); await this.registerTriggers(pipeline, immediateTriggerOnce);
} catch (e) { } 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) { async registerTriggers(pipeline?: Pipeline, immediateTriggerOnce = false) {
@ -375,18 +376,18 @@ export class PipelineService extends BaseService<PipelineEntity> {
if (isComm()) { if (isComm()) {
await this.checkHasDeployCount(id, entity.userId); await this.checkHasDeployCount(id, entity.userId);
} }
await this.checkUserStatus(entity.userId) await this.checkUserStatus(entity.userId);
this.cron.register({ this.cron.register({
name: `pipeline.${id}.trigger.once`, name: `pipeline.${id}.trigger.once`,
cron: null, cron: null,
job: async () => { job: async () => {
logger.info('用户手动启动job'); logger.info("用户手动启动job");
try { try {
await this.doRun(entity, null, stepId); await this.doRun(entity, null, stepId);
} catch (e) { } catch (e) {
logger.error('手动job执行失败', e); logger.error("手动job执行失败", e);
} }
}, }
}); });
} }
@ -398,7 +399,7 @@ export class PipelineService extends BaseService<PipelineEntity> {
logger.error(e.message); logger.error(e.message);
await this.update({ await this.update({
id: pipelineId, id: pipelineId,
status: 'no_deploy_count', status: "no_deploy_count"
}); });
} }
throw e; throw e;
@ -406,7 +407,7 @@ export class PipelineService extends BaseService<PipelineEntity> {
} }
//@ts-ignore //@ts-ignore
async delete(id:any) { async delete(id: any) {
await this.clearTriggers(id); await this.clearTriggers(id);
//TODO 删除storage //TODO 删除storage
// const storage = new DbStorage(pipeline.userId, this.storageService); // const storage = new DbStorage(pipeline.userId, this.storageService);
@ -421,11 +422,11 @@ export class PipelineService extends BaseService<PipelineEntity> {
if (id == null) { if (id == null) {
return; return;
} }
let pipeline:PipelineEntity = null let pipeline: PipelineEntity = null;
if (typeof id === 'number') { if (typeof id === "number") {
pipeline = await this.info(id); pipeline = await this.info(id);
}else{ } else {
pipeline = id pipeline = id;
} }
if (!pipeline) { if (!pipeline) {
return; return;
@ -445,7 +446,7 @@ export class PipelineService extends BaseService<PipelineEntity> {
registerCron(pipelineId, trigger) { registerCron(pipelineId, trigger) {
if (pipelineId == null) { if (pipelineId == null) {
logger.warn('pipelineId为空无法注册定时任务'); logger.warn("pipelineId为空无法注册定时任务");
return; return;
} }
@ -454,11 +455,11 @@ export class PipelineService extends BaseService<PipelineEntity> {
return; return;
} }
cron = cron.trim(); cron = cron.trim();
if (cron.startsWith('* *')) { if (cron.startsWith("* *")) {
cron = cron.replace('* *', '0 0'); cron = cron.replace("* *", "0 0");
} }
if (cron.startsWith('*')) { if (cron.startsWith("*")) {
cron = cron.replace('*', '0'); cron = cron.replace("*", "0");
} }
const triggerId = trigger.id; const triggerId = trigger.id;
const name = this.buildCronKey(pipelineId, triggerId); const name = this.buildCronKey(pipelineId, triggerId);
@ -467,19 +468,19 @@ export class PipelineService extends BaseService<PipelineEntity> {
name, name,
cron, cron,
job: async () => { job: async () => {
logger.info('定时任务触发:', pipelineId, triggerId); logger.info("定时任务触发:", pipelineId, triggerId);
if (pipelineId == null) { if (pipelineId == null) {
logger.warn('pipelineId为空,无法执行'); logger.warn("pipelineId为空,无法执行");
return; return;
} }
try { try {
await this.run(pipelineId, triggerId); await this.run(pipelineId, triggerId);
} catch (e) { } 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<PipelineEntity> {
if (isComm()) { if (isComm()) {
suite = await this.checkHasDeployCount(id, entity.userId); suite = await this.checkHasDeployCount(id, entity.userId);
} }
try{ try {
await this.checkUserStatus(entity.userId) await this.checkUserStatus(entity.userId);
}catch (e) { } catch (e) {
logger.info(e.message) logger.info(e.message);
return return;
} }
@ -521,7 +522,7 @@ export class PipelineService extends BaseService<PipelineEntity> {
return; return;
} }
if (triggerType === 'timer') { if (triggerType === "timer") {
if (entity.disabled) { if (entity.disabled) {
return; return;
} }
@ -530,25 +531,25 @@ export class PipelineService extends BaseService<PipelineEntity> {
const onChanged = async (history: RunHistory) => { const onChanged = async (history: RunHistory) => {
//保存执行历史 //保存执行历史
try { try {
logger.info('保存执行历史:', history.id); logger.info("保存执行历史:", history.id);
await this.saveHistory(history); await this.saveHistory(history);
} catch (e) { } catch (e) {
const pipelineEntity = new PipelineEntity(); const pipelineEntity = new PipelineEntity();
pipelineEntity.id = id; pipelineEntity.id = id;
pipelineEntity.status = 'error'; pipelineEntity.status = "error";
pipelineEntity.lastHistoryTime = history.pipeline.status.startTime; pipelineEntity.lastHistoryTime = history.pipeline.status.startTime;
await this.update(pipelineEntity); await this.update(pipelineEntity);
logger.error('保存执行历史失败:', e); logger.error("保存执行历史失败:", e);
throw e; throw e;
} }
}; };
const userId = entity.userId; 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 userIsAdmin = await this.userService.isAdmin(userId);
const user: UserInfo = { const user: UserInfo = {
id: userId, id: userId,
role: userIsAdmin ? 'admin' : 'user', role: userIsAdmin ? "admin" : "user"
}; };
@ -559,11 +560,11 @@ export class PipelineService extends BaseService<PipelineEntity> {
} }
const taskServiceGetter = this.taskServiceBuilder.create({ const taskServiceGetter = this.taskServiceBuilder.create({
userId, userId
}) });
const accessGetter = await taskServiceGetter.get<IAccessService>("accessService") const accessGetter = await taskServiceGetter.get<IAccessService>("accessService");
const notificationGetter =await taskServiceGetter.get<INotificationService>("notificationService") const notificationGetter = await taskServiceGetter.get<INotificationService>("notificationService");
const cnameProxyService =await taskServiceGetter.get<ICnameProxyService>("cnameProxyService") const cnameProxyService = await taskServiceGetter.get<ICnameProxyService>("cnameProxyService");
const executor = new Executor({ const executor = new Executor({
user, user,
pipeline, pipeline,
@ -577,7 +578,7 @@ export class PipelineService extends BaseService<PipelineEntity> {
notificationService: notificationGetter, notificationService: notificationGetter,
fileRootDir: this.certdConfig.fileRootDir, fileRootDir: this.certdConfig.fileRootDir,
sysInfo, sysInfo,
serviceGetter:taskServiceGetter serviceGetter: taskServiceGetter
}); });
try { try {
runningTasks.set(historyId, executor); runningTasks.set(historyId, executor);
@ -595,7 +596,7 @@ export class PipelineService extends BaseService<PipelineEntity> {
} }
} }
} catch (e) { } catch (e) {
logger.error('执行失败:', e); logger.error("执行失败:", e);
// throw e; // throw e;
} finally { } finally {
runningTasks.delete(historyId); runningTasks.delete(historyId);
@ -619,7 +620,7 @@ export class PipelineService extends BaseService<PipelineEntity> {
} }
private getTriggerType(triggerId, pipeline) { private getTriggerType(triggerId, pipeline) {
let triggerType = 'user'; let triggerType = "user";
if (triggerId != null) { if (triggerId != null) {
//如果不是手动触发 //如果不是手动触发
//查找trigger //查找trigger
@ -629,8 +630,8 @@ export class PipelineService extends BaseService<PipelineEntity> {
this.cron.remove(this.buildCronKey(pipeline.id, triggerId)); this.cron.remove(this.buildCronKey(pipeline.id, triggerId));
triggerType = null; triggerType = null;
} else { } else {
logger.info('timer trigger:' + found.id, found.title, found.cron); logger.info("timer trigger:" + found.id, found.title, found.cron);
triggerType = 'timer'; triggerType = "timer";
} }
} }
return triggerType; return triggerType;
@ -653,7 +654,7 @@ export class PipelineService extends BaseService<PipelineEntity> {
//修改pipeline状态 //修改pipeline状态
const pipelineEntity = new PipelineEntity(); const pipelineEntity = new PipelineEntity();
pipelineEntity.id = parseInt(history.pipeline.id); pipelineEntity.id = parseInt(history.pipeline.id);
pipelineEntity.status = history.pipeline.status.result + ''; pipelineEntity.status = history.pipeline.status.result + "";
pipelineEntity.lastHistoryTime = history.pipeline.status.startTime; pipelineEntity.lastHistoryTime = history.pipeline.status.startTime;
await this.update(pipelineEntity); await this.update(pipelineEntity);
@ -677,8 +678,8 @@ export class PipelineService extends BaseService<PipelineEntity> {
async count(param: { userId?: any }) { async count(param: { userId?: any }) {
const count = await this.repository.count({ const count = await this.repository.count({
where: { where: {
userId: param.userId, userId: param.userId
}, }
}); });
return count; return count;
} }
@ -686,12 +687,12 @@ export class PipelineService extends BaseService<PipelineEntity> {
async statusCount(param: { userId?: any } = {}) { async statusCount(param: { userId?: any } = {}) {
const statusCount = await this.repository const statusCount = await this.repository
.createQueryBuilder() .createQueryBuilder()
.select('status') .select("status")
.addSelect('count(1)', 'count') .addSelect("count(1)", "count")
.where({ .where({
userId: param.userId, userId: param.userId
}) })
.groupBy('status') .groupBy("status")
.getRawMany(); .getRawMany();
return statusCount; return statusCount;
} }
@ -701,11 +702,11 @@ export class PipelineService extends BaseService<PipelineEntity> {
select: { select: {
id: true, id: true,
title: true, title: true,
status: true, status: true
}, },
where: { where: {
userId, userId
}, }
}); });
await this.fillLastVars(list); await this.fillLastVars(list);
list = list.filter(item => { list = list.filter(item => {
@ -719,16 +720,16 @@ export class PipelineService extends BaseService<PipelineEntity> {
} }
async createCountPerDay(param: { days: number } = { days: 7 }) { async createCountPerDay(param: { days: number } = { days: 7 }) {
const todayEnd = dayjs().endOf('day'); const todayEnd = dayjs().endOf("day");
const result = await this.getRepository() const result = await this.getRepository()
.createQueryBuilder('main') .createQueryBuilder("main")
.select(`${this.dbAdapter.date('main.createTime')} AS date`) // 将UNIX时间戳转换为日期 .select(`${this.dbAdapter.date("main.createTime")} AS date`) // 将UNIX时间戳转换为日期
.addSelect('COUNT(1) AS count') .addSelect("COUNT(1) AS count")
.where({ .where({
// 0点 // 0点
createTime: MoreThan(todayEnd.add(-param.days, 'day').toDate()), createTime: MoreThan(todayEnd.add(-param.days, "day").toDate())
}) })
.groupBy('date') .groupBy("date")
.getRawMany(); .getRawMany();
return result; return result;
@ -745,48 +746,48 @@ export class PipelineService extends BaseService<PipelineEntity> {
await this.repository.update( await this.repository.update(
{ {
id: In(ids), id: In(ids),
userId, userId
}, },
{ groupId } { groupId }
); );
} }
async batchUpdateTrigger(ids: number[], trigger: any, userId: any){ async batchUpdateTrigger(ids: number[], trigger: any, userId: any) {
const list = await this.find({ const list = await this.find({
where:{ where: {
id: In(ids), id: In(ids),
userId userId
} }
}) });
for (const item of list) { for (const item of list) {
const pipeline = JSON.parse(item.content); const pipeline = JSON.parse(item.content);
pipeline.triggers = [{ pipeline.triggers = [{
id: nanoid(), id: nanoid(),
title: '定时触发', title: "定时触发",
...trigger ...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({ const list = await this.find({
where:{ where: {
id: In(ids), id: In(ids),
userId userId
} }
}) });
for (const item of list) { for (const item of list) {
const pipeline = JSON.parse(item.content); const pipeline = JSON.parse(item.content);
pipeline.notifications = [{ pipeline.notifications = [{
id: nanoid(), id: nanoid(),
title: '通知', title: "通知",
/** /**
* type: NotificationType; * type: NotificationType;
* when: NotificationWhen[]; * when: NotificationWhen[];
@ -797,44 +798,44 @@ export class PipelineService extends BaseService<PipelineEntity> {
*/ */
type: "other", type: "other",
...notification ...notification
}] }];
await this.doUpdatePipelineJson(item,pipeline) await this.doUpdatePipelineJson(item, pipeline);
} }
} }
async batchRerun(ids: number[], userId: any) { async batchRerun(ids: number[], userId: any) {
if (!isPlus()){ if (!isPlus()) {
throw new NeedVIPException("此功能需要升级专业版") throw new NeedVIPException("此功能需要升级专业版");
} }
if (!userId || ids.length === 0) { if (!userId || ids.length === 0) {
return; return;
} }
const list = await this.repository.find({ const list = await this.repository.find({
select:{ select: {
id:true id: true
}, },
where:{ where: {
id: In(ids), id: In(ids),
userId 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条一批 //20条一批
const batchSize = 20; const batchSize = 20;
for (let i = 0; i < ids.length; i += batchSize) { for (let i = 0; i < ids.length; i += batchSize) {
const batchIds = ids.slice(i, i + batchSize); const batchIds = ids.slice(i, i + batchSize);
const batchPromises = batchIds.map(async (id)=>{ const batchPromises = batchIds.map(async (id) => {
await this.run(id,null,"ALL") await this.run(id, null, "ALL");
}); });
await Promise.all(batchPromises) await Promise.all(batchPromises);
} }
} }
@ -847,35 +848,130 @@ export class PipelineService extends BaseService<PipelineEntity> {
return await this.repository.find({ return await this.repository.find({
select: { select: {
id: true, id: true,
title: true, title: true
}, },
where: { where: {
id: In(pipelineIds), id: In(pipelineIds),
userId, userId
}, }
}); });
} }
private async checkUserStatus(userId: number) { private async checkUserStatus(userId: number) {
const userEntity = await this.userService.info(userId); const userEntity = await this.userService.info(userId);
if(userEntity == null){ if (userEntity == null) {
throw new Error('用户不存在'); throw new Error("用户不存在");
} }
if(userEntity.status === 0){ if (userEntity.status === 0) {
const message = `账户${userId}已被禁用,禁止运行流水线` const message = `账户${userId}已被禁用,禁止运行流水线`;
throw new Error(message) throw new Error(message);
} }
const sysPublic = await this.sysSettingsService.getPublicSettings() const sysPublic = await this.sysSettingsService.getPublicSettings();
if(isPlus() && sysPublic.userValidTimeEnabled === true){ if (isPlus() && sysPublic.userValidTimeEnabled === true) {
//校验用户有效期是否设置 //校验用户有效期是否设置
if(userEntity.validTime!= null && userEntity.validTime > 0){ if (userEntity.validTime != null && userEntity.validTime > 0) {
if(userEntity.validTime < new Date().getTime()){ if (userEntity.validTime < new Date().getTime()) {
//用户已过期 //用户已过期
const message = `账户${userId}已过有效期,禁止运行流水线` const message = `账户${userId}已过有效期,禁止运行流水线`;
throw new Error(message) 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;
}
} }