refactor(plugin): 简化插件模块代码,复用BaseService方法

This commit is contained in:
xiaojunnuo
2026-08-02 02:48:39 +08:00
parent 255960d434
commit 0bf323ab97
7 changed files with 124 additions and 248 deletions
+2
View File
@@ -76,6 +76,7 @@ Certd 是可私有化部署的 SSL/TLS 证书自动化管理平台,提供 Web
- 优先沿用现有模块、插件、service、页面模式;不要为形式上的复用制造过度抽象。
- 代码可读性优先于短写法。复杂条件、三元表达式、链式调用、内联对象和多层 helper 调用要拆成命名清晰的中间变量或小方法。
- 方法调用链不要直接塞进另一个方法参数;先用有意义的局部变量承接返回值,再传入下一步。
- 不要在单一表达式内嵌套分支、对象构造与方法调用。优先使用清晰的 `if/else` 分支;仅在确实能降低复杂度时才提取有意义的中间变量,避免为拆分而增加阅读跳转。
- 注释优先使用中文,尤其是业务规则、兼容逻辑、协议细节和隐藏风险;文件已有英文风格或引用外部术语时可保持一致。
- 遵守 DRY 和单一职责;第三次出现的业务规则、字段转换、权限判断、Repository 选择、事务传播、金额计算等逻辑,应优先抽成合适 helper 或 service 方法。
@@ -104,6 +105,7 @@ Certd 是可私有化部署的 SSL/TLS 证书自动化管理平台,提供 Web
- 只有需要事务传播时才定义 `ctx`;普通查询、纯函数和简单私有方法继续使用明确参数。
- 需要按事务上下文取 Repository 时,用 `BaseService.getRepo(ctx, EntityClass)`
- 需要“有事务则复用、无事务则开启”时,用 `BaseService.transactionWithCtx(ctx, callback)`
- 基础 CRUD 数据访问优先复用 `BaseService``find``findOne``list``page``update``deleteWhere` 等方法;不要从 `repository.createQueryBuilder()` 开始重复实现完整查询或更新。仅将关键词组合筛选、联表、业务排序等基类无法表达的部分放入 `list/page``buildQuery`
- 拼接可选 `projectId` 查询条件时,**必须**使用 `BaseService.buildUserProjectQuery(userId, projectId)`,禁止直接写 `{ userId, projectId }`。因为 `projectId` 可能为 `null`/`undefined`,直接放入查询会生成错误的 `WHERE projectId = NULL` 条件。
- `ctx` 类型复用 `BaseService` 导出的 `ServiceContext`
- 新增 service 方法避免与 `BaseService` 方法签名冲突,例如不要用 `delete(id)` 覆盖 `delete(ids, where?)`;改用 `deleteById` 等具体名称。
@@ -102,6 +102,18 @@ export abstract class BaseService<T> {
});
}
/**
* 按条件直接更新,不触发子类 update 的业务生命周期。
*/
async updateWhere(where: any, data: any) {
await this.getRepository().update(
{
...where,
},
data
);
}
/**
* 删除
* @param ids 删除的ID集合 如:[1,2,3] 或者 1,2,3
+5 -5
View File
@@ -34,11 +34,11 @@
"@aws-sdk/s3-request-presigner": "^3.964.0",
"@certd/vue-js-cron-light": "^4.0.14",
"@ctrl/tinycolor": "^4.1.0",
"@fast-crud/editor-code": "^1.28.1",
"@fast-crud/fast-crud": "^1.28.1",
"@fast-crud/fast-extends": "^1.28.1",
"@fast-crud/ui-antdv4": "^1.28.1",
"@fast-crud/ui-interface": "^1.28.1",
"@fast-crud/editor-code": "^1.28.3",
"@fast-crud/fast-crud": "^1.28.3",
"@fast-crud/fast-extends": "^1.28.3",
"@fast-crud/ui-antdv4": "^1.28.3",
"@fast-crud/ui-interface": "^1.28.3",
"@iconify/tailwind": "^1.2.0",
"@iconify/vue": "^4.1.1",
"@manypkg/get-packages": "^2.2.2",
@@ -42,22 +42,6 @@ export async function GetObj(id: any) {
});
}
export async function GetDetail(id: any) {
return await request({
url: apiPrefix + "/detail",
method: "post",
params: { id },
});
}
export async function DeleteBatch(ids: any[]) {
return await request({
url: apiPrefix + "/deleteByIds",
method: "post",
data: { ids },
});
}
export async function SetDisabled(data: { id?: number; name?: string; type?: string; disabled: boolean }) {
return await request({
url: apiPrefix + "/setDisabled",
@@ -125,15 +109,6 @@ export type OnlinePluginVersionBean = {
aiCheckResult?: string;
};
export type OnlinePluginCommentBean = {
id?: number;
createdAt?: number;
updatedAt?: number;
userId?: number;
score?: number;
comment?: string;
};
export async function OnlinePluginList(body: { pluginType?: string; group?: string; keyword?: string }): Promise<OnlinePluginBean[]> {
return await request({
url: apiPrefix + "/online/list",
@@ -164,37 +139,6 @@ export async function OnlinePluginInstall(body: { fullName: string; version?: st
});
}
export async function OnlinePluginDetail(body: { pluginId?: number; fullName?: string; commentPageNo?: number; commentPageSize?: number }): Promise<{
plugin: OnlinePluginBean;
versions: OnlinePluginVersionBean[];
myScore?: number;
myComment?: string;
comments?: OnlinePluginCommentBean[];
commentsTotal?: number;
}> {
return await request({
url: apiPrefix + "/online/detail",
method: "post",
data: body,
});
}
export async function OnlinePluginSource(body: { pluginId?: number; fullName?: string; version?: string }): Promise<{ plugin: OnlinePluginBean; version: OnlinePluginVersionBean; content: string }> {
return await request({
url: apiPrefix + "/online/source",
method: "post",
data: body,
});
}
export async function OnlinePluginRate(body: { pluginId?: number; fullName?: string; score: number; comment: string }): Promise<{ plugin: OnlinePluginBean; myScore?: number; myComment?: string }> {
return await request({
url: apiPrefix + "/online/rate",
method: "post",
data: body,
});
}
export async function OnlinePluginSubmitVersion(body: { fullName: string; version: string; content: string; minAppVersion?: string; maxAppVersion?: string }) {
return await request({
url: apiPrefix + "/online/version/submit",
@@ -301,14 +245,6 @@ export async function savePluginSetting(req: { name: string; sysSetting: any }):
});
}
export async function DoTest(req: { id: number; input: any }): Promise<void> {
return await request({
url: apiPrefix + "/doTest",
method: "post",
data: req,
});
}
export async function GetPluginByName(name: string): Promise<PluginConfigBean> {
return await request({
url: apiPrefix + "/getPluginByName",
@@ -3,13 +3,10 @@ import { merge } from "lodash-es";
import { CrudController } from "@certd/lib-server";
import {
OnlinePluginAuthorAddReq,
OnlinePluginDetailReq,
OnlinePluginInstallReq,
OnlinePluginListReq,
OnlinePluginPublishInfoReq,
OnlinePluginPublishReq,
OnlinePluginRateReq,
OnlinePluginSourceReq,
OnlinePluginVersionSubmitReq,
PluginImportReq,
PluginService,
@@ -42,11 +39,6 @@ export class PluginController extends CrudController<PluginService> {
return await super.page(body);
}
@Post("/list", { description: "sys:settings:view" })
async list(@Body(ALL) body: any) {
return super.list(body);
}
@Post("/add", { description: "sys:settings:edit", summary: "添加插件" })
async add(@Body(ALL) bean: any) {
const def: any = {
@@ -84,13 +76,6 @@ export class PluginController extends CrudController<PluginService> {
return this.ok(res);
}
@Post("/deleteByIds", { description: "sys:settings:edit", summary: "批量删除插件" })
async deleteByIds(@Body("ids") ids: number[]) {
const res = await this.service.deleteByIds(ids);
await this.auditLog({ content: `批量删除了${ids.length}条插件` });
return this.ok(res);
}
@Post("/setDisabled", { description: "sys:settings:edit", summary: "禁用/启用插件" })
async setDisabled(@Body(ALL) body: { id: number; name: string; type: string; disabled: boolean }) {
await this.service.setDisabled(body);
@@ -159,25 +144,6 @@ export class PluginController extends CrudController<PluginService> {
return this.ok(res);
}
@Post("/online/detail", { description: "sys:settings:view", summary: "查看在线插件详情" })
async onlineDetail(@Body(ALL) body: OnlinePluginDetailReq) {
const res = await this.service.onlinePluginDetail(body);
return this.ok(res);
}
@Post("/online/source", { description: "sys:settings:view", summary: "查看在线插件源代码" })
async onlineSource(@Body(ALL) body: OnlinePluginSourceReq) {
const res = await this.service.onlinePluginSource(body);
return this.ok(res);
}
@Post("/online/rate", { description: "sys:settings:edit", summary: "在线插件评分" })
async onlineRate(@Body(ALL) body: OnlinePluginRateReq) {
const res = await this.service.rateOnlinePlugin(body);
await this.auditLog({ content: `给在线插件「${body.fullName || body.pluginId}」评分` });
return this.ok(res);
}
@Post("/online/version/submit", { description: "sys:settings:edit", summary: "提交在线插件版本" })
async onlineVersionSubmit(@Body(ALL) body: OnlinePluginVersionSubmitReq) {
const res = await this.service.submitOnlinePluginVersion(body);
@@ -13,6 +13,7 @@ function createPluginRepositoryMock(options: RepoOptions = {}) {
const state = {
savedRows: [] as any[],
updates: [] as any[],
findCalls: 0,
queryRows: options.queryRows || options.marketRows || [],
installedRows: options.installedRows || [],
existingStoreRows: options.existingStoreRows || [],
@@ -179,7 +180,11 @@ function createPluginRepositoryMock(options: RepoOptions = {}) {
return record;
},
async find(options?: any) {
state.findCalls += 1;
if (options?.where?.type === "store") {
if (options.where.installed === true) {
return state.installedRows;
}
return state.existingStoreRows;
}
return [];
@@ -244,6 +249,15 @@ describe("PluginService online plugins", () => {
latest: "1.0.1",
title: "Deploy Demo",
},
{
type: "store",
fullName: "greper/DeployToOther",
author: "greper",
name: "DeployToOther",
pluginType: "deploy",
latest: "1.0.1",
title: "Deploy Other",
},
],
installedRows: [
{
@@ -263,11 +277,13 @@ describe("PluginService online plugins", () => {
const list = await service.onlinePluginList({});
assert.equal(list.length, 1);
assert.equal(list.length, 2);
assert.equal(list[0].installed, true);
assert.equal(list[0].installedVersion, "1.0.0");
assert.equal(list[0].upgradeAvailable, true);
assert.equal(list[0].localPluginId, 3);
assert.equal(list[1].installed, false);
assert.equal((service.repository as any).state.findCalls, 1);
});
it("does not treat uninstalled market rows as installed plugins", async () => {
@@ -675,7 +691,6 @@ describe("PluginService online plugins", () => {
assert.equal(requestConfig.url, "/activation/plugin/publish");
assert.equal(requestConfig.method, "post");
assert.deepEqual(requestConfig.data, {
userId: 12,
content: "name: custom-one\nversion: 1.2.3\n",
version: "1.2.3",
minAppVersion: "",
@@ -719,15 +734,12 @@ describe("PluginService online plugins", () => {
assert.deepEqual(requestConfigs[0], {
url: "/activation/plugin/author/get",
method: "post",
data: {
userId: 23,
},
data: {},
});
assert.deepEqual(requestConfigs[1], {
url: "/activation/plugin/author/add",
method: "post",
data: {
userId: 23,
name: "developer",
displayName: "Developer",
avatar: "",
@@ -38,15 +38,6 @@ export type OnlinePluginDetailReq = {
commentPageSize?: number;
};
export type OnlinePluginSourceReq = OnlinePluginDetailReq & {
version?: string;
};
export type OnlinePluginRateReq = OnlinePluginDetailReq & {
score: number;
comment: string;
};
export type OnlinePluginVersionSubmitReq = {
fullName: string;
version: string;
@@ -356,7 +347,7 @@ export class PluginService extends BaseService<PluginEntity> {
}
// 初始化设置
const settingPlugins = await this.repository.find({
const settingPlugins = await this.find({
select: {
id: true,
name: true,
@@ -459,7 +450,7 @@ export class PluginService extends BaseService<PluginEntity> {
}
if (id > 0) {
//update
await this.repository.update({ id }, { disabled });
await this.updateWhere({ id }, { disabled });
return;
}
@@ -484,7 +475,7 @@ export class PluginService extends BaseService<PluginEntity> {
*/
async add(param: any) {
param.type = normalizePluginSourceType(param.type);
const old = await this.repository.findOne({
const old = await this.findOne({
where: {
name: param.name,
author: param.author,
@@ -609,61 +600,52 @@ export class PluginService extends BaseService<PluginEntity> {
private async findOnlinePluginRecords(req: OnlinePluginListReq) {
const query = req || {};
const keyword = (query.keyword || "").trim().toLowerCase();
const builder = this.repository.createQueryBuilder("item");
builder.where("item.type = :type", {
const listQuery: Partial<PluginEntity> = {
type: "store",
});
};
if (query.pluginType) {
builder.andWhere("item.pluginType = :pluginType", {
pluginType: query.pluginType,
});
listQuery.pluginType = query.pluginType;
}
if (query.group) {
builder.andWhere("item.group = :group", {
group: query.group,
});
listQuery.group = query.group;
}
if (keyword) {
builder.andWhere(
new Brackets(qb => {
qb.where("LOWER(COALESCE(item.fullName, '')) LIKE :keyword", { keyword: `%${keyword}%` })
.orWhere("LOWER(item.author) LIKE :keyword", { keyword: `%${keyword}%` })
.orWhere("LOWER(item.name) LIKE :keyword", { keyword: `%${keyword}%` })
.orWhere("LOWER(item.title) LIKE :keyword", { keyword: `%${keyword}%` })
.orWhere("LOWER(item.desc) LIKE :keyword", { keyword: `%${keyword}%` })
.orWhere("LOWER(item.group) LIKE :keyword", { keyword: `%${keyword}%` })
.orWhere("LOWER(item.pluginType) LIKE :keyword", { keyword: `%${keyword}%` });
})
);
}
return await builder.orderBy("item.pluginType", "ASC").addOrderBy("item.group", "ASC").addOrderBy("item.title", "ASC").addOrderBy("item.fullName", "ASC").addOrderBy("item.id", "ASC").getMany();
return await this.list({
query: listQuery,
buildQuery: builder => {
if (keyword) {
builder.andWhere(
new Brackets(qb => {
qb.where("LOWER(COALESCE(main.fullName, '')) LIKE :keyword", { keyword: `%${keyword}%` })
.orWhere("LOWER(main.author) LIKE :keyword", { keyword: `%${keyword}%` })
.orWhere("LOWER(main.name) LIKE :keyword", { keyword: `%${keyword}%` })
.orWhere("LOWER(main.title) LIKE :keyword", { keyword: `%${keyword}%` })
.orWhere("LOWER(main.desc) LIKE :keyword", { keyword: `%${keyword}%` })
.orWhere("LOWER(main.group) LIKE :keyword", { keyword: `%${keyword}%` })
.orWhere("LOWER(main.pluginType) LIKE :keyword", { keyword: `%${keyword}%` });
})
);
}
builder.orderBy("main.pluginType", "ASC").addOrderBy("main.group", "ASC").addOrderBy("main.title", "ASC").addOrderBy("main.fullName", "ASC").addOrderBy("main.id", "ASC");
},
});
}
private async findInstalledOnlinePlugin(parsed: { author: string; name: string }, record: OnlinePluginBean) {
private findInstalledOnlinePlugin(installedPlugins: PluginEntity[], parsed: { author: string; name: string }, record: OnlinePluginBean) {
const fullName = record.fullName || `${parsed.author}/${parsed.name}`;
const builder = this.repository
.createQueryBuilder("plugin")
.where("plugin.type = :type", { type: "store" })
.andWhere("plugin.installed = :installed", { installed: true })
.andWhere(
new Brackets(qb => {
qb.where("plugin.fullName = :fullName", { fullName }).orWhere("plugin.name = :fullName", { fullName }).orWhere("plugin.author = :author AND plugin.name = :name", {
author: parsed.author,
name: parsed.name,
});
if (record.pluginType) {
qb.orWhere("plugin.pluginType = :pluginType AND plugin.name = :name", {
pluginType: record.pluginType,
name: parsed.name,
});
}
})
);
return await builder.getOne();
return installedPlugins.find(plugin => {
if (plugin.fullName === fullName || plugin.name === fullName) {
return true;
}
if (plugin.author === parsed.author && plugin.name === parsed.name) {
return true;
}
return !!record.pluginType && plugin.pluginType === record.pluginType && plugin.name === parsed.name;
});
}
private async attachOnlineInstallState(list: OnlinePluginBean[]) {
const records: OnlinePluginBean[] = [];
const parsedRecords = new Map<OnlinePluginBean, { author: string; name: string }>();
let bindUserId = 0;
if (this.sysSettingsService) {
const installInfo = await this.sysSettingsService.getSetting<SysInstallInfo>(SysInstallInfo);
@@ -686,14 +668,31 @@ export class PluginService extends BaseService<PluginEntity> {
continue;
}
const localPlugin = await this.findInstalledOnlinePlugin(parsed, record);
parsedRecords.set(record, parsed);
records.push(record);
}
const installedPlugins =
parsedRecords.size > 0
? await this.find({
where: {
type: "store",
installed: true,
},
})
: [];
for (const record of records) {
const parsed = parsedRecords.get(record);
if (!parsed) {
continue;
}
const localPlugin = this.findInstalledOnlinePlugin(installedPlugins, parsed, record);
record.installed = !!localPlugin;
record.localPluginId = localPlugin?.id;
record.localDisabled = localPlugin?.disabled;
record.installedVersion = localPlugin?.version;
record.upgradeAvailable = localPlugin ? isOnlinePluginUpgradeAvailable(localPlugin.version, record.latest) : false;
record.localEditable = canEditStorePlugin(localPlugin?.developerId ?? record.developerId, bindUserId);
records.push(record);
}
return records;
}
@@ -751,7 +750,7 @@ export class PluginService extends BaseService<PluginEntity> {
}
pageStart += ONLINE_PLUGIN_SYNC_PAGE_SIZE;
}
const existingList = await this.repository.find({
const existingList = await this.find({
where: {
type: "store",
},
@@ -767,7 +766,7 @@ export class PluginService extends BaseService<PluginEntity> {
const records = Array.from(pluginMap.values()).map(item => {
return this.normalizeOnlinePluginRecord(item, syncTime, existingMap.get(item.fullName));
});
await this.repository.save(records);
await this.addOrUpdate(records);
await this.saveOnlinePluginSyncTime(syncTime);
return await this.onlinePluginList({});
}
@@ -810,14 +809,12 @@ export class PluginService extends BaseService<PluginEntity> {
async onlinePluginDetail(req: OnlinePluginDetailReq) {
await this.plusService.register();
const installInfo = await this.sysSettingsService.getSetting<SysInstallInfo>(SysInstallInfo);
const res = await this.plusService.request({
url: "/activation/plugin/detail",
method: "post",
data: {
pluginId: req.pluginId || 0,
fullName: req.fullName || "",
userId: installInfo.bindUserId || 0,
commentPageNo: req.commentPageNo || 1,
commentPageSize: req.commentPageSize || 5,
},
@@ -829,52 +826,6 @@ export class PluginService extends BaseService<PluginEntity> {
return res;
}
async onlinePluginSource(req: OnlinePluginSourceReq) {
await this.plusService.register();
const installInfo = await this.sysSettingsService.getSetting<SysInstallInfo>(SysInstallInfo);
return await this.plusService.request({
url: "/activation/plugin/source",
method: "post",
data: {
pluginId: req.pluginId || 0,
fullName: req.fullName || "",
version: req.version || "",
userId: installInfo.bindUserId || 0,
},
});
}
async rateOnlinePlugin(req: OnlinePluginRateReq) {
const installInfo = await this.sysSettingsService.getSetting<SysInstallInfo>(SysInstallInfo);
if (!installInfo.bindUserId) {
throw new Error("请先绑定账号后再评分");
}
await this.plusService.register();
const res = await this.plusService.request({
url: "/activation/plugin/rate",
method: "post",
data: {
pluginId: req.pluginId || 0,
fullName: req.fullName || "",
userId: installInfo.bindUserId,
score: req.score,
comment: req.comment,
},
});
if (res?.plugin?.score != null) {
await this.repository.update(
{
type: "store",
fullName: res.plugin.fullName || req.fullName,
},
{
score: res.plugin.score,
}
);
}
return res;
}
async submitOnlinePluginVersion(req: OnlinePluginVersionSubmitReq) {
const installInfo = await this.sysSettingsService.getSetting<SysInstallInfo>(SysInstallInfo);
if (!installInfo.bindUserId) {
@@ -885,7 +836,6 @@ export class PluginService extends BaseService<PluginEntity> {
url: "/activation/plugin/version/submit",
method: "post",
data: {
userId: installInfo.bindUserId,
fullName: req.fullName,
version: req.version,
content: req.content,
@@ -919,7 +869,6 @@ export class PluginService extends BaseService<PluginEntity> {
url: "/activation/plugin/publish",
method: "post",
data: {
userId: installInfo.bindUserId,
content,
version: req.version || plugin.version || "",
minAppVersion: req.minAppVersion || "",
@@ -955,25 +904,22 @@ export class PluginService extends BaseService<PluginEntity> {
}
async getOnlinePluginAuthor(): Promise<OnlinePluginAuthorGetReply> {
const bindUserId = await this.getBindUserId("发布插件");
await this.getBindUserId("发布插件");
await this.plusService.register();
return await this.plusService.request({
url: "/activation/plugin/author/get",
method: "post",
data: {
userId: bindUserId,
},
data: {},
});
}
async addOnlinePluginAuthor(req: OnlinePluginAuthorAddReq): Promise<OnlinePluginAuthorBean> {
const bindUserId = await this.getBindUserId("注册插件作者");
await this.getBindUserId("注册插件作者");
await this.plusService.register();
return await this.plusService.request({
url: "/activation/plugin/author/add",
method: "post",
data: {
userId: bindUserId,
name: req.name,
displayName: req.displayName || "",
avatar: req.avatar || "",
@@ -1042,7 +988,7 @@ export class PluginService extends BaseService<PluginEntity> {
if (!fullName || downloadCount == null) {
return;
}
await this.repository.update(
await this.updateWhere(
{
type: "store",
fullName,
@@ -1092,7 +1038,7 @@ export class PluginService extends BaseService<PluginEntity> {
async update(param: any) {
param.type = normalizePluginSourceType(param.type);
const old = await this.repository.findOne({
const old = await this.findOne({
where: {
name: param.name,
author: param.author,
@@ -1303,27 +1249,29 @@ export class PluginService extends BaseService<PluginEntity> {
const fullName = loaded.fullName || (loaded.author && loaded.name ? `${loaded.author}/${loaded.name}` : "");
const entityType = normalizePluginSourceType(req.type || loaded.type);
const old =
entityType === "store"
? await this.repository.findOne({
where: [
{
type: "store",
fullName,
},
{
type: "store",
author: loaded.author,
name: loaded.name,
},
],
})
: await this.repository.findOne({
where: {
name: loaded.name,
author: loaded.author,
},
});
let old: PluginEntity | null;
if (entityType === "store") {
old = await this.findOne({
where: [
{
type: "store",
fullName,
},
{
type: "store",
author: loaded.author,
name: loaded.name,
},
],
});
} else {
old = await this.findOne({
where: {
name: loaded.name,
author: loaded.author,
},
});
}
const metadata = {
input: loaded.input,
@@ -1381,7 +1329,7 @@ export class PluginService extends BaseService<PluginEntity> {
}
await this.unRegisterById(id);
if (item.type === "store" && item.developerId) {
await this.repository.update(
await this.updateWhere(
{ id },
{
installed: false,