feat: 新增管理员针对用户流水线和证书监控管理功能

1.  新增后台管理页面:用户流水线管理、用户证书监控管理
2.  新增对应前后端接口与控制器
3.  添加多语言国际化配置
4.  修复导入顺序与多余空行问题
5.  补充证书申请类型选项
This commit is contained in:
xiaojunnuo
2026-06-03 01:01:19 +08:00
parent 3db87218ee
commit 021155278e
14 changed files with 1041 additions and 3 deletions
@@ -45,6 +45,9 @@ export default {
permissionManager: "Permission Management",
roleManager: "Role Management",
userManager: "User Management",
userDataManager: "User Data Management",
pipelineManager: "User Pipeline Management",
siteMonitorManager: "User Certificate Monitor",
suiteManager: "Suite Management",
suiteSetting: "Suite Settings",
orderManager: "Order Management",
@@ -46,6 +46,9 @@ export default {
permissionManager: "权限管理",
roleManager: "角色管理",
userManager: "用户管理",
userDataManager: "用户数据管理",
pipelineManager: "用户流水线管理",
siteMonitorManager: "用户证书监控管理",
suiteManager: "套餐管理",
suiteSetting: "套餐设置",
orderManager: "订单管理",
@@ -230,6 +230,44 @@ export const sysResources = [
auth: true,
},
},
{
title: "certd.sysResources.userDataManager",
name: "UserDataManager",
path: "/sys/user-data",
redirect: "/sys/pipeline",
meta: {
icon: "ion:folder-open-outline",
permission: "sys:settings:view",
keepAlive: true,
auth: true,
},
children: [
{
title: "certd.sysResources.pipelineManager",
name: "SysPipelineManager",
path: "/sys/pipeline",
component: "/sys/pipeline/index.vue",
meta: {
icon: "ion:analytics-sharp",
permission: "sys:settings:view",
keepAlive: true,
auth: true,
},
},
{
title: "certd.sysResources.siteMonitorManager",
name: "SysSiteMonitorManager",
path: "/sys/monitor/site",
component: "/sys/monitor/site/index.vue",
meta: {
icon: "ion:videocam-outline",
permission: "sys:settings:view",
keepAlive: true,
auth: true,
},
},
],
},
{
title: "certd.sysResources.suiteManager",
name: "SuiteManager",
@@ -568,6 +568,7 @@ export default function ({ crudExpose, context: { selectedRowKeys, openCertApply
{ value: "cert_upload", label: t("certd.types.certUpload") },
{ value: "custom", label: t("certd.types.custom") },
{ value: "template", label: t("certd.types.template") },
{ value: "cert_auto", label: t("certd.types.certApply") },
],
}),
form: {
@@ -0,0 +1,37 @@
import { request } from "/src/api/service";
const apiPrefix = "/sys/monitor/site";
export const sysSiteMonitorApi = {
async GetList(query: any) {
return await request({
url: apiPrefix + "/page",
method: "post",
data: query,
});
},
async DelObj(id: number) {
return await request({
url: apiPrefix + "/delete",
method: "post",
params: { id },
});
},
async BatchDelObj(ids: number[]) {
return await request({
url: apiPrefix + "/batchDelete",
method: "post",
data: { ids },
});
},
async GetSimpleUserByIds(ids: number[]) {
return await request({
url: "/sys/authority/user/getSimpleUserByIds",
method: "post",
data: { ids },
});
},
};
@@ -0,0 +1,379 @@
import createCrudOptionsUser from "/@/views/sys/authority/user/crud";
import { CreateCrudOptionsProps, CreateCrudOptionsRet, DelReq, dict, UserPageQuery, UserPageRes } from "@fast-crud/fast-crud";
import { message, Modal } from "ant-design-vue";
import dayjs from "dayjs";
import { ref } from "vue";
import { sysSiteMonitorApi } from "./api";
export default function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet {
const api = sysSiteMonitorApi;
const pageRequest = async (query: UserPageQuery): Promise<UserPageRes> => {
return await api.GetList(query);
};
const delRequest = async ({ row }: DelReq) => {
return await api.DelObj(row.id);
};
const selectedRowKeys = ref<number[]>([]);
const handleBatchDelete = () => {
if (!selectedRowKeys.value?.length) {
message.error("请先选择要删除的记录");
return;
}
Modal.confirm({
title: "确认",
content: `确认删除选中的 ${selectedRowKeys.value.length} 条站点监控记录?`,
async onOk() {
await api.BatchDelObj(selectedRowKeys.value);
message.success("删除成功");
selectedRowKeys.value = [];
await crudExpose.doRefresh();
},
});
};
context.handleBatchDelete = handleBatchDelete;
const checkStatusDict = dict({
data: [
{ label: "正常", value: "ok", color: "green" },
{ label: "检查中", value: "checking", color: "blue" },
{ label: "异常", value: "error", color: "red" },
],
});
return {
crudOptions: {
request: {
pageRequest,
delRequest,
},
actionbar: {
show: false,
},
toolbar: {
buttons: {
export: {
show: true,
},
},
export: {
dataFrom: "search",
},
},
pagination: {
pageSizeOptions: ["10", "20", "50", "100", "200"],
},
settings: {
plugins: {
rowSelection: {
enabled: true,
props: {
multiple: true,
crossPage: false,
selectedRowKeys: () => {
return selectedRowKeys;
},
},
},
},
},
rowHandle: {
fixed: "right",
width: 100,
buttons: {
view: { show: false },
copy: { show: false },
edit: { show: false },
remove: {
show: true,
},
},
},
columns: {
id: {
title: "ID",
key: "id",
type: "number",
column: {
width: 80,
align: "center",
},
form: {
show: false,
},
},
userId: {
title: "用户",
type: "table-select",
search: {
show: true,
col: {
span: 4,
},
},
dict: dict({
async getNodesByValues(ids: number[]) {
return await api.GetSimpleUserByIds(ids);
},
value: "id",
label: "nickName",
}),
form: {
show: false,
component: {
crossPage: true,
multiple: false,
select: {
placeholder: "点击选择用户",
},
createCrudOptions: createCrudOptionsUser,
},
},
column: {
width: 150,
},
},
projectId: {
title: "项目ID",
type: "number",
search: {
show: true,
col: {
span: 3,
},
},
column: {
width: 100,
align: "center",
},
form: {
show: false,
},
},
name: {
title: "站点名称",
type: "text",
search: {
show: true,
col: {
span: 4,
},
},
column: {
width: 160,
},
form: {
show: false,
},
},
domain: {
title: "域名",
type: "text",
search: {
show: true,
col: {
span: 4,
},
},
column: {
width: 230,
sorter: true,
cellRender({ value, row }) {
const domainPort = `${value}:${row.httpsPort || 443}`;
return (
<a-tooltip title={domainPort} placement="left">
<fs-copyable modelValue={domainPort} title={domainPort}>
<a target="_blank" href={`https://${domainPort}`}>
{domainPort}
</a>
</fs-copyable>
</a-tooltip>
);
},
},
form: {
show: false,
},
},
certDomains: {
title: "证书域名",
type: "text",
search: {
show: true,
col: {
span: 4,
},
},
column: {
width: 260,
sorter: true,
ellipsis: true,
cellRender({ value }) {
return <a-tooltip title={value}>{value}</a-tooltip>;
},
},
form: {
show: false,
},
},
certProvider: {
title: "颁发机构",
type: "text",
column: {
width: 200,
sorter: true,
ellipsis: true,
cellRender({ value }) {
return <a-tooltip title={value}>{value}</a-tooltip>;
},
},
form: {
show: false,
},
},
certStatus: {
title: "证书状态",
type: "dict-select",
search: {
show: true,
col: {
span: 3,
},
},
dict: dict({
data: [
{ label: "正常", value: "ok", color: "green" },
{ label: "已过期", value: "expired", color: "red" },
],
}),
column: {
width: 100,
sorter: true,
align: "center",
},
form: {
show: false,
},
},
checkStatus: {
title: "检查状态",
type: "dict-select",
search: {
show: true,
col: {
span: 3,
},
},
dict: checkStatusDict,
column: {
width: 100,
sorter: true,
align: "center",
cellRender({ value, row }) {
return (
<a-tooltip title={row.error}>
<fs-values-format v-model={value} dict={checkStatusDict}></fs-values-format>
</a-tooltip>
);
},
},
form: {
show: false,
},
},
certExpiresTime: {
title: "证书到期时间",
type: "datetime",
column: {
sorter: true,
width: 155,
},
form: {
show: false,
},
},
remainingValidity: {
title: "剩余有效期",
type: "date",
column: {
width: 120,
conditionalRender: false,
cellRender({ row }) {
if (!row.certExpiresTime) {
return "-";
}
const leftDays = dayjs(row.certExpiresTime).diff(dayjs(), "day");
const color = leftDays < 15 ? "red" : "#389e0d";
return <span style={{ color }}>{leftDays}</span>;
},
},
form: {
show: false,
},
},
lastCheckTime: {
title: "上次检查时间",
type: "datetime",
column: {
sorter: true,
width: 155,
},
form: {
show: false,
},
},
disabled: {
title: "状态",
type: "dict-select",
search: {
show: true,
col: {
span: 3,
},
},
dict: dict({
data: [
{ label: "启用", value: false, color: "green" },
{ label: "禁用", value: true, color: "red" },
],
}),
column: {
width: 90,
sorter: true,
align: "center",
},
form: {
show: false,
},
},
remark: {
title: "备注",
type: "textarea",
column: {
width: 200,
sorter: true,
ellipsis: true,
cellRender({ value }) {
return <a-tooltip title={value}>{value}</a-tooltip>;
},
},
form: {
show: false,
},
},
createTime: {
title: "创建时间",
type: "datetime",
column: {
width: 155,
sorter: true,
show: false,
},
form: {
show: false,
},
},
},
},
};
}
@@ -0,0 +1,35 @@
<template>
<fs-page>
<template #header>
<div class="title">用户证书监控管理</div>
</template>
<fs-crud ref="crudRef" v-bind="crudBinding">
<template #pagination-left>
<a-tooltip title="批量删除">
<fs-button icon="DeleteOutlined" @click="handleBatchDelete"></fs-button>
</a-tooltip>
</template>
</fs-crud>
</fs-page>
</template>
<script lang="ts" setup>
import { useFs } from "@fast-crud/fast-crud";
import { onActivated, onMounted } from "vue";
import createCrudOptions from "./crud";
defineOptions({
name: "SysSiteMonitorManager",
});
const context: any = {};
const { crudBinding, crudRef, crudExpose } = useFs({ createCrudOptions, context });
const handleBatchDelete = context.handleBatchDelete;
onMounted(() => {
crudExpose.doRefresh();
});
onActivated(() => {
crudExpose.doRefresh();
});
</script>
@@ -0,0 +1,37 @@
import { request } from "/src/api/service";
const apiPrefix = "/sys/pipeline";
export const sysPipelineApi = {
async GetList(query: any) {
return await request({
url: apiPrefix + "/page",
method: "post",
data: query,
});
},
async DelObj(id: number) {
return await request({
url: apiPrefix + "/delete",
method: "post",
params: { id },
});
},
async BatchDelObj(ids: number[]) {
return await request({
url: apiPrefix + "/batchDelete",
method: "post",
data: { ids },
});
},
async GetSimpleUserByIds(ids: number[]) {
return await request({
url: "/sys/authority/user/getSimpleUserByIds",
method: "post",
data: { ids },
});
},
};
@@ -0,0 +1,363 @@
import createCrudOptionsUser from "/@/views/sys/authority/user/crud";
import { CreateCrudOptionsProps, CreateCrudOptionsRet, DelReq, dict, UserPageQuery, UserPageRes } from "@fast-crud/fast-crud";
import { message, Modal } from "ant-design-vue";
import dayjs from "dayjs";
import { ref } from "vue";
import { statusUtil } from "/@/views/certd/pipeline/pipeline/utils/util.status";
import { sysPipelineApi } from "./api";
import { useSettingStore } from "/@/store/settings";
export default function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet {
const api = sysPipelineApi;
const settingStore = useSettingStore();
const pageRequest = async (query: UserPageQuery): Promise<UserPageRes> => {
return await api.GetList(query);
};
const delRequest = async ({ row }: DelReq) => {
return await api.DelObj(row.id);
};
const selectedRowKeys = ref<number[]>([]);
const handleBatchDelete = () => {
if (!selectedRowKeys.value?.length) {
message.error("请先选择要删除的记录");
return;
}
settingStore.checkPlus();
Modal.confirm({
title: "确认",
content: `确认删除选中的 ${selectedRowKeys.value.length} 条用户流水线?删除后会清理对应执行历史、日志和证书仓库记录。`,
async onOk() {
await api.BatchDelObj(selectedRowKeys.value);
message.success("删除成功");
selectedRowKeys.value = [];
await crudExpose.doRefresh();
},
});
};
context.handleBatchDelete = handleBatchDelete;
return {
crudOptions: {
request: {
pageRequest,
delRequest,
},
actionbar: {
show: false,
},
toolbar: {
buttons: {
export: {
show: true,
},
},
export: {
dataFrom: "search",
},
},
pagination: {
pageSizeOptions: ["10", "20", "50", "100", "200"],
},
settings: {
plugins: {
rowSelection: {
enabled: true,
props: {
multiple: true,
crossPage: false,
selectedRowKeys: () => {
return selectedRowKeys;
},
},
},
},
},
rowHandle: {
fixed: "right",
width: 100,
buttons: {
view: { show: false },
copy: { show: false },
edit: { show: false },
remove: {
show: true,
},
},
},
columns: {
id: {
title: "ID",
key: "id",
type: "number",
search: {
show: true,
col: {
span: 2,
},
},
column: {
width: 90,
align: "center",
},
form: {
show: false,
},
},
userId: {
title: "用户",
type: "table-select",
search: {
show: true,
col: {
span: 4,
},
},
dict: dict({
async getNodesByValues(ids: number[]) {
return await api.GetSimpleUserByIds(ids);
},
value: "id",
label: "nickName",
}),
form: {
show: false,
component: {
crossPage: true,
multiple: false,
select: {
placeholder: "点击选择用户",
},
createCrudOptions: createCrudOptionsUser,
},
},
column: {
width: 150,
},
},
projectId: {
title: "项目ID",
type: "number",
search: {
show: true,
col: {
span: 3,
},
},
column: {
width: 100,
align: "center",
},
form: {
show: false,
},
},
title: {
title: "流水线名称",
type: "text",
search: {
show: true,
title: "关键词",
component: {
name: "a-input",
},
col: {
span: 4,
},
},
column: {
width: 320,
sorter: true,
ellipsis: true,
showTitle: true,
},
form: {
show: false,
},
},
type: {
title: "类型",
type: "dict-select",
search: {
show: true,
col: {
span: 3,
},
},
dict: dict({
data: [
{ value: "cert", label: "证书申请" },
{ value: "cert_upload", label: "证书上传" },
{ value: "custom", label: "自定义" },
{ value: "template", label: "模板" },
{ value: "cert_auto", label: "证书申请" },
],
}),
column: {
width: 110,
align: "center",
sorter: true,
component: {
color: "auto",
},
},
form: {
show: false,
},
},
status: {
title: "运行状态",
type: "dict-select",
search: {
show: true,
col: {
span: 3,
},
},
dict: dict({
data: statusUtil.getOptions(),
}),
column: {
sorter: true,
width: 120,
align: "center",
},
form: {
show: false,
},
},
disabled: {
title: "状态",
type: "dict-select",
search: {
show: true,
col: {
span: 3,
},
},
dict: dict({
data: [
{ label: "启用", value: false, color: "green" },
{ label: "禁用", value: true, color: "red" },
],
}),
column: {
width: 90,
sorter: true,
align: "center",
},
form: {
show: false,
},
},
stepCount: {
title: "部署任务数",
type: "number",
column: {
align: "center",
width: 110,
},
form: {
show: false,
},
},
triggerCount: {
title: "定时任务数",
type: "number",
column: {
align: "center",
width: 110,
sorter: true,
},
form: {
show: false,
},
},
lastHistoryTime: {
title: "最后执行时间",
type: "datetime",
column: {
sorter: true,
width: 155,
align: "center",
},
form: {
show: false,
},
},
nextRunTime: {
title: "下次执行时间",
type: "datetime",
column: {
sorter: true,
width: 155,
align: "center",
},
form: {
show: false,
},
},
validTime: {
title: "有效期",
type: "date",
column: {
sorter: true,
width: 130,
align: "center",
cellRender({ value }) {
if (!value || value <= 0) {
return "-";
}
if (value < Date.now()) {
return <span style={{ color: "red" }}></span>;
}
return dayjs(value).format("YYYY-MM-DD");
},
},
form: {
show: false,
},
},
remark: {
title: "备注",
type: "textarea",
column: {
width: 200,
sorter: true,
ellipsis: true,
cellRender({ value }) {
return <a-tooltip title={value}>{value}</a-tooltip>;
},
},
form: {
show: false,
},
},
createTime: {
title: "创建时间",
type: "datetime",
column: {
width: 155,
sorter: true,
show: false,
},
form: {
show: false,
},
},
updateTime: {
title: "更新时间",
type: "datetime",
column: {
width: 155,
sorter: true,
show: false,
},
form: {
show: false,
},
},
},
},
};
}
@@ -0,0 +1,35 @@
<template>
<fs-page>
<template #header>
<div class="title flex items-center">用户流水线管理</div>
</template>
<fs-crud ref="crudRef" v-bind="crudBinding">
<template #pagination-left>
<a-tooltip title="批量删除">
<fs-button icon="DeleteOutlined" class="need-plus" @click="handleBatchDelete"></fs-button>
</a-tooltip>
</template>
</fs-crud>
</fs-page>
</template>
<script lang="ts" setup>
import { useFs } from "@fast-crud/fast-crud";
import { onActivated, onMounted } from "vue";
import createCrudOptions from "./crud";
defineOptions({
name: "SysPipelineManager",
});
const context: any = {};
const { crudBinding, crudRef, crudExpose } = useFs({ createCrudOptions, context });
const handleBatchDelete = context.handleBatchDelete;
onMounted(() => {
crudExpose.doRefresh();
});
onActivated(() => {
crudExpose.doRefresh();
});
</script>
@@ -134,6 +134,5 @@ export class MainConfiguration {
});
logger.info("当前环境:", this.app.getEnv()); // prod
}
}
@@ -0,0 +1,55 @@
import { ALL, Body, Controller, Inject, Post, Provide, Query } from "@midwayjs/core";
import { CrudController } from "@certd/lib-server";
import { SiteInfoService } from "../../../modules/monitor/service/site-info-service.js";
import { ApiTags } from "@midwayjs/swagger";
@Provide()
@Controller("/api/sys/monitor/site")
@ApiTags(["sys-monitor"])
export class SysSiteInfoController extends CrudController<SiteInfoService> {
@Inject()
service: SiteInfoService;
getService(): SiteInfoService {
return this.service;
}
@Post("/page", { description: "sys:settings:view", summary: "管理员查询站点监控分页列表" })
async page(@Body(ALL) body: any) {
body.query = body.query ?? {};
const certDomains = body.query.certDomains;
const domain = body.query.domain;
const name = body.query.name;
delete body.query.certDomains;
delete body.query.domain;
delete body.query.name;
const res = await this.service.page({
query: body.query,
page: body.page,
sort: body.sort,
buildQuery: bq => {
if (domain) {
bq.andWhere("domain like :domain", { domain: `%${domain}%` });
}
if (certDomains) {
bq.andWhere("cert_domains like :cert_domains", { cert_domains: `%${certDomains}%` });
}
if (name) {
bq.andWhere("name like :name", { name: `%${name}%` });
}
},
});
return this.ok(res);
}
@Post("/delete", { description: "sys:settings:edit", summary: "管理员删除站点监控" })
async delete(@Query("id") id: number) {
return await super.delete(id);
}
@Post("/batchDelete", { description: "sys:settings:edit", summary: "管理员批量删除站点监控" })
async batchDelete(@Body("ids") ids: number[]) {
await this.service.delete(ids);
return this.ok();
}
}
@@ -0,0 +1,53 @@
import { ALL, Body, Controller, Inject, Post, Provide, Query } from "@midwayjs/core";
import { CrudController } from "@certd/lib-server";
import { ApiTags } from "@midwayjs/swagger";
import { PipelineService } from "../../../modules/pipeline/service/pipeline-service.js";
import { checkPlus } from "@certd/plus-core";
@Provide()
@Controller("/api/sys/pipeline")
@ApiTags(["sys-pipeline"])
export class SysPipelineController extends CrudController<PipelineService> {
@Inject()
service: PipelineService;
getService(): PipelineService {
return this.service;
}
@Post("/page", { description: "sys:settings:view", summary: "管理员查询用户流水线分页列表" })
async page(@Body(ALL) body: any) {
body.query = body.query ?? {};
const title = body.query.title;
delete body.query.title;
if (!body.sort || !body.sort?.prop) {
body.sort = { prop: "order", asc: false };
}
const res = await this.service.page({
query: body.query,
page: body.page,
sort: body.sort,
buildQuery: bq => {
if (title) {
bq.andWhere("(title like :title or content like :content)", { title: `%${title}%`, content: `%${title}%` });
}
},
});
return this.ok(res);
}
@Post("/delete", { description: "sys:settings:edit", summary: "管理员删除用户流水线" })
async delete(@Query("id") id: number) {
await this.service.delete(id);
return this.ok();
}
@Post("/batchDelete", { description: "sys:settings:edit", summary: "管理员批量删除用户流水线" })
async batchDelete(@Body("ids") ids: number[]) {
checkPlus();
await this.service.batchDelete(ids);
return this.ok();
}
}
@@ -1,7 +1,7 @@
import { BaseService, ValidateException } from "@certd/lib-server";
import { Provide, Scope, ScopeEnum } from "@midwayjs/core";
import { InjectEntityModel } from "@midwayjs/typeorm";
import { BaseService, ValidateException } from "@certd/lib-server";
import { IsNull, Repository } from "typeorm";
import { Repository } from "typeorm";
import { CertApplyTemplateEntity } from "../entity/cert-apply-template.js";
import { CertApplyTemplateParams, pickCertApplyCustomParams, pickCertApplyTemplateParams } from "./cert-apply-template-fields.js";