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等大部分插件 | 群晖 |
| 通知 | 邮件通知、自定义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"> |
## 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.
https://afdian.com/a/greper

View File

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

View File

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

View File

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

View File

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

View File

@ -220,22 +220,47 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
sorter: true,
conditionalRender: false,
cellRender({ row }) {
const value = row.expiresTime;
if (!value) {
const {
applyTime,
effectiveTime,
expiresTime,
} = row || {};
if (!expiresTime) {
return "-";
}
const expireDate = dayjs(value).format("YYYY-MM-DD");
const leftDays = dayjs(value).diff(dayjs(), "day");
// 申请时间 ps:此处为证书在certd创建的时间而非实际证书申请时间
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 percent = (leftDays / 90) * 100;
const percent = (leftDays / effectiveDays) * 100;
const textColor = leftDays < 20 ? "red" : leftDays > 60 ? "#389e0d" : "";
const format = () => {
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} />;
},
},
},
effectiveTime: {
title: t("certd.effectiveTime"),
search: {
show: false,
},
type: "datetime",
form: {
show: false,
},
column: {
sorter: true,
show: false,
},
},
expiresTime: {
title: t("certd.expireTime"),
search: {

View File

@ -345,25 +345,64 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
align: "center",
},
},
certEffectiveTime: {
title: t("certd.monitor.certEffectiveTime"),
search: {
show: false,
},
type: "datetime",
form: {
show: false,
},
column: {
sorter: true,
width: 155,
},
},
certExpiresTime: {
title: t("certd.monitor.certExpiresTime"),
search: {
show: false,
},
type: "datetime",
form: {
show: false,
},
column: {
sorter: true,
width: 155,
},
},
remainingValidity: {
title: t("certd.monitor.remainingValidity"),
search: {
show: false,
},
type: "date",
form: {
show: false,
},
column: {
sorter: true,
cellRender({ value }) {
if (!value) {
conditionalRender: false,
cellRender({ row }) {
const {
certEffectiveTime: effectiveTime,
certExpiresTime: expiresTime,
} = row || {};
if (!expiresTime) {
return "-";
}
const expireDate = dayjs(value).format("YYYY-MM-DD");
const leftDays = dayjs(value).diff(dayjs(), "day");
// 申请时间 ps:此处为证书在certd创建的时间而非实际证书申请时间
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 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")}`} />;
},
},

View File

@ -366,23 +366,49 @@ export default function ({ crudExpose, context: { groupDictRef, selectedRowKeys
},
column: {
cellRender({ row }) {
const value = row.lastVars?.certExpiresTime;
if (!value) {
const {
certEffectiveTime: effectiveTime,
certExpiresTime: expiresTime,
} = row?.lastVars || {};
if (!expiresTime) {
return "-";
}
const expireDate = dayjs(value).format("YYYY-MM-DD");
const leftDays = dayjs(value).diff(dayjs(), "day");
// 申请时间 ps:此处为证书在certd创建的时间而非实际证书申请时间
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 percent = (leftDays / 90) * 100;
const percent = (leftDays / effectiveDays) * 100;
const textColor = leftDays < 20 ? "red" : leftDays > 60 ? "#389e0d" : "";
const format = () => {
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} />;
},
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": {
title: t("certd.fields.expiryTime"),
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: '证书颁发机构' })
certProvider: string;
@Column({ name: 'effective_time', comment: '生效时间' })
effectiveTime: number;
@Column({ name: 'expires_time', comment: '过期时间' })
expiresTime: number;

View File

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

View File

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

View File

@ -134,6 +134,7 @@ export class SiteInfoService extends BaseService<SiteInfoEntity> {
if (!certi) {
throw new Error("没有发现证书");
}
const effective = certi.valid_from;
const expires = certi.valid_to;
const allDomains = certi.subjectaltname?.replaceAll("DNS:", "").split(",") || [];
const mainDomain = certi.subject?.CN;
@ -149,12 +150,13 @@ export class SiteInfoService extends BaseService<SiteInfoEntity> {
certDomains: domains.join(","),
certStatus: status,
certProvider: issuer,
certEffectiveTime: dayjs(effective).valueOf(),
certExpiresTime: dayjs(expires).valueOf(),
lastCheckTime: dayjs().valueOf(),
error: null,
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) {
delete updateData.checkStatus
}