mirror of
https://github.com/certd/certd.git
synced 2026-08-07 14:55:55 +08:00
feat: 新增套餐激活码功能,通过CDK兑换套餐
This commit is contained in:
@@ -77,6 +77,58 @@ container:{}, //容器配置 ,对应fs-container
|
|||||||
- 有固定操作栏、统计区、说明区时,这些区域应 `flex: none`,把剩余空间交给表格区域。
|
- 有固定操作栏、统计区、说明区时,这些区域应 `flex: none`,把剩余空间交给表格区域。
|
||||||
- 修改嵌入式 Fast Crud 页面后,要检查空数据、少量数据和多页数据时表格高度、分页器和空状态是否仍在预期区域内。
|
- 修改嵌入式 Fast Crud 页面后,要检查空数据、少量数据和多页数据时表格高度、分页器和空状态是否仍在预期区域内。
|
||||||
|
|
||||||
|
## 内置 CRUD 按钮
|
||||||
|
|
||||||
|
只要在 `request` 中配置了 `addRequest`、`editRequest`、`delRequest`,Fast Crud 会自动在 `rowHandle` 渲染新增、编辑、删除按钮并完成对应操作,**不需要手写 `openDeleteConfirm`、`openEditDialog` 等方法**。
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// crud.tsx
|
||||||
|
const addRequest = async ({ form }: AddReq) => await api.AddObj(form);
|
||||||
|
const editRequest = async ({ form, row }: EditReq) => {
|
||||||
|
form.id = row.id;
|
||||||
|
return await api.UpdateObj(form);
|
||||||
|
};
|
||||||
|
const delRequest = async ({ row }: DelReq) => await api.DelObj(row.id);
|
||||||
|
|
||||||
|
return {
|
||||||
|
crudOptions: {
|
||||||
|
request: { pageRequest, addRequest, editRequest, delRequest },
|
||||||
|
rowHandle: {
|
||||||
|
buttons: {
|
||||||
|
view: { show: false }, // 不需要查看就隐藏
|
||||||
|
edit: {}, // 自动调用 editRequest
|
||||||
|
remove: {}, // 自动调用 delRequest,自带确认弹窗和错误提示
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
- 删除按钮自带确认弹窗,不需要额外包装 `Modal.confirm`。
|
||||||
|
- 只有**自定义操作**(如禁用、审核、生成激活码)才需要在 `rowHandle.buttons` 中手写 `click` 处理方法。
|
||||||
|
- 如果不需要某列操作,直接把对应 key 去掉或设 `show: false`。
|
||||||
|
|
||||||
|
## compute 动态计算
|
||||||
|
|
||||||
|
当 `rowHandle.buttons` 的 `show`、`disabled` 等属性需要根据行数据动态决定时,**必须使用 `compute` 包裹**,不能直接传函数。
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { compute } from "@fast-crud/fast-crud";
|
||||||
|
|
||||||
|
// WRONG: 直接传函数
|
||||||
|
show: ({ row }) => row.status === "unused"
|
||||||
|
|
||||||
|
// CORRECT: 用 compute 包裹
|
||||||
|
show: compute(({ row }) => row.status === "unused")
|
||||||
|
```
|
||||||
|
|
||||||
|
`compute` 基于 Vue 的 `computed`,但额外支持上下文参数。适用位置:
|
||||||
|
- `rowHandle.buttons` 的 `show`、`disabled` 等属性
|
||||||
|
- `columns.key.column` 的 `show`、`cellRender` 等
|
||||||
|
- `columns.key.form` / `search` 的表单字段属性
|
||||||
|
|
||||||
|
参考文档:http://fast-crud.docmirror.cn/guide/advance/compute.html
|
||||||
|
|
||||||
## 代码习惯
|
## 代码习惯
|
||||||
|
|
||||||
- 页面命名、API 命名、权限标识和路由结构要贴近同目录已有页面。
|
- 页面命名、API 命名、权限标识和路由结构要贴近同目录已有页面。
|
||||||
|
|||||||
@@ -185,7 +185,11 @@ Certd 是一个支持私有化部署的 SSL/TLS 证书自动化管理平台。
|
|||||||
- 遵守单一职责原则:一个方法只负责一个清晰的业务步骤或技术步骤。流程编排方法可以串联多个步骤,但具体的校验、计算、持久化、状态变更、展示数据组装应尽量拆到命名明确的小方法中;不要让一个方法同时承担查询、校验、计算、写库、格式化返回等过多职责。
|
- 遵守单一职责原则:一个方法只负责一个清晰的业务步骤或技术步骤。流程编排方法可以串联多个步骤,但具体的校验、计算、持久化、状态变更、展示数据组装应尽量拆到命名明确的小方法中;不要让一个方法同时承担查询、校验、计算、写库、格式化返回等过多职责。
|
||||||
- 后端方法参数超过 3 个时,尽量改为对象参数传入;需要传入 `manager` / `EntityManager` 做事务传播的方法,必须使用对象参数,不要把 `manager` 作为位置参数藏在参数列表末尾。
|
- 后端方法参数超过 3 个时,尽量改为对象参数传入;需要传入 `manager` / `EntityManager` 做事务传播的方法,必须使用对象参数,不要把 `manager` 作为位置参数藏在参数列表末尾。
|
||||||
- 后端 service 层只有存在事务链路传播需求时才定义 `ctx`,不要为了将来可能需要而提前给普通方法加 `ctx`。事务链路方法统一采用 `method(ctx, req)` 形式,`ctx` 放第一位并承载 `manager?: EntityManager` 等横切上下文,业务参数放在 `req` 对象里,例如 `settleCommission({ manager }, { tradeId, userId, amount })`。无事务链路需求的普通查询、纯函数和简单私有方法继续使用明确参数。
|
- 后端 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 里重复定义。
|
- service 内部需要根据事务上下文选择 Repository 时,优先使用 `BaseService.getRepo(ctx, EntityClass)`;不要在业务方法里反复写 `ctx.manager?.getRepository(Entity) || this.xxxRepository`。拿到 repo 后 save/update/delete/find 都能做,不需要再包一层 `saveEntity` 之类的单一用途方法。`ctx` 类型统一从 `BaseService` 导出的 `ServiceContext` 复用,不要在每个 service 里重复定义。
|
||||||
|
|
||||||
|
- 需要"有事务则复用、无事务则开启"时,使用 `BaseService.transactionWithCtx(ctx, callback)`:ctx.manager 存在则直接执行 callback,否则自动 `this.transaction()`。不要在业务代码里手写 `if (ctx.manager) { ... } else { await this.transaction(...) }`。
|
||||||
|
|
||||||
|
- 新增方法注意不要与 `BaseService` 基类方法签名冲突(如 `delete(id)` vs `BaseService.delete(ids, where?)`),ts-node 下会直接 TS2416 编译报错。冲突时改用具体名称如 `deleteById`。
|
||||||
|
|
||||||
## 插件开发技能
|
## 插件开发技能
|
||||||
|
|
||||||
|
|||||||
@@ -23,62 +23,63 @@
|
|||||||
| 19.| **微软云Azure授权** | |
|
| 19.| **微软云Azure授权** | |
|
||||||
| 20.| **BIND9 DNS 授权** | 通过 SSH 连接到 BIND9 服务器,使用 nsupdate 命令管理 DNS 记录 |
|
| 20.| **BIND9 DNS 授权** | 通过 SSH 连接到 BIND9 服务器,使用 nsupdate 命令管理 DNS 记录 |
|
||||||
| 21.| **CacheFly** | CacheFly |
|
| 21.| **CacheFly** | CacheFly |
|
||||||
| 22.| **EAB授权** | ZeroSSL证书申请需要EAB授权 |
|
| 22.| **ACME账号** | 用于复用ACME账号私钥和账号地址,证书申请时不再临时创建账号 |
|
||||||
| 23.| **google cloud** | 谷歌云授权 |
|
| 23.| **EAB授权** | ZeroSSL证书申请需要EAB授权 |
|
||||||
| 24.| **cloudflare授权** | |
|
| 24.| **google cloud** | 谷歌云授权 |
|
||||||
| 25.| **中国移动CND授权** | |
|
| 25.| **cloudflare授权** | |
|
||||||
| 26.| **授权插件示例** | 这是一个示例授权插件,用于演示如何实现一个授权插件 |
|
| 26.| **中国移动CND授权** | |
|
||||||
| 27.| **dns.la授权** | |
|
| 27.| **授权插件示例** | 这是一个示例授权插件,用于演示如何实现一个授权插件 |
|
||||||
| 28.| **彩虹DNS** | 彩虹DNS管理系统授权 |
|
| 28.| **dns.la授权** | |
|
||||||
| 29.| **多吉云** | |
|
| 29.| **彩虹DNS** | 彩虹DNS管理系统授权 |
|
||||||
| 30.| **Dokploy授权** | |
|
| 30.| **多吉云** | |
|
||||||
| 31.| **farcdn授权** | |
|
| 31.| **Dokploy授权** | |
|
||||||
| 32.| **FlexCDN授权** | |
|
| 32.| **farcdn授权** | |
|
||||||
| 33.| **Gcore** | Gcore |
|
| 33.| **FlexCDN授权** | |
|
||||||
| 34.| **Github授权** | |
|
| 34.| **Gcore** | Gcore |
|
||||||
| 35.| **godaddy授权** | |
|
| 35.| **Github授权** | |
|
||||||
| 36.| **HiPM DNSMgr** | HiPM DNSMgr API Token 授权 |
|
| 36.| **godaddy授权** | |
|
||||||
| 37.| **金山云授权** | |
|
| 37.| **HiPM DNSMgr** | HiPM DNSMgr API Token 授权 |
|
||||||
| 38.| **FTP授权** | |
|
| 38.| **金山云授权** | |
|
||||||
| 39.| **七牛OSS授权** | |
|
| 39.| **FTP授权** | |
|
||||||
| 40.| **腾讯云COS授权** | 腾讯云对象存储授权,包含地域和存储桶 |
|
| 40.| **七牛OSS授权** | |
|
||||||
| 41.| **s3/minio授权** | S3/minio oss授权 |
|
| 41.| **腾讯云COS授权** | 腾讯云对象存储授权,包含地域和存储桶 |
|
||||||
| 42.| **namesilo授权** | |
|
| 42.| **s3/minio授权** | S3/minio oss授权 |
|
||||||
| 43.| **Next Terminal 授权** | 用于访问 Next Terminal API 的授权配置 |
|
| 43.| **namesilo授权** | |
|
||||||
| 44.| **Nginx Proxy Manager 授权** | 用于登录 Nginx Proxy Manager,并为代理主机证书部署提供授权。 |
|
| 44.| **Next Terminal 授权** | 用于访问 Next Terminal API 的授权配置 |
|
||||||
| 45.| **1panel授权** | 账号和密码 |
|
| 45.| **Nginx Proxy Manager 授权** | 用于登录 Nginx Proxy Manager,并为代理主机证书部署提供授权。 |
|
||||||
| 46.| **支付宝** | |
|
| 46.| **1panel授权** | 账号和密码 |
|
||||||
| 47.| **白山云授权** | |
|
| 47.| **支付宝** | |
|
||||||
| 48.| **宝塔云WAF授权** | 用于连接和管理宝塔云WAF服务的授权配置 |
|
| 48.| **白山云授权** | |
|
||||||
| 49.| **cdnfly授权** | |
|
| 49.| **宝塔云WAF授权** | 用于连接和管理宝塔云WAF服务的授权配置 |
|
||||||
| 50.| **k8s授权** | |
|
| 50.| **cdnfly授权** | |
|
||||||
| 51.| **括彩云cdn授权** | 括彩云CDN,每月免费30G,[注册即领](https://kuocaicdn.com/register?code=8mn536rrzfbf8) |
|
| 51.| **k8s授权** | |
|
||||||
| 52.| **LeCDN授权** | |
|
| 52.| **括彩云cdn授权** | 括彩云CDN,每月免费30G,[注册即领](https://kuocaicdn.com/register?code=8mn536rrzfbf8) |
|
||||||
| 53.| **lucky** | |
|
| 53.| **LeCDN授权** | |
|
||||||
| 54.| **猫云授权** | |
|
| 54.| **lucky** | |
|
||||||
| 55.| **plesk授权** | |
|
| 55.| **猫云授权** | |
|
||||||
| 56.| **长亭雷池授权** | |
|
| 56.| **plesk授权** | |
|
||||||
| 57.| **群晖登录授权** | |
|
| 57.| **长亭雷池授权** | |
|
||||||
| 58.| **uniCloud** | unicloud授权 |
|
| 58.| **群晖登录授权** | |
|
||||||
| 59.| **微信支付** | |
|
| 59.| **uniCloud** | unicloud授权 |
|
||||||
| 60.| **易盾rcdn授权** | 易盾CDN,每月免费30G,[注册即领](https://rhcdn.yiduncdn.com/register?code=8mn536rrzfbf8) |
|
| 60.| **微信支付** | |
|
||||||
| 61.| **易发云短信** | sms.yfyidc.cn/ |
|
| 61.| **易盾rcdn授权** | 易盾CDN,每月免费30G,[注册即领](https://rhcdn.yiduncdn.com/register?code=8mn536rrzfbf8) |
|
||||||
| 62.| **易盾DCDN授权** | https://user.yiduncdn.com |
|
| 62.| **易发云短信** | sms.yfyidc.cn/ |
|
||||||
| 63.| **易支付** | |
|
| 63.| **易盾DCDN授权** | https://user.yiduncdn.com |
|
||||||
| 64.| **proxmox** | |
|
| 64.| **易支付** | |
|
||||||
| 65.| **Spaceship.com 授权** | Spaceship.com API 授权插件 |
|
| 65.| **proxmox** | |
|
||||||
| 66.| **Technitium DNS Server** | Technitium DNS Server 自建DNS服务器授权 |
|
| 66.| **Spaceship.com 授权** | Spaceship.com API 授权插件 |
|
||||||
| 67.| **UCloud授权** | 优刻得授权 |
|
| 67.| **Technitium DNS Server** | Technitium DNS Server 自建DNS服务器授权 |
|
||||||
| 68.| **又拍云** | |
|
| 68.| **UCloud授权** | 优刻得授权 |
|
||||||
| 69.| **网宿授权** | |
|
| 69.| **又拍云** | |
|
||||||
| 70.| **西部数码授权** | |
|
| 70.| **网宿授权** | |
|
||||||
| 71.| **我爱云授权** | 我爱云CDN |
|
| 71.| **西部数码授权** | |
|
||||||
| 72.| **新网授权(代理方式)** | |
|
| 72.| **我爱云授权** | 我爱云CDN |
|
||||||
| 73.| **新网授权** | |
|
| 73.| **新网授权(代理方式)** | |
|
||||||
| 74.| **新网互联授权** | 仅支持代理账号,ip需要加入白名单 |
|
| 74.| **新网授权** | |
|
||||||
| 75.| **Zenlayer授权** | Zenlayer授权 |
|
| 75.| **新网互联授权** | 仅支持代理账号,ip需要加入白名单 |
|
||||||
| 76.| **GoEdge授权** | |
|
| 76.| **Zenlayer授权** | Zenlayer授权 |
|
||||||
| 77.| **雨云授权** | https://app.rainyun.com/ |
|
| 77.| **GoEdge授权** | |
|
||||||
|
| 78.| **雨云授权** | https://app.rainyun.com/ |
|
||||||
|
|
||||||
<style module>
|
<style module>
|
||||||
table th:first-of-type {
|
table th:first-of-type {
|
||||||
|
|||||||
@@ -38,6 +38,16 @@ export abstract class BaseService<T> {
|
|||||||
return await dataSource.transaction(callback as any);
|
return await dataSource.transaction(callback as any);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 如果 ctx 有 manager 则复用已有事务,否则开启新事务
|
||||||
|
*/
|
||||||
|
protected async transactionWithCtx<T>(ctx: ServiceContext, callback: (manager: EntityManager) => Promise<T>): Promise<T> {
|
||||||
|
if (ctx.manager) {
|
||||||
|
return await callback(ctx.manager);
|
||||||
|
}
|
||||||
|
return (await this.transaction(callback)) as T;
|
||||||
|
}
|
||||||
|
|
||||||
protected getRepo<E>(ctx: ServiceContext, entity: EntityTarget<E>): Repository<E> {
|
protected getRepo<E>(ctx: ServiceContext, entity: EntityTarget<E>): Repository<E> {
|
||||||
if (ctx.manager) {
|
if (ctx.manager) {
|
||||||
return ctx.manager.getRepository(entity);
|
return ctx.manager.getRepository(entity);
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ export default {
|
|||||||
inviteLevel: "Promotion Levels",
|
inviteLevel: "Promotion Levels",
|
||||||
inviteUserLevel: "User Promotion Levels",
|
inviteUserLevel: "User Promotion Levels",
|
||||||
inviteWithdraw: "Withdrawal Requests",
|
inviteWithdraw: "Withdrawal Requests",
|
||||||
|
activationCodeManager: "Activation Code",
|
||||||
netTest: "Network Test",
|
netTest: "Network Test",
|
||||||
|
|
||||||
enterpriseSetting: "Enterprise Settings",
|
enterpriseSetting: "Enterprise Settings",
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ export default {
|
|||||||
inviteLevel: "推广等级",
|
inviteLevel: "推广等级",
|
||||||
inviteUserLevel: "用户推广等级",
|
inviteUserLevel: "用户推广等级",
|
||||||
inviteWithdraw: "提现申请记录",
|
inviteWithdraw: "提现申请记录",
|
||||||
|
activationCodeManager: "激活码管理",
|
||||||
netTest: "网络测试",
|
netTest: "网络测试",
|
||||||
enterpriseManager: "企业管理设置",
|
enterpriseManager: "企业管理设置",
|
||||||
projectManager: "项目管理",
|
projectManager: "项目管理",
|
||||||
|
|||||||
@@ -354,6 +354,22 @@ export const sysResources = [
|
|||||||
keepAlive: true,
|
keepAlive: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: "certd.sysResources.activationCodeManager",
|
||||||
|
name: "SysProductActivationCode",
|
||||||
|
path: "/sys/suite/activation-code",
|
||||||
|
component: "/sys/suite/activation-code/index.vue",
|
||||||
|
meta: {
|
||||||
|
show: () => {
|
||||||
|
const settingStore = useSettingStore();
|
||||||
|
return settingStore.isComm;
|
||||||
|
},
|
||||||
|
icon: "ion:key-outline",
|
||||||
|
permission: "sys:settings:edit",
|
||||||
|
auth: true,
|
||||||
|
keepAlive: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -78,3 +78,11 @@ export async function GetSuiteSetting() {
|
|||||||
method: "POST",
|
method: "POST",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function UseActivationCode(code: string) {
|
||||||
|
return await request({
|
||||||
|
url: "/suite/activation-code/use",
|
||||||
|
method: "POST",
|
||||||
|
data: { code },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -14,7 +14,10 @@
|
|||||||
</a-card>
|
</a-card>
|
||||||
</a-col>
|
</a-col>
|
||||||
</a-row>
|
</a-row>
|
||||||
<a-row :gutter="8" class="mt-10">
|
<div class="suite-buy-action-row mt-10 pl-1">
|
||||||
|
<a-button type="primary" :loading="activating" @click="openActivateDialog">激活码兑换</a-button>
|
||||||
|
</div>
|
||||||
|
<a-row :gutter="8">
|
||||||
<a-col v-for="item of suites" :key="item.id" class="mb-10 suite-card-col">
|
<a-col v-for="item of suites" :key="item.id" class="mb-10 suite-card-col">
|
||||||
<product-info :product="item" @order="doOrder" />
|
<product-info :product="item" @order="doOrder" />
|
||||||
</a-col>
|
</a-col>
|
||||||
@@ -32,14 +35,69 @@
|
|||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, ref } from "vue";
|
import { computed, ref } from "vue";
|
||||||
|
import { message } from "ant-design-vue";
|
||||||
import * as api from "./api";
|
import * as api from "./api";
|
||||||
import ProductInfo from "/@/views/certd/suite/product-info.vue";
|
import ProductInfo from "/@/views/certd/suite/product-info.vue";
|
||||||
import OrderModal from "/@/views/certd/suite/order-modal.vue";
|
import OrderModal from "/@/views/certd/suite/order-modal.vue";
|
||||||
import { notification } from "ant-design-vue";
|
import { notification } from "ant-design-vue";
|
||||||
|
import { useFormDialog } from "/@/use/use-dialog";
|
||||||
|
|
||||||
const suites = ref([]);
|
const suites = ref([]);
|
||||||
const addons = ref([]);
|
const addons = ref([]);
|
||||||
|
|
||||||
|
const activationCode = ref("");
|
||||||
|
const activating = ref(false);
|
||||||
|
const { openFormDialog } = useFormDialog();
|
||||||
|
|
||||||
|
async function openActivateDialog() {
|
||||||
|
await openFormDialog({
|
||||||
|
title: "激活码兑换",
|
||||||
|
wrapper: { width: 520 },
|
||||||
|
initialForm: {
|
||||||
|
code: activationCode.value,
|
||||||
|
},
|
||||||
|
columns: {
|
||||||
|
code: {
|
||||||
|
title: "激活码",
|
||||||
|
type: "text",
|
||||||
|
form: {
|
||||||
|
col: { span: 24 },
|
||||||
|
rules: [{ required: true, message: "请输入激活码" }],
|
||||||
|
component: {
|
||||||
|
placeholder: "请输入 CDK 激活码",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
async onSubmit(form: any) {
|
||||||
|
activationCode.value = form.code;
|
||||||
|
await doActivate();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function doActivate() {
|
||||||
|
const code = activationCode.value.trim().toUpperCase();
|
||||||
|
if (!code) {
|
||||||
|
message.warning("请输入激活码");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
activationCode.value = code;
|
||||||
|
activating.value = true;
|
||||||
|
try {
|
||||||
|
const res = await api.UseActivationCode(code);
|
||||||
|
activationCode.value = "";
|
||||||
|
notification.success({
|
||||||
|
message: "激活成功",
|
||||||
|
description: `您已成功激活 ${res.title},时长 ${res.duration} 天`,
|
||||||
|
});
|
||||||
|
} catch (e: any) {
|
||||||
|
message.error(e?.message || "兑换失败");
|
||||||
|
} finally {
|
||||||
|
activating.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function loadProducts() {
|
async function loadProducts() {
|
||||||
const list = await api.ProductList();
|
const list = await api.ProductList();
|
||||||
suites.value = list.filter((x: any) => x.type === "suite");
|
suites.value = list.filter((x: any) => x.type === "suite");
|
||||||
@@ -87,6 +145,12 @@ loadSuiteIntro();
|
|||||||
//overflow: hidden;
|
//overflow: hidden;
|
||||||
//text-overflow: ellipsis;
|
//text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
.suite-buy-action-row {
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
.suite-list {
|
.suite-list {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
import { request } from "/src/api/service";
|
||||||
|
|
||||||
|
const apiPrefix = "/sys/suite/activation-code";
|
||||||
|
|
||||||
|
export async function GetList(query: any) {
|
||||||
|
return await request({
|
||||||
|
url: apiPrefix + "/page",
|
||||||
|
method: "post",
|
||||||
|
data: query,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function Generate(data: { productId: number; duration: number; count: number; expireTime?: number; exported?: boolean; remark?: string }) {
|
||||||
|
return await request({
|
||||||
|
url: apiPrefix + "/generate",
|
||||||
|
method: "post",
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function ExportCodes(query: any) {
|
||||||
|
return await request({
|
||||||
|
url: apiPrefix + "/export",
|
||||||
|
method: "post",
|
||||||
|
data: query,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function Disable(id: number) {
|
||||||
|
return await request({
|
||||||
|
url: apiPrefix + "/disable",
|
||||||
|
method: "post",
|
||||||
|
params: { id },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function Enable(id: number) {
|
||||||
|
return await request({
|
||||||
|
url: apiPrefix + "/enable",
|
||||||
|
method: "post",
|
||||||
|
params: { id },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function DeleteObj(id: number) {
|
||||||
|
return await request({
|
||||||
|
url: apiPrefix + "/delete",
|
||||||
|
method: "post",
|
||||||
|
params: { id },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function GetSimpleUserByIds(ids: number[]) {
|
||||||
|
return await request({
|
||||||
|
url: "/sys/authority/user/getSimpleUserByIds",
|
||||||
|
method: "post",
|
||||||
|
data: { ids },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function GetProductDetail(id: number) {
|
||||||
|
return await request({
|
||||||
|
url: "/sys/suite/product/info",
|
||||||
|
method: "post",
|
||||||
|
params: { id },
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,378 @@
|
|||||||
|
import { compute, CreateCrudOptionsProps, CreateCrudOptionsRet, DelReq, dict, UserPageQuery, UserPageRes } from "@fast-crud/fast-crud";
|
||||||
|
import { Modal, message, notification } from "ant-design-vue";
|
||||||
|
import { Ref, ref } from "vue";
|
||||||
|
import * as api from "./api";
|
||||||
|
import { useFormDialog } from "/@/use/use-dialog";
|
||||||
|
import createCrudOptionsUser from "/@/views/sys/authority/user/crud";
|
||||||
|
import { downloadFileFromBlobPart } from "/@/vben/shared/utils/download";
|
||||||
|
|
||||||
|
export default function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet {
|
||||||
|
const { openFormDialog } = useFormDialog();
|
||||||
|
const selectedRowKeys: Ref<any[]> = ref([]);
|
||||||
|
context.selectedRowKeys = selectedRowKeys;
|
||||||
|
|
||||||
|
const productDict = dict({
|
||||||
|
url: "/sys/suite/product/list",
|
||||||
|
value: "id",
|
||||||
|
label: "title",
|
||||||
|
});
|
||||||
|
|
||||||
|
const statusDict = dict({
|
||||||
|
data: [
|
||||||
|
{ label: "未使用", value: "unused", color: "success" },
|
||||||
|
{ label: "已使用", value: "used", color: "processing" },
|
||||||
|
{ label: "已禁用", value: "disabled", color: "default" },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
const userDict = dict({
|
||||||
|
async getNodesByValues(ids: number[]) {
|
||||||
|
return await api.GetSimpleUserByIds(ids);
|
||||||
|
},
|
||||||
|
value: "id",
|
||||||
|
label: "nickName",
|
||||||
|
});
|
||||||
|
|
||||||
|
const pageRequest = async (query: UserPageQuery): Promise<UserPageRes> => {
|
||||||
|
return await api.GetList(query);
|
||||||
|
};
|
||||||
|
|
||||||
|
const delRequest = async ({ row }: DelReq) => {
|
||||||
|
return await api.DeleteObj(row.id);
|
||||||
|
};
|
||||||
|
|
||||||
|
async function openGenerate() {
|
||||||
|
const durationOptions = ref<{ label: string; value: number }[]>([]);
|
||||||
|
async function loadDurationOptions(productId: number) {
|
||||||
|
if (!productId) {
|
||||||
|
durationOptions.value = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const product = await api.GetProductDetail(productId);
|
||||||
|
const prices = JSON.parse(product.durationPrices || "[]");
|
||||||
|
durationOptions.value = prices.map((item: any) => ({
|
||||||
|
label: item.duration === -1 ? "永久" : `${item.duration} 天`,
|
||||||
|
value: item.duration,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
await openFormDialog({
|
||||||
|
title: "批量生成激活码",
|
||||||
|
wrapper: { width: 560 },
|
||||||
|
initialForm: {
|
||||||
|
productId: null,
|
||||||
|
duration: null,
|
||||||
|
count: 10,
|
||||||
|
expireTime: null,
|
||||||
|
exported: true,
|
||||||
|
remark: "",
|
||||||
|
},
|
||||||
|
columns: {
|
||||||
|
productId: {
|
||||||
|
title: "选择套餐",
|
||||||
|
type: "dict-select",
|
||||||
|
dict: productDict,
|
||||||
|
form: {
|
||||||
|
col: { span: 24 },
|
||||||
|
rules: [{ required: true, message: "请选择套餐" }],
|
||||||
|
valueChange({ form, value }: any) {
|
||||||
|
form.duration = null;
|
||||||
|
loadDurationOptions(value);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
duration: {
|
||||||
|
title: "时长(天)",
|
||||||
|
type: "text",
|
||||||
|
form: {
|
||||||
|
col: { span: 24 },
|
||||||
|
rules: [{ required: true, message: "请输入时长" }],
|
||||||
|
helper: "请先选择套餐,再选择该套餐已配置的时长",
|
||||||
|
component: {
|
||||||
|
name: "a-select",
|
||||||
|
vModel: "value",
|
||||||
|
options: durationOptions,
|
||||||
|
placeholder: "请选择时长",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
count: {
|
||||||
|
title: "生成数量",
|
||||||
|
type: "number",
|
||||||
|
form: {
|
||||||
|
col: { span: 24 },
|
||||||
|
rules: [{ required: true, message: "请输入生成数量" }],
|
||||||
|
helper: "单次最多生成 1000 个",
|
||||||
|
component: { min: 1, max: 1000 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expireTime: {
|
||||||
|
title: "过期时间",
|
||||||
|
type: "datetime",
|
||||||
|
form: {
|
||||||
|
col: { span: 24 },
|
||||||
|
helper: "选填,留空则长期有效",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
exported: {
|
||||||
|
title: "生成后立即导出",
|
||||||
|
type: "dict-switch",
|
||||||
|
dict: dict({
|
||||||
|
data: [
|
||||||
|
{ label: "导出", value: true, color: "success" },
|
||||||
|
{ label: "不导出", value: false, color: "default" },
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
form: {
|
||||||
|
col: { span: 24 },
|
||||||
|
helper: "开启后生成完成会自动下载 CSV,并把激活码标记为已导出",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
remark: {
|
||||||
|
title: "备注",
|
||||||
|
type: "text",
|
||||||
|
form: {
|
||||||
|
col: { span: 24 },
|
||||||
|
helper: "选填",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
async onSubmit(form: any) {
|
||||||
|
if (form.expireTime) {
|
||||||
|
form.expireTime = form.expireTime.valueOf ? form.expireTime.valueOf() : new Date(form.expireTime).getTime();
|
||||||
|
}
|
||||||
|
const res = await api.Generate(form);
|
||||||
|
if (form.exported) {
|
||||||
|
downloadCodes(res.codes || [], "activation-codes-generated");
|
||||||
|
}
|
||||||
|
await crudExpose.doRefresh();
|
||||||
|
notification.success({
|
||||||
|
message: `激活码已生成,批次号:${res.batchNo},数量:${res.count}`,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function doDisable(row: any) {
|
||||||
|
Modal.confirm({
|
||||||
|
title: "确认禁用激活码?",
|
||||||
|
content: `禁用后用户将不能兑换该激活码:${row.code}`,
|
||||||
|
async onOk() {
|
||||||
|
await api.Disable(row.id);
|
||||||
|
notification.success({ message: "激活码已禁用" });
|
||||||
|
await crudExpose.doRefresh();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function doEnable(row: any) {
|
||||||
|
Modal.confirm({
|
||||||
|
title: "确认启用激活码?",
|
||||||
|
content: `启用后用户可以继续兑换该激活码:${row.code}`,
|
||||||
|
async onOk() {
|
||||||
|
await api.Enable(row.id);
|
||||||
|
notification.success({ message: "激活码已启用" });
|
||||||
|
await crudExpose.doRefresh();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildCsv(list: any[]) {
|
||||||
|
const headers = ["ID", "激活码", "套餐ID", "时长", "批次号", "状态", "过期时间", "备注"];
|
||||||
|
const rows = list.map(item => [item.id, item.code, item.productId, item.duration, item.batchNo, item.status, item.expireTime || "", item.remark || ""]);
|
||||||
|
const escapeCsv = (value: any) => {
|
||||||
|
const text = String(value ?? "");
|
||||||
|
return `"${text.replaceAll('"', '""')}"`;
|
||||||
|
};
|
||||||
|
return [headers, ...rows].map(row => row.map(escapeCsv).join(",")).join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
function downloadCodes(list: any[], prefix = "activation-codes") {
|
||||||
|
downloadFileFromBlobPart({
|
||||||
|
fileName: `${prefix}-${Date.now()}.csv`,
|
||||||
|
source: "\uFEFF" + buildCsv(list),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function doExport() {
|
||||||
|
if (selectedRowKeys.value.length === 0) {
|
||||||
|
message.warning("请先勾选要导出的激活码");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Modal.confirm({
|
||||||
|
title: "确认导出激活码?",
|
||||||
|
content: `将导出已勾选的 ${selectedRowKeys.value.length} 个激活码,并标记导出时间。已使用和已禁用的激活码会自动跳过。`,
|
||||||
|
async onOk() {
|
||||||
|
const list = await api.ExportCodes({ ids: selectedRowKeys.value });
|
||||||
|
downloadCodes(list);
|
||||||
|
selectedRowKeys.value = [];
|
||||||
|
notification.success({ message: `已导出 ${list.length} 个激活码` });
|
||||||
|
await crudExpose.doRefresh();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
crudOptions: {
|
||||||
|
settings: {
|
||||||
|
plugins: {
|
||||||
|
rowSelection: {
|
||||||
|
enabled: true,
|
||||||
|
order: -2,
|
||||||
|
before: true,
|
||||||
|
props: {
|
||||||
|
multiple: true,
|
||||||
|
crossPage: true,
|
||||||
|
selectedRowKeys,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
request: { pageRequest, delRequest },
|
||||||
|
actionbar: {
|
||||||
|
buttons: {
|
||||||
|
add: { show: false },
|
||||||
|
generate: {
|
||||||
|
text: "批量生成激活码",
|
||||||
|
type: "primary",
|
||||||
|
click: openGenerate,
|
||||||
|
},
|
||||||
|
export: {
|
||||||
|
text: "导出激活码",
|
||||||
|
click: doExport,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rowHandle: {
|
||||||
|
width: 210,
|
||||||
|
fixed: "right",
|
||||||
|
buttons: {
|
||||||
|
view: { show: false },
|
||||||
|
edit: { show: false },
|
||||||
|
copy: { show: false },
|
||||||
|
disable: {
|
||||||
|
text: "禁用",
|
||||||
|
type: "link",
|
||||||
|
show: compute(({ row }) => row.status === "unused" || row.status === "exported"),
|
||||||
|
click: ({ row }) => doDisable(row),
|
||||||
|
},
|
||||||
|
enable: {
|
||||||
|
text: "启用",
|
||||||
|
type: "link",
|
||||||
|
show: compute(({ row }) => row.status === "disabled"),
|
||||||
|
click: ({ row }) => doEnable(row),
|
||||||
|
},
|
||||||
|
remove: {
|
||||||
|
show: compute(({ row }) => row.status !== "used"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
columns: {
|
||||||
|
id: {
|
||||||
|
title: "ID",
|
||||||
|
type: "number",
|
||||||
|
column: { width: 80 },
|
||||||
|
form: { show: false },
|
||||||
|
},
|
||||||
|
code: {
|
||||||
|
title: "激活码",
|
||||||
|
type: "copyable",
|
||||||
|
search: { show: true },
|
||||||
|
column: { width: 300 },
|
||||||
|
},
|
||||||
|
productId: {
|
||||||
|
title: "绑定套餐",
|
||||||
|
type: "dict-select",
|
||||||
|
dict: productDict,
|
||||||
|
search: { show: true },
|
||||||
|
column: { width: 150 },
|
||||||
|
},
|
||||||
|
duration: {
|
||||||
|
title: "时长(天)",
|
||||||
|
type: "number",
|
||||||
|
column: { width: 100, align: "center" },
|
||||||
|
},
|
||||||
|
batchNo: {
|
||||||
|
title: "批次号",
|
||||||
|
type: "text",
|
||||||
|
search: { show: true },
|
||||||
|
column: { width: 180 },
|
||||||
|
},
|
||||||
|
status: {
|
||||||
|
title: "状态",
|
||||||
|
type: "dict-select",
|
||||||
|
dict: statusDict,
|
||||||
|
search: { show: true },
|
||||||
|
column: { width: 100, align: "center" },
|
||||||
|
},
|
||||||
|
usedUserId: {
|
||||||
|
title: "使用用户",
|
||||||
|
type: "table-select",
|
||||||
|
dict: userDict,
|
||||||
|
search: { show: true },
|
||||||
|
column: { width: 140 },
|
||||||
|
form: {
|
||||||
|
show: false,
|
||||||
|
component: {
|
||||||
|
crossPage: true,
|
||||||
|
multiple: false,
|
||||||
|
select: {
|
||||||
|
placeholder: "点击选择用户",
|
||||||
|
},
|
||||||
|
createCrudOptions: createCrudOptionsUser,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
usedTime: {
|
||||||
|
title: "使用时间",
|
||||||
|
type: "datetime",
|
||||||
|
column: { width: 170 },
|
||||||
|
},
|
||||||
|
exported: {
|
||||||
|
title: "是否已导出",
|
||||||
|
type: "dict-switch",
|
||||||
|
dict: dict({
|
||||||
|
data: [
|
||||||
|
{ label: "未导出", value: false, color: "default" },
|
||||||
|
{ label: "已导出", value: true, color: "warning" },
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
search: { show: true },
|
||||||
|
column: { width: 110, align: "center" },
|
||||||
|
},
|
||||||
|
exportTime: {
|
||||||
|
title: "导出时间",
|
||||||
|
type: "datetime",
|
||||||
|
column: { width: 170 },
|
||||||
|
},
|
||||||
|
expireTime: {
|
||||||
|
title: "过期时间",
|
||||||
|
type: "datetime",
|
||||||
|
search: { show: false },
|
||||||
|
column: { width: 170 },
|
||||||
|
},
|
||||||
|
disabledTime: {
|
||||||
|
title: "禁用时间",
|
||||||
|
type: "datetime",
|
||||||
|
column: { width: 170, show: false },
|
||||||
|
},
|
||||||
|
remark: {
|
||||||
|
title: "备注",
|
||||||
|
type: "text",
|
||||||
|
column: { width: 150 },
|
||||||
|
},
|
||||||
|
createTime: {
|
||||||
|
title: "创建时间",
|
||||||
|
type: "datetime",
|
||||||
|
form: { show: false },
|
||||||
|
column: { sorter: true, width: 170 },
|
||||||
|
},
|
||||||
|
updateTime: {
|
||||||
|
title: "更新时间",
|
||||||
|
type: "datetime",
|
||||||
|
form: { show: false },
|
||||||
|
column: { width: 170 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
<template>
|
||||||
|
<fs-page>
|
||||||
|
<template #header>
|
||||||
|
<div class="title">
|
||||||
|
激活码管理
|
||||||
|
<span class="sub">生成、禁用和查询套餐激活码</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<fs-crud ref="crudRef" v-bind="crudBinding"> </fs-crud>
|
||||||
|
</fs-page>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { onActivated, onMounted } from "vue";
|
||||||
|
import { useFs } from "@fast-crud/fast-crud";
|
||||||
|
import createCrudOptions from "./crud";
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: "ProductActivationCodeManager",
|
||||||
|
});
|
||||||
|
|
||||||
|
const { crudBinding, crudRef, crudExpose } = useFs({ createCrudOptions });
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
crudExpose.doRefresh();
|
||||||
|
});
|
||||||
|
onActivated(async () => {
|
||||||
|
await crudExpose.doRefresh();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<style lang="less"></style>
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
-- 激活码表
|
||||||
|
CREATE TABLE "cd_product_activation_code"
|
||||||
|
(
|
||||||
|
"id" integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||||
|
"code" varchar(50) NOT NULL,
|
||||||
|
"product_id" integer NOT NULL,
|
||||||
|
"duration" integer NOT NULL,
|
||||||
|
"batch_no" varchar(50) NOT NULL DEFAULT '',
|
||||||
|
"status" varchar(20) NOT NULL DEFAULT 'unused',
|
||||||
|
"used_user_id" integer,
|
||||||
|
"used_time" integer,
|
||||||
|
"expire_time" integer,
|
||||||
|
"disabled_time" integer,
|
||||||
|
"exported" integer NOT NULL DEFAULT 0,
|
||||||
|
"export_time" integer,
|
||||||
|
"remark" varchar(500) NOT NULL DEFAULT '',
|
||||||
|
"create_time" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP),
|
||||||
|
"update_time" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX "index_activation_code_code" ON "cd_product_activation_code" ("code");
|
||||||
|
CREATE INDEX "index_activation_code_batch_no" ON "cd_product_activation_code" ("batch_no");
|
||||||
|
CREATE INDEX "index_activation_code_status" ON "cd_product_activation_code" ("status");
|
||||||
|
CREATE INDEX "index_activation_code_expire_time" ON "cd_product_activation_code" ("expire_time");
|
||||||
|
CREATE INDEX "index_activation_code_exported" ON "cd_product_activation_code" ("exported");
|
||||||
|
|
||||||
|
-- cd_user_suite 增加激活码来源追溯
|
||||||
|
ALTER TABLE "cd_user_suite"
|
||||||
|
ADD COLUMN "activation_code_id" integer;
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
name: acmeAccount
|
||||||
|
title: ACME账号
|
||||||
|
desc: 用于复用ACME账号私钥和账号地址,证书申请时不再临时创建账号
|
||||||
|
icon: ph:certificate
|
||||||
|
subtype: caType
|
||||||
|
input:
|
||||||
|
caType:
|
||||||
|
title: 颁发机构
|
||||||
|
component:
|
||||||
|
name: a-select
|
||||||
|
options:
|
||||||
|
- value: letsencrypt
|
||||||
|
label: Let's Encrypt
|
||||||
|
- value: letsencrypt_staging
|
||||||
|
label: Let's Encrypt测试环境
|
||||||
|
- value: google
|
||||||
|
label: Google
|
||||||
|
- value: zerossl
|
||||||
|
label: ZeroSSL
|
||||||
|
- value: litessl
|
||||||
|
label: litessl
|
||||||
|
- value: sslcom
|
||||||
|
label: SSL.com
|
||||||
|
required: true
|
||||||
|
mergeScript: |2-
|
||||||
|
|
||||||
|
return {
|
||||||
|
component: {
|
||||||
|
disabled: ctx.compute(({form})=> !!form.access?.account)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
email:
|
||||||
|
title: 邮箱
|
||||||
|
component:
|
||||||
|
placeholder: user@example.com
|
||||||
|
rules:
|
||||||
|
- type: email
|
||||||
|
message: 请输入正确的邮箱
|
||||||
|
required: true
|
||||||
|
mergeScript: |2-
|
||||||
|
|
||||||
|
return {
|
||||||
|
component: {
|
||||||
|
disabled: ctx.compute(({form})=> !!form.access?.account)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
directoryUrl:
|
||||||
|
title: ACME Directory URL
|
||||||
|
component:
|
||||||
|
placeholder: 自定义ACME服务端点
|
||||||
|
helper: 自定义ACME时必填,其他颁发机构默认自动使用内置端点
|
||||||
|
required: false
|
||||||
|
mergeScript: |2-
|
||||||
|
|
||||||
|
return {
|
||||||
|
show: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
eabKid:
|
||||||
|
title: EAB KID
|
||||||
|
component:
|
||||||
|
placeholder: 需要EAB的颁发机构生成账号时填写
|
||||||
|
helper: >-
|
||||||
|
需要提供EAB授权
|
||||||
|
|
||||||
|
ZeroSSL:请前往[zerossl开发者中心](https://app.zerossl.com/developer),生成 'EAB
|
||||||
|
Credentials'
|
||||||
|
|
||||||
|
Google:请查看[google获取eab帮助文档](https://certd.docmirror.cn/guide/use/google/),用过一次后会绑定邮箱,后续复用EAB要用同一个邮箱
|
||||||
|
|
||||||
|
SSL.com:[SSL.com账号页面](https://secure.ssl.com/account),然后点击api
|
||||||
|
credentials链接,然后点击编辑按钮,查看Secret key和HMAC key
|
||||||
|
|
||||||
|
litessl:[litesslEAB页面](https://freessl.cn/automation/eab-manager),然后点击新增EAB
|
||||||
|
required: false
|
||||||
|
encrypt: true
|
||||||
|
mergeScript: |2-
|
||||||
|
|
||||||
|
return {
|
||||||
|
show: ctx.compute(({form})=>{
|
||||||
|
const caType = form.access?.caType;
|
||||||
|
return ['google','zerossl','sslcom','litessl'].includes(caType);
|
||||||
|
}),
|
||||||
|
component: {
|
||||||
|
disabled: ctx.compute(({form})=> !!form.access?.account)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
eabHmacKey:
|
||||||
|
title: EAB HMAC Key
|
||||||
|
component:
|
||||||
|
placeholder: 需要EAB的颁发机构生成账号时填写
|
||||||
|
required: false
|
||||||
|
encrypt: true
|
||||||
|
mergeScript: |2-
|
||||||
|
|
||||||
|
return {
|
||||||
|
show: ctx.compute(({form})=>{
|
||||||
|
const caType = form.access?.caType;
|
||||||
|
return ['google','zerossl','sslcom','litessl'].includes(caType);
|
||||||
|
}),
|
||||||
|
component: {
|
||||||
|
disabled: ctx.compute(({form})=> !!form.access?.account)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
account:
|
||||||
|
title: ACME账号信息
|
||||||
|
component:
|
||||||
|
name: refresh-input
|
||||||
|
action: GenerateAccount
|
||||||
|
buttonText: 生成ACME账号
|
||||||
|
successMessage: ACME账号已生成,请保存授权配置
|
||||||
|
required: true
|
||||||
|
helper: 请生成ACME账号,账号一旦生成不允许修改
|
||||||
|
encrypt: true
|
||||||
|
mergeScript: |2-
|
||||||
|
|
||||||
|
return {
|
||||||
|
component: {
|
||||||
|
disabled: ctx.compute(({form})=> !!form.access?.account)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pluginType: access
|
||||||
|
type: builtIn
|
||||||
|
scriptFilePath: /plugins/plugin-cert/access/acme-account-access.js
|
||||||
@@ -51,6 +51,12 @@ input:
|
|||||||
required: true
|
required: true
|
||||||
order: -1
|
order: -1
|
||||||
helper: 请输入邮箱
|
helper: 请输入邮箱
|
||||||
|
version:
|
||||||
|
title: 版本
|
||||||
|
value: 2
|
||||||
|
isSys: true
|
||||||
|
show: false
|
||||||
|
order: 0
|
||||||
challengeType:
|
challengeType:
|
||||||
title: 域名验证方式
|
title: 域名验证方式
|
||||||
value: dns
|
value: dns
|
||||||
@@ -60,6 +66,8 @@ input:
|
|||||||
options:
|
options:
|
||||||
- value: dns
|
- value: dns
|
||||||
label: DNS直接验证
|
label: DNS直接验证
|
||||||
|
- value: dns-persist
|
||||||
|
label: DNS持久验证
|
||||||
- value: cname
|
- value: cname
|
||||||
label: CNAME代理验证
|
label: CNAME代理验证
|
||||||
- value: http
|
- value: http
|
||||||
@@ -80,6 +88,9 @@ input:
|
|||||||
4. <b>多DNS提供商</b>:每个域名可以选择独立的DNS提供商
|
4. <b>多DNS提供商</b>:每个域名可以选择独立的DNS提供商
|
||||||
|
|
||||||
5. <b>自动匹配</b>:此处无需选择校验方式,需要在[域名管理](#/certd/cert/domain)中提前配置好校验方式
|
5. <b>自动匹配</b>:此处无需选择校验方式,需要在[域名管理](#/certd/cert/domain)中提前配置好校验方式
|
||||||
|
|
||||||
|
6. <b>DNS持久验证</b>:需要先配置ACME账号和_validation-persist持久TXT记录,续期时不再增删DNS记录;当前仅
|
||||||
|
Let's Encrypt 测试环境可以申请
|
||||||
order: 0
|
order: 0
|
||||||
dnsProviderType:
|
dnsProviderType:
|
||||||
title: DNS解析服务商
|
title: DNS解析服务商
|
||||||
@@ -103,7 +114,7 @@ input:
|
|||||||
required: true
|
required: true
|
||||||
helper: |-
|
helper: |-
|
||||||
您的域名注册商,或者域名的dns服务器属于哪个平台
|
您的域名注册商,或者域名的dns服务器属于哪个平台
|
||||||
如果这里没有,请选择CNAME代理验证校验方式
|
如果这里没有,请选择CNAME代理验证
|
||||||
order: 0
|
order: 0
|
||||||
dnsProviderAccess:
|
dnsProviderAccess:
|
||||||
title: DNS解析授权
|
title: DNS解析授权
|
||||||
@@ -141,18 +152,30 @@ input:
|
|||||||
}),
|
}),
|
||||||
defaultType: ctx.compute(({form})=>{
|
defaultType: ctx.compute(({form})=>{
|
||||||
return form.challengeType || 'cname'
|
return form.challengeType || 'cname'
|
||||||
|
}),
|
||||||
|
caType: ctx.compute(({form})=>{
|
||||||
|
return form.sslProvider
|
||||||
|
}),
|
||||||
|
acmeAccountAccessId: ctx.compute(({form})=>{
|
||||||
|
return form.acmeAccountAccessId
|
||||||
|
}),
|
||||||
|
commonAcmeAccountAccessId: ctx.compute(({form})=>{
|
||||||
|
const key = form.sslProvider + 'CommonAcmeAccountAccessId';
|
||||||
|
return form[key]
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
show: ctx.compute(({form})=>{
|
show: ctx.compute(({form})=>{
|
||||||
return form.challengeType === 'cname' || form.challengeType === 'http' || form.challengeType === 'dnses'
|
return form.challengeType === 'cname' || form.challengeType === 'http' || form.challengeType === 'dnses' || form.challengeType === 'dns-persist'
|
||||||
}),
|
}),
|
||||||
helper: ctx.compute(({form})=>{
|
helper: ctx.compute(({form})=>{
|
||||||
if(form.challengeType === 'cname' ){
|
if(form.challengeType === 'cname' ){
|
||||||
return '请按照上面的提示,给要申请证书的域名添加CNAME记录,添加后,点击验证,验证成功后不要删除记录,申请和续期证书会一直用它'
|
return '请按照上面的提示,给要申请证书的域名添加CNAME记录,添加后,点击验证,验证成功后不要删除记录,申请和续期证书会一直用它'
|
||||||
}else if (form.challengeType === 'http'){
|
}else if (form.challengeType === 'http'){
|
||||||
return '请按照上面的提示,给每个域名设置文件上传配置,证书申请过程中会上传校验文件到网站根目录的.well-known/acme-challenge/目录下'
|
return '请按照上面的提示,给每个域名设置文件上传配置,证书申请过程中会上传校验文件到网站根目录文件夹下,请确保该校验文件可以公网http访问到'
|
||||||
}else if (form.challengeType === 'http'){
|
}else if (form.challengeType === 'dnses'){
|
||||||
return '给每个域名单独配置dns提供商'
|
return '给每个域名单独配置dns提供商'
|
||||||
|
}else if (form.challengeType === 'dns-persist'){
|
||||||
|
return '请先创建并校验_validation-persist TXT持久记录,校验成功后才能提交流水线;当前仅 Let\'s Encrypt 测试环境可以申请'
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -195,21 +218,41 @@ input:
|
|||||||
isSys: true
|
isSys: true
|
||||||
show: false
|
show: false
|
||||||
order: 0
|
order: 0
|
||||||
|
googleCommonAcmeAccountAccessId:
|
||||||
|
title: Google公共ACME账号
|
||||||
|
isSys: true
|
||||||
|
show: false
|
||||||
|
order: 0
|
||||||
zerosslCommonEabAccessId:
|
zerosslCommonEabAccessId:
|
||||||
title: ZeroSSL公共EAB授权
|
title: ZeroSSL公共EAB授权
|
||||||
isSys: true
|
isSys: true
|
||||||
show: false
|
show: false
|
||||||
order: 0
|
order: 0
|
||||||
|
zerosslCommonAcmeAccountAccessId:
|
||||||
|
title: ZeroSSL公共ACME账号
|
||||||
|
isSys: true
|
||||||
|
show: false
|
||||||
|
order: 0
|
||||||
sslcomCommonEabAccessId:
|
sslcomCommonEabAccessId:
|
||||||
title: SSL.com公共EAB授权
|
title: SSL.com公共EAB授权
|
||||||
isSys: true
|
isSys: true
|
||||||
show: false
|
show: false
|
||||||
order: 0
|
order: 0
|
||||||
|
sslcomCommonAcmeAccountAccessId:
|
||||||
|
title: SSL.com公共ACME账号
|
||||||
|
isSys: true
|
||||||
|
show: false
|
||||||
|
order: 0
|
||||||
litesslCommonEabAccessId:
|
litesslCommonEabAccessId:
|
||||||
title: litessl公共EAB授权
|
title: litessl公共EAB授权
|
||||||
isSys: true
|
isSys: true
|
||||||
show: false
|
show: false
|
||||||
order: 0
|
order: 0
|
||||||
|
litesslCommonAcmeAccountAccessId:
|
||||||
|
title: litessl公共ACME账号
|
||||||
|
isSys: true
|
||||||
|
show: false
|
||||||
|
order: 0
|
||||||
eabAccessId:
|
eabAccessId:
|
||||||
title: EAB授权
|
title: EAB授权
|
||||||
component:
|
component:
|
||||||
@@ -233,7 +276,16 @@ input:
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
show: ctx.compute(({form})=>{
|
show: ctx.compute(({form})=>{
|
||||||
console.log("show",form)
|
if (form.version === 2) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if(form.acmeAccountAccessId){
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
const commonAcmeKey = form.sslProvider + 'CommonAcmeAccountAccessId';
|
||||||
|
if (form[commonAcmeKey]) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
return (form.sslProvider === 'zerossl' && !form.zerosslCommonEabAccessId)
|
return (form.sslProvider === 'zerossl' && !form.zerosslCommonEabAccessId)
|
||||||
|| (form.sslProvider === 'google' && !form.googleCommonEabAccessId)
|
|| (form.sslProvider === 'google' && !form.googleCommonEabAccessId)
|
||||||
|| (form.sslProvider === 'sslcom' && !form.sslcomCommonEabAccessId)
|
|| (form.sslProvider === 'sslcom' && !form.sslcomCommonEabAccessId)
|
||||||
@@ -241,6 +293,36 @@ input:
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
order: 0
|
||||||
|
acmeAccountAccessId:
|
||||||
|
title: ACME账号
|
||||||
|
component:
|
||||||
|
name: access-selector
|
||||||
|
type: acmeAccount
|
||||||
|
required: false
|
||||||
|
helper: 请选择颁发机构对应的ACME账号
|
||||||
|
mergeScript: |2-
|
||||||
|
|
||||||
|
return {
|
||||||
|
show: ctx.compute(({form})=>{
|
||||||
|
const commonKey = form.sslProvider + 'CommonAcmeAccountAccessId';
|
||||||
|
if (form[commonKey]) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return !!form.sslProvider
|
||||||
|
}),
|
||||||
|
component:{
|
||||||
|
subtype: ctx.compute(({form})=> form.sslProvider)
|
||||||
|
},
|
||||||
|
required: ctx.compute(({form})=>{
|
||||||
|
const commonKey = form.sslProvider + 'CommonAcmeAccountAccessId';
|
||||||
|
if (form[commonKey]) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return form.version === 2
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
order: 0
|
order: 0
|
||||||
googleAccessId:
|
googleAccessId:
|
||||||
title: 服务账号授权
|
title: 服务账号授权
|
||||||
@@ -257,6 +339,15 @@ input:
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
show: ctx.compute(({form})=>{
|
show: ctx.compute(({form})=>{
|
||||||
|
if (form.version === 2) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if(form.acmeAccountAccessId){
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if(form.googleCommonAcmeAccountAccessId){
|
||||||
|
return false
|
||||||
|
}
|
||||||
return form.sslProvider === 'google' && !form.googleCommonEabAccessId
|
return form.sslProvider === 'google' && !form.googleCommonEabAccessId
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -81,7 +81,6 @@ input:
|
|||||||
search: false
|
search: false
|
||||||
pager: true
|
pager: true
|
||||||
single: true
|
single: true
|
||||||
pageSize: 50
|
|
||||||
watches:
|
watches:
|
||||||
- certDomains
|
- certDomains
|
||||||
- accessId
|
- accessId
|
||||||
@@ -96,7 +95,7 @@ input:
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
helper: 订阅模式的证书订单 Id(在新建流水线时暂时无法获取,可以先随便填个数字,先创建,进入流水线编辑页面再获取选择即可)
|
helper: 订阅模式的证书订单 Id
|
||||||
order: 0
|
order: 0
|
||||||
pfxPassword:
|
pfxPassword:
|
||||||
title: 证书加密密码
|
title: 证书加密密码
|
||||||
|
|||||||
@@ -134,5 +134,6 @@ export class MainConfiguration {
|
|||||||
});
|
});
|
||||||
|
|
||||||
logger.info("当前环境:", this.app.getEnv()); // prod
|
logger.info("当前环境:", this.app.getEnv()); // prod
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user