perf: 证书到期剩余天数进度条根据实际证书有效期计算 (#528) nicheng-he

* Create FUNDING.yml

* Update FUNDING.yml

* Update README.md

* Update README.md

* Update README.md

* Update README.md

* Update README_en.md

* 证书到期剩余天数进度条根据实际证书时间计算

---------

Co-authored-by: greper <xiaojunnuo@qq.com>
v2
ahe 2025-09-13 23:40:06 +08:00 committed by GitHub
parent 1476b9cb9c
commit 2d4586b1c4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 144 additions and 20 deletions

5
.github/FUNDING.yml vendored Normal file
View File

@ -0,0 +1,5 @@
# These are supported funding model platforms
github: greper
buy_me_a_coffee: greper
custom: ['https://afdian.com/a/greper']

View File

@ -171,6 +171,7 @@ https://afdian.com/a/greper
| 自动部署插件 | 阿里云CDN、腾讯云、七牛CDN、主机部署、宝塔、1Panel等大部分插件 | 群晖 | | 自动部署插件 | 阿里云CDN、腾讯云、七牛CDN、主机部署、宝塔、1Panel等大部分插件 | 群晖 |
| 通知 | 邮件通知、自定义webhook | 邮件免配置、企微、钉钉、飞书、anpush、server酱等 | | 通知 | 邮件通知、自定义webhook | 邮件免配置、企微、钉钉、飞书、anpush、server酱等 |
************************
************************ ************************

View File

@ -134,6 +134,8 @@ You can also add the author as a friend.
| QR Code | <img height="230" src="./docs/guide/contact/images/me.png"> | | QR Code | <img height="230" src="./docs/guide/contact/images/me.png"> |
## 8. Donation ## 8. Donation
************************
[![Sponsor](https://img.shields.io/badge/Sponsor-%E2%9D%A4-red)](https://github.com/sponsors/greper)
************************ ************************
Support open-source projects and contribute with love. I've joined Afdian. Support open-source projects and contribute with love. I've joined Afdian.
https://afdian.com/a/greper https://afdian.com/a/greper

View File

@ -99,6 +99,7 @@ export abstract class CertApplyBaseConvertPlugin extends AbstractTaskPlugin {
const cert: CertInfo = certReader.toCertInfo(); const cert: CertInfo = certReader.toCertInfo();
this.cert = cert; this.cert = cert;
this._result.pipelineVars.certEffectiveTime = dayjs(certReader.detail.notBefore).valueOf();
this._result.pipelineVars.certExpiresTime = dayjs(certReader.detail.notAfter).valueOf(); this._result.pipelineVars.certExpiresTime = dayjs(certReader.detail.notAfter).valueOf();
if (!this._result.pipelinePrivateVars) { if (!this._result.pipelinePrivateVars) {
this._result.pipelinePrivateVars = {}; this._result.pipelinePrivateVars = {};

View File

@ -35,6 +35,7 @@ export class CertReader {
detail: CertificateInfo; detail: CertificateInfo;
//毫秒时间戳 //毫秒时间戳
effective: number;
expires: number; expires: number;
constructor(certInfo: CertInfo) { constructor(certInfo: CertInfo) {
this.cert = certInfo; this.cert = certInfo;
@ -52,8 +53,9 @@ export class CertReader {
} }
try { try {
const { detail, expires } = this.getCrtDetail(this.cert.crt); const { detail, effective, expires } = this.getCrtDetail(this.cert.crt);
this.detail = detail; this.detail = detail;
this.effective = effective.getTime();
this.expires = expires.getTime(); this.expires = expires.getTime();
} catch (e) { } catch (e) {
throw new Error("证书解析失败:" + e.message); throw new Error("证书解析失败:" + e.message);
@ -102,8 +104,9 @@ export class CertReader {
static readCertDetail(crt: string) { static readCertDetail(crt: string) {
const detail = crypto.readCertificateInfo(crt.toString()); const detail = crypto.readCertificateInfo(crt.toString());
const effective = detail.notBefore;
const expires = detail.notAfter; const expires = detail.notAfter;
return { detail, expires }; return { detail, effective, expires };
} }
getAllDomains() { getAllDomains() {

View File

@ -119,6 +119,7 @@ export default {
scheduledTaskCount: "Scheduled Task Count", scheduledTaskCount: "Scheduled Task Count",
deployTaskCount: "Deployment Task Count", deployTaskCount: "Deployment Task Count",
remainingValidity: "Remaining Validity", remainingValidity: "Remaining Validity",
effectiveTime: "Effective time",
expiryTime: "Expiry Time", expiryTime: "Expiry Time",
status: "Status", status: "Status",
lastRun: "Last Run", lastRun: "Last Run",
@ -250,7 +251,9 @@ export default {
ok: "Valid", ok: "Valid",
expired: "Expired", expired: "Expired",
}, },
certEffectiveTime: "Certificate Effective",
certExpiresTime: "Certificate Expiration", certExpiresTime: "Certificate Expiration",
remainingValidity: "Remaining Validity",
expired: "expired", expired: "expired",
days: "days", days: "days",
lastCheckTime: "Last Check Time", lastCheckTime: "Last Check Time",
@ -465,6 +468,7 @@ export default {
validDays: "Valid Days", validDays: "Valid Days",
expires: " expires", expires: " expires",
days: " days", days: " days",
effectiveTime: "Effective Time",
expireTime: "Expiration Time", expireTime: "Expiration Time",
certIssuer: "Certificate Issuer", certIssuer: "Certificate Issuer",
applyTime: "Application Time", applyTime: "Application Time",

View File

@ -125,6 +125,7 @@ export default {
scheduledTaskCount: "定时任务数", scheduledTaskCount: "定时任务数",
deployTaskCount: "部署任务数", deployTaskCount: "部署任务数",
remainingValidity: "到期剩余", remainingValidity: "到期剩余",
effectiveTime: "生效时间",
expiryTime: "过期时间", expiryTime: "过期时间",
status: "状态", status: "状态",
lastRun: "最后运行", lastRun: "最后运行",
@ -255,7 +256,9 @@ export default {
ok: "正常", ok: "正常",
expired: "过期", expired: "过期",
}, },
certEffectiveTime: "证书生效时间",
certExpiresTime: "证书到期时间", certExpiresTime: "证书到期时间",
remainingValidity: "到期剩余",
expired: "过期", expired: "过期",
days: "天", days: "天",
lastCheckTime: "上次检查时间", lastCheckTime: "上次检查时间",
@ -471,6 +474,7 @@ export default {
validDays: "有效天数", validDays: "有效天数",
expires: "过期", expires: "过期",
days: "天", days: "天",
effectiveTime: "生效时间",
expireTime: "过期时间", expireTime: "过期时间",
certIssuer: "证书颁发机构", certIssuer: "证书颁发机构",
applyTime: "申请时间", applyTime: "申请时间",

View File

@ -220,22 +220,47 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
sorter: true, sorter: true,
conditionalRender: false, conditionalRender: false,
cellRender({ row }) { cellRender({ row }) {
const value = row.expiresTime; const {
if (!value) { applyTime,
effectiveTime,
expiresTime,
} = row || {};
if (!expiresTime) {
return "-"; return "-";
} }
const expireDate = dayjs(value).format("YYYY-MM-DD"); // 申请时间 ps:此处为证书在certd创建的时间而非实际证书申请时间
const leftDays = dayjs(value).diff(dayjs(), "day"); const applyDate = dayjs(effectiveTime ?? applyTime ?? Date.now()).format("YYYY-MM-DD");
// 失效时间
const expireDate = dayjs(expiresTime).format("YYYY-MM-DD");
// 有效天数 ps:此处证书最小设置为90d
const effectiveDays = Math.max(90, dayjs(expiresTime).diff(applyDate, "day"));
// 距离失效时间剩余天数
const leftDays = dayjs(expiresTime).diff(dayjs(), "day");
const color = leftDays < 20 ? "red" : "#389e0d"; const color = leftDays < 20 ? "red" : "#389e0d";
const percent = (leftDays / 90) * 100; const percent = (leftDays / effectiveDays) * 100;
const textColor = leftDays < 20 ? "red" : leftDays > 60 ? "#389e0d" : ""; const textColor = leftDays < 20 ? "red" : leftDays > 60 ? "#389e0d" : "";
const format = () => { const format = () => {
return <span style={{ color: textColor }}>{`${leftDays}${t("certd.days")}`}</span>; return <span style={{ color: textColor }}>{`${leftDays}${t("certd.days")}`}</span>;
}; };
// console.log('cellRender', 'effectiveDays', effectiveDays, 'expiresTime', expiresTime, 'applyTime', applyTime, 'percent', percent, row)
return <a-progress title={expireDate + t("certd.expires")} percent={percent} strokeColor={color} format={format} />; return <a-progress title={expireDate + t("certd.expires")} percent={percent} strokeColor={color} format={format} />;
}, },
}, },
}, },
effectiveTime: {
title: t("certd.effectiveTime"),
search: {
show: false,
},
type: "datetime",
form: {
show: false,
},
column: {
sorter: true,
show: false,
},
},
expiresTime: { expiresTime: {
title: t("certd.expireTime"), title: t("certd.expireTime"),
search: { search: {

View File

@ -345,25 +345,64 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
align: "center", align: "center",
}, },
}, },
certEffectiveTime: {
title: t("certd.monitor.certEffectiveTime"),
search: {
show: false,
},
type: "datetime",
form: {
show: false,
},
column: {
sorter: true,
width: 155,
},
},
certExpiresTime: { certExpiresTime: {
title: t("certd.monitor.certExpiresTime"), title: t("certd.monitor.certExpiresTime"),
search: { search: {
show: false, show: false,
}, },
type: "datetime",
form: {
show: false,
},
column: {
sorter: true,
width: 155,
},
},
remainingValidity: {
title: t("certd.monitor.remainingValidity"),
search: {
show: false,
},
type: "date", type: "date",
form: { form: {
show: false, show: false,
}, },
column: { column: {
sorter: true, conditionalRender: false,
cellRender({ value }) { cellRender({ row }) {
if (!value) { const {
certEffectiveTime: effectiveTime,
certExpiresTime: expiresTime,
} = row || {};
if (!expiresTime) {
return "-"; return "-";
} }
const expireDate = dayjs(value).format("YYYY-MM-DD"); // 申请时间 ps:此处为证书在certd创建的时间而非实际证书申请时间
const leftDays = dayjs(value).diff(dayjs(), "day"); const applyDate = dayjs(effectiveTime ?? Date.now()).format("YYYY-MM-DD");
// 失效时间
const expireDate = dayjs(expiresTime).format("YYYY-MM-DD");
// 有效天数 ps:此处证书最小设置为90d
const effectiveDays = Math.max(90, dayjs(expiresTime).diff(applyDate, "day"));
// 距离失效时间剩余天数
const leftDays = dayjs(expiresTime).diff(dayjs(), "day");
const color = leftDays < 20 ? "red" : "#389e0d"; const color = leftDays < 20 ? "red" : "#389e0d";
const percent = (leftDays / 90) * 100; const percent = (leftDays / effectiveDays) * 100;
// console.log('cellRender', 'effectiveDays', effectiveDays, 'expiresTime', expiresTime, 'applyTime', applyTime, 'percent', percent, row)
return <a-progress title={expireDate + t("certd.monitor.expired")} percent={percent} strokeColor={color} format={(percent: number) => `${leftDays}${t("certd.monitor.days")}`} />; return <a-progress title={expireDate + t("certd.monitor.expired")} percent={percent} strokeColor={color} format={(percent: number) => `${leftDays}${t("certd.monitor.days")}`} />;
}, },
}, },

View File

@ -366,23 +366,49 @@ export default function ({ crudExpose, context: { groupDictRef, selectedRowKeys
}, },
column: { column: {
cellRender({ row }) { cellRender({ row }) {
const value = row.lastVars?.certExpiresTime; const {
if (!value) { certEffectiveTime: effectiveTime,
certExpiresTime: expiresTime,
} = row?.lastVars || {};
if (!expiresTime) {
return "-"; return "-";
} }
const expireDate = dayjs(value).format("YYYY-MM-DD"); // 申请时间 ps:此处为证书在certd创建的时间而非实际证书申请时间
const leftDays = dayjs(value).diff(dayjs(), "day"); const applyDate = dayjs(effectiveTime ?? Date.now()).format("YYYY-MM-DD");
// 失效时间
const expireDate = dayjs(expiresTime).format("YYYY-MM-DD");
// 有效天数 ps:此处证书最小设置为90d
const effectiveDays = Math.max(90, dayjs(expiresTime).diff(applyDate, "day"));
// 距离失效时间剩余天数
const leftDays = dayjs(expiresTime).diff(dayjs(), "day");
const color = leftDays < 20 ? "red" : "#389e0d"; const color = leftDays < 20 ? "red" : "#389e0d";
const percent = (leftDays / 90) * 100; const percent = (leftDays / effectiveDays) * 100;
const textColor = leftDays < 20 ? "red" : leftDays > 60 ? "#389e0d" : ""; const textColor = leftDays < 20 ? "red" : leftDays > 60 ? "#389e0d" : "";
const format = () => { const format = () => {
return <span style={{ color: textColor }}>{`${leftDays}${t("certd.days")}`}</span>; return <span style={{ color: textColor }}>{`${leftDays}${t("certd.days")}`}</span>;
}; };
// console.log('cellRender', 'effectiveDays', effectiveDays, 'expiresTime', expiresTime, 'applyTime', applyTime, 'percent', percent, row)
return <a-progress title={expireDate + t("certd.expires")} percent={percent} strokeColor={color} format={format} />; return <a-progress title={expireDate + t("certd.expires")} percent={percent} strokeColor={color} format={format} />;
}, },
width: 150, width: 150,
}, },
}, },
"lastVars.certEffectiveTime": {
title: t("certd.fields.effectiveTime"),
search: {
show: false,
},
type: "datetime",
form: {
show: false,
},
column: {
sorter: false,
show: false,
width: 150,
align: "center",
},
},
"lastVars.certExpiresTime": { "lastVars.certExpiresTime": {
title: t("certd.fields.expiryTime"), title: t("certd.fields.expiryTime"),
search: { search: {

View File

@ -0,0 +1,2 @@
ALTER TABLE cd_cert_info ADD COLUMN effective_time INTEGER;
ALTER TABLE cd_site_info ADD COLUMN cert_effective_time INTEGER;

View File

@ -0,0 +1,2 @@
ALTER TABLE cd_cert_info ADD COLUMN effective_time INTEGER;
ALTER TABLE cd_site_info ADD COLUMN cert_effective_time INTEGER;

View File

@ -0,0 +1,2 @@
ALTER TABLE cd_cert_info ADD COLUMN effective_time INTEGER;
ALTER TABLE cd_site_info ADD COLUMN cert_effective_time INTEGER;

View File

@ -30,6 +30,9 @@ export class CertInfoEntity {
@Column({ name: 'cert_provider', comment: '证书颁发机构' }) @Column({ name: 'cert_provider', comment: '证书颁发机构' })
certProvider: string; certProvider: string;
@Column({ name: 'effective_time', comment: '生效时间' })
effectiveTime: number;
@Column({ name: 'expires_time', comment: '过期时间' }) @Column({ name: 'expires_time', comment: '过期时间' })
expiresTime: number; expiresTime: number;

View File

@ -26,6 +26,8 @@ export class SiteInfoEntity {
@Column({ name: 'cert_provider', comment: '证书颁发机构', length: 100 }) @Column({ name: 'cert_provider', comment: '证书颁发机构', length: 100 })
certProvider: string; certProvider: string;
@Column({ name: 'cert_effective_time', comment: '证书生效时间' })
certEffectiveTime: number;
@Column({ name: 'cert_expires_time', comment: '证书到期时间' }) @Column({ name: 'cert_expires_time', comment: '证书到期时间' })
certExpiresTime: number; certExpiresTime: number;
@Column({ name: 'last_check_time', comment: '上次检查时间' }) @Column({ name: 'last_check_time', comment: '上次检查时间' })

View File

@ -164,6 +164,7 @@ export class CertInfoService extends BaseService<CertInfoEntity> {
bean.domains = domains.join(','); bean.domains = domains.join(',');
bean.domain = domains[0]; bean.domain = domains[0];
bean.domainCount = domains.length; bean.domainCount = domains.length;
bean.effectiveTime = certReader.effective;
bean.expiresTime = certReader.expires; bean.expiresTime = certReader.expires;
bean.certProvider = certReader.detail.issuer.commonName; bean.certProvider = certReader.detail.issuer.commonName;
bean.userId = userId bean.userId = userId

View File

@ -134,6 +134,7 @@ export class SiteInfoService extends BaseService<SiteInfoEntity> {
if (!certi) { if (!certi) {
throw new Error("没有发现证书"); throw new Error("没有发现证书");
} }
const effective = certi.valid_from;
const expires = certi.valid_to; const expires = certi.valid_to;
const allDomains = certi.subjectaltname?.replaceAll("DNS:", "").split(",") || []; const allDomains = certi.subjectaltname?.replaceAll("DNS:", "").split(",") || [];
const mainDomain = certi.subject?.CN; const mainDomain = certi.subject?.CN;
@ -149,12 +150,13 @@ export class SiteInfoService extends BaseService<SiteInfoEntity> {
certDomains: domains.join(","), certDomains: domains.join(","),
certStatus: status, certStatus: status,
certProvider: issuer, certProvider: issuer,
certEffectiveTime: dayjs(effective).valueOf(),
certExpiresTime: dayjs(expires).valueOf(), certExpiresTime: dayjs(expires).valueOf(),
lastCheckTime: dayjs().valueOf(), lastCheckTime: dayjs().valueOf(),
error: null, error: null,
checkStatus: "ok" checkStatus: "ok"
}; };
logger.info(`测试站点成功id=${updateData.id},site=${site.name},expiresTime=${updateData.certExpiresTime}`) logger.info(`测试站点成功id=${updateData.id},site=${site.name},certEffectiveTime=${updateData.certEffectiveTime},expiresTime=${updateData.certExpiresTime}`)
if (site.ipCheck) { if (site.ipCheck) {
delete updateData.checkStatus delete updateData.checkStatus
} }