diff --git a/AGENTS.md b/AGENTS.md index 3a008f4ec..f2f1c7c8a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -35,6 +35,8 @@ Certd 是一个支持私有化部署的 SSL/TLS 证书自动化管理平台。 - `packages/ui/certd-server/`:后端服务 - `packages/ui/certd-client/`:前端 Web 管理台 +`packages/pro/` 是独立 Git 工作区,使用 `packages/pro/.git` 管理。根仓库的 `git status` / `git diff` 默认看不到这里的实际改动;修改商业版代码后,要在 `packages/pro` 目录内单独执行 `git status` / `git diff` 检查。 + ## 后端 主要后端包:`packages/ui/certd-server`。 @@ -108,6 +110,14 @@ Certd 是一个支持私有化部署的 SSL/TLS 证书自动化管理平台。 - 不要运行前端 `pnpm tsc` / `vue-tsc`:当前依赖组合中 `vue-tsc@1.8.27` 会直接抛内部错误 `Search string not found: "/supportedTSExtensions = .*(?=;)/"`,不是有效的项目类型检查结果。 - 前端暂不跑单元测试;当前 `test:unit` 只是占位脚本 +前端列表管理页面约定: + +- 列表管理、后台管理、记录查询、CRUD 表格类页面,默认优先使用 Fast Crud(`@fast-crud/fast-crud`、`fs-crud`、`useFs`、`createCrudOptions`)实现。 +- 只有轻量只读展示、强交互自定义界面或已有页面模式明确不适合 Fast Crud 时,才手写 `a-table` / 自定义列表,并在回复中说明原因。 +- 开发或重构这类页面前,先读取 `.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 依赖外部元素高度,不能只依赖表格默认高度。 + ## 流水线与插件模型 项目最关键的架构概念是证书流水线。 @@ -168,6 +178,7 @@ Certd 是一个支持私有化部署的 SSL/TLS 证书自动化管理平台。 - `packages/ui/certd-server/data/`、`logs/`、生成的 metadata/dist 等通常视为运行时或构建产物,除非任务明确要求处理它们。 - 注意本地数据和配置里可能包含凭据、证书材料等敏感信息。 - 本仓库代码注释优先使用中文,尤其是解释业务规则、兼容逻辑、协议细节和隐藏风险时;除非文件已有明确英文注释风格或引用外部英文术语,否则不要新增英文说明性注释。 +- 代码可读性优先于短写法。遇到包含业务分支的复杂三元表达式、内联对象、链式调用或条件组合时,优先拆成命名清晰的中间变量、独立分支或小函数,让读代码的人能一眼看出业务意图;不要为了少写几行把逻辑压成难读的一坨。 ## 插件开发技能 @@ -182,6 +193,7 @@ Certd 是一个支持私有化部署的 SSL/TLS 证书自动化管理平台。 - `access-plugin-dev`:开发 Access 授权插件 - `dns-provider-dev`:开发 DNS Provider 插件 +- `fast-crud-page-dev`:开发或重构前端 Fast Crud 列表管理页面 - `task-plugin-dev`:开发 Task 部署任务插件 - `plugin-converter`:将插件转换为 YAML 配置 diff --git a/packages/ui/certd-client/src/locales/langs/zh-CN/certd/navigation.ts b/packages/ui/certd-client/src/locales/langs/zh-CN/certd/navigation.ts index 37b4eca9d..c0ab9874c 100644 --- a/packages/ui/certd-client/src/locales/langs/zh-CN/certd/navigation.ts +++ b/packages/ui/certd-client/src/locales/langs/zh-CN/certd/navigation.ts @@ -22,6 +22,8 @@ export default { mySuite: "我的套餐", suiteBuy: "套餐购买", myTrade: "我的订单", + myWallet: "我的钱包", + inviteCommission: "邀请返佣", paymentReturn: "支付返回", source: "源码", github: "github", @@ -46,6 +48,8 @@ export default { suiteSetting: "套餐设置", orderManager: "订单管理", userSuites: "用户套餐", + inviteCommissionSetting: "邀请返佣设置", + inviteWithdraw: "提现申请记录", netTest: "网络测试", enterpriseManager: "企业管理设置", projectManager: "项目管理", diff --git a/packages/ui/certd-client/src/main.ts b/packages/ui/certd-client/src/main.ts index 5d0d7c599..f45cba3f7 100644 --- a/packages/ui/certd-client/src/main.ts +++ b/packages/ui/certd-client/src/main.ts @@ -12,9 +12,11 @@ import plugin from "./plugin/"; import { setupVben } from "./vben"; import { util } from "/@/utils"; import { initPreferences } from "/@/vben/preferences"; +import { inviteUtils } from "/@/utils/util.invite"; // import "./components/code-editor/import-works"; // @ts-ignore async function bootstrap() { + inviteUtils.captureFromLocation(); const app = createApp(App); // app.use(Antd); app.use(Antd); diff --git a/packages/ui/certd-client/src/router/source/modules/certd.ts b/packages/ui/certd-client/src/router/source/modules/certd.ts index 90deb6767..3f42fc4ea 100644 --- a/packages/ui/certd-client/src/router/source/modules/certd.ts +++ b/packages/ui/certd-client/src/router/source/modules/certd.ts @@ -324,7 +324,7 @@ export const certdResources = [ meta: { show: () => { const settingStore = useSettingStore(); - return settingStore.isComm; + return settingStore.isInviteCommissionEnabled; }, icon: "ion:gift-outline", auth: true, @@ -359,6 +359,36 @@ export const certdResources = [ keepAlive: true, }, }, + { + title: "certd.myWallet", + name: "MyWallet", + path: "/certd/wallet", + component: "/certd/wallet/index.vue", + meta: { + show: () => { + const settingStore = useSettingStore(); + return settingStore.isComm; + }, + icon: "ion:wallet-outline", + auth: true, + keepAlive: true, + }, + }, + { + title: "certd.inviteCommission", + name: "InviteCommission", + path: "/certd/invite", + component: "/certd/invite/index.vue", + meta: { + show: () => { + const settingStore = useSettingStore(); + return settingStore.isComm; + }, + icon: "ion:gift-outline", + auth: true, + keepAlive: true, + }, + }, { title: "certd.paymentReturn", name: "PaymentReturn", 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 161ba44cd..30fc7fab0 100644 --- a/packages/ui/certd-client/src/router/source/modules/sys.ts +++ b/packages/ui/certd-client/src/router/source/modules/sys.ts @@ -286,6 +286,38 @@ export const sysResources = [ keepAlive: true, }, }, + { + title: "certd.sysResources.inviteCommissionSetting", + name: "SysInviteCommissionSetting", + path: "/sys/suite/invite/setting", + component: "/sys/suite/invite/setting.vue", + meta: { + show: () => { + const settingStore = useSettingStore(); + return settingStore.isComm; + }, + icon: "ion:gift-outline", + permission: "sys:settings:edit", + auth: true, + keepAlive: true, + }, + }, + { + title: "certd.sysResources.inviteWithdraw", + name: "SysInviteWithdraw", + path: "/sys/suite/invite/withdraw", + component: "/sys/suite/invite/withdraw.vue", + meta: { + show: () => { + const settingStore = useSettingStore(); + return settingStore.isComm; + }, + icon: "ion:cash-outline", + permission: "sys:settings:edit", + auth: true, + keepAlive: true, + }, + }, ], }, { 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 1d6a248f2..05a6deb21 100644 --- a/packages/ui/certd-client/src/store/settings/api.basic.ts +++ b/packages/ui/certd-client/src/store/settings/api.basic.ts @@ -97,6 +97,9 @@ export type SysPublicSetting = { export type SuiteSetting = { enabled?: boolean; }; +export type InviteSetting = { + enabled?: boolean; +}; export type SysPrivateSetting = { httpProxy?: string; httpsProxy?: string; @@ -142,6 +145,7 @@ export type AllSettings = { siteEnv: SiteEnv; headerMenus: HeaderMenus; suiteSetting: SuiteSetting; + inviteSetting: InviteSetting; app: AppInfo; }; diff --git a/packages/ui/certd-client/src/store/settings/index.tsx b/packages/ui/certd-client/src/store/settings/index.tsx index c5e2bc0d1..c68f14384 100644 --- a/packages/ui/certd-client/src/store/settings/index.tsx +++ b/packages/ui/certd-client/src/store/settings/index.tsx @@ -30,6 +30,9 @@ export interface SettingState { headerMenus?: HeaderMenus; inited?: boolean; suiteSetting?: SuiteSetting; + inviteSetting?: { + enabled?: boolean; + }; app: { version?: string; time?: number; @@ -102,6 +105,7 @@ export const useSettingStore = defineStore({ menus: [], }, suiteSetting: { enabled: false }, + inviteSetting: { enabled: false }, inited: false, app: { version: "", @@ -196,6 +200,9 @@ export const useSettingStore = defineStore({ // @ts-ignore return this.suiteSetting?.enabled === true; }, + isInviteCommissionEnabled(): boolean { + return this.isComm && this.inviteSetting?.enabled === true; + }, }, actions: { checkPlus() { @@ -215,6 +222,7 @@ export const useSettingStore = defineStore({ merge(this.plusInfo, allSettings.plusInfo || {}); merge(this.headerMenus, allSettings.headerMenus || {}); merge(this.suiteSetting, allSettings.suiteSetting || {}); + merge(this.inviteSetting, allSettings.inviteSetting || {}); //@ts-ignore this.initSiteInfo(allSettings.siteInfo || {}); this.initAppInfo(allSettings.app || {}); diff --git a/packages/ui/certd-client/src/store/user/api.user.ts b/packages/ui/certd-client/src/store/user/api.user.ts index 498c08abe..64ac46cec 100644 --- a/packages/ui/certd-client/src/store/user/api.user.ts +++ b/packages/ui/certd-client/src/store/user/api.user.ts @@ -4,6 +4,7 @@ export interface RegisterReq { username: string; password: string; confirmPassword: string; + inviteCode?: string; } /** * @description: Login interface parameters @@ -18,6 +19,7 @@ export interface SmsLoginReq { phoneCode: string; smsCode: string; randomStr: string; + inviteCode?: string; } export interface ForgotPasswordReq { diff --git a/packages/ui/certd-client/src/utils/util.invite.ts b/packages/ui/certd-client/src/utils/util.invite.ts new file mode 100644 index 000000000..37c0cf5b6 --- /dev/null +++ b/packages/ui/certd-client/src/utils/util.invite.ts @@ -0,0 +1,54 @@ +const INVITE_STORAGE_KEY = "certd_invite_code"; +const INVITE_TTL = 3 * 24 * 60 * 60 * 1000; + +export type InviteCache = { + code: string; + expiresAt: number; +}; + +function normalizeInviteCode(code?: string | null) { + return code?.trim().toUpperCase(); +} + +export const inviteUtils = { + save(code?: string | null) { + const normalized = normalizeInviteCode(code); + if (!normalized) { + return; + } + const cache: InviteCache = { + code: normalized, + expiresAt: Date.now() + INVITE_TTL, + }; + localStorage.setItem(INVITE_STORAGE_KEY, JSON.stringify(cache)); + }, + + get() { + const text = localStorage.getItem(INVITE_STORAGE_KEY); + if (!text) { + return ""; + } + try { + const cache = JSON.parse(text) as InviteCache; + if (!cache.code || !cache.expiresAt || cache.expiresAt < Date.now()) { + localStorage.removeItem(INVITE_STORAGE_KEY); + return ""; + } + return cache.code; + } catch (e) { + localStorage.removeItem(INVITE_STORAGE_KEY); + return ""; + } + }, + + captureFromLocation() { + const hashQuery = window.location.hash?.split("?")[1] || ""; + const search = window.location.search?.replace(/^\?/, "") || ""; + const hashParams = new URLSearchParams(hashQuery); + const searchParams = new URLSearchParams(search); + const code = hashParams.get("inviteCode") || searchParams.get("inviteCode"); + if (code) { + this.save(code); + } + }, +}; diff --git a/packages/ui/certd-client/src/views/certd/invite/api.ts b/packages/ui/certd-client/src/views/certd/invite/api.ts new file mode 100644 index 000000000..215c745ff --- /dev/null +++ b/packages/ui/certd-client/src/views/certd/invite/api.ts @@ -0,0 +1,13 @@ +import { request } from "/@/api/service"; + +export async function GetMyInvite() { + return await request({ url: "/invite/my", method: "post" }); +} + +export async function GetInvitees(query: any) { + return await request({ url: "/invite/invitees/page", method: "post", data: query }); +} + +export async function GetCommissionLogs(query: any) { + return await request({ url: "/invite/commission/page", method: "post", data: query }); +} diff --git a/packages/ui/certd-client/src/views/certd/invite/crud-invitees.tsx b/packages/ui/certd-client/src/views/certd/invite/crud-invitees.tsx new file mode 100644 index 000000000..c41ed4a13 --- /dev/null +++ b/packages/ui/certd-client/src/views/certd/invite/crud-invitees.tsx @@ -0,0 +1,34 @@ +import { CreateCrudOptionsProps, CreateCrudOptionsRet, UserPageQuery, UserPageRes } from "@fast-crud/fast-crud"; +import * as api from "./api"; + +export default function (): CreateCrudOptionsRet { + const pageRequest = async (query: UserPageQuery): Promise => { + return await api.GetInvitees(query); + }; + + return { + crudOptions: { + request: { pageRequest }, + actionbar: { show: false }, + toolbar: { show: false }, + rowHandle: { show: false }, + columns: { + inviteeUserId: { + title: "被邀请人ID", + type: "number", + column: { width: 140 }, + }, + inviteCode: { + title: "邀请码", + type: "text", + column: { width: 160 }, + }, + createTime: { + title: "邀请时间", + type: "datetime", + column: { width: 180 }, + }, + }, + }, + }; +} diff --git a/packages/ui/certd-client/src/views/certd/invite/crud-logs.tsx b/packages/ui/certd-client/src/views/certd/invite/crud-logs.tsx new file mode 100644 index 000000000..1b8651b6e --- /dev/null +++ b/packages/ui/certd-client/src/views/certd/invite/crud-logs.tsx @@ -0,0 +1,59 @@ +import { CreateCrudOptionsRet, dict, UserPageQuery, UserPageRes } from "@fast-crud/fast-crud"; +import * as api from "./api"; +import PriceInput from "/@/views/sys/suite/product/price-input.vue"; + +export default function (): CreateCrudOptionsRet { + const pageRequest = async (query: UserPageQuery): Promise => { + return await api.GetCommissionLogs(query); + }; + + return { + crudOptions: { + request: { pageRequest }, + actionbar: { show: false }, + toolbar: { show: false }, + rowHandle: { show: false }, + columns: { + type: { + title: "类型", + type: "dict-select", + dict: dict({ + data: [{ label: "佣金入账", value: "commission", color: "success" }], + }), + column: { width: 130 }, + }, + amount: { + title: "金额", + type: "number", + column: { + width: 120, + component: { name: PriceInput, vModel: "modelValue", edit: false }, + }, + }, + inviteeUserDisplay: { + title: "被邀请用户", + type: "text", + column: { width: 150 }, + }, + consumeAmount: { + title: "消费金额", + type: "number", + column: { + width: 120, + component: { name: PriceInput, vModel: "modelValue", edit: false }, + }, + }, + remark: { + title: "备注", + type: "text", + column: { minWidth: 220 }, + }, + createTime: { + title: "时间", + type: "datetime", + column: { width: 180 }, + }, + }, + }, + }; +} diff --git a/packages/ui/certd-client/src/views/certd/invite/index.vue b/packages/ui/certd-client/src/views/certd/invite/index.vue new file mode 100644 index 000000000..41a9b7658 --- /dev/null +++ b/packages/ui/certd-client/src/views/certd/invite/index.vue @@ -0,0 +1,137 @@ + + + + + diff --git a/packages/ui/certd-client/src/views/certd/suite/api.ts b/packages/ui/certd-client/src/views/certd/suite/api.ts index 9b6d8cdec..e8eac4d37 100644 --- a/packages/ui/certd-client/src/views/certd/suite/api.ts +++ b/packages/ui/certd-client/src/views/certd/suite/api.ts @@ -46,6 +46,7 @@ export type TradeCreateReq = { duration: number; num: number; payType: string; + useRebateBalance?: boolean; }; export async function TradeCreate(form: TradeCreateReq) { diff --git a/packages/ui/certd-client/src/views/certd/suite/order-modal.vue b/packages/ui/certd-client/src/views/certd/suite/order-modal.vue index ac195f127..b5a91f912 100644 --- a/packages/ui/certd-client/src/views/certd/suite/order-modal.vue +++ b/packages/ui/certd-client/src/views/certd/suite/order-modal.vue @@ -28,10 +28,19 @@ {{ $t("certd.order.price") }}: +
+ 返利抵扣: + + 可用 {{ amountToYuan(wallet.availableAmount) }} 元,预计抵扣 {{ amountToYuan(expectedRebateAmount) }} 元 +
+
+ 还需支付: + {{ amountToYuan(expectedThirdPartyAmount) }} 元 +
{{ $t("certd.order.paymentMethod") }}: -
{{ $t("certd.order.free") }}
+
{{ $t("certd.order.free") }}
@@ -39,7 +48,7 @@ + + diff --git a/packages/ui/certd-client/src/views/framework/login/index.vue b/packages/ui/certd-client/src/views/framework/login/index.vue index f10b3fc50..222bf42cc 100644 --- a/packages/ui/certd-client/src/views/framework/login/index.vue +++ b/packages/ui/certd-client/src/views/framework/login/index.vue @@ -1,3 +1,4 @@ +