fix: 修复编辑证书申请任务导致清空ACME账号的bug

这一版本新增了用户证书生成统计、流水线运行统计功能,优化了插件配置获取和更新流程:
1.  新增用户统计设置模型,统计单域名/多域名/泛域名证书生成数和流水线运行次数
2.  仪表盘新增累计证书数、分类统计和节省金额估算卡片
3.  重构插件配置相关接口,统一插件获取、配置保存逻辑
4.  修复远程选择组件初始值触发逻辑,新增emitImmediate配置项
5.  优化ACME账号切换逻辑,修复对象展开语法语法问题
6.  补充多语言国际化文案,调整部分前端样式细节
This commit is contained in:
xiaojunnuo
2026-08-31 22:54:28 +08:00
parent 4f020aa8a5
commit d67379f297
26 changed files with 333 additions and 136 deletions
@@ -71,6 +71,7 @@ const props = defineProps<
pager?: boolean;
single?: boolean;
pageSize?: number;
emitImmediate?: boolean;
uploadCert?: UploadCertProps;
} & ComponentPropsType
>();
@@ -210,8 +211,9 @@ const getOptions = async () => {
if (isFirst) {
isFirst = false;
if (props.value) {
if (props.value && props.emitImmediate) {
emitSelectedChange(props.value);
emit("update:value", props.value);
}
return;
}
@@ -32,6 +32,16 @@ export default {
recentRun: "Recent Run Statistics",
runCount: "Run Count",
expiringCerts: "Soon-to-Expire Certificates",
totalGeneratedCertCount: "Total Certificates Generated",
singleDomainCertShortCount: "Single-domain",
multiDomainCertShortCount: "Multi-domain",
wildcardCertShortCount: "Wildcard",
singleDomainCertCount: "Single-domain Certificates Generated",
multiDomainCertCount: "Multi-domain Certificates Generated",
wildcardCertCount: "Wildcard Certificates Generated",
savedAmount: "Estimated savings: ¥{amount}",
savedAmountRule: "Estimated at one quarter of Tencent Cloud's annual certificate prices: single-domain ¥532, multi-domain ¥912, and wildcard ¥2147. Actual savings may vary.",
totalPipelineRuns: "Total Pipeline Runs",
supportedTasks: "Overview of Supported Deployment Tasks",
changeLog: "Change Log",
stableRelease: "Stable",
@@ -33,6 +33,16 @@ export default {
recentRun: "最近运行统计",
runCount: "运行次数",
expiringCerts: "最快到期证书",
totalGeneratedCertCount: "累计生成证书数量",
singleDomainCertShortCount: "单域名",
multiDomainCertShortCount: "多域名",
wildcardCertShortCount: "泛域名",
singleDomainCertCount: "累计单域名证书数量",
multiDomainCertCount: "累计多域名证书数量",
wildcardCertCount: "累计泛域名证书数量",
savedAmount: "已为您累计节省 {amount} 元",
savedAmountRule: "按腾讯云证书平台一年期单价的 1/4 估算:单域名证书 532 元,多域名证书 912 元,泛域名证书 2147 元。实际节省金额仅供参考。",
totalPipelineRuns: "累计流水线运行次数",
supportedTasks: "已支持的部署任务总览",
changeLog: "更新日志",
stableRelease: "稳定版",
@@ -1,13 +1,54 @@
import { DynamicType, FormItemProps, useMerge } from "@fast-crud/fast-crud";
import { merge, set } from "lodash-es";
import { defineStore } from "pinia";
import * as api from "./api.plugin";
import { DynamicType, FormItemProps, useMerge } from "@fast-crud/fast-crud";
import { i18n } from "/src/locales/i18n";
import { cloneDeep } from "lodash-es";
interface PluginState {
group?: PluginGroups;
originGroup?: PluginGroups;
}
export function fillPluginDefine(plugin: any) {
const columns = plugin.input;
for (const key in columns) {
const column = columns[key];
if (!column.component) {
column.component = {
name: "a-input",
vModel: "value",
};
} else {
if (column.component.vModel == null && column.component.name?.startsWith("a-")) {
column.component.vModel = "value";
}
}
}
if (plugin.sysSetting?.metadata) {
merge(plugin.input, plugin.sysSetting.metadata?.input || {});
// 应用选项映射
for (const key of Object.keys(plugin.input)) {
const inputDef = plugin.input[key];
if (inputDef.optionsMapping && inputDef.component?.options) {
const mapping = inputDef.optionsMapping;
for (const opt of inputDef.component.options) {
if (mapping[opt.value] !== undefined) {
opt.label = mapping[opt.value];
}
}
}
}
}
if (plugin.sysSetting?.input) {
// 设置系统默认值
for (const key in plugin.input) {
const sysValue = plugin.sysSetting?.input[key];
if (sysValue != undefined) {
set(plugin, `input.${key}.value`, sysValue);
}
}
}
}
export type PluginGroup = {
key: string;
title: string;
@@ -35,10 +76,9 @@ export class PluginGroups {
map!: { [key: string]: PluginDefine };
t: any;
mergeSetting?: boolean;
constructor(groups: { [key: string]: PluginGroup }, opts?: { mergeSetting?: boolean }) {
constructor(groups: { [key: string]: PluginGroup }) {
this.groups = groups;
this.t = i18n.global.t;
this.mergeSetting = opts?.mergeSetting ?? false;
this.initGroup(groups);
this.initMap();
}
@@ -53,26 +93,9 @@ export class PluginGroups {
icon: "material-symbols:border-all-rounded",
};
for (const key in groups) {
if (this.mergeSetting) {
for (const plugin of groups[key].plugins) {
if (plugin.sysSetting) {
merge(plugin.input, plugin.sysSetting.metadata?.input || {});
// 应用选项映射
for (const key of Object.keys(plugin.input)) {
const inputDef = plugin.input[key];
if (inputDef.optionsMapping && inputDef.component?.options) {
const mapping = inputDef.optionsMapping;
for (const opt of inputDef.component.options) {
if (mapping[opt.value] !== undefined) {
opt.label = mapping[opt.value];
}
}
}
}
}
}
for (const plugin of groups[key].plugins) {
fillPluginDefine(plugin);
}
all.plugins.push(...groups[key].plugins);
}
this.groups = {
@@ -173,16 +196,12 @@ export const usePluginStore = defineStore({
id: "app.plugin",
state: (): PluginState => ({
group: null,
originGroup: null,
}),
actions: {
async reload() {
const groups = await api.GetGroups({});
const installedGroups = filterInstalledPluginGroups(groups);
this.group = new PluginGroups(installedGroups, { mergeSetting: true });
this.originGroup = new PluginGroups(cloneDeep(installedGroups));
this.group = new PluginGroups(groups);
console.log("group", this.group);
console.log("originGroup", this.originGroup);
},
async init() {
if (!this.group) {
@@ -206,10 +225,6 @@ export const usePluginStore = defineStore({
await this.init();
return this.group.get(name);
},
async getPluginDefineFromOrigin(name: string): Promise<PluginDefine> {
await this.init();
return this.originGroup.get(name);
},
async getPluginConfig(query: any) {
return await api.GetPluginConfig(query);
},
@@ -280,6 +280,8 @@ button.ant-btn.isPlus {
align-items: center;
img {
width: 16px;
height: 16px;
&.font-size-16 {
width: 16px;
height: 16px;
@@ -556,12 +556,12 @@ export function useCertPipelineCreator({ formWrapperRef }: { formWrapperRef: Ref
//设置系统初始值
const initialForm: any = { input: {} };
const pluginSysConfig = await pluginStore.getPluginConfig({ name: req.pluginName, type: "builtIn" });
if (pluginSysConfig.sysSetting?.input) {
for (const key in pluginSysConfig.sysSetting?.input) {
initialForm.input[key] = pluginSysConfig.sysSetting?.input[key];
}
}
// const pluginSysConfig = await pluginStore.getPluginConfig({ name: req.pluginName, type: "builtIn" });
// if (pluginSysConfig.sysSetting?.input) {
// for (const key in pluginSysConfig.sysSetting?.input) {
// initialForm.input[key] = pluginSysConfig.sysSetting?.input[key];
// }
// }
async function doSubmit({ form }: any) {
// const certDetail = readCertDetail(form.cert.crt);
// 添加certd pipeline
@@ -263,13 +263,13 @@ function useStepForm() {
currentStep.input[key] = column.default ?? column.value;
}
}
//设置系统初始值
const pluginSysConfig = await pluginStore.getPluginConfig({ name: pluginDefine.name, type: "builtIn" });
if (pluginSysConfig.sysSetting?.input) {
for (const key in pluginSysConfig.sysSetting?.input) {
currentStep.input[key] = pluginSysConfig.sysSetting?.input[key];
}
}
// //设置系统初始值
// const pluginSysConfig = await pluginStore.getPluginConfig({ name: pluginDefine.name, type: pluginDefine.type });
// if (pluginSysConfig.sysSetting?.input) {
// for (const key in pluginSysConfig.sysSetting?.input) {
// currentStep.input[key] = pluginSysConfig.sysSetting?.input[key];
// }
// }
};
const stepSave = async (e: any) => {
@@ -125,6 +125,20 @@
<expiring-list v-if="count.expiringList" :data="count.expiringList"></expiring-list>
</statistic-card>
</a-col>
<a-col :md="6" :xs="24">
<statistic-card
icon="fluent-color:certificate-24"
:title="t('certd.dashboard.totalGeneratedCertCount')"
:count="count.totalGeneratedCertCount"
:sub-counts="count.generatedCertTypeCounts"
:helper-text="t('certd.dashboard.savedAmount', { amount: formatSavedAmount(count.totalSavedAmount) })"
:helper-tooltip="t('certd.dashboard.savedAmountRule')"
:footer="false"
/>
</a-col>
<a-col :md="6" :xs="24">
<statistic-card icon="fluent-color:data-trending-24" :title="t('certd.dashboard.totalPipelineRuns')" :count="count.genCertCount?.totalPipelineRuns" :footer="false" />
</a-col>
</a-row>
</div>
@@ -275,6 +289,10 @@ function goPipeline() {
}
const count: any = ref({});
function formatSavedAmount(amountValue: number) {
const amount = Number(amountValue) || 0;
return Number.isInteger(amount) ? amount.toString() : amount.toFixed(2);
}
function transformStatusCount() {
const data = count.value.pipelineStatusCount;
const sorted = [
@@ -330,6 +348,18 @@ function transformStatusCount() {
{ name: t("certd.dashboard.domainNotExpiredCount"), value: domainCount.notExpired, color: "green" },
];
count.value.domainCount = domainCount.total;
const genCertCount = count.value.genCertCount || {};
const singleDomainCertCount = Number(genCertCount.singleDomainCertCount) || 0;
const multiDomainCertCount = Number(genCertCount.multiDomainCertCount) || 0;
const wildcardCertCount = Number(genCertCount.wildcardCertCount) || 0;
count.value.totalGeneratedCertCount = singleDomainCertCount + multiDomainCertCount + wildcardCertCount;
count.value.generatedCertTypeCounts = [
{ name: t("certd.dashboard.singleDomainCertShortCount"), value: singleDomainCertCount, color: "blue" },
{ name: t("certd.dashboard.multiDomainCertShortCount"), value: multiDomainCertCount, color: "blue" },
{ name: t("certd.dashboard.wildcardCertShortCount"), value: wildcardCertCount, color: "blue" },
];
count.value.totalSavedAmount = singleDomainCertCount * (532 / 4) + multiDomainCertCount * (912 / 4) + wildcardCertCount * (2147 / 4);
}
async function loadCount() {
count.value = await GetStatisticCount();
@@ -15,6 +15,12 @@
<div class="total flex-center flex-1 flex-col pointer" @click="goDetail(link)">
<span>{{ count ?? 0 }}</span>
<span class="sub-title">{{ title }}</span>
<span v-if="helperText" class="helper-text">
{{ helperText }}
<a-tooltip v-if="helperTooltip" :title="helperTooltip">
<fs-icon icon="mingcute:information-line" class="helper-icon" />
</a-tooltip>
</span>
</div>
<a-divider type="vertical h-10"></a-divider>
<div class="sub flex-1 flex-col h-[80%] flex-evenly pl-1 2xl:pl-4">
@@ -59,6 +65,8 @@ const props = defineProps<{
title?: string;
link?: any;
}[];
helperText?: string;
helperTooltip?: string;
}>();
const slots = defineSlots();
const router = useRouter();
@@ -175,6 +183,19 @@ function goDetail(link: any) {
font-weight: 400;
color: #626262;
}
.helper-text {
color: #52a26a;
font-size: 12px;
font-weight: 500;
line-height: 20px;
.helper-icon {
cursor: help;
font-size: 14px;
margin-left: 4px;
vertical-align: -2px;
}
}
}
.sub-item {
@@ -1,3 +1,4 @@
import { PluginDefine } from "@certd/pipeline";
import { request } from "/src/api/service";
const apiPrefix = "/sys/plugin";
@@ -246,6 +247,7 @@ export type PluginConfigBean = {
name: string;
disabled: boolean;
sysSetting: {
metadata?: Record<string, any>;
input?: Record<string, any>;
};
};
@@ -283,7 +285,7 @@ export async function SaveCommPluginConfigs(data: CommPluginConfig): Promise<voi
});
}
export async function savePluginSetting(req: { name: string; sysSetting: any }): Promise<void> {
export async function savePluginSetting(req: { name: string; sysSetting: any; type: string }): Promise<void> {
return await request({
url: apiPrefix + "/saveSetting",
method: "post",
@@ -291,11 +293,11 @@ export async function savePluginSetting(req: { name: string; sysSetting: any }):
});
}
export async function GetPluginByName(name: string): Promise<PluginConfigBean> {
export async function getPluginSetting(name: string, type: string = "builtIn"): Promise<PluginConfigBean> {
return await request({
url: apiPrefix + "/getPluginByName",
url: apiPrefix + "/getSetting",
method: "post",
data: { name },
data: { name, type },
});
}
@@ -305,3 +307,11 @@ export async function ClearRuntimeDeps(): Promise<void> {
method: "post",
});
}
export async function getPluginDefine(name: string): Promise<PluginDefine> {
return await request({
url: apiPrefix + "/getPluginDefine",
method: "post",
data: { name },
});
}
@@ -69,7 +69,7 @@
<script lang="ts" setup>
import AccessSelector from "/@/views/certd/access/access-selector/index.vue";
import { reactive, ref } from "vue";
import { CommPluginConfig, GetCommPluginConfigs, SaveCommPluginConfigs, GetPluginByName } from "/@/views/sys/plugin/api";
import { CommPluginConfig, GetCommPluginConfigs, SaveCommPluginConfigs, getPluginSetting } from "/@/views/sys/plugin/api";
import { merge } from "lodash-es";
import { notification } from "ant-design-vue";
import { usePluginConfig } from "./use-config";
@@ -113,7 +113,7 @@ const onFinishFailed = (errorInfo: any) => {
const { openConfigDialog } = usePluginConfig();
async function doPluginConfig() {
const certApplyInfo = await GetPluginByName("CertApply");
const certApplyInfo = await getPluginSetting("CertApply");
await openConfigDialog({
row: certApplyInfo,
onSuccess: async () => {
@@ -48,7 +48,7 @@ import { cloneDeep, merge, unset } from "lodash-es";
import { computed, onMounted, reactive, ref } from "vue";
import { useRoute, useRouter } from "vue-router";
import Rollbackable from "./rollbackable.vue";
import { usePluginStore } from "/@/store/plugin";
import { fillPluginDefine, usePluginStore } from "/@/store/plugin";
import * as api from "./api";
// @ts-ignore js-yaml 没有在当前前端包中提供类型声明。
import yaml from "js-yaml";
@@ -106,7 +106,7 @@ const editableKeys = ref([
},
editRender(item: any) {
return () => {
return <fs-component-render {...item.component} vModel:modelValue={configForm[item.key]["value"]} scope={getScope()} />;
return <fs-component-render {...item.component} vModel:modelValue={configForm[item.key].value} scope={getScope()} />;
};
},
},
@@ -267,38 +267,18 @@ function clearFormValue(key: string) {
console.log(key, configForm);
}
function getPluginOriginName() {
if (props.plugin.fullName) {
return props.plugin.fullName;
}
if (props.plugin.author && props.plugin.name) {
return `${props.plugin.author}/${props.plugin.name}`;
}
return props.plugin.name;
}
async function loadPluginSetting() {
const originName = getPluginOriginName();
currentPlugin.value = await pluginStore.getPluginDefineFromOrigin(originName);
if (!currentPlugin.value?.input && originName !== props.plugin.name) {
currentPlugin.value = await pluginStore.getPluginDefineFromOrigin(props.plugin.name);
}
if (!currentPlugin.value?.input && props.plugin.id) {
const plugin = await api.GetObj(props.plugin.id);
const metadata = plugin.metadata ? yaml.load(plugin.metadata) : {};
currentPlugin.value = {
...plugin,
...(metadata || {}),
};
}
const pluginName = props.plugin.fullName || props.plugin.name; // 这里必须两种进行兼容
currentPlugin.value = await api.getPluginDefine(pluginName);
fillPluginDefine(currentPlugin.value);
const input = currentPlugin.value?.input || {};
for (const key in input) {
configForm[key] = {};
}
const setting = currentPlugin.value?.sysSetting || props.plugin.sysSetting;
if (setting) {
const settingJson = JSON.parse(setting);
merge(configForm, settingJson.metadata?.input || {});
const pluginConfig = await api.getPluginSetting(pluginName, currentPlugin.value.type);
if (pluginConfig?.sysSetting) {
merge(configForm, pluginConfig.sysSetting?.metadata?.input || {});
}
}
@@ -49,12 +49,13 @@ export function usePluginConfig() {
}
}
const res = await api.savePluginSetting({
name: row.name,
name: row.fullName,
sysSetting: {
metadata: {
input: newForm,
},
},
type: row.type,
});
await pluginStore.clear();
return res;
@@ -104,11 +104,11 @@ export class PluginController extends CrudController<PluginService> {
await this.auditLog({ content: "保存了公共插件配置" });
return this.ok(res);
}
@Post("/getPluginByName", { description: "sys:settings:view" })
async getPluginByName(@Body("name") name: string) {
@Post("/getSetting", { description: "sys:settings:view" })
async getSetting(@Body("name") name: string, @Body("type") type: string) {
const res = await this.pluginConfigService.getPluginConfig({
name: name,
type: "builtIn",
name,
type,
});
return this.ok(res);
}
@@ -209,4 +209,10 @@ export class PluginController extends CrudController<PluginService> {
const res = await this.service.exportPlugin(id);
return this.ok(res);
}
@Post("/getPluginDefine", { description: "sys:settings:view", summary: "根据插件名称获取插件定义" })
async getPluginDefine(@Body("name") name: string) {
const res = await this.service.getPluginDefine(name);
return this.ok(res);
}
}
@@ -9,6 +9,8 @@ import { SiteInfoService } from "../../../modules/monitor/service/site-info-serv
import { DomainService } from "../../../modules/cert/service/domain-service.js";
import { Between, LessThan } from "typeorm";
import { ApiTags } from "@midwayjs/swagger";
import { UserSettingsService } from "../../../modules/mine/service/user-settings-service.js";
import { UserStatisticSetting } from "../../../modules/mine/service/models.js";
export type ChartItem = {
name: string;
@@ -40,6 +42,12 @@ export type UserStatisticCount = {
notExpired: number;
};
expiringList: any[];
genCertCount: {
singleDomainCertCount: number;
multiDomainCertCount: number;
wildcardCertCount: number;
totalPipelineRuns: number;
};
};
/**
*/
@@ -66,6 +74,9 @@ export class StatisticController extends BaseController {
@Inject()
domainService: DomainService;
@Inject()
userSettingsService: UserSettingsService;
@Post("/count", { description: Constants.per.authOnly, summary: "查询仪表盘统计数据" })
public async count() {
const { userId, projectId } = await this.getProjectUserIdRead();
@@ -92,6 +103,8 @@ export class StatisticController extends BaseController {
const domainExpiring = await this.domainService.repository.count({
where: { ...domainProjectQuery, expirationDate: Between(now, oneMonthLater) },
});
const rawUserStatistic = await this.userSettingsService.getSetting<UserStatisticSetting>(userId, projectId, UserStatisticSetting);
const userStatistic = this.userSettingsService.normalizeStatisticSetting(rawUserStatistic);
const count: UserStatisticCount = {
pipelineCount,
@@ -102,6 +115,12 @@ export class StatisticController extends BaseController {
domainCount: { total: domainTotal, expired: domainExpired, expiring: domainExpiring, notExpired: domainTotal - domainExpired - domainExpiring },
historyCountPerDay: historyCount,
expiringList,
genCertCount: {
singleDomainCertCount: Number(userStatistic.genCertCount.singleDomainCertCount) || 0,
multiDomainCertCount: Number(userStatistic.genCertCount.multiDomainCertCount) || 0,
wildcardCertCount: Number(userStatistic.genCertCount.wildcardCertCount) || 0,
totalPipelineRuns: Number(userStatistic.genCertCount.totalPipelineRuns) || 0,
},
};
return this.ok(count);
}
@@ -93,6 +93,7 @@ export class CommonEabToAcmeAccountFix {
...certApplyConfig.sysSetting,
input,
},
type: "builtIn",
});
}
return true;
@@ -77,7 +77,7 @@ export class DomainService extends BaseService<DomainEntity> {
const old = await this.findOne({
where: {
domain: param.domain,
...query
...query,
},
});
if (old) {
@@ -52,6 +52,18 @@ export class UserGrantSetting extends BaseSettings {
allowAdminViewCerts = false;
}
export class UserStatisticSetting extends BaseSettings {
static __title__ = "用户累计统计";
static __key__ = "user.statistic";
genCertCount = {
singleDomainCertCount: 0,
multiDomainCertCount: 0,
wildcardCertCount: 0,
totalPipelineRuns: 0,
};
}
export class UserPreferencesSetting extends BaseSettings {
static __title__ = "用户偏好设置";
static __key__ = "user.preferences";
@@ -4,6 +4,7 @@ import { Repository } from "typeorm";
import { BaseService, BaseSettings } from "@certd/lib-server";
import { UserSettingsEntity } from "../entity/user-settings.js";
import { LocalCache, mergeUtils } from "@certd/basic";
import { UserStatisticSetting } from "./models.js";
const { merge } = mergeUtils;
const UserSettingCache = new LocalCache({
@@ -133,4 +134,45 @@ export class UserSettingsService extends BaseService<UserSettingsEntity> {
newEntity.setting = JSON.stringify(bean);
await this.repository.save(newEntity);
}
async incrementStatistic(userId: number, projectId: number | null | undefined, field: keyof UserStatisticSetting["genCertCount"]) {
for (let attempt = 0; attempt < 3; attempt++) {
try {
await this.repository.manager.transaction("SERIALIZABLE", async manager => {
const repo = manager.getRepository(UserSettingsEntity);
const query = this.buildUserProjectQuery(userId, projectId);
let entity = await repo.findOne({ where: { key: UserStatisticSetting.__key__, ...query }, lock: { mode: "pessimistic_write" } });
const savedSetting = entity ? JSON.parse(entity.setting || "{}") : {};
const setting = this.normalizeStatisticSetting(savedSetting);
setting.genCertCount[field] += 1;
if (!entity) {
entity = new UserSettingsEntity();
entity.userId = userId;
entity.projectId = projectId;
entity.key = UserStatisticSetting.__key__;
entity.title = UserStatisticSetting.__title__;
}
entity.setting = JSON.stringify(setting);
await repo.save(entity);
});
return;
} catch (error: any) {
const retryable = error?.code === "40001" || error?.code === "40P01" || error?.code === "ER_LOCK_DEADLOCK";
if (!retryable || attempt === 2) {
throw error;
}
}
}
}
normalizeStatisticSetting(savedSetting: any): UserStatisticSetting {
const setting = merge(new UserStatisticSetting(), savedSetting);
const legacySetting = savedSetting as Partial<Record<keyof UserStatisticSetting["genCertCount"], number>>;
for (const field of Object.keys(setting.genCertCount) as (keyof UserStatisticSetting["genCertCount"])[]) {
if (setting.genCertCount[field] === 0 && legacySetting[field] != null) {
setting.genCertCount[field] = Number(legacySetting[field]) || 0;
}
}
return setting;
}
}
@@ -7,6 +7,7 @@ import { logger, utils } from "@certd/basic";
import { CertInfo, CertReader } from "@certd/plugin-cert";
import { AcmeService } from "../../../plugins/plugin-cert/plugin/cert-plugin/acme.js";
import { PipelineEntity } from "../../pipeline/entity/pipeline.js";
import { UserSettingsService } from "../../mine/service/user-settings-service.js";
export type UploadCertReq = {
id?: number;
@@ -39,6 +40,9 @@ export class CertInfoService extends BaseService<CertInfoEntity> {
@InjectEntityModel(PipelineEntity)
pipelineRepository: Repository<PipelineEntity>;
@Inject()
userSettingsService: UserSettingsService;
//@ts-ignore
getRepository() {
return this.repository;
@@ -213,6 +217,12 @@ export class CertInfoService extends BaseService<CertInfoEntity> {
status: CertStatus.inactive,
}
);
if (certUserId != null) {
const domains = cert?.crt ? new CertReader(cert).getAltNames() : [];
const wildcardCount = domains.filter(domain => String(domain).trim().toLowerCase().startsWith("*.")).length;
const field = wildcardCount > 0 ? "wildcardCertCount" : domains.length > 1 ? "multiDomainCertCount" : "singleDomainCertCount";
await this.userSettingsService.incrementStatistic(certUserId, certProjectId, field);
}
return bean;
}
@@ -256,7 +266,7 @@ export class CertInfoService extends BaseService<CertInfoEntity> {
// 每个申请任务维护一条 active 记录(不重复创建,最新真证书申请成功后旧记录会被标记为 inactive)
const bean = new CertInfoEntity();
let found = foundHistory.find(item => item.taskId === task.taskId);
if (!found && foundHistory.length == 1 && applyTasks.length == 1){
if (!found && foundHistory.length == 1 && applyTasks.length == 1) {
found = foundHistory[0];
}
if (found) {
@@ -33,6 +33,7 @@ import { ProjectService } from "../../sys/enterprise/service/project-service.js"
import { CertApplyStepInputPatch, updateCertApplyStepInputs } from "./pipeline-batch-update.js";
import { calcNextSuiteCountUsed } from "./pipeline-suite-limit.js";
import { CertApplyTemplateParams } from "../../cert/service/cert-apply-template-fields.js";
import { UserSettingsService } from "../../mine/service/user-settings-service.js";
const runningTasks: Map<string | number, Executor> = new Map();
/**
@@ -90,6 +91,8 @@ export class PipelineService extends BaseService<PipelineEntity> {
@Inject()
projectService: ProjectService;
@Inject()
userSettingsService: UserSettingsService;
//@ts-ignore
getRepository() {
@@ -786,6 +789,7 @@ export class PipelineService extends BaseService<PipelineEntity> {
};
const historyId = await this.historyService.start(entity, triggerType);
await this.userSettingsService.incrementStatistic(entity.userId, entity.projectId, "totalPipelineRuns");
const sysInfo: SysInfo = {};
if (isComm()) {
const siteInfo = await this.sysSettingsService.getSetting<SysSiteInfo>(SysSiteInfo);
@@ -915,6 +919,15 @@ export class PipelineService extends BaseService<PipelineEntity> {
}
return pipelineEntity.projectId;
}
async getUserProjectId(pipelineId: number) {
const pipelineEntity = await this.repository.findOne({
select: { userId: true, projectId: true },
where: { id: pipelineId },
});
return pipelineEntity ? { userId: pipelineEntity.userId, projectId: pipelineEntity.projectId } : null;
}
private async saveHistory(history: RunHistory) {
//修改pipeline状态
const pipelineEntity = new PipelineEntity();
@@ -8,9 +8,9 @@ export class PluginConfigGetter implements IPluginConfigService {
@Inject()
pluginConfigService: PluginConfigService;
async getPluginConfig(pluginName: string): Promise<PluginConfig> {
async getPluginConfig(name: string): Promise<PluginConfig> {
const res = await this.pluginConfigService.getPluginConfig({
name: pluginName,
name,
type: "builtIn",
});
return {
@@ -4,7 +4,8 @@ import { PluginService } from "./plugin-service.js";
export type PluginConfig = {
name: string;
disabled?: boolean;
sysSetting: {
type: string;
sysSetting?: {
input?: Record<string, any>;
metadata?: Record<string, any>;
};
@@ -38,6 +39,7 @@ export class PluginConfigService {
async saveCommPluginConfig(config: CommPluginConfig) {
config.CertApply.name = "CertApply";
config.CertApply.type = "builtIn";
await this.savePluginConfig(config.CertApply);
}
@@ -47,17 +49,23 @@ export class PluginConfigService {
if (!sysSetting) {
throw new Error(`${name}.sysSetting is required`);
}
const pluginEntity = await this.pluginService.getRepository().findOne({
where: { name },
let pluginEntity:any = await this.pluginService.getRepository().findOne({
where: { fullName:name, type: config.type },
});
if (!pluginEntity) {
await this.pluginService.add({
name,
if (config.type !== "builtIn") {
//只有内置插件才需要add config
throw new Error(`${name}.type must be builtIn`);
}
pluginEntity = {
name: name,
fullName:name,
sysSetting: JSON.stringify(sysSetting),
type: "builtIn",
disabled: false,
author: "certd",
});
}
const {id } = await this.pluginService.add(pluginEntity);
pluginEntity.id = id;
} else {
const setting = JSON.parse(pluginEntity.sysSetting || "{}");
if (sysSetting.metadata) {
@@ -66,18 +74,18 @@ export class PluginConfigService {
if (sysSetting.input) {
setting.input = sysSetting.input;
}
await this.pluginService.getRepository().update({ name }, { sysSetting: JSON.stringify(setting) });
await this.pluginService.getRepository().update({ fullName:name }, { sysSetting: JSON.stringify(setting) });
}
}
async get(req: PluginFindReq) {
if (!req.name && !req.id) {
throw new Error("plugin s name or id is required");
throw new Error("plugin name or id is required");
}
return await this.pluginService.getRepository().findOne({
where: {
id: req.id,
name: req.name,
fullName: req.name,
type: req.type,
},
});
@@ -98,8 +106,9 @@ export class PluginConfigService {
sysSetting = JSON.parse(plugin.sysSetting);
}
return {
name: plugin.name,
name: plugin.fullName,
disabled: plugin.disabled,
type: plugin.type,
sysSetting,
};
}
@@ -538,7 +538,7 @@ export class PluginService extends BaseService<PluginEntity> {
const settingPlugins = await this.find({
select: {
id: true,
name: true,
fullName: true,
sysSetting: true,
},
where: {
@@ -551,7 +551,7 @@ export class PluginService extends BaseService<PluginEntity> {
if (!item.sysSetting) {
continue;
}
pluginSettingMap[item.name] = JSON.parse(item.sysSetting);
pluginSettingMap[item.fullName] = JSON.parse(item.sysSetting);
}
for (const key in groups) {
const group = groups[key];
@@ -559,7 +559,7 @@ export class PluginService extends BaseService<PluginEntity> {
continue;
}
for (const item of group.plugins) {
const pluginSetting = pluginSettingMap[item.name];
const pluginSetting = pluginSettingMap[item.fullName];
if (pluginSetting) {
item.sysSetting = pluginSetting;
}
@@ -572,7 +572,7 @@ export class PluginService extends BaseService<PluginEntity> {
disabled: true,
},
});
const disabledNames = list.map(it => it.name);
const disabledNames = list.map(it => it.fullName);
for (const key in groups) {
const group = groups[key];
if (!group.plugins) {
@@ -703,13 +703,13 @@ export class PluginService extends BaseService<PluginEntity> {
}
const old = await this.findOne({
where: {
type: "store",
type: param.type,
fullName: param.fullName,
},
});
if (old) {
throw new Error(`插件${param.author}/${param.name}已存在`);
throw new Error(`插件${param.fullName}已存在`);
}
if (param.type === "builtIn") {
@@ -769,7 +769,7 @@ export class PluginService extends BaseService<PluginEntity> {
private normalizeOnlinePluginRecord(item: OnlinePluginBean, syncTime: number) {
const fullName = this.getOnlinePluginFullName(item);
let extra: Record<string, any> = {};
const extra: Record<string, any> = {};
return {
appId: item.appId,
developerId: item.developerId,
@@ -990,32 +990,31 @@ export class PluginService extends BaseService<PluginEntity> {
});
const existingMap = new Map(existingRecords.map(item => [item.fullName, item]));
for (const item of list) {
const itemFromStore:any = this.normalizeOnlinePluginRecord(item, syncTime);
const itemFromStore: any = this.normalizeOnlinePluginRecord(item, syncTime);
const fullName = itemFromStore.fullName;
const existingRecord = existingMap.get(fullName);
if (existingRecord) {
//本地已存在,只更新市场相关字段
if (existingRecord.installed){
//如果本地已经安装的,则只同步版本 分数 下载量
const updateBean: Record<string, any> = {
id: existingRecord.id,
score: itemFromStore.score,
downloadCount: itemFromStore.downloadCount,
latestVersion: itemFromStore.version,
}
await this.update(updateBean);
}else{
//如果本地未安装的,则同步所有字段
const updateBean: Record<string, any> = {
id: existingRecord.id,
...itemFromStore,
}
await this.update(updateBean);
}
continue;
}else{
//本地已存在,只更新市场相关字段
if (existingRecord.installed) {
//如果本地已经安装的,则只同步版本 分数 下载量
const updateBean: Record<string, any> = {
id: existingRecord.id,
score: itemFromStore.score,
downloadCount: itemFromStore.downloadCount,
latestVersion: itemFromStore.version,
};
await this.update(updateBean);
} else {
//如果本地未安装的,则同步所有字段
const updateBean: Record<string, any> = {
id: existingRecord.id,
...itemFromStore,
};
await this.update(updateBean);
}
continue;
} else {
itemFromStore.disabled = false;
await this.add(itemFromStore);
}
@@ -1593,7 +1592,7 @@ export class PluginService extends BaseService<PluginEntity> {
this.normalizeStorePluginAuthor(loaded);
}
const fullName = loaded.fullName || (loaded.author && loaded.name ? `${loaded.author}/${loaded.name}` : "");
let old = await this.findOne({
const old = await this.findOne({
where: {
type: "store",
fullName,
@@ -1637,10 +1636,10 @@ export class PluginService extends BaseService<PluginEntity> {
throw new Error(`插件${loaded.author}/${loaded.name}已存在`);
}
pluginEntity.id = old.id;
//update
//update
await this.update(pluginEntity);
}
return {
id: pluginEntity.id,
};
@@ -1672,7 +1671,6 @@ export class PluginService extends BaseService<PluginEntity> {
);
}
isNewVersion(version: string, latestVersion: string) {
if (!latestVersion) {
return false;
@@ -1692,4 +1690,8 @@ export class PluginService extends BaseService<PluginEntity> {
}
return false;
}
async getPluginDefine(name: string) {
return pluginRegistry.getDefine(name);
}
}
@@ -44,6 +44,7 @@ export class AcmeAccountAccess extends BaseAccess {
action: "onCaTypeList",
single: true,
mode: "tags",
emitImmediate: true,
},
required: true,
mergeScript: `
@@ -256,12 +256,13 @@ export class CertApplyPlugin extends CertApplyBasePlugin {
action: "onSslProviderList",
single: true,
value: "letsencrypt",
emitImmediate: false,
required: true,
helper: "Let's Encrypt:申请最简单\nGoogle:大厂光环,兼容性好,无需配置翻墙代理\nSSL.com:仅主域名和www免费,必须设置CAA记录\n自定义ACME:管理员可在「系统设置-流水线设置」中配置",
mergeScript: `return {
component:{
on: {
"selected-change": (scope)=>{
"selectedChange": (scope)=>{
// 切换颁发机构后,清空已选择的ACME账号,避免账号与颁发机构不匹配
let form = scope.form || {};
form = form.input || form.body || form;