diff --git a/AGENTS.md b/AGENTS.md index cd4f1545c..05d393942 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -117,6 +117,7 @@ Certd 是一个支持私有化部署的 SSL/TLS 证书自动化管理平台。 - 开发或重构这类页面前,先读取 `.trae/skills/fast-crud-page-dev/SKILL.md`,按仓库内 Fast Crud 页面拆分与验证方式实现。 - 前端对话框里只做纯确认时可以使用 `Modal.confirm`;只要对话框里有字段输入、表单校验或提交字段,统一使用 `useFormDialog` / `openFormDialog`,不要在 `Modal.confirm` 的 `content` 里手写输入框。 - 页面内嵌 Fast Crud 表格时,要显式给外层容器稳定高度或 `flex: 1; min-height: 0` 的撑满链路;Fast Crud 依赖外部元素高度,不能只依赖表格默认高度。 +- 后台管理列表里展示或筛选用户字段时,优先参考 `packages/ui/certd-client/src/views/sys/suite/user-suite/crud.tsx` 的 `userId` 字段模式:前端使用 `table-select` + `/sys/authority/user/getSimpleUserByIds` 字典回显和搜索;不要为了展示用户名让后端列表接口额外 `fillSimpleUser` / `userDisplay`,除非该接口本身就是用户端业务列表且已有明确模式。 ## 流水线与插件模型 @@ -180,6 +181,11 @@ Certd 是一个支持私有化部署的 SSL/TLS 证书自动化管理平台。 - 使用 `/basic/file/upload` 上传文件后,接口返回的是临时缓存 key。业务保存表单或设置时,后端必须调用 `FileService.saveFile(userId, key, "public" | "private")` 转成永久文件 key 后再入库/入设置;不要直接保存 `tmpfile_key_...`,否则后续回显或下载会失效。 - 本仓库代码注释优先使用中文,尤其是解释业务规则、兼容逻辑、协议细节和隐藏风险时;除非文件已有明确英文注释风格或引用外部英文术语,否则不要新增英文说明性注释。 - 代码可读性优先于短写法。遇到包含业务分支的复杂三元表达式、内联对象、链式调用或条件组合时,优先拆成命名清晰的中间变量、独立分支或小函数,让读代码的人能一眼看出业务意图;不要为了少写几行把逻辑压成难读的一坨。 +- 遵守 DRY 原则:同一业务规则、字段转换、权限判断、Repository 选择、事务传播、金额计算等逻辑不要在多个地方复制粘贴。第二次出现时可以先保持清晰,第三次出现前应优先抽成局部 helper、service 方法或已有公共工具;抽象要服务于减少真实重复和降低修改风险,不要为了形式上的“复用”制造过度设计。 +- 遵守单一职责原则:一个方法只负责一个清晰的业务步骤或技术步骤。流程编排方法可以串联多个步骤,但具体的校验、计算、持久化、状态变更、展示数据组装应尽量拆到命名明确的小方法中;不要让一个方法同时承担查询、校验、计算、写库、格式化返回等过多职责。 +- 后端方法参数超过 3 个时,尽量改为对象参数传入;需要传入 `manager` / `EntityManager` 做事务传播的方法,必须使用对象参数,不要把 `manager` 作为位置参数藏在参数列表末尾。 +- 后端 service 层只有存在事务链路传播需求时才定义 `ctx`,不要为了将来可能需要而提前给普通方法加 `ctx`。事务链路方法统一采用 `method(ctx, req)` 形式,`ctx` 放第一位并承载 `manager?: EntityManager` 等横切上下文,业务参数放在 `req` 对象里,例如 `settleCommission({ manager }, { tradeId, userId, amount })`。无事务链路需求的普通查询、纯函数和简单私有方法继续使用明确参数。 +- service 内部需要根据事务上下文选择 Repository 时,优先使用 `BaseService.getRepo(ctx, entity)`;不要在业务方法里反复写 `ctx.manager?.getRepository(Entity) || this.xxxRepository`。`ctx` 类型统一从 `BaseService` 导出的 `ServiceContext` 复用,不要在每个 service 里重复定义。 ## 插件开发技能 diff --git a/docs/guide/use/email/images/qq-11.png b/docs/guide/use/email/images/qq-11.png index 5a7d0b302..0b8b13f77 100644 Binary files a/docs/guide/use/email/images/qq-11.png and b/docs/guide/use/email/images/qq-11.png differ diff --git a/docs/guide/use/email/index.md b/docs/guide/use/email/index.md index a1905f3a0..de6e48548 100644 --- a/docs/guide/use/email/index.md +++ b/docs/guide/use/email/index.md @@ -2,13 +2,13 @@ ## 腾讯企业邮箱配置 -1. 开启smtp +1. 开启smtp ![](./images/qq-3.png) -2. 获取授权码作为密码 +2. 获取授权码作为密码 ![](./images/qq-1.png) ![](./images/qq-2.png) -3. 填写域名、端口和密码 - ![](./images/qq-0.png) +3. 填写域名、端口和密码 +![](./images/qq-0.png) ## QQ邮箱配置 1. smtp配置 @@ -19,5 +19,7 @@ smtp端口: 465 是否SSL: 是 ``` -2. 获取授权码 +2. 获取授权码 +登录qq邮箱,点击账号与安全 + ![](./images/qq-11.png) diff --git a/packages/libs/lib-server/src/basic/base-service.ts b/packages/libs/lib-server/src/basic/base-service.ts index 399a777e0..09dff7781 100644 --- a/packages/libs/lib-server/src/basic/base-service.ts +++ b/packages/libs/lib-server/src/basic/base-service.ts @@ -1,5 +1,5 @@ import { PermissionException, ValidateException } from './exception/index.js'; -import { FindOneOptions, In, Repository, SelectQueryBuilder } from 'typeorm'; +import { EntityTarget, FindOneOptions, In, Repository, SelectQueryBuilder } from 'typeorm'; import { Inject } from '@midwayjs/core'; import { TypeORMDataSourceManager } from '@midwayjs/typeorm'; import { EntityManager } from 'typeorm/entity-manager/EntityManager.js'; @@ -20,6 +20,10 @@ export type ListReq = { select?: any; }; +export type ServiceContext = { + manager?: EntityManager; +}; + /** * 服务基类 */ @@ -34,6 +38,14 @@ export abstract class BaseService { return await dataSource.transaction(callback as any); } + protected getRepo(ctx: ServiceContext, entity: EntityTarget): Repository { + if (ctx.manager) { + return ctx.manager.getRepository(entity); + } + const dataSource = this.dataSourceManager.getDataSource('default'); + return dataSource.getRepository(entity); + } + /** * 获得单个ID * @param id ID @@ -81,7 +93,7 @@ export abstract class BaseService { if (idArr.length === 0) { return; } - + await this.getRepository().delete({ id: In(idArr), ...where, @@ -283,4 +295,4 @@ export function checkUserProjectParam(userId: number, projectId: number) { } throw new ValidateException('userId不能为空'); } -} \ No newline at end of file +} diff --git a/packages/ui/certd-client/src/router/source/modules/sys.ts b/packages/ui/certd-client/src/router/source/modules/sys.ts index 155f239b9..a2c3987d9 100644 --- a/packages/ui/certd-client/src/router/source/modules/sys.ts +++ b/packages/ui/certd-client/src/router/source/modules/sys.ts @@ -1,5 +1,11 @@ import { useSettingStore } from "/@/store/settings"; +function isInviteLevelEnabled() { + const settingStore = useSettingStore(); + const levelEnabled = settingStore.inviteSetting?.levelEnabled; + return settingStore.isComm && levelEnabled === true; +} + export const sysResources = [ { title: "certd.sysResources.sysRoot", @@ -309,8 +315,7 @@ export const sysResources = [ component: "/sys/suite/invite/level.vue", meta: { show: () => { - const settingStore = useSettingStore(); - return settingStore.isComm; + return isInviteLevelEnabled(); }, icon: "ion:ribbon-outline", permission: "sys:settings:edit", @@ -325,8 +330,7 @@ export const sysResources = [ component: "/sys/suite/invite/user-level.vue", meta: { show: () => { - const settingStore = useSettingStore(); - return settingStore.isComm; + return isInviteLevelEnabled(); }, icon: "ion:people-outline", permission: "sys:settings:edit", diff --git a/packages/ui/certd-client/src/store/settings/api.basic.ts b/packages/ui/certd-client/src/store/settings/api.basic.ts index 05a6deb21..c6ada0f37 100644 --- a/packages/ui/certd-client/src/store/settings/api.basic.ts +++ b/packages/ui/certd-client/src/store/settings/api.basic.ts @@ -99,6 +99,8 @@ export type SuiteSetting = { }; export type InviteSetting = { enabled?: boolean; + levelEnabled?: boolean; + fixedCommissionRate?: number; }; export type SysPrivateSetting = { httpProxy?: string; diff --git a/packages/ui/certd-client/src/store/settings/index.tsx b/packages/ui/certd-client/src/store/settings/index.tsx index c68f14384..8857eb518 100644 --- a/packages/ui/certd-client/src/store/settings/index.tsx +++ b/packages/ui/certd-client/src/store/settings/index.tsx @@ -1,7 +1,7 @@ import { defineStore } from "pinia"; import { notification } from "ant-design-vue"; import * as basicApi from "./api.basic"; -import { AppInfo, HeaderMenus, PlusInfo, SiteEnv, SiteInfo, SuiteSetting, SysInstallInfo, SysPublicSetting } from "./api.basic"; +import { AppInfo, HeaderMenus, InviteSetting, PlusInfo, SiteEnv, SiteInfo, SuiteSetting, SysInstallInfo, SysPublicSetting } from "./api.basic"; import { useUserStore } from "../user"; import { mitter } from "/@/utils/util.mitt"; import { env } from "/@/utils/util.env"; @@ -30,9 +30,7 @@ export interface SettingState { headerMenus?: HeaderMenus; inited?: boolean; suiteSetting?: SuiteSetting; - inviteSetting?: { - enabled?: boolean; - }; + inviteSetting?: InviteSetting; app: { version?: string; time?: number; @@ -105,7 +103,7 @@ export const useSettingStore = defineStore({ menus: [], }, suiteSetting: { enabled: false }, - inviteSetting: { enabled: false }, + inviteSetting: { enabled: false, levelEnabled: false, fixedCommissionRate: 10 }, inited: false, app: { version: "", diff --git a/packages/ui/certd-client/src/views/certd/invite/index.vue b/packages/ui/certd-client/src/views/certd/invite/index.vue index 9ee1ca178..b9967e07c 100644 --- a/packages/ui/certd-client/src/views/certd/invite/index.vue +++ b/packages/ui/certd-client/src/views/certd/invite/index.vue @@ -45,7 +45,7 @@ -
+
@@ -61,6 +61,19 @@
+
+
+ +
+ 返佣比例 +
+ + 比例 + {{ inviteInfo.fixedCommissionRate || 0 }}% + + 好友付费后按此比例计算佣金 +
+
@@ -80,7 +93,7 @@ - +
推广越多,等级越高,返佣比例越高
@@ -88,13 +101,13 @@
¥ {{ amountToYuan(inviteInfo.summary.promotionAmount) }}
- - + +