perf: 支持域名到期时间监控通知

This commit is contained in:
xiaojunnuo
2026-04-05 23:49:25 +08:00
parent 6b29972399
commit c6628e7311
22 changed files with 637 additions and 77 deletions
@@ -0,0 +1,22 @@
CREATE TABLE "cd_job_history"
(
"id" integer PRIMARY KEY AUTOINCREMENT NOT NULL,
"user_id" integer NOT NULL,
"project_id" integer NOT NULL,
"type" varchar(100) NOT NULL,
"title" varchar(512) NOT NULL,
"related_id" varchar(100),
"result" varchar(100) NOT NULL,
"content" text ,
"start_at" integer NOT NULL,
"end_at" integer ,
"create_time" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP),
"update_time" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP)
);
CREATE INDEX "index_job_history_user_id" ON "cd_job_history" ("user_id");
CREATE INDEX "index_job_history_project_id" ON "cd_job_history" ("project_id");
CREATE INDEX "index_job_history_type" ON "cd_job_history" ("type");
+1
View File
@@ -141,6 +141,7 @@
"typeorm": "^0.3.20",
"uuid": "^10.0.0",
"wechatpay-node-v3": "^2.2.1",
"whoiser": "2.0.0-beta.10",
"xml2js": "^0.6.2"
},
"devDependencies": {
@@ -20,7 +20,7 @@ export class DomainController extends CrudController<DomainService> {
@Post('/page', { description: Constants.per.authOnly, summary: "查询域名分页列表" })
async page(@Body(ALL) body: any) {
const {projectId,userId} = await this.getProjectUserIdRead();
const { projectId, userId } = await this.getProjectUserIdRead();
body.query = body.query ?? {};
body.query.projectId = projectId;
body.query.userId = userId;
@@ -44,7 +44,7 @@ export class DomainController extends CrudController<DomainService> {
@Post('/list', { description: Constants.per.authOnly, summary: "查询域名列表" })
async list(@Body(ALL) body: any) {
const {projectId,userId} = await this.getProjectUserIdRead();
const { projectId, userId } = await this.getProjectUserIdRead();
body.query = body.query ?? {};
body.query.projectId = projectId;
body.query.userId = userId;
@@ -54,7 +54,7 @@ export class DomainController extends CrudController<DomainService> {
@Post('/add', { description: Constants.per.authOnly, summary: "添加域名" })
async add(@Body(ALL) bean: any) {
const {projectId,userId} = await this.getProjectUserIdRead();
const { projectId, userId } = await this.getProjectUserIdRead();
bean.projectId = projectId;
bean.userId = userId;
return super.add(bean);
@@ -82,7 +82,7 @@ export class DomainController extends CrudController<DomainService> {
@Post('/deleteByIds', { description: Constants.per.authOnly, summary: "批量删除域名" })
async deleteByIds(@Body(ALL) body: any) {
const {projectId,userId} = await this.getProjectUserIdRead();
const { projectId, userId } = await this.getProjectUserIdRead();
await this.service.delete(body.ids, {
userId: userId,
projectId: projectId,
@@ -94,10 +94,10 @@ export class DomainController extends CrudController<DomainService> {
@Post('/import/start', { description: Constants.per.authOnly, summary: "开始域名导入任务" })
async importStart(@Body(ALL) body: any) {
checkPlus();
const {projectId,userId} = await this.getProjectUserIdRead();
const { projectId, userId } = await this.getProjectUserIdRead();
const { key } = body;
const req = {
key,
key,
userId: userId,
projectId: projectId,
}
@@ -107,7 +107,7 @@ export class DomainController extends CrudController<DomainService> {
@Post('/import/status', { description: Constants.per.authOnly, summary: "查询域名导入任务状态" })
async importStatus() {
const {projectId,userId} = await this.getProjectUserIdRead();
const { projectId, userId } = await this.getProjectUserIdRead();
const req = {
userId: userId,
projectId: projectId,
@@ -119,7 +119,7 @@ export class DomainController extends CrudController<DomainService> {
@Post('/import/delete', { description: Constants.per.authOnly, summary: "删除域名导入任务" })
async importDelete(@Body(ALL) body: any) {
const {projectId,userId} = await this.getProjectUserIdRead();
const { projectId, userId } = await this.getProjectUserIdRead();
const { key } = body;
const req = {
userId: userId,
@@ -133,12 +133,12 @@ export class DomainController extends CrudController<DomainService> {
@Post('/import/save', { description: Constants.per.authOnly, summary: "保存域名导入任务" })
async importSave(@Body(ALL) body: any) {
checkPlus();
const {projectId,userId} = await this.getProjectUserIdRead();
const { projectId, userId } = await this.getProjectUserIdRead();
const { dnsProviderType, dnsProviderAccessId, key } = body;
const req = {
userId: userId,
projectId: projectId,
dnsProviderType, dnsProviderAccessId, key
dnsProviderType, dnsProviderAccessId, key
}
const item = await this.service.saveDomainImportTask(req);
return this.ok(item);
@@ -147,7 +147,7 @@ export class DomainController extends CrudController<DomainService> {
@Post('/sync/expiration/start', { description: Constants.per.authOnly, summary: "开始同步域名过期时间任务" })
async syncExpirationStart(@Body(ALL) body: any) {
const {projectId,userId} = await this.getProjectUserIdRead();
const { projectId, userId } = await this.getProjectUserIdRead();
await this.service.startSyncExpirationTask({
userId: userId,
projectId: projectId,
@@ -156,7 +156,7 @@ export class DomainController extends CrudController<DomainService> {
}
@Post('/sync/expiration/status', { description: Constants.per.authOnly, summary: "查询同步域名过期时间任务状态" })
async syncExpirationStatus(@Body(ALL) body: any) {
const {projectId,userId} = await this.getProjectUserIdRead();
const { projectId, userId } = await this.getProjectUserIdRead();
const status = await this.service.getSyncExpirationTaskStatus({
userId: userId,
projectId: projectId,
@@ -165,4 +165,26 @@ export class DomainController extends CrudController<DomainService> {
}
@Post('/setting/save', { description: Constants.per.authOnly, summary: "保存域名监控设置" })
async settingSave(@Body(ALL) body: any) {
const { projectId, userId } = await this.getProjectUserIdWrite();
await this.service.monitorSettingSave({
userId: userId,
projectId: projectId,
setting: {...body},
})
return this.ok();
}
@Post('/setting/get', { description: Constants.per.authOnly, summary: "查询域名监控设置" })
async settingGet() {
const { projectId, userId } = await this.getProjectUserIdRead();
const setting = await this.service.monitorSettingGet({
userId: userId,
projectId: projectId,
})
return this.ok(setting);
}
}
@@ -0,0 +1,62 @@
import { Constants, CrudController } from "@certd/lib-server";
import { ALL, Body, Controller, Inject, Post, Provide, Query } from "@midwayjs/core";
import { ApiTags } from "@midwayjs/swagger";
import { SiteInfoService } from "../../../modules/monitor/index.js";
import { JobHistoryService } from "../../../modules/monitor/service/job-history-service.js";
import { AuthService } from "../../../modules/sys/authority/service/auth-service.js";
/**
*/
@Provide()
@Controller('/api/monitor/job-history')
@ApiTags(['monitor'])
export class JobHistoryController extends CrudController<JobHistoryService> {
@Inject()
service: JobHistoryService;
@Inject()
authService: AuthService;
@Inject()
siteInfoService: SiteInfoService;
getService(): JobHistoryService {
return this.service;
}
@Post('/page', { description: Constants.per.authOnly, summary: "查询监控运行历史分页列表" })
async page(@Body(ALL) body: any) {
const { projectId, userId } = await this.getProjectUserIdRead()
body.query = body.query ?? {};
body.query.userId = userId;
body.query.projectId = projectId
const res = await this.service.page({
query: body.query,
page: body.page,
sort: body.sort,
});
return this.ok(res);
}
@Post('/list', { description: Constants.per.authOnly, summary: "查询监控运行历史列表" })
async list(@Body(ALL) body: any) {
body.query = body.query ?? {};
const { projectId, userId } = await this.getProjectUserIdRead()
body.query.userId = userId;
body.query.projectId = projectId
return await super.list(body);
}
@Post('/info', { description: Constants.per.authOnly, summary: "查询监控运行历史详情" })
async info(@Query('id') id: number) {
await this.checkOwner(this.service,id,"read");
return await super.info(id);
}
@Post('/delete', { description: Constants.per.authOnly, summary: "删除监控运行历史" })
async delete(@Query('id') id: number) {
await this.checkOwner(this.service,id,"write");
const res = await super.delete(id);
return res
}
}
@@ -67,6 +67,7 @@ export class AutoCRegisterCron {
await this.registerUserExpireCheckCron();
await this.registerDomainExpireCheckCron();
}
async registerSiteMonitorCron() {
@@ -211,11 +212,11 @@ export class AutoCRegisterCron {
if (!isPlus()){
return
}
// 添加域名即将到期检查任务
// 添加域名即将到期同步任务
const randomWeek = Math.floor(Math.random() * 7) + 1
const randomHour = Math.floor(Math.random() * 24)
const randomMinute = Math.floor(Math.random() * 60)
logger.info(`注册域名注册过期时间检查任务,每周${randomWeek} ${randomHour}:${randomMinute}检查一次`)
logger.info(`注册域名注册过期时间同步任务,每周${randomWeek} ${randomHour}:${randomMinute}检查一次`)
this.cron.register({
name: 'domain-expire-check',
cron: `0 ${randomMinute} ${randomHour} ? * ${randomWeek}`, // 每周随机一天检查一次
@@ -1,20 +1,23 @@
import { http, logger, utils } from '@certd/basic';
import { AccessService, BaseService } from '@certd/lib-server';
import { AccessService, BaseService, isEnterprise } from '@certd/lib-server';
import { doPageTurn, Pager, PageRes } from '@certd/pipeline';
import { DomainVerifiers } from "@certd/plugin-cert";
import { createDnsProvider, dnsProviderRegistry, DomainParser, parseDomainByPsl } from "@certd/plugin-lib";
import { Inject, Provide, Scope, ScopeEnum } from '@midwayjs/core';
import { InjectEntityModel } from '@midwayjs/typeorm';
import dayjs from 'dayjs';
import { In, Not, Repository } from 'typeorm';
import { merge } from 'lodash-es';
import { In, LessThan, Not, Repository } from 'typeorm';
import { BackTask, taskExecutor } from '../../basic/service/task-executor.js';
import { CnameRecordEntity } from "../../cname/entity/cname-record.js";
import { CnameRecordService } from '../../cname/service/cname-record-service.js';
import { UserDomainImportSetting } from '../../mine/service/models.js';
import { UserDomainImportSetting, UserDomainMonitorSetting } from '../../mine/service/models.js';
import { UserSettingsService } from '../../mine/service/user-settings-service.js';
import { JobHistoryService } from '../../monitor/service/job-history-service.js';
import { TaskServiceBuilder } from '../../pipeline/service/getter/task-service-getter.js';
import { SubDomainService } from "../../pipeline/service/sub-domain-service.js";
import { DomainEntity } from '../entity/domain.js';
import { Cron } from '../../cron/cron.js';
export interface SyncFromProviderReq {
userId: number;
@@ -27,6 +30,8 @@ export interface SyncFromProviderReq {
const DOMAIN_IMPORT_TASK_TYPE = 'domainImportTask'
const DOMAIN_EXPIRE_TASK_TYPE = 'domainExpirationSyncTask'
const DOMAIN_EXPIRE_CHECK_TYPE = 'domainExpirationCheck'
/**
*
@@ -51,6 +56,14 @@ export class DomainService extends BaseService<DomainEntity> {
@Inject()
userSettingService: UserSettingsService;
@Inject()
jobHistoryService: JobHistoryService;
@Inject()
cron: Cron;
//@ts-ignore
getRepository() {
return this.repository;
@@ -320,9 +333,9 @@ export class DomainService extends BaseService<DomainEntity> {
logger.info(`从域名提供商${dnsProviderType}导入域名完成(${key}),共导入${task.total}个域名,跳过${task.getSkipCount()}个域名,成功${task.getSuccessCount()}个域名,失败${task.getErrorCount()}个域名`)
}
async getDomainImportTaskStatus(req: { userId?: number ,projectId?: number}) {
async getDomainImportTaskStatus(req: { userId?: number, projectId?: number }) {
const userId = req.userId || 0
const projectId = req.projectId
const projectId = req.projectId
const setting = await this.userSettingService.getSetting<UserDomainImportSetting>(userId, projectId, UserDomainImportSetting)
const list = setting?.domainImportList || []
@@ -429,8 +442,6 @@ export class DomainService extends BaseService<DomainEntity> {
await this.deleteDomainImportTask({ userId, projectId, key })
}
return await this.addDomainImportTask({ userId, projectId, dnsProviderType, dnsProviderAccessId, index })
}
@@ -441,7 +452,7 @@ export class DomainService extends BaseService<DomainEntity> {
const userId = req.userId ?? 'all'
const projectId = req.projectId
let key = `user_${userId}`
if (projectId!=null) {
if (projectId != null) {
key += `_${projectId}`
}
const task = taskExecutor.get(DOMAIN_EXPIRE_TASK_TYPE, key)
@@ -452,7 +463,7 @@ export class DomainService extends BaseService<DomainEntity> {
const userId = req.userId
const projectId = req.projectId
let key = `user_${userId ?? 'all'}`
if (projectId!=null) {
if (projectId != null) {
key += `_${projectId}`
}
taskExecutor.start(new BackTask({
@@ -461,11 +472,15 @@ export class DomainService extends BaseService<DomainEntity> {
title: `同步注册域名过期时间(${key}))`,
run: async (task: BackTask) => {
await this._syncDomainsExpirationDate({ userId, projectId, task })
if (userId != null) {
await this.startCheckDomainExpiration({ userId, projectId })
}
}
}))
}
private async _syncDomainsExpirationDate(req: { userId?: number, projectId?: number, task: BackTask }) {
//同步所有域名的过期时间
const pager = new Pager({
pageNo: 1,
@@ -573,7 +588,138 @@ export class DomainService extends BaseService<DomainEntity> {
await doPageTurn({ pager, getPage: getDomainPage, itemHandle: itemHandle })
const key = `user_${req.userId || 'all'}`
logger.info(`同步用户(${key})注册域名过期时间完成(${req.task.getSuccessCount()}个成功,${req.task.getErrorCount()}个失败)`)
const log = `同步用户(${key})注册域名过期时间完成(${req.task.getSuccessCount()}个成功,${req.task.getErrorCount()}个失败)`
logger.info(log)
}
public async startCheckDomainExpiration(req: { userId?: number, projectId?: number }) {
const { userId, projectId } = req
if (userId == null) {
throw new Error('userId is required');
}
if (projectId && !isEnterprise()) {
logger.warn(`当前未开启企业模式,跳过检查项目(${projectId})的域名过期时间`)
return
}
const setting = await this.monitorSettingGet({ userId, projectId })
if (!setting || !setting.enabled) {
return
}
const jobHistory: any = {
userId,
projectId,
type: DOMAIN_EXPIRE_CHECK_TYPE,
title: `检查注册域名过期时间`,
startAt: dayjs().valueOf(),
result: "start",
}
await this.jobHistoryService.add(jobHistory)
const expireDays = setting.willExpireDays || 30
const ltTime = dayjs().add(expireDays, 'day').valueOf()
const total = await this.repository.count({
where:{
userId,
projectId,
disabled: false,
}
})
//开始检查域名过期时间
const list = await this.repository.find({
where: {
userId,
projectId,
disabled: false,
expirationDate: LessThan(ltTime)
}
})
const now = dayjs().valueOf()
let willExpireDomains = []
let hasExpireDomains = []
for (const item of list) {
const { expirationDate } = item
if (expirationDate < now) {
hasExpireDomains.push(item.domain)
} else {
willExpireDomains.push(item.domain)
}
}
const title = `域名过期检查:共${total}个域名,即将过期${willExpireDomains.length}个域名,已过期${hasExpireDomains.length}个域名`
try {
await this.jobHistoryService.update({
id: jobHistory.id,
content: title,
result: "done",
endAt: dayjs().valueOf(),
})
} catch (error) {
logger.error(`更新域名过期检查任务状态失败:${error.message ?? error}`)
}
if (list.length == 0) {
//没有过期域名 不发通知
return
}
//发送通知
const content = `即将过期域名【${willExpireDomains.length}】:${willExpireDomains.join('')}
\n已过期域名【${hasExpireDomains.length}】:${hasExpireDomains.join('')}`
const taskService = this.taskServiceBuilder.create({ userId: userId, projectId: projectId });
const notificationService = await taskService.getNotificationService()
const url = await notificationService.getBindUrl("#/certd/cert/domain");
await notificationService.send({
id: setting.notificationId,
useDefault: true,
logger: logger,
body: {
title: title,
content: content,
url: url,
notificationType: DOMAIN_EXPIRE_CHECK_TYPE
}
})
}
public async monitorSettingGet(req: { userId?: number, projectId?: number }) {
const { userId, projectId } = req
const setting = await this.userSettingService.getSetting<UserDomainMonitorSetting>(userId, projectId, UserDomainMonitorSetting)
return setting || {}
}
public async monitorSettingSave(req: { userId?: number, projectId?: number, setting?: any }) {
const { userId, projectId, setting } = req
const bean: UserDomainMonitorSetting = new UserDomainMonitorSetting()
merge(bean, setting)
await this.userSettingService.saveSetting<UserDomainMonitorSetting>(userId, projectId, bean)
await this.registerMonitorCron({ userId, projectId })
}
public async registerMonitorCron(req: { userId?: number, projectId?: number }) {
const { userId, projectId } = req
const setting = await this.monitorSettingGet(req)
const key = `${DOMAIN_EXPIRE_CHECK_TYPE}:${userId}_${projectId || ''}`
this.cron.remove(key)
if (setting.enabled) {
this.cron.register({
cron: setting.cron,
name: key,
job: async () => {
await this.startCheckDomainExpiration({ userId, projectId })
},
})
}
}
}
@@ -1,6 +1,5 @@
import { Config, Configuration, Logger } from '@midwayjs/core';
import { ILogger } from '@midwayjs/logger';
import { IMidwayContainer } from '@midwayjs/core';
import { logger } from '@certd/basic';
import { Config, Configuration, IMidwayContainer } from '@midwayjs/core';
import { Cron } from './cron.js';
// ... (see below) ...
@@ -11,18 +10,15 @@ import { Cron } from './cron.js';
export class CronConfiguration {
@Config()
config;
@Logger()
logger: ILogger;
cron: Cron;
async onReady(container: IMidwayContainer) {
this.logger.info('cron start');
logger.info('cron start');
this.cron = new Cron({
logger: this.logger,
logger: logger,
...this.config,
});
container.registerObject('cron', this.cron);
this.cron.start();
this.logger.info('cron started');
logger.info('cron started');
}
}
@@ -31,6 +31,18 @@ export class UserSiteMonitorSetting extends BaseSettings {
certValidDays?:number = 14;
}
export class UserDomainMonitorSetting extends BaseSettings {
static __title__ = "域名到期监控设置";
static __key__ = "user.domain.monitor";
enabled?:boolean = false;
notificationId?:number= 0;
cron?:string = undefined;
willExpireDays?:number = 30;
}
export class UserEmailSetting extends BaseSettings {
static __title__ = "用户邮箱设置";
static __key__ = "user.email";
@@ -0,0 +1,52 @@
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
import { PipelineEntity } from '../../pipeline/entity/pipeline.js';
@Entity('cd_job_history')
export class JobHistoryEntity {
@PrimaryGeneratedColumn()
id: number;
@Column({ name: 'user_id', comment: '用户id' })
userId: number;
@Column({ name: 'project_id', comment: '项目id' })
projectId: number;
@Column({ name: 'type', comment: '类型' })
type: string;
@Column({ name: 'title', comment: '标题' })
title: string;
@Column({ name: 'content', comment: '内容' })
content: string;
@Column({ name: 'related_id', comment: '关联id' })
relatedId: string;
@Column({ name: 'result', comment: '结果' })
result: string;
@Column({ name: 'start_at', comment: '开始时间' })
startAt: number;
@Column({ name: 'end_at', comment: '结束时间' })
endAt: number;
@Column({
name: 'create_time',
comment: '创建时间',
default: () => 'CURRENT_TIMESTAMP',
})
createTime: Date;
@Column({
name: 'update_time',
comment: '修改时间',
default: () => 'CURRENT_TIMESTAMP',
})
updateTime: Date;
pipeline?: PipelineEntity;
}
@@ -0,0 +1,23 @@
import { BaseService } from "@certd/lib-server";
import { Inject, Provide, Scope, ScopeEnum } from "@midwayjs/core";
import { InjectEntityModel } from "@midwayjs/typeorm";
import { Repository } from "typeorm";
import { UserSettingsService } from "../../mine/service/user-settings-service.js";
import { JobHistoryEntity } from "../entity/job-history.js";
@Provide()
@Scope(ScopeEnum.Request, { allowDowngrade: true })
export class JobHistoryService extends BaseService<JobHistoryEntity> {
@InjectEntityModel(JobHistoryEntity)
repository: Repository<JobHistoryEntity>;
@Inject()
userSettingsService: UserSettingsService;
//@ts-ignore
getRepository() {
return this.repository;
}
}
@@ -17,6 +17,8 @@ import {SiteIpEntity} from "../entity/site-ip.js";
import {Cron} from "../../cron/cron.js";
import { dnsContainer } from "./dns-custom.js";
import { merge } from "lodash-es";
import { JobHistoryService } from "./job-history-service.js";
import { JobHistoryEntity } from "../entity/job-history.js";
@Provide()
@Scope(ScopeEnum.Request, {allowDowngrade: true})
@@ -39,6 +41,9 @@ export class SiteInfoService extends BaseService<SiteInfoEntity> {
@Inject()
siteIpService: SiteIpService;
@Inject()
jobHistoryService: JobHistoryService;
@Inject()
cron: Cron;
@@ -516,6 +521,7 @@ export class SiteInfoService extends BaseService<SiteInfoEntity> {
async triggerJobOnce(userId?:number,projectId?:number) {
logger.info(`站点证书检查开始执行[${userId??'所有用户'}_${projectId??'所有项目'}]`);
const query:any = { disabled: false };
let jobEntity :Partial<JobHistoryEntity> = null;
if(userId!=null){
query.userId = userId;
if(projectId){
@@ -526,9 +532,19 @@ export class SiteInfoService extends BaseService<SiteInfoEntity> {
if (!setting.cron) {
return;
}
jobEntity = {
userId,
projectId,
type:"siteCertMonitor",
title: '站点证书检查',
result:"start",
startAt:new Date().getTime(),
}
await this.jobHistoryService.add(jobEntity);
}
let offset = 0;
const limit = 50;
let count = 0;
while (true) {
const res = await this.page({
query: query,
@@ -541,10 +557,20 @@ export class SiteInfoService extends BaseService<SiteInfoEntity> {
}
offset += records.length;
const isCommon = !userId;
count += records.length;
await this.checkList(records,isCommon);
}
logger.info(`站点证书检查完成[${userId??'所有用户'}_${projectId??'所有项目'}]`);
if(jobEntity){
await this.jobHistoryService.update({
id: jobEntity.id,
result: "done",
content:`共检查${count}个站点`,
endAt:new Date().getTime(),
updateTime:new Date(),
});
}
}
async batchDelete(ids: number[], userId: number,projectId?:number): Promise<void> {
@@ -23,4 +23,8 @@ export class NotificationGetter implements INotificationService {
async send(req: NotificationSendReq): Promise<void> {
return await this.notificationService.send(req, this.userId, this.projectId);
}
async getBindUrl(url: string) {
return await this.notificationService.getBindUrl(url);
}
}