perf: cname记录支持批量导入和导出

This commit is contained in:
xiaojunnuo
2026-01-22 10:56:45 +08:00
parent a97cee84f3
commit 607afe864a
10 changed files with 166 additions and 9 deletions

View File

@@ -60,7 +60,7 @@ export async function DeleteBatch(ids: any[]) {
export async function SyncSubmit(body: any) {
return await request({
url: apiPrefix + "/sync/submit",
url: apiPrefix + "/sync/import",
method: "post",
data: body,
});

View File

@@ -157,6 +157,9 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
expirationDate: {
title: t("certd.domain.expirationDate"),
type: "date",
form: {
show: false,
},
},
challengeType: {
title: t("certd.domain.challengeType"),

View File

@@ -77,3 +77,11 @@ export async function ResetStatus(id: number) {
},
});
}
export async function Import(form: { domainList: string; cnameProviderId: any }) {
return await request({
url: apiPrefix + "/import",
method: "post",
data: form,
});
}

View File

@@ -7,7 +7,9 @@ import { useUserStore } from "/@/store/user";
import { useSettingStore } from "/@/store/settings";
import { message, Modal } from "ant-design-vue";
import CnameTip from "/@/components/plugins/cert/domains-verify-plan-editor/cname-tip.vue";
import { useCnameImport } from "./use";
export default function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet {
const crudBinding = crudExpose.crudBinding;
const router = useRouter();
const { t } = useI18n();
const pageRequest = async (query: UserPageQuery): Promise<UserPageRes> => {
@@ -27,10 +29,13 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
return res;
};
const openCnameImportDialog = useCnameImport();
const userStore = useUserStore();
const settingStore = useSettingStore();
const selectedRowKeys: Ref<any[]> = ref([]);
context.selectedRowKeys = selectedRowKeys;
const dictRef = dict({
data: [
{ label: t("certd.pending_cname_setup"), value: "cname", color: "warning" },
@@ -64,6 +69,32 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
editRequest,
delRequest,
},
actionbar: {
buttons: {
import: {
title: "导入CNAME记录",
type: "primary",
text: "批量导入",
click: () => {
openCnameImportDialog({
afterSubmit: () => {
setTimeout(() => {
crudExpose?.doRefresh();
}, 2000);
},
});
},
},
export: {
title: "导出CNAME记录之后可用于批量导入cname解析到域名注册商",
type: "primary",
text: "批量导出",
click: () => {
crudBinding.value.toolbar.buttons.export.click({});
},
},
},
},
tabs: {
name: "status",
show: true,

View File

@@ -0,0 +1,56 @@
import { dict } from "@fast-crud/fast-crud";
import { message } from "ant-design-vue";
import * as api from "./api";
import { useFormDialog } from "/@/use/use-dialog";
export const cnameProviderDict = dict({
url: "/cname/provider/list",
value: "id",
label: "domain",
});
export function useCnameImport() {
const { openFormDialog } = useFormDialog();
const columns = {
domainList: {
title: "域名列表",
type: "text",
form: {
component: {
name: "a-textarea",
rows: 5,
},
col: {
span: 24,
},
required: true,
helper: "每个域名一行,批量导入\n泛域名请去掉*.\n已经存在的会自动跳过",
},
},
cnameProviderId: {
title: "CNAME服务",
type: "dict-select",
dict: cnameProviderDict,
form: {
required: true,
},
},
};
return function openCnameImportDialog(req: { afterSubmit?: () => void }) {
openFormDialog({
title: "导入CNAME记录",
columns: columns,
onSubmit: async (form: any) => {
await api.Import({
domainList: form.domainList,
cnameProviderId: form.cnameProviderId,
});
message.success("导入任务已提交");
if (req.afterSubmit) {
req.afterSubmit();
}
},
});
};
}

View File

@@ -1,6 +1,7 @@
import { ALL, Body, Controller, Inject, Post, Provide, Query } from '@midwayjs/core';
import { Constants, CrudController } from '@certd/lib-server';
import {DomainService} from "../../../modules/cert/service/domain-service.js";
import { checkPlus } from '@certd/plus-core';
/**
* 授权
@@ -79,12 +80,13 @@ export class DomainController extends CrudController<DomainService> {
}
@Post('/sync/submit', { summary: Constants.per.authOnly })
async syncSubmit(@Body(ALL) body: any) {
@Post('/sync/import', { summary: Constants.per.authOnly })
async syncImport(@Body(ALL) body: any) {
const { dnsProviderType, dnsProviderAccessId } = body;
const req = {
dnsProviderType, dnsProviderAccessId, userId: this.getUserId(),
}
checkPlus()
await this.service.doSyncFromProvider(req);
return this.ok();
}

View File

@@ -99,4 +99,15 @@ export class CnameRecordController extends CrudController<CnameRecordService> {
const res = await this.service.resetStatus(body.id);
return this.ok(res);
}
@Post('/import', { summary: Constants.per.authOnly })
async import(@Body(ALL) body: { domainList: string; cnameProviderId: any }) {
const userId = this.getUserId();
const res = await this.service.doImport({
userId,
domainList: body.domainList,
cnameProviderId: body.cnameProviderId,
});
return this.ok(res);
}
}

View File

@@ -350,9 +350,9 @@ export class DomainService extends BaseService<DomainEntity> {
const getDomainPage = async (pager: Pager) => {
const pageRes = await this.page({
query: query,
buildQuery(bq) {
bq.andWhere(" (expiration_date is null or expiration_date < :now) ", { now: dayjs().add(1, 'month').valueOf() })
},
// buildQuery(bq) {
// bq.andWhere(" (expiration_date is null or expiration_date < :now) ", { now: dayjs().add(1, 'month').valueOf() })
// },
page: {
offset: pager.getOffset(),
limit: pager.pageSize,

View File

@@ -9,7 +9,7 @@ export class BackTaskExecutor {
}
const oldTask = this.tasks[type][task.key]
if (oldTask && oldTask.status === "running") {
throw new Error(`任务 ${task.key} 正在运行中`)
throw new Error(`任务 ${type}${task.key} 正在运行中`)
}
this.tasks[type][task.key] = task
this.run(type, task);
@@ -39,7 +39,7 @@ export class BackTaskExecutor {
private async run(type: string, task: any) {
if (task.status === "running") {
throw new Error(`任务 ${task.key} 正在运行中`)
throw new Error(`任务 ${type}${task.key} 正在运行中`)
}
task.startTime = Date.now();
task.clearTimeout();
@@ -47,7 +47,7 @@ export class BackTaskExecutor {
task.status = "running";
return await task.run(task);
} catch (e) {
logger.error(`任务 ${task.title}[${task.key}] 执行失败`, e.message);
logger.error(`任务 ${task.title}[${type}-${task.key}] 执行失败`, e.message);
task.status = "failed";
task.error = e.message;
} finally {

View File

@@ -22,6 +22,7 @@ import punycode from "punycode.js";
import { SubDomainService } from "../../pipeline/service/sub-domain-service.js";
import { SubDomainsGetter } from "../../pipeline/service/getter/sub-domain-getter.js";
import { TaskServiceBuilder } from "../../pipeline/service/getter/task-service-getter.js";
import { BackTask, taskExecutor } from "../../cert/service/task-executor.js";
type CnameCheckCacheValue = {
validating: boolean;
@@ -487,4 +488,49 @@ export class CnameRecordService extends BaseService<CnameRecordEntity> {
}
await this.getRepository().update(id, { status: "cname", mainDomain: "" });
}
async doImport(req:{ userId: number; domainList: string; cnameProviderId: any }) {
const {userId,cnameProviderId,domainList} = req;
const domains = domainList.split("\n").map(item => item.trim()).filter(item => item.length > 0);
if (domains.length === 0) {
throw new ValidateException("域名列表不能为空");
}
if (!req.cnameProviderId) {
throw new ValidateException("CNAME服务提供商不能为空");
}
taskExecutor.start("cnameImport",new BackTask({
key: "user_"+userId,
title: "导入CNAME记录",
run: async (task) => {
await this._import({ userId, domains, cnameProviderId },task);
}
}));
}
async _import(req :{ userId: number; domains: string[]; cnameProviderId: any },task:BackTask) {
const userId = req.userId;
for (const domain of req.domains) {
const old = await this.getRepository().findOne({
where: {
userId: req.userId,
domain,
},
});
if (old) {
logger.warn(`域名${domain}已存在,跳过`);
}
//开始导入
try{
await this.add({
userId,
domain: domain,
cnameProviderId: req.cnameProviderId,
});
}catch(e){
logger.error(`导入域名${domain}失败:${e.message}`);
}
}
}
}