mirror of
https://github.com/certd/certd.git
synced 2026-07-21 10:33:36 +08:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4efe12d2d3 | |||
| 67b05e2d75 | |||
| f4bb459b5e | |||
| 83a5a21f95 | |||
| 85c633fddf | |||
| f9a310b6c3 | |||
| 1bdcfe646f |
@@ -35,6 +35,8 @@ Certd 是一个支持私有化部署的 SSL/TLS 证书自动化管理平台。
|
|||||||
- `packages/ui/certd-server/`:后端服务
|
- `packages/ui/certd-server/`:后端服务
|
||||||
- `packages/ui/certd-client/`:前端 Web 管理台
|
- `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`。
|
主要后端包:`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 = .*(?=;)/"`,不是有效的项目类型检查结果。
|
- 不要运行前端 `pnpm tsc` / `vue-tsc`:当前依赖组合中 `vue-tsc@1.8.27` 会直接抛内部错误 `Search string not found: "/supportedTSExtensions = .*(?=;)/"`,不是有效的项目类型检查结果。
|
||||||
- 前端暂不跑单元测试;当前 `test:unit` 只是占位脚本
|
- 前端暂不跑单元测试;当前 `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 等通常视为运行时或构建产物,除非任务明确要求处理它们。
|
- `packages/ui/certd-server/data/`、`logs/`、生成的 metadata/dist 等通常视为运行时或构建产物,除非任务明确要求处理它们。
|
||||||
- 注意本地数据和配置里可能包含凭据、证书材料等敏感信息。
|
- 注意本地数据和配置里可能包含凭据、证书材料等敏感信息。
|
||||||
- 本仓库代码注释优先使用中文,尤其是解释业务规则、兼容逻辑、协议细节和隐藏风险时;除非文件已有明确英文注释风格或引用外部英文术语,否则不要新增英文说明性注释。
|
- 本仓库代码注释优先使用中文,尤其是解释业务规则、兼容逻辑、协议细节和隐藏风险时;除非文件已有明确英文注释风格或引用外部英文术语,否则不要新增英文说明性注释。
|
||||||
|
- 代码可读性优先于短写法。遇到包含业务分支的复杂三元表达式、内联对象、链式调用或条件组合时,优先拆成命名清晰的中间变量、独立分支或小函数,让读代码的人能一眼看出业务意图;不要为了少写几行把逻辑压成难读的一坨。
|
||||||
|
|
||||||
## 插件开发技能
|
## 插件开发技能
|
||||||
|
|
||||||
@@ -182,6 +193,7 @@ Certd 是一个支持私有化部署的 SSL/TLS 证书自动化管理平台。
|
|||||||
|
|
||||||
- `access-plugin-dev`:开发 Access 授权插件
|
- `access-plugin-dev`:开发 Access 授权插件
|
||||||
- `dns-provider-dev`:开发 DNS Provider 插件
|
- `dns-provider-dev`:开发 DNS Provider 插件
|
||||||
|
- `fast-crud-page-dev`:开发或重构前端 Fast Crud 列表管理页面
|
||||||
- `task-plugin-dev`:开发 Task 部署任务插件
|
- `task-plugin-dev`:开发 Task 部署任务插件
|
||||||
- `plugin-converter`:将插件转换为 YAML 配置
|
- `plugin-converter`:将插件转换为 YAML 配置
|
||||||
|
|
||||||
|
|||||||
@@ -3,17 +3,6 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
|
|
||||||
## [1.40.4](https://github.com/certd/certd/compare/v1.40.3...v1.40.4) (2026-05-24)
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
* **pipeline-service:** 修复流水线运行时超过套餐部署次数仍然能够正常运行的bug ([5e59651](https://github.com/certd/certd/commit/5e59651d45bc91919629e35995ff1b3cff6b87ea))
|
|
||||||
|
|
||||||
### Performance Improvements
|
|
||||||
|
|
||||||
* 商业版套餐只支持设置为可叠加 ([5e72f75](https://github.com/certd/certd/commit/5e72f75395fb632a30e80c07d35d8ba40ef631fa))
|
|
||||||
* 新增阿里云直播证书部署插件 ([8edb6f8](https://github.com/certd/certd/commit/8edb6f8727bd148f106801bef25567880fd35e9e))
|
|
||||||
|
|
||||||
## [1.40.3](https://github.com/certd/certd/compare/v1.40.2...v1.40.3) (2026-05-21)
|
## [1.40.3](https://github.com/certd/certd/compare/v1.40.2...v1.40.3) (2026-05-21)
|
||||||
|
|
||||||
### Bug Fixes
|
### Bug Fixes
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# 任务插件
|
# 任务插件
|
||||||
共 `132` 款任务插件
|
共 `131` 款任务插件
|
||||||
## 1. 证书申请
|
## 1. 证书申请
|
||||||
|
|
||||||
| 序号 | 名称 | 说明 |
|
| 序号 | 名称 | 说明 |
|
||||||
@@ -93,14 +93,13 @@
|
|||||||
| 9.| **阿里云-部署至ESA** | 部署证书到阿里云ESA(边缘安全加速),自动删除过期证书 |
|
| 9.| **阿里云-部署至ESA** | 部署证书到阿里云ESA(边缘安全加速),自动删除过期证书 |
|
||||||
| 10.| **阿里云-部署至阿里云FC(3.0)** | 部署证书到阿里云函数计算(FC3.0) |
|
| 10.| **阿里云-部署至阿里云FC(3.0)** | 部署证书到阿里云函数计算(FC3.0) |
|
||||||
| 11.| **阿里云-部署至GA** | 部署证书到阿里云GA(全球加速),支持更新默认证书和扩展证书 |
|
| 11.| **阿里云-部署至GA** | 部署证书到阿里云GA(全球加速),支持更新默认证书和扩展证书 |
|
||||||
| 12.| **阿里云-部署至直播(Live)** | 部署证书到阿里云视频直播(Live)域名 |
|
| 12.| **阿里云-部署至NLB(网络负载均衡)** | NLB,网络负载均衡,更新监听器的默认证书 |
|
||||||
| 13.| **阿里云-部署至NLB(网络负载均衡)** | NLB,网络负载均衡,更新监听器的默认证书 |
|
| 13.| **阿里云-部署证书至OSS** | 部署域名证书至阿里云OSS自定义域名,不是上传到阿里云oss |
|
||||||
| 14.| **阿里云-部署证书至OSS** | 部署域名证书至阿里云OSS自定义域名,不是上传到阿里云oss |
|
| 14.| **阿里云-部署至CLB(传统负载均衡)** | 部署证书到阿里云CLB(传统负载均衡) |
|
||||||
| 15.| **阿里云-部署至CLB(传统负载均衡)** | 部署证书到阿里云CLB(传统负载均衡) |
|
| 15.| **阿里云-部署至VOD** | 部署证书到阿里云视频点播(vod) |
|
||||||
| 16.| **阿里云-部署至VOD** | 部署证书到阿里云视频点播(vod) |
|
| 16.| **阿里云-部署至阿里云WAF(云产品接入)** | 部署证书到阿里云WAF(云产品接入),CNAME方式接入的请选择另外一个waf插件 |
|
||||||
| 17.| **阿里云-部署至阿里云WAF(云产品接入)** | 部署证书到阿里云WAF(云产品接入),CNAME方式接入的请选择另外一个waf插件 |
|
| 17.| **阿里云-部署至阿里云WAF(cname接入)** | 部署证书到阿里云WAF(cname接入),云资源的请选择另外一个waf插件 |
|
||||||
| 18.| **阿里云-部署至阿里云WAF(cname接入)** | 部署证书到阿里云WAF(cname接入),云资源的请选择另外一个waf插件 |
|
| 18.| **阿里云-上传证书到CAS** | 上传证书到阿里云证书管理服务(CAS),如果不想在阿里云上同一份证书上传多次,可以把此任务作为前置任务,其他阿里云任务证书那一项选择此任务的输出 |
|
||||||
| 19.| **阿里云-上传证书到CAS** | 上传证书到阿里云证书管理服务(CAS),如果不想在阿里云上同一份证书上传多次,可以把此任务作为前置任务,其他阿里云任务证书那一项选择此任务的输出 |
|
|
||||||
## 6. 华为云
|
## 6. 华为云
|
||||||
|
|
||||||
| 序号 | 名称 | 说明 |
|
| 序号 | 名称 | 说明 |
|
||||||
|
|||||||
+1
-1
@@ -9,5 +9,5 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"npmClient": "pnpm",
|
"npmClient": "pnpm",
|
||||||
"version": "1.40.4"
|
"version": "1.40.3"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,10 +3,6 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
|
|
||||||
## [1.40.4](https://github.com/publishlab/node-acme-client/compare/v1.40.3...v1.40.4) (2026-05-24)
|
|
||||||
|
|
||||||
**Note:** Version bump only for package @certd/acme-client
|
|
||||||
|
|
||||||
## [1.40.3](https://github.com/publishlab/node-acme-client/compare/v1.40.2...v1.40.3) (2026-05-21)
|
## [1.40.3](https://github.com/publishlab/node-acme-client/compare/v1.40.2...v1.40.3) (2026-05-21)
|
||||||
|
|
||||||
**Note:** Version bump only for package @certd/acme-client
|
**Note:** Version bump only for package @certd/acme-client
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
"description": "Simple and unopinionated ACME client",
|
"description": "Simple and unopinionated ACME client",
|
||||||
"private": false,
|
"private": false,
|
||||||
"author": "nmorsman",
|
"author": "nmorsman",
|
||||||
"version": "1.40.4",
|
"version": "1.40.3",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"module": "./dist/index.js",
|
"module": "./dist/index.js",
|
||||||
"main": "./dist/index.js",
|
"main": "./dist/index.js",
|
||||||
@@ -18,7 +18,7 @@
|
|||||||
"types"
|
"types"
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@certd/basic": "^1.40.4",
|
"@certd/basic": "^1.40.3",
|
||||||
"@peculiar/x509": "^1.11.0",
|
"@peculiar/x509": "^1.11.0",
|
||||||
"asn1js": "^3.0.5",
|
"asn1js": "^3.0.5",
|
||||||
"axios": "^1.9.0",
|
"axios": "^1.9.0",
|
||||||
|
|||||||
@@ -467,6 +467,10 @@ class AcmeClient {
|
|||||||
return createHash('sha256').update(result).digest('base64url');
|
return createHash('sha256').update(result).digest('base64url');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (challenge.type === 'dns-persist-01') {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
/* https://datatracker.ietf.org/doc/html/rfc8737 */
|
/* https://datatracker.ietf.org/doc/html/rfc8737 */
|
||||||
if (challenge.type === 'tls-alpn-01') {
|
if (challenge.type === 'tls-alpn-01') {
|
||||||
return result;
|
return result;
|
||||||
|
|||||||
@@ -97,7 +97,11 @@ export interface DnsChallenge extends ChallengeAbstract {
|
|||||||
token: string;
|
token: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Challenge = HttpChallenge | DnsChallenge;
|
export interface DnsPersistChallenge extends ChallengeAbstract {
|
||||||
|
type: "dns-persist-01";
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Challenge = HttpChallenge | DnsChallenge | DnsPersistChallenge;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Certificate
|
* Certificate
|
||||||
|
|||||||
@@ -170,7 +170,7 @@ export function createChallengeFn(opts = {}) {
|
|||||||
|
|
||||||
|
|
||||||
if (txtRecords.length === 0) {
|
if (txtRecords.length === 0) {
|
||||||
throw new Error(`没有找到TXT解析记录(${recordName})`);
|
throw new Error(`没有找到TXT解析记录(${recordName}),请稍后重试`);
|
||||||
}
|
}
|
||||||
return txtRecords;
|
return txtRecords;
|
||||||
}
|
}
|
||||||
@@ -203,6 +203,24 @@ export function createChallengeFn(opts = {}) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function verifyDnsPersistChallenge(authz, challenge, keyAuthorization, prefix = '_validation-persist.') {
|
||||||
|
const recordName = `${prefix}${authz.identifier.value.replace(/^\*\./, '')}`;
|
||||||
|
log(`本地校验DNS持久验证TXT记录: ${recordName}`);
|
||||||
|
let recordValues = await walkTxtRecord(recordName, 0, walkFromAuthoritative);
|
||||||
|
recordValues = [...new Set(recordValues)];
|
||||||
|
const expected = challenge.expectedRecordValue;
|
||||||
|
if (!expected) {
|
||||||
|
log(`未提供dns-persist-01本地校验期望值,跳过精确匹配,仅确认TXT记录存在`);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
log(`DNS查询成功, 找到 ${recordValues.length} 条TXT记录:${recordValues}`);
|
||||||
|
if (!recordValues.length || !recordValues.includes(expected)) {
|
||||||
|
throw new Error(`没有找到需要的DNS持久验证TXT记录: ${recordName},请稍后重试,期望:${expected},结果:${recordValues}`);
|
||||||
|
}
|
||||||
|
log(`DNS持久验证记录匹配成功(${challenge.type}/${recordName}):${expected}`);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Verify ACME TLS ALPN challenge
|
* Verify ACME TLS ALPN challenge
|
||||||
*
|
*
|
||||||
@@ -234,6 +252,7 @@ export function createChallengeFn(opts = {}) {
|
|||||||
challenges: {
|
challenges: {
|
||||||
'http-01': verifyHttpChallenge,
|
'http-01': verifyHttpChallenge,
|
||||||
'dns-01': verifyDnsChallenge,
|
'dns-01': verifyDnsChallenge,
|
||||||
|
'dns-persist-01': verifyDnsPersistChallenge,
|
||||||
'tls-alpn-01': verifyTlsAlpnChallenge,
|
'tls-alpn-01': verifyTlsAlpnChallenge,
|
||||||
},
|
},
|
||||||
walkTxtRecord,
|
walkTxtRecord,
|
||||||
|
|||||||
+1
-1
@@ -57,7 +57,7 @@ export interface ClientExternalAccountBindingOptions {
|
|||||||
|
|
||||||
export interface ClientAutoOptions {
|
export interface ClientAutoOptions {
|
||||||
csr: CsrBuffer | CsrString;
|
csr: CsrBuffer | CsrString;
|
||||||
challengeCreateFn: (authz: Authorization, keyAuthorization: (challenge:rfc8555.Challenge)=>Promise<string>) => Promise<{recordReq?:any,recordRes?:any,dnsProvider?:any,challenge: rfc8555.Challenge,keyAuthorization:string}>;
|
challengeCreateFn: (authz: Authorization, keyAuthorization: (challenge:rfc8555.Challenge)=>Promise<string>) => Promise<{recordReq?:any,recordRes?:any,dnsProvider?:any,challenge: rfc8555.Challenge,keyAuthorization:string,httpUploader?:any}>;
|
||||||
challengeRemoveFn: (authz: Authorization, challenge: rfc8555.Challenge, keyAuthorization: string,recordReq:any, recordRes:any,dnsProvider:any,httpUploader:any) => Promise<any>;
|
challengeRemoveFn: (authz: Authorization, challenge: rfc8555.Challenge, keyAuthorization: string,recordReq:any, recordRes:any,dnsProvider:any,httpUploader:any) => Promise<any>;
|
||||||
email?: string;
|
email?: string;
|
||||||
termsOfServiceAgreed?: boolean;
|
termsOfServiceAgreed?: boolean;
|
||||||
|
|||||||
+5
-1
@@ -97,7 +97,11 @@ export interface DnsChallenge extends ChallengeAbstract {
|
|||||||
token: string;
|
token: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Challenge = HttpChallenge | DnsChallenge;
|
export interface DnsPersistChallenge extends ChallengeAbstract {
|
||||||
|
type: 'dns-persist-01';
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Challenge = HttpChallenge | DnsChallenge | DnsPersistChallenge;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Certificate
|
* Certificate
|
||||||
|
|||||||
@@ -3,10 +3,6 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
|
|
||||||
## [1.40.4](https://github.com/certd/certd/compare/v1.40.3...v1.40.4) (2026-05-24)
|
|
||||||
|
|
||||||
**Note:** Version bump only for package @certd/basic
|
|
||||||
|
|
||||||
## [1.40.3](https://github.com/certd/certd/compare/v1.40.2...v1.40.3) (2026-05-21)
|
## [1.40.3](https://github.com/certd/certd/compare/v1.40.2...v1.40.3) (2026-05-21)
|
||||||
|
|
||||||
**Note:** Version bump only for package @certd/basic
|
**Note:** Version bump only for package @certd/basic
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
00:18
|
22:57
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@certd/basic",
|
"name": "@certd/basic",
|
||||||
"private": false,
|
"private": false,
|
||||||
"version": "1.40.4",
|
"version": "1.40.3",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "./dist/index.js",
|
"main": "./dist/index.js",
|
||||||
"module": "./dist/index.js",
|
"module": "./dist/index.js",
|
||||||
|
|||||||
@@ -3,10 +3,6 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
|
|
||||||
## [1.40.4](https://github.com/certd/certd/compare/v1.40.3...v1.40.4) (2026-05-24)
|
|
||||||
|
|
||||||
**Note:** Version bump only for package @certd/pipeline
|
|
||||||
|
|
||||||
## [1.40.3](https://github.com/certd/certd/compare/v1.40.2...v1.40.3) (2026-05-21)
|
## [1.40.3](https://github.com/certd/certd/compare/v1.40.2...v1.40.3) (2026-05-21)
|
||||||
|
|
||||||
**Note:** Version bump only for package @certd/pipeline
|
**Note:** Version bump only for package @certd/pipeline
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@certd/pipeline",
|
"name": "@certd/pipeline",
|
||||||
"private": false,
|
"private": false,
|
||||||
"version": "1.40.4",
|
"version": "1.40.3",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "./dist/index.js",
|
"main": "./dist/index.js",
|
||||||
"module": "./dist/index.js",
|
"module": "./dist/index.js",
|
||||||
@@ -19,8 +19,8 @@
|
|||||||
"compile": "tsc --skipLibCheck --watch"
|
"compile": "tsc --skipLibCheck --watch"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@certd/basic": "^1.40.4",
|
"@certd/basic": "^1.40.3",
|
||||||
"@certd/plus-core": "^1.40.4",
|
"@certd/plus-core": "^1.40.3",
|
||||||
"dayjs": "^1.11.7",
|
"dayjs": "^1.11.7",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
"reflect-metadata": "^0.1.13"
|
"reflect-metadata": "^0.1.13"
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ export type AccessInputDefine = FormItemProps & {
|
|||||||
};
|
};
|
||||||
export type AccessDefine = Registrable & {
|
export type AccessDefine = Registrable & {
|
||||||
icon?: string;
|
icon?: string;
|
||||||
|
subtype?: string;
|
||||||
input?: {
|
input?: {
|
||||||
[key: string]: AccessInputDefine;
|
[key: string]: AccessInputDefine;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,10 +3,6 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
|
|
||||||
## [1.40.4](https://github.com/certd/certd/compare/v1.40.3...v1.40.4) (2026-05-24)
|
|
||||||
|
|
||||||
**Note:** Version bump only for package @certd/lib-huawei
|
|
||||||
|
|
||||||
## [1.40.3](https://github.com/certd/certd/compare/v1.40.2...v1.40.3) (2026-05-21)
|
## [1.40.3](https://github.com/certd/certd/compare/v1.40.2...v1.40.3) (2026-05-21)
|
||||||
|
|
||||||
**Note:** Version bump only for package @certd/lib-huawei
|
**Note:** Version bump only for package @certd/lib-huawei
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@certd/lib-huawei",
|
"name": "@certd/lib-huawei",
|
||||||
"private": false,
|
"private": false,
|
||||||
"version": "1.40.4",
|
"version": "1.40.3",
|
||||||
"main": "./dist/bundle.js",
|
"main": "./dist/bundle.js",
|
||||||
"module": "./dist/bundle.js",
|
"module": "./dist/bundle.js",
|
||||||
"types": "./dist/d/index.d.ts",
|
"types": "./dist/d/index.d.ts",
|
||||||
|
|||||||
@@ -3,10 +3,6 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
|
|
||||||
## [1.40.4](https://github.com/certd/certd/compare/v1.40.3...v1.40.4) (2026-05-24)
|
|
||||||
|
|
||||||
**Note:** Version bump only for package @certd/lib-iframe
|
|
||||||
|
|
||||||
## [1.40.3](https://github.com/certd/certd/compare/v1.40.2...v1.40.3) (2026-05-21)
|
## [1.40.3](https://github.com/certd/certd/compare/v1.40.2...v1.40.3) (2026-05-21)
|
||||||
|
|
||||||
**Note:** Version bump only for package @certd/lib-iframe
|
**Note:** Version bump only for package @certd/lib-iframe
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@certd/lib-iframe",
|
"name": "@certd/lib-iframe",
|
||||||
"private": false,
|
"private": false,
|
||||||
"version": "1.40.4",
|
"version": "1.40.3",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "./dist/index.js",
|
"main": "./dist/index.js",
|
||||||
"module": "./dist/index.js",
|
"module": "./dist/index.js",
|
||||||
|
|||||||
@@ -3,10 +3,6 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
|
|
||||||
## [1.40.4](https://github.com/certd/certd/compare/v1.40.3...v1.40.4) (2026-05-24)
|
|
||||||
|
|
||||||
**Note:** Version bump only for package @certd/jdcloud
|
|
||||||
|
|
||||||
## [1.40.3](https://github.com/certd/certd/compare/v1.40.2...v1.40.3) (2026-05-21)
|
## [1.40.3](https://github.com/certd/certd/compare/v1.40.2...v1.40.3) (2026-05-21)
|
||||||
|
|
||||||
**Note:** Version bump only for package @certd/jdcloud
|
**Note:** Version bump only for package @certd/jdcloud
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@certd/jdcloud",
|
"name": "@certd/jdcloud",
|
||||||
"version": "1.40.4",
|
"version": "1.40.3",
|
||||||
"description": "jdcloud openApi sdk",
|
"description": "jdcloud openApi sdk",
|
||||||
"main": "./dist/bundle.js",
|
"main": "./dist/bundle.js",
|
||||||
"module": "./dist/bundle.js",
|
"module": "./dist/bundle.js",
|
||||||
|
|||||||
@@ -3,10 +3,6 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
|
|
||||||
## [1.40.4](https://github.com/certd/certd/compare/v1.40.3...v1.40.4) (2026-05-24)
|
|
||||||
|
|
||||||
**Note:** Version bump only for package @certd/lib-k8s
|
|
||||||
|
|
||||||
## [1.40.3](https://github.com/certd/certd/compare/v1.40.2...v1.40.3) (2026-05-21)
|
## [1.40.3](https://github.com/certd/certd/compare/v1.40.2...v1.40.3) (2026-05-21)
|
||||||
|
|
||||||
**Note:** Version bump only for package @certd/lib-k8s
|
**Note:** Version bump only for package @certd/lib-k8s
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@certd/lib-k8s",
|
"name": "@certd/lib-k8s",
|
||||||
"private": false,
|
"private": false,
|
||||||
"version": "1.40.4",
|
"version": "1.40.3",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "./dist/index.js",
|
"main": "./dist/index.js",
|
||||||
"module": "./dist/index.js",
|
"module": "./dist/index.js",
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
"compile": "tsc --skipLibCheck --watch"
|
"compile": "tsc --skipLibCheck --watch"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@certd/basic": "^1.40.4",
|
"@certd/basic": "^1.40.3",
|
||||||
"@kubernetes/client-node": "0.21.0"
|
"@kubernetes/client-node": "0.21.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -3,12 +3,6 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
|
|
||||||
## [1.40.4](https://github.com/certd/certd/compare/v1.40.3...v1.40.4) (2026-05-24)
|
|
||||||
|
|
||||||
### Performance Improvements
|
|
||||||
|
|
||||||
* 商业版套餐只支持设置为可叠加 ([5e72f75](https://github.com/certd/certd/commit/5e72f75395fb632a30e80c07d35d8ba40ef631fa))
|
|
||||||
|
|
||||||
## [1.40.3](https://github.com/certd/certd/compare/v1.40.2...v1.40.3) (2026-05-21)
|
## [1.40.3](https://github.com/certd/certd/compare/v1.40.2...v1.40.3) (2026-05-21)
|
||||||
|
|
||||||
**Note:** Version bump only for package @certd/lib-server
|
**Note:** Version bump only for package @certd/lib-server
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@certd/lib-server",
|
"name": "@certd/lib-server",
|
||||||
"version": "1.40.4",
|
"version": "1.40.3",
|
||||||
"description": "midway with flyway, sql upgrade way ",
|
"description": "midway with flyway, sql upgrade way ",
|
||||||
"private": false,
|
"private": false,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
@@ -29,11 +29,11 @@
|
|||||||
],
|
],
|
||||||
"license": "AGPL",
|
"license": "AGPL",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@certd/acme-client": "^1.40.4",
|
"@certd/acme-client": "^1.40.3",
|
||||||
"@certd/basic": "^1.40.4",
|
"@certd/basic": "^1.40.3",
|
||||||
"@certd/pipeline": "^1.40.4",
|
"@certd/pipeline": "^1.40.3",
|
||||||
"@certd/plugin-lib": "^1.40.4",
|
"@certd/plugin-lib": "^1.40.3",
|
||||||
"@certd/plus-core": "^1.40.4",
|
"@certd/plus-core": "^1.40.3",
|
||||||
"@midwayjs/cache": "3.14.0",
|
"@midwayjs/cache": "3.14.0",
|
||||||
"@midwayjs/core": "3.20.11",
|
"@midwayjs/core": "3.20.11",
|
||||||
"@midwayjs/i18n": "3.20.13",
|
"@midwayjs/i18n": "3.20.13",
|
||||||
|
|||||||
@@ -245,8 +245,6 @@ export class SysSuiteSetting extends BaseSettings {
|
|||||||
|
|
||||||
enabled: boolean = false;
|
enabled: boolean = false;
|
||||||
|
|
||||||
allowSuiteStack: boolean = false;
|
|
||||||
|
|
||||||
registerGift?: {
|
registerGift?: {
|
||||||
productId: number;
|
productId: number;
|
||||||
duration: number;
|
duration: number;
|
||||||
|
|||||||
@@ -19,6 +19,9 @@ export class AccessEntity {
|
|||||||
@Column({ comment: '类型', length: 100 })
|
@Column({ comment: '类型', length: 100 })
|
||||||
type: string;
|
type: string;
|
||||||
|
|
||||||
|
@Column({ name: 'subtype', comment: '子类型', length: 100, nullable: true })
|
||||||
|
subtype: string;
|
||||||
|
|
||||||
@Column({ name: 'setting', comment: '设置', length: 10240, nullable: true })
|
@Column({ name: 'setting', comment: '设置', length: 10240, nullable: true })
|
||||||
setting: string;
|
setting: string;
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { IAccessService } from '@certd/pipeline';
|
import { IAccessService } from "@certd/pipeline";
|
||||||
|
|
||||||
export class AccessGetter implements IAccessService {
|
export class AccessGetter implements IAccessService {
|
||||||
userId: number;
|
userId: number;
|
||||||
@@ -15,6 +15,6 @@ export class AccessGetter implements IAccessService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getCommonById<T = any>(id: any) {
|
async getCommonById<T = any>(id: any) {
|
||||||
return await this.getter<T>(id, 0,null);
|
return await this.getter<T>(id, 0, null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
import assert from "assert";
|
import assert from "assert";
|
||||||
|
import esmock from "esmock";
|
||||||
import { AccessService } from "./access-service.js";
|
import { AccessService } from "./access-service.js";
|
||||||
|
|
||||||
describe("AccessService", () => {
|
describe("AccessService", () => {
|
||||||
it("does not write id into access setting when updating selected fields", async () => {
|
it("does not write id into access setting when updating selected fields", async () => {
|
||||||
let updateParam: any;
|
let updateParam: any;
|
||||||
const service = new AccessService();
|
const service = new AccessService();
|
||||||
service.info = async () => ({
|
service.info = async () =>
|
||||||
id: 12,
|
({
|
||||||
type: "eab",
|
id: 12,
|
||||||
} as any);
|
type: "eab",
|
||||||
|
}) as any;
|
||||||
service.decryptAccessEntity = () => ({
|
service.decryptAccessEntity = () => ({
|
||||||
kid: "kid-1",
|
kid: "kid-1",
|
||||||
});
|
});
|
||||||
@@ -27,4 +29,82 @@ describe("AccessService", () => {
|
|||||||
accountKey: "account-key",
|
accountKey: "account-key",
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("writes subtype from access define field", async () => {
|
||||||
|
const { AccessService: MockedAccessService } = await esmock("./access-service.js", {
|
||||||
|
"@certd/pipeline": {
|
||||||
|
accessRegistry: {
|
||||||
|
getDefine(type: string) {
|
||||||
|
assert.equal(type, "acmeAccount");
|
||||||
|
return {
|
||||||
|
name: "acmeAccount",
|
||||||
|
subtype: "caType",
|
||||||
|
input: {
|
||||||
|
caType: {},
|
||||||
|
account: {
|
||||||
|
encrypt: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const service = new MockedAccessService();
|
||||||
|
service.encryptService = {
|
||||||
|
encrypt(value: string) {
|
||||||
|
return `encrypted:${value}`;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const param: any = {
|
||||||
|
type: "acmeAccount",
|
||||||
|
setting: JSON.stringify({
|
||||||
|
caType: "letsencrypt",
|
||||||
|
account: JSON.stringify({
|
||||||
|
accountKey: "key",
|
||||||
|
accountUri: "https://example.com/acct/1",
|
||||||
|
caType: "letsencrypt",
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
service.encryptSetting(param);
|
||||||
|
|
||||||
|
assert.equal(param.subtype, "letsencrypt");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("allows acme account access to be saved before account generation", async () => {
|
||||||
|
const { AccessService: MockedAccessService } = await esmock("./access-service.js", {
|
||||||
|
"@certd/pipeline": {
|
||||||
|
accessRegistry: {
|
||||||
|
getDefine() {
|
||||||
|
return {
|
||||||
|
name: "acmeAccount",
|
||||||
|
subtype: "caType",
|
||||||
|
input: {
|
||||||
|
caType: {},
|
||||||
|
account: {
|
||||||
|
encrypt: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const service = new MockedAccessService();
|
||||||
|
const param: any = {
|
||||||
|
type: "acmeAccount",
|
||||||
|
setting: JSON.stringify({
|
||||||
|
caType: "letsencrypt",
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
service.encryptSetting(param);
|
||||||
|
|
||||||
|
assert.equal(param.subtype, "letsencrypt");
|
||||||
|
assert.deepEqual(JSON.parse(param.setting), {
|
||||||
|
caType: "letsencrypt",
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
import {Inject, Provide, Scope, ScopeEnum} from '@midwayjs/core';
|
import { Inject, Provide, Scope, ScopeEnum } from "@midwayjs/core";
|
||||||
import {InjectEntityModel} from '@midwayjs/typeorm';
|
import { InjectEntityModel } from "@midwayjs/typeorm";
|
||||||
import { In, Repository } from "typeorm";
|
import { In, Repository } from "typeorm";
|
||||||
import {AccessGetter, BaseService, PageReq, PermissionException, ValidateException} from '../../../index.js';
|
import { AccessGetter, BaseService, PageReq, PermissionException, ValidateException } from "../../../index.js";
|
||||||
import {AccessEntity} from '../entity/access.js';
|
import { AccessEntity } from "../entity/access.js";
|
||||||
import {AccessDefine, accessRegistry, newAccess} from '@certd/pipeline';
|
import { AccessDefine, accessRegistry, newAccess } from "@certd/pipeline";
|
||||||
import {EncryptService} from './encrypt-service.js';
|
import { EncryptService } from "./encrypt-service.js";
|
||||||
import { logger, utils } from '@certd/basic';
|
import { logger, utils } from "@certd/basic";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 授权
|
* 授权
|
||||||
*/
|
*/
|
||||||
@Provide()
|
@Provide()
|
||||||
@Scope(ScopeEnum.Request, {allowDowngrade: true})
|
@Scope(ScopeEnum.Request, { allowDowngrade: true })
|
||||||
export class AccessService extends BaseService<AccessEntity> {
|
export class AccessService extends BaseService<AccessEntity> {
|
||||||
@InjectEntityModel(AccessEntity)
|
@InjectEntityModel(AccessEntity)
|
||||||
repository: Repository<AccessEntity>;
|
repository: Repository<AccessEntity>;
|
||||||
@@ -36,16 +36,16 @@ export class AccessService extends BaseService<AccessEntity> {
|
|||||||
|
|
||||||
async add(param) {
|
async add(param) {
|
||||||
let oldEntity = null;
|
let oldEntity = null;
|
||||||
if (param._copyFrom){
|
if (param._copyFrom) {
|
||||||
oldEntity = await this.info(param._copyFrom);
|
oldEntity = await this.info(param._copyFrom);
|
||||||
if (oldEntity == null) {
|
if (oldEntity == null) {
|
||||||
throw new ValidateException('该授权配置不存在,请确认是否已被删除');
|
throw new ValidateException("该授权配置不存在,请确认是否已被删除");
|
||||||
}
|
}
|
||||||
if (oldEntity.userId !== param.userId) {
|
if (oldEntity.userId !== param.userId) {
|
||||||
throw new ValidateException('您无权查看该授权配置');
|
throw new ValidateException("您无权查看该授权配置");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
delete param._copyFrom
|
delete param._copyFrom;
|
||||||
this.encryptSetting(param, oldEntity);
|
this.encryptSetting(param, oldEntity);
|
||||||
param.keyId = "ac_" + utils.id.simpleNanoId();
|
param.keyId = "ac_" + utils.id.simpleNanoId();
|
||||||
return await super.add(param);
|
return await super.add(param);
|
||||||
@@ -62,17 +62,20 @@ export class AccessService extends BaseService<AccessEntity> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const json = JSON.parse(setting);
|
const json = JSON.parse(setting);
|
||||||
|
if (accessDefine.subtype) {
|
||||||
|
param.subtype = json[accessDefine.subtype] || null;
|
||||||
|
}
|
||||||
let oldSetting = {};
|
let oldSetting = {};
|
||||||
let encryptSetting = {};
|
let encryptSetting = {};
|
||||||
const firstEncrypt = !oldSettingEntity || !oldSettingEntity.encryptSetting || oldSettingEntity.encryptSetting === '{}';
|
const firstEncrypt = !oldSettingEntity || !oldSettingEntity.encryptSetting || oldSettingEntity.encryptSetting === "{}";
|
||||||
if (oldSettingEntity) {
|
if (oldSettingEntity) {
|
||||||
oldSetting = JSON.parse(oldSettingEntity.setting || '{}');
|
oldSetting = JSON.parse(oldSettingEntity.setting || "{}");
|
||||||
encryptSetting = JSON.parse(oldSettingEntity.encryptSetting || '{}');
|
encryptSetting = JSON.parse(oldSettingEntity.encryptSetting || "{}");
|
||||||
}
|
}
|
||||||
for (const key in json) {
|
for (const key in json) {
|
||||||
//加密
|
//加密
|
||||||
let value = json[key];
|
let value = json[key];
|
||||||
if (value && typeof value === 'string') {
|
if (value && typeof value === "string") {
|
||||||
//去除前后空格
|
//去除前后空格
|
||||||
value = value.trim();
|
value = value.trim();
|
||||||
json[key] = value;
|
json[key] = value;
|
||||||
@@ -81,7 +84,7 @@ export class AccessService extends BaseService<AccessEntity> {
|
|||||||
if (!accessInputDefine) {
|
if (!accessInputDefine) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (!accessInputDefine.encrypt || !value || typeof value !== 'string') {
|
if (!accessInputDefine.encrypt || !value || typeof value !== "string") {
|
||||||
//定义无需加密、value为空、不是字符串 这些不需要加密
|
//定义无需加密、value为空、不是字符串 这些不需要加密
|
||||||
encryptSetting[key] = {
|
encryptSetting[key] = {
|
||||||
value: value,
|
value: value,
|
||||||
@@ -96,7 +99,7 @@ export class AccessService extends BaseService<AccessEntity> {
|
|||||||
const subIndex = Math.min(2, length);
|
const subIndex = Math.min(2, length);
|
||||||
let starLength = length - subIndex * 2;
|
let starLength = length - subIndex * 2;
|
||||||
starLength = Math.max(2, starLength);
|
starLength = Math.max(2, starLength);
|
||||||
const starString = '*'.repeat(starLength);
|
const starString = "*".repeat(starLength);
|
||||||
json[key] = value.substring(0, subIndex) + starString + value.substring(value.length - subIndex);
|
json[key] = value.substring(0, subIndex) + starString + value.substring(value.length - subIndex);
|
||||||
encryptSetting[key] = {
|
encryptSetting[key] = {
|
||||||
value: this.encryptService.encrypt(value),
|
value: this.encryptService.encrypt(value),
|
||||||
@@ -116,21 +119,21 @@ export class AccessService extends BaseService<AccessEntity> {
|
|||||||
async update(param) {
|
async update(param) {
|
||||||
const oldEntity = await this.info(param.id);
|
const oldEntity = await this.info(param.id);
|
||||||
if (oldEntity == null) {
|
if (oldEntity == null) {
|
||||||
throw new ValidateException('该授权配置不存在,请确认是否已被删除');
|
throw new ValidateException("该授权配置不存在,请确认是否已被删除");
|
||||||
}
|
}
|
||||||
this.encryptSetting(param, oldEntity);
|
this.encryptSetting(param, oldEntity);
|
||||||
delete param.keyId
|
delete param.keyId;
|
||||||
return await super.update(param);
|
return await super.update(param);
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateAccess(access: any) {
|
async updateAccess(access: any) {
|
||||||
const oldEntity = await this.info(access.id);
|
const oldEntity = await this.info(access.id);
|
||||||
if (oldEntity == null) {
|
if (oldEntity == null) {
|
||||||
throw new ValidateException('该授权配置不存在,请确认是否已被删除');
|
throw new ValidateException("该授权配置不存在,请确认是否已被删除");
|
||||||
}
|
}
|
||||||
const setting = this.decryptAccessEntity(oldEntity);
|
const setting = this.decryptAccessEntity(oldEntity);
|
||||||
for (const key of Object.keys(access)) {
|
for (const key of Object.keys(access)) {
|
||||||
if (key === 'id') {
|
if (key === "id") {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
setting[key] = access[key];
|
setting[key] = access[key];
|
||||||
@@ -145,11 +148,13 @@ export class AccessService extends BaseService<AccessEntity> {
|
|||||||
async getSimpleInfo(id: number) {
|
async getSimpleInfo(id: number) {
|
||||||
const entity = await this.info(id);
|
const entity = await this.info(id);
|
||||||
if (entity == null) {
|
if (entity == null) {
|
||||||
throw new ValidateException('该授权配置不存在,请确认是否已被删除');
|
throw new ValidateException("该授权配置不存在,请确认是否已被删除");
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
id: entity.id,
|
id: entity.id,
|
||||||
name: entity.name,
|
name: entity.name,
|
||||||
|
type: entity.type,
|
||||||
|
subtype: entity.subtype,
|
||||||
userId: entity.userId,
|
userId: entity.userId,
|
||||||
projectId: entity.projectId,
|
projectId: entity.projectId,
|
||||||
};
|
};
|
||||||
@@ -162,14 +167,14 @@ export class AccessService extends BaseService<AccessEntity> {
|
|||||||
}
|
}
|
||||||
if (checkUserId) {
|
if (checkUserId) {
|
||||||
if (userId == null) {
|
if (userId == null) {
|
||||||
throw new ValidateException('userId不能为空');
|
throw new ValidateException("userId不能为空");
|
||||||
}
|
}
|
||||||
if (userId !== entity.userId) {
|
if (userId !== entity.userId) {
|
||||||
throw new PermissionException('您对该Access授权无访问权限');
|
throw new PermissionException("您对该Access授权无访问权限");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (projectId != null && projectId !== entity.projectId) {
|
if (projectId != null && projectId !== entity.projectId) {
|
||||||
throw new PermissionException('您对该Access授权无访问权限');
|
throw new PermissionException("您对该Access授权无访问权限");
|
||||||
}
|
}
|
||||||
|
|
||||||
// const access = accessRegistry.get(entity.type);
|
// const access = accessRegistry.get(entity.type);
|
||||||
@@ -178,8 +183,8 @@ export class AccessService extends BaseService<AccessEntity> {
|
|||||||
id: entity.id,
|
id: entity.id,
|
||||||
...setting,
|
...setting,
|
||||||
};
|
};
|
||||||
const accessGetter = new AccessGetter(userId,projectId, this.getById.bind(this));
|
const accessGetter = new AccessGetter(userId, projectId, this.getById.bind(this));
|
||||||
return await newAccess(entity.type, input,accessGetter);
|
return await newAccess(entity.type, input, accessGetter);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getById(id: any, userId: number, projectId?: number): Promise<any> {
|
async getById(id: any, userId: number, projectId?: number): Promise<any> {
|
||||||
@@ -188,7 +193,7 @@ export class AccessService extends BaseService<AccessEntity> {
|
|||||||
|
|
||||||
decryptAccessEntity(entity: AccessEntity): any {
|
decryptAccessEntity(entity: AccessEntity): any {
|
||||||
let setting = {};
|
let setting = {};
|
||||||
if (entity.encryptSetting && entity.encryptSetting !== '{}') {
|
if (entity.encryptSetting && entity.encryptSetting !== "{}") {
|
||||||
setting = JSON.parse(entity.encryptSetting);
|
setting = JSON.parse(entity.encryptSetting);
|
||||||
for (const key in setting) {
|
for (const key in setting) {
|
||||||
//解密
|
//解密
|
||||||
@@ -213,12 +218,11 @@ export class AccessService extends BaseService<AccessEntity> {
|
|||||||
return accessRegistry.getDefine(type);
|
return accessRegistry.getDefine(type);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async getSimpleByIds(ids: number[], userId: any, projectId?: number) {
|
async getSimpleByIds(ids: number[], userId: any, projectId?: number) {
|
||||||
if (ids.length === 0) {
|
if (ids.length === 0) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
if (userId==null) {
|
if (userId == null) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
return await this.repository.find({
|
return await this.repository.find({
|
||||||
@@ -231,24 +235,24 @@ export class AccessService extends BaseService<AccessEntity> {
|
|||||||
id: true,
|
id: true,
|
||||||
name: true,
|
name: true,
|
||||||
type: true,
|
type: true,
|
||||||
userId:true,
|
subtype: true,
|
||||||
projectId:true,
|
userId: true,
|
||||||
|
projectId: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 复制授权到其他项目
|
* 复制授权到其他项目
|
||||||
* @param accessId
|
* @param accessId
|
||||||
* @param projectId
|
* @param projectId
|
||||||
*/
|
*/
|
||||||
async copyTo(accessId: number,projectId?: number) {
|
async copyTo(accessId: number, projectId?: number) {
|
||||||
const access = await this.info(accessId);
|
const access = await this.info(accessId);
|
||||||
if (access == null) {
|
if (access == null) {
|
||||||
throw new Error(`该授权配置不存在,请确认是否已被删除:id=${accessId}`);
|
throw new Error(`该授权配置不存在,请确认是否已被删除:id=${accessId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const keyId = access.keyId;
|
const keyId = access.keyId;
|
||||||
//检查目标项目里是否已经有相同keyId的配置
|
//检查目标项目里是否已经有相同keyId的配置
|
||||||
const existAccess = await this.repository.findOne({
|
const existAccess = await this.repository.findOne({
|
||||||
@@ -263,10 +267,10 @@ export class AccessService extends BaseService<AccessEntity> {
|
|||||||
}
|
}
|
||||||
const newAccess = {
|
const newAccess = {
|
||||||
...access,
|
...access,
|
||||||
userId:-1,
|
userId: -1,
|
||||||
id: undefined,
|
id: undefined,
|
||||||
projectId,
|
projectId,
|
||||||
}
|
};
|
||||||
await this.repository.save(newAccess);
|
await this.repository.save(newAccess);
|
||||||
return newAccess.id;
|
return newAccess.id;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,10 +3,6 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
|
|
||||||
## [1.40.4](https://github.com/certd/certd/compare/v1.40.3...v1.40.4) (2026-05-24)
|
|
||||||
|
|
||||||
**Note:** Version bump only for package @certd/midway-flyway-js
|
|
||||||
|
|
||||||
## [1.40.3](https://github.com/certd/certd/compare/v1.40.2...v1.40.3) (2026-05-21)
|
## [1.40.3](https://github.com/certd/certd/compare/v1.40.2...v1.40.3) (2026-05-21)
|
||||||
|
|
||||||
**Note:** Version bump only for package @certd/midway-flyway-js
|
**Note:** Version bump only for package @certd/midway-flyway-js
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@certd/midway-flyway-js",
|
"name": "@certd/midway-flyway-js",
|
||||||
"version": "1.40.4",
|
"version": "1.40.3",
|
||||||
"description": "midway with flyway, sql upgrade way ",
|
"description": "midway with flyway, sql upgrade way ",
|
||||||
"private": false,
|
"private": false,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -3,10 +3,6 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
|
|
||||||
## [1.40.4](https://github.com/certd/certd/compare/v1.40.3...v1.40.4) (2026-05-24)
|
|
||||||
|
|
||||||
**Note:** Version bump only for package @certd/plugin-cert
|
|
||||||
|
|
||||||
## [1.40.3](https://github.com/certd/certd/compare/v1.40.2...v1.40.3) (2026-05-21)
|
## [1.40.3](https://github.com/certd/certd/compare/v1.40.2...v1.40.3) (2026-05-21)
|
||||||
|
|
||||||
**Note:** Version bump only for package @certd/plugin-cert
|
**Note:** Version bump only for package @certd/plugin-cert
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@certd/plugin-cert",
|
"name": "@certd/plugin-cert",
|
||||||
"private": false,
|
"private": false,
|
||||||
"version": "1.40.4",
|
"version": "1.40.3",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "./dist/index.js",
|
"main": "./dist/index.js",
|
||||||
"types": "./dist/index.d.ts",
|
"types": "./dist/index.d.ts",
|
||||||
@@ -18,10 +18,10 @@
|
|||||||
"compile": "tsc --skipLibCheck --watch"
|
"compile": "tsc --skipLibCheck --watch"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@certd/acme-client": "^1.40.4",
|
"@certd/acme-client": "^1.40.3",
|
||||||
"@certd/basic": "^1.40.4",
|
"@certd/basic": "^1.40.3",
|
||||||
"@certd/pipeline": "^1.40.4",
|
"@certd/pipeline": "^1.40.3",
|
||||||
"@certd/plugin-lib": "^1.40.4",
|
"@certd/plugin-lib": "^1.40.3",
|
||||||
"psl": "^1.9.0",
|
"psl": "^1.9.0",
|
||||||
"punycode.js": "^2.3.1"
|
"punycode.js": "^2.3.1"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -3,12 +3,6 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
|
|
||||||
## [1.40.4](https://github.com/certd/certd/compare/v1.40.3...v1.40.4) (2026-05-24)
|
|
||||||
|
|
||||||
### Performance Improvements
|
|
||||||
|
|
||||||
* 新增阿里云直播证书部署插件 ([8edb6f8](https://github.com/certd/certd/commit/8edb6f8727bd148f106801bef25567880fd35e9e))
|
|
||||||
|
|
||||||
## [1.40.3](https://github.com/certd/certd/compare/v1.40.2...v1.40.3) (2026-05-21)
|
## [1.40.3](https://github.com/certd/certd/compare/v1.40.2...v1.40.3) (2026-05-21)
|
||||||
|
|
||||||
**Note:** Version bump only for package @certd/plugin-lib
|
**Note:** Version bump only for package @certd/plugin-lib
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@certd/plugin-lib",
|
"name": "@certd/plugin-lib",
|
||||||
"private": false,
|
"private": false,
|
||||||
"version": "1.40.4",
|
"version": "1.40.3",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "./dist/index.js",
|
"main": "./dist/index.js",
|
||||||
"types": "./dist/index.d.ts",
|
"types": "./dist/index.d.ts",
|
||||||
@@ -23,10 +23,10 @@
|
|||||||
"@alicloud/pop-core": "^1.7.10",
|
"@alicloud/pop-core": "^1.7.10",
|
||||||
"@alicloud/tea-util": "^1.4.11",
|
"@alicloud/tea-util": "^1.4.11",
|
||||||
"@aws-sdk/client-s3": "^3.964.0",
|
"@aws-sdk/client-s3": "^3.964.0",
|
||||||
"@certd/acme-client": "^1.40.4",
|
"@certd/acme-client": "^1.40.3",
|
||||||
"@certd/basic": "^1.40.4",
|
"@certd/basic": "^1.40.3",
|
||||||
"@certd/pipeline": "^1.40.4",
|
"@certd/pipeline": "^1.40.3",
|
||||||
"@certd/plus-core": "^1.40.4",
|
"@certd/plus-core": "^1.40.3",
|
||||||
"@kubernetes/client-node": "0.21.0",
|
"@kubernetes/client-node": "0.21.0",
|
||||||
"ali-oss": "^6.22.0",
|
"ali-oss": "^6.22.0",
|
||||||
"basic-ftp": "^5.0.5",
|
"basic-ftp": "^5.0.5",
|
||||||
|
|||||||
@@ -3,12 +3,6 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
|
|
||||||
## [1.40.4](https://github.com/certd/certd/compare/v1.40.3...v1.40.4) (2026-05-24)
|
|
||||||
|
|
||||||
### Performance Improvements
|
|
||||||
|
|
||||||
* 商业版套餐只支持设置为可叠加 ([5e72f75](https://github.com/certd/certd/commit/5e72f75395fb632a30e80c07d35d8ba40ef631fa))
|
|
||||||
|
|
||||||
## [1.40.3](https://github.com/certd/certd/compare/v1.40.2...v1.40.3) (2026-05-21)
|
## [1.40.3](https://github.com/certd/certd/compare/v1.40.2...v1.40.3) (2026-05-21)
|
||||||
|
|
||||||
### Bug Fixes
|
### Bug Fixes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@certd/ui-client",
|
"name": "@certd/ui-client",
|
||||||
"version": "1.40.4",
|
"version": "1.40.3",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite --open",
|
"dev": "vite --open",
|
||||||
@@ -106,8 +106,8 @@
|
|||||||
"zod-defaults": "^0.1.3"
|
"zod-defaults": "^0.1.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@certd/lib-iframe": "^1.40.4",
|
"@certd/lib-iframe": "^1.40.3",
|
||||||
"@certd/pipeline": "^1.40.4",
|
"@certd/pipeline": "^1.40.3",
|
||||||
"@rollup/plugin-commonjs": "^25.0.7",
|
"@rollup/plugin-commonjs": "^25.0.7",
|
||||||
"@rollup/plugin-node-resolve": "^15.2.3",
|
"@rollup/plugin-node-resolve": "^15.2.3",
|
||||||
"@types/chai": "^4.3.12",
|
"@types/chai": "^4.3.12",
|
||||||
|
|||||||
+15
-8
@@ -11,14 +11,16 @@
|
|||||||
<td class="record-value" :title="cnameRecord.recordValue">
|
<td class="record-value" :title="cnameRecord.recordValue">
|
||||||
<fs-copyable v-model="cnameRecord.recordValue"></fs-copyable>
|
<fs-copyable v-model="cnameRecord.recordValue"></fs-copyable>
|
||||||
</td>
|
</td>
|
||||||
<td class="status center flex-center">
|
<td class="status center">
|
||||||
<fs-values-format v-model="cnameRecord.status" :dict="statusDict" />
|
<span class="status-content">
|
||||||
<a-tooltip v-if="cnameRecord.error" :title="cnameRecord.error">
|
<fs-values-format v-model="cnameRecord.status" :dict="statusDict" />
|
||||||
<fs-icon class="ml-5 color-red" icon="ion:warning-outline"></fs-icon>
|
<a-tooltip v-if="cnameRecord.error" :title="cnameRecord.error">
|
||||||
</a-tooltip>
|
<fs-icon class="ml-5 color-red" icon="ion:warning-outline"></fs-icon>
|
||||||
<a-tooltip v-if="cnameRecord.status === 'valid'" :title="t('certd.verifyPlan.resetStatusTooltip')">
|
</a-tooltip>
|
||||||
<fs-icon class="ml-2 color-yellow text-md pointer" icon="solar:undo-left-square-bold" @click="resetStatus"></fs-icon>
|
<a-tooltip v-if="cnameRecord.status === 'valid'" :title="t('certd.verifyPlan.resetStatusTooltip')">
|
||||||
</a-tooltip>
|
<fs-icon class="ml-2 color-yellow text-md pointer" icon="solar:undo-left-square-bold" @click="resetStatus"></fs-icon>
|
||||||
|
</a-tooltip>
|
||||||
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="center">
|
<td class="center">
|
||||||
<template v-if="cnameRecord.status !== 'valid'">
|
<template v-if="cnameRecord.status !== 'valid'">
|
||||||
@@ -142,5 +144,10 @@ async function resetStatus() {
|
|||||||
.fs-copyable {
|
.fs-copyable {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
.status-content {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
+20
-5
@@ -2,11 +2,11 @@
|
|||||||
<table class="cname-verify-plan">
|
<table class="cname-verify-plan">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<td style="width: 160px">{{ t("certd.verifyPlan.hostRecord") }}</td>
|
<td class="col-host">{{ t("certd.verifyPlan.hostRecord") }}</td>
|
||||||
<td style="width: 100px; text-align: center">{{ t("certd.verifyPlan.recordType") }}</td>
|
<td class="col-type center">{{ t("certd.verifyPlan.recordType") }}</td>
|
||||||
<td style="width: 250px">{{ t("certd.verifyPlan.setCnameRecord") }}</td>
|
<td class="col-value">{{ t("certd.verifyPlan.setCnameRecord") }}</td>
|
||||||
<td style="width: 120px" class="center">{{ t("certd.status") }}</td>
|
<td class="col-status center">{{ t("certd.status") }}</td>
|
||||||
<td style="width: 90px" class="center">{{ t("certd.verifyPlan.operation") }}</td>
|
<td class="col-action center">{{ t("certd.verifyPlan.operation") }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<template v-for="key in domains" :key="key">
|
<template v-for="key in domains" :key="key">
|
||||||
@@ -49,6 +49,21 @@ function onRecordChange(domain: string, record: CnameRecord) {
|
|||||||
.cname-verify-plan {
|
.cname-verify-plan {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
table-layout: fixed;
|
table-layout: fixed;
|
||||||
|
.col-host {
|
||||||
|
width: 220px;
|
||||||
|
}
|
||||||
|
.col-type {
|
||||||
|
width: 100px;
|
||||||
|
}
|
||||||
|
.col-value {
|
||||||
|
width: 360px;
|
||||||
|
}
|
||||||
|
.col-status {
|
||||||
|
width: 120px;
|
||||||
|
}
|
||||||
|
.col-action {
|
||||||
|
width: 150px;
|
||||||
|
}
|
||||||
tbody tr td {
|
tbody tr td {
|
||||||
border-top: 1px solid #e8e8e8 !important;
|
border-top: 1px solid #e8e8e8 !important;
|
||||||
}
|
}
|
||||||
|
|||||||
+144
@@ -0,0 +1,144 @@
|
|||||||
|
<template>
|
||||||
|
<tbody class="dns-persist-record-info">
|
||||||
|
<tr v-if="dnsPersistRecord">
|
||||||
|
<td class="host-record" :title="dnsPersistRecord.hostRecord">
|
||||||
|
<fs-copyable v-model="dnsPersistRecord.hostRecord"></fs-copyable>
|
||||||
|
</td>
|
||||||
|
<td style="text-align: center">TXT</td>
|
||||||
|
<td class="record-value" :title="dnsPersistRecord.recordValue">
|
||||||
|
<fs-copyable v-model="dnsPersistRecord.recordValue"></fs-copyable>
|
||||||
|
</td>
|
||||||
|
<td class="status center">
|
||||||
|
<fs-values-format v-model="dnsPersistRecord.status" :dict="statusDict" />
|
||||||
|
</td>
|
||||||
|
<td class="center">
|
||||||
|
<template v-if="dnsPersistRecord.status !== 'valid'">
|
||||||
|
<a-space>
|
||||||
|
<a-button type="primary" size="small" @click="openSettingDialog">设置TXT</a-button>
|
||||||
|
<a-button type="primary" size="small" :loading="loading" @click="doVerify">校验</a-button>
|
||||||
|
</a-space>
|
||||||
|
</template>
|
||||||
|
<div v-else class="helper">请勿删除TXT记录</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr v-else>
|
||||||
|
<td colspan="5" class="color-red">{{ errorMessage || "请先选择ACME账号授权" }}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { dict } from "@fast-crud/fast-crud";
|
||||||
|
import { message } from "ant-design-vue";
|
||||||
|
import { ref, watch } from "vue";
|
||||||
|
import { GetByDomain, Verify } from "/@/views/certd/cert/dns-persist/api";
|
||||||
|
import { useDnsPersistSettingDialog } from "/@/views/certd/cert/dns-persist/use-setting-dialog";
|
||||||
|
import { DnsPersistRecord } from "./type";
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: "DnsPersistRecordInfo",
|
||||||
|
});
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
domain: string;
|
||||||
|
caType?: string;
|
||||||
|
acmeAccountAccessId?: number;
|
||||||
|
commonAcmeAccountAccessId?: number;
|
||||||
|
wildcard?: boolean;
|
||||||
|
persistUntil?: number;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
change: [DnsPersistRecord];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const statusDict = dict({
|
||||||
|
data: [
|
||||||
|
{ value: "pending", label: "待设置", color: "warning" },
|
||||||
|
{ value: "validating", label: "校验中", color: "blue" },
|
||||||
|
{ value: "valid", label: "有效", color: "green" },
|
||||||
|
{ value: "failed", label: "请重试", color: "red" },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const dnsPersistRecord = ref<DnsPersistRecord | null>(null);
|
||||||
|
const loading = ref(false);
|
||||||
|
const errorMessage = ref("");
|
||||||
|
const { openDnsPersistSettingDialog } = useDnsPersistSettingDialog();
|
||||||
|
|
||||||
|
function onRecordChange() {
|
||||||
|
if (dnsPersistRecord.value) {
|
||||||
|
emit("change", dnsPersistRecord.value);
|
||||||
|
} else {
|
||||||
|
emit("change", {
|
||||||
|
domain: props.domain,
|
||||||
|
status: null,
|
||||||
|
} as any);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadRecord() {
|
||||||
|
errorMessage.value = "";
|
||||||
|
dnsPersistRecord.value = null;
|
||||||
|
if (!props.domain || (!props.acmeAccountAccessId && !props.commonAcmeAccountAccessId)) {
|
||||||
|
onRecordChange();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
dnsPersistRecord.value = await GetByDomain({
|
||||||
|
domain: props.domain,
|
||||||
|
caType: props.caType,
|
||||||
|
acmeAccountAccessId: props.acmeAccountAccessId,
|
||||||
|
commonAcmeAccountAccessId: props.commonAcmeAccountAccessId,
|
||||||
|
wildcard: props.wildcard,
|
||||||
|
persistUntil: props.persistUntil,
|
||||||
|
createOnNotFound: true,
|
||||||
|
});
|
||||||
|
onRecordChange();
|
||||||
|
} catch (e: any) {
|
||||||
|
errorMessage.value = e.message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => [props.domain, props.caType, props.acmeAccountAccessId, props.commonAcmeAccountAccessId, props.wildcard, props.persistUntil],
|
||||||
|
async () => {
|
||||||
|
await loadRecord();
|
||||||
|
},
|
||||||
|
{
|
||||||
|
immediate: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
async function doVerify() {
|
||||||
|
if (!dnsPersistRecord.value?.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
const ok = await Verify(dnsPersistRecord.value.id);
|
||||||
|
message[ok ? "success" : "error"](ok ? "校验成功" : "未找到匹配的TXT记录,请稍后重试");
|
||||||
|
await loadRecord();
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function openSettingDialog() {
|
||||||
|
if (!dnsPersistRecord.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
openDnsPersistSettingDialog({
|
||||||
|
record: dnsPersistRecord.value,
|
||||||
|
onDone: loadRecord,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less">
|
||||||
|
.dns-persist-record-info {
|
||||||
|
.fs-copyable {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
+94
@@ -0,0 +1,94 @@
|
|||||||
|
<template>
|
||||||
|
<table class="dns-persist-verify-plan">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<td class="col-host">TXT主机名</td>
|
||||||
|
<td class="col-type center">记录类型</td>
|
||||||
|
<td class="col-value">请设置TXT记录(验证成功以后不要删除)</td>
|
||||||
|
<td class="col-status center">状态</td>
|
||||||
|
<td class="col-action center">操作</td>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<template v-for="key in domains" :key="key">
|
||||||
|
<dns-persist-record-info
|
||||||
|
:domain="key"
|
||||||
|
:ca-type="caType"
|
||||||
|
:acme-account-access-id="acmeAccountAccessId"
|
||||||
|
:common-acme-account-access-id="commonAcmeAccountAccessId"
|
||||||
|
:wildcard="modelValue[key]?.wildcard"
|
||||||
|
:persist-until="modelValue[key]?.persistUntil"
|
||||||
|
@change="onRecordChange(key, $event)"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</table>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed } from "vue";
|
||||||
|
import DnsPersistRecordInfo from "./dns-persist-record-info.vue";
|
||||||
|
import { DnsPersistRecord } from "./type";
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: "DnsPersistVerifyPlan",
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(["update:modelValue", "change"]);
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
modelValue: Record<string, DnsPersistRecord>;
|
||||||
|
caType?: string;
|
||||||
|
acmeAccountAccessId?: number;
|
||||||
|
commonAcmeAccountAccessId?: number;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const domains = computed(() => {
|
||||||
|
return Object.keys(props.modelValue || {});
|
||||||
|
});
|
||||||
|
|
||||||
|
function onRecordChange(domain: string, record: DnsPersistRecord) {
|
||||||
|
const value = { ...props.modelValue };
|
||||||
|
value[domain] = {
|
||||||
|
...value[domain],
|
||||||
|
...record,
|
||||||
|
};
|
||||||
|
emit("update:modelValue", value);
|
||||||
|
emit("change", value);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less">
|
||||||
|
.dns-persist-verify-plan {
|
||||||
|
width: 100%;
|
||||||
|
table-layout: fixed;
|
||||||
|
.col-host {
|
||||||
|
width: 220px;
|
||||||
|
}
|
||||||
|
.col-type {
|
||||||
|
width: 100px;
|
||||||
|
}
|
||||||
|
.col-value {
|
||||||
|
width: 360px;
|
||||||
|
}
|
||||||
|
.col-status {
|
||||||
|
width: 120px;
|
||||||
|
}
|
||||||
|
.col-action {
|
||||||
|
width: 150px;
|
||||||
|
}
|
||||||
|
tbody tr td {
|
||||||
|
border-top: 1px solid #e8e8e8 !important;
|
||||||
|
}
|
||||||
|
tr {
|
||||||
|
td {
|
||||||
|
border: 0 !important;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
|
&.center {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
+48
-1
@@ -46,13 +46,28 @@
|
|||||||
<div class="form-item">
|
<div class="form-item">
|
||||||
<span class="label">{{ t("certd.verifyPlan.dnsAccess") }}:</span>
|
<span class="label">{{ t("certd.verifyPlan.dnsAccess") }}:</span>
|
||||||
<span class="input">
|
<span class="input">
|
||||||
<access-selector v-model="item.dnsProviderAccessId" size="small" :type="item.dnsProviderAccessType || item.dnsProviderType" :placeholder="t('certd.verifyPlan.pleaseSelect')" @change="onPlanChanged"></access-selector>
|
<access-selector
|
||||||
|
v-model="item.dnsProviderAccessId"
|
||||||
|
size="small"
|
||||||
|
:type="item.dnsProviderAccessType || item.dnsProviderType"
|
||||||
|
:placeholder="t('certd.verifyPlan.pleaseSelect')"
|
||||||
|
@change="onPlanChanged"
|
||||||
|
></access-selector>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="item.type === 'cname'" class="plan-cname">
|
<div v-if="item.type === 'cname'" class="plan-cname">
|
||||||
<cname-verify-plan v-model="item.cnameVerifyPlan" @change="onPlanChanged" />
|
<cname-verify-plan v-model="item.cnameVerifyPlan" @change="onPlanChanged" />
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="item.type === 'dns-persist'" class="plan-dns-persist">
|
||||||
|
<dns-persist-verify-plan
|
||||||
|
v-model="item.dnsPersistVerifyPlan"
|
||||||
|
:ca-type="caType"
|
||||||
|
:acme-account-access-id="acmeAccountAccessId"
|
||||||
|
:common-acme-account-access-id="commonAcmeAccountAccessId"
|
||||||
|
@change="onPlanChanged"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<div v-if="item.type === 'http'" class="plan-http">
|
<div v-if="item.type === 'http'" class="plan-http">
|
||||||
<http-verify-plan v-model="item.httpVerifyPlan" @change="onPlanChanged" />
|
<http-verify-plan v-model="item.httpVerifyPlan" @change="onPlanChanged" />
|
||||||
<div class="helper">{{ t("certd.verifyPlan.httpHelper") }}</div>
|
<div class="helper">{{ t("certd.verifyPlan.httpHelper") }}</div>
|
||||||
@@ -76,6 +91,7 @@ import { useI18n } from "vue-i18n";
|
|||||||
import { dict, FsDictSelect } from "@fast-crud/fast-crud";
|
import { dict, FsDictSelect } from "@fast-crud/fast-crud";
|
||||||
import AccessSelector from "/@/views/certd/access/access-selector/index.vue";
|
import AccessSelector from "/@/views/certd/access/access-selector/index.vue";
|
||||||
import CnameVerifyPlan from "./cname-verify-plan.vue";
|
import CnameVerifyPlan from "./cname-verify-plan.vue";
|
||||||
|
import DnsPersistVerifyPlan from "./dns-persist-verify-plan.vue";
|
||||||
import HttpVerifyPlan from "./http-verify-plan.vue";
|
import HttpVerifyPlan from "./http-verify-plan.vue";
|
||||||
import { Form } from "ant-design-vue";
|
import { Form } from "ant-design-vue";
|
||||||
import { DomainsVerifyPlanInput } from "./type";
|
import { DomainsVerifyPlanInput } from "./type";
|
||||||
@@ -92,6 +108,10 @@ const challengeTypeOptions = ref<any[]>([
|
|||||||
label: t("certd.verifyPlan.dnsChallenge"),
|
label: t("certd.verifyPlan.dnsChallenge"),
|
||||||
value: "dns",
|
value: "dns",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: "DNS持久验证",
|
||||||
|
value: "dns-persist",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: t("certd.verifyPlan.cnameChallenge"),
|
label: t("certd.verifyPlan.cnameChallenge"),
|
||||||
value: "cname",
|
value: "cname",
|
||||||
@@ -106,6 +126,9 @@ const props = defineProps<{
|
|||||||
modelValue?: DomainsVerifyPlanInput;
|
modelValue?: DomainsVerifyPlanInput;
|
||||||
domains?: string[];
|
domains?: string[];
|
||||||
defaultType?: string;
|
defaultType?: string;
|
||||||
|
caType?: string;
|
||||||
|
acmeAccountAccessId?: number;
|
||||||
|
commonAcmeAccountAccessId?: number;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
@@ -189,11 +212,15 @@ async function onDomainsChanged(domains: string[]) {
|
|||||||
|
|
||||||
const cnameOrigin = planItem.cnameVerifyPlan;
|
const cnameOrigin = planItem.cnameVerifyPlan;
|
||||||
const httpOrigin = planItem.httpVerifyPlan;
|
const httpOrigin = planItem.httpVerifyPlan;
|
||||||
|
const dnsPersistOrigin = planItem.dnsPersistVerifyPlan;
|
||||||
planItem.cnameVerifyPlan = {};
|
planItem.cnameVerifyPlan = {};
|
||||||
planItem.httpVerifyPlan = {};
|
planItem.httpVerifyPlan = {};
|
||||||
|
planItem.dnsPersistVerifyPlan = {};
|
||||||
const cnamePlan = planItem.cnameVerifyPlan;
|
const cnamePlan = planItem.cnameVerifyPlan;
|
||||||
const httpPlan = planItem.httpVerifyPlan;
|
const httpPlan = planItem.httpVerifyPlan;
|
||||||
|
const dnsPersistPlan = planItem.dnsPersistVerifyPlan;
|
||||||
for (const subDomain of domainGroupItem.keySubDomains) {
|
for (const subDomain of domainGroupItem.keySubDomains) {
|
||||||
|
const wildcard = true;
|
||||||
if (!cnameOrigin[subDomain]) {
|
if (!cnameOrigin[subDomain]) {
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
planItem.cnameVerifyPlan[subDomain] = {
|
planItem.cnameVerifyPlan[subDomain] = {
|
||||||
@@ -225,6 +252,19 @@ async function onDomainsChanged(domains: string[]) {
|
|||||||
domain: subDomain,
|
domain: subDomain,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!dnsPersistOrigin?.[subDomain]) {
|
||||||
|
//@ts-ignore
|
||||||
|
dnsPersistPlan[subDomain] = {
|
||||||
|
domain: subDomain,
|
||||||
|
wildcard,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
dnsPersistPlan[subDomain] = {
|
||||||
|
...dnsPersistOrigin[subDomain],
|
||||||
|
wildcard,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const subDomain of Object.keys(cnamePlan)) {
|
for (const subDomain of Object.keys(cnamePlan)) {
|
||||||
@@ -238,6 +278,12 @@ async function onDomainsChanged(domains: string[]) {
|
|||||||
delete httpPlan[subDomain];
|
delete httpPlan[subDomain];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (const subDomain of Object.keys(dnsPersistPlan)) {
|
||||||
|
if (!domainGroupItem.keySubDomains.includes(subDomain)) {
|
||||||
|
delete dnsPersistPlan[subDomain];
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
for (const domain of Object.keys(planRef.value)) {
|
for (const domain of Object.keys(planRef.value)) {
|
||||||
const mainDomains = Object.keys(domainGroups);
|
const mainDomains = Object.keys(domainGroups);
|
||||||
@@ -268,6 +314,7 @@ watch(
|
|||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
.fullscreen-modal {
|
.fullscreen-modal {
|
||||||
display: none;
|
display: none;
|
||||||
|
background-color: rgba(0, 0, 0, 0.42);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.fullscreen {
|
&.fullscreen {
|
||||||
|
|||||||
+18
-1
@@ -7,15 +7,32 @@ export type HttpRecord = {
|
|||||||
httpUploadRootDir: string;
|
httpUploadRootDir: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type DnsPersistRecord = {
|
||||||
|
id?: number;
|
||||||
|
domain: string;
|
||||||
|
mainDomain?: string;
|
||||||
|
status?: string;
|
||||||
|
hostRecord?: string;
|
||||||
|
recordValue?: string;
|
||||||
|
caType?: string;
|
||||||
|
acmeAccountAccessId?: number;
|
||||||
|
accountUri?: string;
|
||||||
|
wildcard?: boolean;
|
||||||
|
persistUntil?: number;
|
||||||
|
dnsProviderType?: string;
|
||||||
|
dnsProviderAccess?: number;
|
||||||
|
};
|
||||||
|
|
||||||
export type DomainVerifyPlanInput = {
|
export type DomainVerifyPlanInput = {
|
||||||
domain: string;
|
domain: string;
|
||||||
domains: string[];
|
domains: string[];
|
||||||
type: "cname" | "dns" | "http";
|
type: "cname" | "dns" | "http" | "dns-persist";
|
||||||
dnsProviderType?: string;
|
dnsProviderType?: string;
|
||||||
dnsProviderAccessType?: string;
|
dnsProviderAccessType?: string;
|
||||||
dnsProviderAccessId?: number;
|
dnsProviderAccessId?: number;
|
||||||
cnameVerifyPlan?: Record<string, CnameRecord>;
|
cnameVerifyPlan?: Record<string, CnameRecord>;
|
||||||
httpVerifyPlan?: Record<string, HttpRecord>;
|
httpVerifyPlan?: Record<string, HttpRecord>;
|
||||||
|
dnsPersistVerifyPlan?: Record<string, DnsPersistRecord>;
|
||||||
};
|
};
|
||||||
export type DomainsVerifyPlanInput = {
|
export type DomainsVerifyPlanInput = {
|
||||||
[key: string]: DomainVerifyPlanInput;
|
[key: string]: DomainVerifyPlanInput;
|
||||||
|
|||||||
+8
@@ -46,6 +46,14 @@ function checkDomainVerifyPlan(rule: any, value: DomainsVerifyPlanInput) {
|
|||||||
if (!value[domain].dnsProviderType || !value[domain].dnsProviderAccessId) {
|
if (!value[domain].dnsProviderType || !value[domain].dnsProviderAccessId) {
|
||||||
throw new Error($t("certd.verifyPlan.errors.dnsProviderRequired", { domain }));
|
throw new Error($t("certd.verifyPlan.errors.dnsProviderRequired", { domain }));
|
||||||
}
|
}
|
||||||
|
} else if (type === "dns-persist") {
|
||||||
|
const subDomains = Object.keys(value[domain].dnsPersistVerifyPlan || {});
|
||||||
|
for (const subDomain of subDomains) {
|
||||||
|
const plan = value[domain].dnsPersistVerifyPlan[subDomain];
|
||||||
|
if (plan.status !== "valid") {
|
||||||
|
throw new Error(`DNS持久验证记录(${subDomain})还未校验成功`);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="refresh-input">
|
<div class="refresh-input">
|
||||||
<div class="refresh-input-line">
|
<div class="refresh-input-line">
|
||||||
<a-input class="refresh-input-control" :value="value" :placeholder="placeholder" allow-clear @update:value="emit('update:value', $event)"></a-input>
|
<a-input class="refresh-input-control" :value="value" :placeholder="placeholder" :allow-clear="!disabled" :disabled="disabled" @update:value="emit('update:value', $event)"></a-input>
|
||||||
<fs-button :loading="loading" type="primary" :text="buttonText" :icon="icon" @click="doRefresh"></fs-button>
|
<fs-button :loading="loading" :disabled="disabled" type="primary" :text="buttonText" :icon="icon" @click="doRefresh"></fs-button>
|
||||||
</div>
|
</div>
|
||||||
<div class="helper" :class="{ error: hasError }">
|
<div class="helper" :class="{ error: hasError }">
|
||||||
{{ message }}
|
{{ message }}
|
||||||
@@ -25,6 +25,7 @@ type RefreshInputProps = ComponentPropsType & {
|
|||||||
icon?: string;
|
icon?: string;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
successMessage?: string;
|
successMessage?: string;
|
||||||
|
disabled?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const fromType: any = inject("getFromType");
|
const fromType: any = inject("getFromType");
|
||||||
@@ -49,6 +50,9 @@ const placeholder = computed(() => props.placeholder || "");
|
|||||||
const successMessage = computed(() => props.successMessage || "刷新成功,请保存配置");
|
const successMessage = computed(() => props.successMessage || "刷新成功,请保存配置");
|
||||||
|
|
||||||
const doRefresh = async () => {
|
const doRefresh = async () => {
|
||||||
|
if (props.disabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (loading.value) {
|
if (loading.value) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ function createChallengeTypeDict() {
|
|||||||
return dict({
|
return dict({
|
||||||
data: [
|
data: [
|
||||||
{ value: "dns", label: $t("certd.verifyPlan.dnsChallenge"), color: "green" },
|
{ value: "dns", label: $t("certd.verifyPlan.dnsChallenge"), color: "green" },
|
||||||
|
{ value: "dns-persist", label: "DNS持久验证", color: "cyan" },
|
||||||
{ value: "cname", label: $t("certd.verifyPlan.cnameProxyChallenge"), color: "blue" },
|
{ value: "cname", label: $t("certd.verifyPlan.cnameProxyChallenge"), color: "blue" },
|
||||||
{ value: "http", label: $t("certd.verifyPlan.httpChallenge"), color: "yellow" },
|
{ value: "http", label: $t("certd.verifyPlan.httpChallenge"), color: "yellow" },
|
||||||
],
|
],
|
||||||
@@ -39,7 +40,12 @@ export const Dicts = {
|
|||||||
sslProviderDict: dict({
|
sslProviderDict: dict({
|
||||||
data: [
|
data: [
|
||||||
{ value: "letsencrypt", label: "Let's Encrypt" },
|
{ value: "letsencrypt", label: "Let's Encrypt" },
|
||||||
|
{ value: "letsencrypt_staging", label: "Let's Encrypt测试环境" },
|
||||||
|
{ value: "google", label: "Google" },
|
||||||
{ value: "zerossl", label: "ZeroSSL" },
|
{ value: "zerossl", label: "ZeroSSL" },
|
||||||
|
{ value: "sslcom", label: "SSL.com" },
|
||||||
|
{ value: "litessl", label: "litessl" },
|
||||||
|
{ value: "custom", label: "自定义ACME" },
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
get challengeTypeDict() {
|
get challengeTypeDict() {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ export default {
|
|||||||
siteMonitor: "Site Certificate Monitor",
|
siteMonitor: "Site Certificate Monitor",
|
||||||
settings: "Settings",
|
settings: "Settings",
|
||||||
accessManager: "Access Management",
|
accessManager: "Access Management",
|
||||||
|
dnsPersistRecord: "DNS Persist Records",
|
||||||
subDomain: "Subdomain Delegation Settings",
|
subDomain: "Subdomain Delegation Settings",
|
||||||
pipelineGroup: "Pipeline Group Management",
|
pipelineGroup: "Pipeline Group Management",
|
||||||
openKey: "Open API Key",
|
openKey: "Open API Key",
|
||||||
@@ -22,6 +23,7 @@ export default {
|
|||||||
mySuite: "My Suite",
|
mySuite: "My Suite",
|
||||||
suiteBuy: "Suite Purchase",
|
suiteBuy: "Suite Purchase",
|
||||||
myTrade: "My Orders",
|
myTrade: "My Orders",
|
||||||
|
inviteCommission: "Incentive Plan",
|
||||||
paymentReturn: "Payment Return",
|
paymentReturn: "Payment Return",
|
||||||
source: "Source Code",
|
source: "Source Code",
|
||||||
github: "GitHub",
|
github: "GitHub",
|
||||||
@@ -46,6 +48,10 @@ export default {
|
|||||||
suiteSetting: "Suite Settings",
|
suiteSetting: "Suite Settings",
|
||||||
orderManager: "Order Management",
|
orderManager: "Order Management",
|
||||||
userSuites: "User Suites",
|
userSuites: "User Suites",
|
||||||
|
inviteCommissionSetting: "Incentive Plan Settings",
|
||||||
|
inviteLevel: "Promotion Levels",
|
||||||
|
inviteUserLevel: "User Promotion Levels",
|
||||||
|
inviteWithdraw: "Withdrawal Requests",
|
||||||
netTest: "Network Test",
|
netTest: "Network Test",
|
||||||
|
|
||||||
enterpriseSetting: "Enterprise Settings",
|
enterpriseSetting: "Enterprise Settings",
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ export default {
|
|||||||
siteMonitor: "站点证书监控",
|
siteMonitor: "站点证书监控",
|
||||||
settings: "设置",
|
settings: "设置",
|
||||||
accessManager: "授权管理",
|
accessManager: "授权管理",
|
||||||
|
dnsPersistRecord: "DNS持久验证记录",
|
||||||
subDomain: "子域名托管设置",
|
subDomain: "子域名托管设置",
|
||||||
pipelineGroup: "流水线分组管理",
|
pipelineGroup: "流水线分组管理",
|
||||||
openKey: "开放接口密钥",
|
openKey: "开放接口密钥",
|
||||||
@@ -22,6 +23,8 @@ export default {
|
|||||||
mySuite: "我的套餐",
|
mySuite: "我的套餐",
|
||||||
suiteBuy: "套餐购买",
|
suiteBuy: "套餐购买",
|
||||||
myTrade: "我的订单",
|
myTrade: "我的订单",
|
||||||
|
myWallet: "我的钱包",
|
||||||
|
inviteCommission: "激励计划",
|
||||||
paymentReturn: "支付返回",
|
paymentReturn: "支付返回",
|
||||||
source: "源码",
|
source: "源码",
|
||||||
github: "github",
|
github: "github",
|
||||||
@@ -46,6 +49,10 @@ export default {
|
|||||||
suiteSetting: "套餐设置",
|
suiteSetting: "套餐设置",
|
||||||
orderManager: "订单管理",
|
orderManager: "订单管理",
|
||||||
userSuites: "用户套餐",
|
userSuites: "用户套餐",
|
||||||
|
inviteCommissionSetting: "激励计划设置",
|
||||||
|
inviteLevel: "推广等级",
|
||||||
|
inviteUserLevel: "用户推广等级",
|
||||||
|
inviteWithdraw: "提现申请记录",
|
||||||
netTest: "网络测试",
|
netTest: "网络测试",
|
||||||
enterpriseManager: "企业管理设置",
|
enterpriseManager: "企业管理设置",
|
||||||
projectManager: "项目管理",
|
projectManager: "项目管理",
|
||||||
|
|||||||
@@ -12,9 +12,11 @@ import plugin from "./plugin/";
|
|||||||
import { setupVben } from "./vben";
|
import { setupVben } from "./vben";
|
||||||
import { util } from "/@/utils";
|
import { util } from "/@/utils";
|
||||||
import { initPreferences } from "/@/vben/preferences";
|
import { initPreferences } from "/@/vben/preferences";
|
||||||
|
import { inviteUtils } from "/@/utils/util.invite";
|
||||||
// import "./components/code-editor/import-works";
|
// import "./components/code-editor/import-works";
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
async function bootstrap() {
|
async function bootstrap() {
|
||||||
|
inviteUtils.captureFromLocation();
|
||||||
const app = createApp(App);
|
const app = createApp(App);
|
||||||
// app.use(Antd);
|
// app.use(Antd);
|
||||||
app.use(Antd);
|
app.use(Antd);
|
||||||
|
|||||||
@@ -186,6 +186,17 @@ export const certdResources = [
|
|||||||
keepAlive: true,
|
keepAlive: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: "certd.dnsPersistRecord",
|
||||||
|
name: "DnsPersistRecord",
|
||||||
|
path: "/certd/cert/dns-persist",
|
||||||
|
component: "/certd/cert/dns-persist/index.vue",
|
||||||
|
meta: {
|
||||||
|
icon: "ion:shield-half-outline",
|
||||||
|
auth: true,
|
||||||
|
keepAlive: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: "certd.subDomain",
|
title: "certd.subDomain",
|
||||||
name: "SubDomain",
|
name: "SubDomain",
|
||||||
@@ -324,7 +335,7 @@ export const certdResources = [
|
|||||||
meta: {
|
meta: {
|
||||||
show: () => {
|
show: () => {
|
||||||
const settingStore = useSettingStore();
|
const settingStore = useSettingStore();
|
||||||
return settingStore.isComm;
|
return settingStore.isInviteCommissionEnabled;
|
||||||
},
|
},
|
||||||
icon: "ion:gift-outline",
|
icon: "ion:gift-outline",
|
||||||
auth: true,
|
auth: true,
|
||||||
@@ -359,6 +370,36 @@ export const certdResources = [
|
|||||||
keepAlive: true,
|
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",
|
title: "certd.paymentReturn",
|
||||||
name: "PaymentReturn",
|
name: "PaymentReturn",
|
||||||
|
|||||||
@@ -286,6 +286,70 @@ export const sysResources = [
|
|||||||
keepAlive: true,
|
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.inviteLevel",
|
||||||
|
name: "SysInviteLevel",
|
||||||
|
path: "/sys/suite/invite/level",
|
||||||
|
component: "/sys/suite/invite/level.vue",
|
||||||
|
meta: {
|
||||||
|
show: () => {
|
||||||
|
const settingStore = useSettingStore();
|
||||||
|
return settingStore.isComm;
|
||||||
|
},
|
||||||
|
icon: "ion:ribbon-outline",
|
||||||
|
permission: "sys:settings:edit",
|
||||||
|
auth: true,
|
||||||
|
keepAlive: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "certd.sysResources.inviteUserLevel",
|
||||||
|
name: "SysInviteUserLevel",
|
||||||
|
path: "/sys/suite/invite/user-level",
|
||||||
|
component: "/sys/suite/invite/user-level.vue",
|
||||||
|
meta: {
|
||||||
|
show: () => {
|
||||||
|
const settingStore = useSettingStore();
|
||||||
|
return settingStore.isComm;
|
||||||
|
},
|
||||||
|
icon: "ion:people-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,
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -97,6 +97,9 @@ export type SysPublicSetting = {
|
|||||||
export type SuiteSetting = {
|
export type SuiteSetting = {
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
};
|
};
|
||||||
|
export type InviteSetting = {
|
||||||
|
enabled?: boolean;
|
||||||
|
};
|
||||||
export type SysPrivateSetting = {
|
export type SysPrivateSetting = {
|
||||||
httpProxy?: string;
|
httpProxy?: string;
|
||||||
httpsProxy?: string;
|
httpsProxy?: string;
|
||||||
@@ -142,6 +145,7 @@ export type AllSettings = {
|
|||||||
siteEnv: SiteEnv;
|
siteEnv: SiteEnv;
|
||||||
headerMenus: HeaderMenus;
|
headerMenus: HeaderMenus;
|
||||||
suiteSetting: SuiteSetting;
|
suiteSetting: SuiteSetting;
|
||||||
|
inviteSetting: InviteSetting;
|
||||||
app: AppInfo;
|
app: AppInfo;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -30,6 +30,9 @@ export interface SettingState {
|
|||||||
headerMenus?: HeaderMenus;
|
headerMenus?: HeaderMenus;
|
||||||
inited?: boolean;
|
inited?: boolean;
|
||||||
suiteSetting?: SuiteSetting;
|
suiteSetting?: SuiteSetting;
|
||||||
|
inviteSetting?: {
|
||||||
|
enabled?: boolean;
|
||||||
|
};
|
||||||
app: {
|
app: {
|
||||||
version?: string;
|
version?: string;
|
||||||
time?: number;
|
time?: number;
|
||||||
@@ -102,6 +105,7 @@ export const useSettingStore = defineStore({
|
|||||||
menus: [],
|
menus: [],
|
||||||
},
|
},
|
||||||
suiteSetting: { enabled: false },
|
suiteSetting: { enabled: false },
|
||||||
|
inviteSetting: { enabled: false },
|
||||||
inited: false,
|
inited: false,
|
||||||
app: {
|
app: {
|
||||||
version: "",
|
version: "",
|
||||||
@@ -196,6 +200,9 @@ export const useSettingStore = defineStore({
|
|||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
return this.suiteSetting?.enabled === true;
|
return this.suiteSetting?.enabled === true;
|
||||||
},
|
},
|
||||||
|
isInviteCommissionEnabled(): boolean {
|
||||||
|
return this.isComm && this.inviteSetting?.enabled === true;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
checkPlus() {
|
checkPlus() {
|
||||||
@@ -215,6 +222,7 @@ export const useSettingStore = defineStore({
|
|||||||
merge(this.plusInfo, allSettings.plusInfo || {});
|
merge(this.plusInfo, allSettings.plusInfo || {});
|
||||||
merge(this.headerMenus, allSettings.headerMenus || {});
|
merge(this.headerMenus, allSettings.headerMenus || {});
|
||||||
merge(this.suiteSetting, allSettings.suiteSetting || {});
|
merge(this.suiteSetting, allSettings.suiteSetting || {});
|
||||||
|
merge(this.inviteSetting, allSettings.inviteSetting || {});
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
this.initSiteInfo(allSettings.siteInfo || {});
|
this.initSiteInfo(allSettings.siteInfo || {});
|
||||||
this.initAppInfo(allSettings.app || {});
|
this.initAppInfo(allSettings.app || {});
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ export interface RegisterReq {
|
|||||||
username: string;
|
username: string;
|
||||||
password: string;
|
password: string;
|
||||||
confirmPassword: string;
|
confirmPassword: string;
|
||||||
|
inviteCode?: string;
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* @description: Login interface parameters
|
* @description: Login interface parameters
|
||||||
@@ -18,6 +19,7 @@ export interface SmsLoginReq {
|
|||||||
phoneCode: string;
|
phoneCode: string;
|
||||||
smsCode: string;
|
smsCode: string;
|
||||||
randomStr: string;
|
randomStr: string;
|
||||||
|
inviteCode?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ForgotPasswordReq {
|
export interface ForgotPasswordReq {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -12,6 +12,12 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
|
|||||||
const { props, ctx, api } = context;
|
const { props, ctx, api } = context;
|
||||||
const lastResRef = ref();
|
const lastResRef = ref();
|
||||||
const pageRequest = async (query: UserPageQuery): Promise<UserPageRes> => {
|
const pageRequest = async (query: UserPageQuery): Promise<UserPageRes> => {
|
||||||
|
query.query = query.query || {};
|
||||||
|
if (props.subtype) {
|
||||||
|
query.query.subtype = props.subtype;
|
||||||
|
} else {
|
||||||
|
delete query.query.subtype;
|
||||||
|
}
|
||||||
return await context.api.GetList(query);
|
return await context.api.GetList(query);
|
||||||
};
|
};
|
||||||
const editRequest = async (req: EditReq) => {
|
const editRequest = async (req: EditReq) => {
|
||||||
@@ -47,7 +53,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
|
|||||||
const { myProjectDict } = useDicts();
|
const { myProjectDict } = useDicts();
|
||||||
const typeRef = ref("aliyun");
|
const typeRef = ref("aliyun");
|
||||||
context.typeRef = typeRef;
|
context.typeRef = typeRef;
|
||||||
const commonColumnsDefine = getCommonColumnDefine(crudExpose, typeRef, api);
|
const commonColumnsDefine = getCommonColumnDefine(crudExpose, typeRef, api, props.subtype);
|
||||||
commonColumnsDefine.type.form.component.disabled = true;
|
commonColumnsDefine.type.form.component.disabled = true;
|
||||||
const projectStore = useProjectStore();
|
const projectStore = useProjectStore();
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -21,6 +21,10 @@ export default defineComponent({
|
|||||||
type: String, //user | sys
|
type: String, //user | sys
|
||||||
default: "user",
|
default: "user",
|
||||||
},
|
},
|
||||||
|
subtype: {
|
||||||
|
type: String,
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
modelValue: {},
|
modelValue: {},
|
||||||
},
|
},
|
||||||
emits: ["update:modelValue"],
|
emits: ["update:modelValue"],
|
||||||
@@ -30,10 +34,17 @@ export default defineComponent({
|
|||||||
const { crudBinding, crudRef, crudExpose } = useFs({ createCrudOptions, context });
|
const { crudBinding, crudRef, crudExpose } = useFs({ createCrudOptions, context });
|
||||||
|
|
||||||
// 你可以调用此方法,重新初始化crud配置
|
// 你可以调用此方法,重新初始化crud配置
|
||||||
|
function refreshSearch() {
|
||||||
|
const form: any = { type: props.type };
|
||||||
|
if (props.subtype) {
|
||||||
|
form.subtype = props.subtype;
|
||||||
|
}
|
||||||
|
crudExpose.setSearchFormData({ form, mergeForm: true });
|
||||||
|
crudExpose.doRefresh();
|
||||||
|
}
|
||||||
function onTypeChanged(value: any) {
|
function onTypeChanged(value: any) {
|
||||||
context.typeRef.value = value;
|
context.typeRef.value = value;
|
||||||
crudExpose.setSearchFormData({ form: { type: value }, mergeForm: true });
|
refreshSearch();
|
||||||
crudExpose.doRefresh();
|
|
||||||
}
|
}
|
||||||
watch(
|
watch(
|
||||||
() => {
|
() => {
|
||||||
@@ -44,6 +55,14 @@ export default defineComponent({
|
|||||||
onTypeChanged(value);
|
onTypeChanged(value);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
watch(
|
||||||
|
() => {
|
||||||
|
return props.subtype;
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
refreshSearch();
|
||||||
|
}
|
||||||
|
);
|
||||||
// 页面打开后获取列表数据
|
// 页面打开后获取列表数据
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
onTypeChanged(props.type);
|
onTypeChanged(props.type);
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
<a-form-item-rest v-if="chooseForm.show">
|
<a-form-item-rest v-if="chooseForm.show">
|
||||||
<a-modal v-model:open="chooseForm.show" title="选择授权提供者" width="900px" @ok="chooseForm.ok">
|
<a-modal v-model:open="chooseForm.show" title="选择授权提供者" width="900px" @ok="chooseForm.ok">
|
||||||
<div style="height: 400px; position: relative">
|
<div style="height: 400px; position: relative">
|
||||||
<cert-access-modal v-model="selectedId" :type="type" :from="from"></cert-access-modal>
|
<cert-access-modal v-model="selectedId" :type="type" :subtype="subtype" :from="from"></cert-access-modal>
|
||||||
</div>
|
</div>
|
||||||
</a-modal>
|
</a-modal>
|
||||||
</a-form-item-rest>
|
</a-form-item-rest>
|
||||||
@@ -35,6 +35,10 @@ export default defineComponent({
|
|||||||
type: String,
|
type: String,
|
||||||
default: "aliyun",
|
default: "aliyun",
|
||||||
},
|
},
|
||||||
|
subtype: {
|
||||||
|
type: String,
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
placeholder: {
|
placeholder: {
|
||||||
type: String,
|
type: String,
|
||||||
default: "请选择",
|
default: "请选择",
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { ColumnCompositionProps, dict } from "@fast-crud/fast-crud";
|
import { ColumnCompositionProps, dict } from "@fast-crud/fast-crud";
|
||||||
import { computed, provide, ref, toRef } from "vue";
|
import { provide, ref, toRef } from "vue";
|
||||||
import { useReference } from "/@/use/use-refrence";
|
import { useReference } from "/@/use/use-refrence";
|
||||||
import { forEach, get, merge, set } from "lodash-es";
|
import { forEach, get, merge, set } from "lodash-es";
|
||||||
import SecretPlainGetter from "/@/views/certd/access/access-selector/access/secret-plain-getter.vue";
|
import SecretPlainGetter from "/@/views/certd/access/access-selector/access/secret-plain-getter.vue";
|
||||||
import { utils } from "/@/utils";
|
import { utils } from "/@/utils";
|
||||||
|
|
||||||
export function getCommonColumnDefine(crudExpose: any, typeRef: any, api: any) {
|
export function getCommonColumnDefine(crudExpose: any, typeRef: any, api: any, fixedSubtype?: string) {
|
||||||
provide("getFromType", api.from);
|
provide("getFromType", api.from);
|
||||||
provide("accessApi", api);
|
provide("accessApi", api);
|
||||||
provide("get:plugin:type", () => {
|
provide("get:plugin:type", () => {
|
||||||
@@ -34,6 +34,13 @@ export function getCommonColumnDefine(crudExpose: any, typeRef: any, api: any) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
console.log('crudBinding.value[mode + "Form"].columns', columnsRef.value);
|
console.log('crudBinding.value[mode + "Form"].columns', columnsRef.value);
|
||||||
|
if (mode === "add" && define.subtype && fixedSubtype) {
|
||||||
|
form.access = form.access || {};
|
||||||
|
const subtypeKey = `access.${define.subtype}`;
|
||||||
|
if (get(form, subtypeKey) == null) {
|
||||||
|
set(form, subtypeKey, fixedSubtype);
|
||||||
|
}
|
||||||
|
}
|
||||||
forEach(define.input, (value: any, mapKey: any) => {
|
forEach(define.input, (value: any, mapKey: any) => {
|
||||||
const key = "access." + mapKey;
|
const key = "access." + mapKey;
|
||||||
const field = {
|
const field = {
|
||||||
|
|||||||
@@ -0,0 +1,83 @@
|
|||||||
|
import { request } from "/src/api/service";
|
||||||
|
|
||||||
|
const apiPrefix = "/cert/dns-persist";
|
||||||
|
|
||||||
|
export async function GetList(query: any) {
|
||||||
|
return await request({
|
||||||
|
url: apiPrefix + "/page",
|
||||||
|
method: "post",
|
||||||
|
data: query,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function AddObj(obj: any) {
|
||||||
|
return await request({
|
||||||
|
url: apiPrefix + "/add",
|
||||||
|
method: "post",
|
||||||
|
data: obj,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function UpdateObj(obj: any) {
|
||||||
|
return await request({
|
||||||
|
url: apiPrefix + "/update",
|
||||||
|
method: "post",
|
||||||
|
data: obj,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function DelObj(id: any) {
|
||||||
|
return await request({
|
||||||
|
url: apiPrefix + "/delete",
|
||||||
|
method: "post",
|
||||||
|
params: { id },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function BuildRecord(body: { domain: string; accountUri: string; wildcard?: boolean; persistUntil?: number }) {
|
||||||
|
return await request({
|
||||||
|
url: apiPrefix + "/build",
|
||||||
|
method: "post",
|
||||||
|
data: body,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function GetByDomain(body: { domain: string; caType?: string; acmeAccountAccessId?: number; commonAcmeAccountAccessId?: number; wildcard?: boolean; persistUntil?: number; createOnNotFound?: boolean }) {
|
||||||
|
return await request({
|
||||||
|
url: apiPrefix + "/getByDomain",
|
||||||
|
method: "post",
|
||||||
|
data: body,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function CheckRecord(body: { hostRecord: string; recordValue: string }) {
|
||||||
|
return await request({
|
||||||
|
url: apiPrefix + "/check",
|
||||||
|
method: "post",
|
||||||
|
data: body,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function Verify(id: number) {
|
||||||
|
return await request({
|
||||||
|
url: apiPrefix + "/verify",
|
||||||
|
method: "post",
|
||||||
|
data: { id },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function TriggerVerify(id: number) {
|
||||||
|
return await request({
|
||||||
|
url: apiPrefix + "/triggerVerify",
|
||||||
|
method: "post",
|
||||||
|
data: { id },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function CreateTxt(body: { id: number; dnsProviderType?: string; dnsProviderAccess?: number }) {
|
||||||
|
return await request({
|
||||||
|
url: apiPrefix + "/createTxt",
|
||||||
|
method: "post",
|
||||||
|
data: body,
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,349 @@
|
|||||||
|
import { AddReq, compute, CreateCrudOptionsProps, CreateCrudOptionsRet, DelReq, dict, EditReq, UserPageQuery, UserPageRes } from "@fast-crud/fast-crud";
|
||||||
|
import { message, Modal, notification } from "ant-design-vue";
|
||||||
|
import * as api from "./api";
|
||||||
|
import { Dicts } from "/@/components/plugins/lib/dicts";
|
||||||
|
import { createAccessApi } from "/@/views/certd/access/api";
|
||||||
|
import { useDnsPersistSettingDialog } from "./use-setting-dialog";
|
||||||
|
|
||||||
|
function parseAccount(account: any) {
|
||||||
|
if (!account) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (typeof account === "string") {
|
||||||
|
return JSON.parse(account);
|
||||||
|
}
|
||||||
|
return account;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ({ crudExpose }: CreateCrudOptionsProps): CreateCrudOptionsRet {
|
||||||
|
const accessApi = createAccessApi();
|
||||||
|
const { openDnsPersistSettingDialog } = useDnsPersistSettingDialog();
|
||||||
|
const accessDict = dict({
|
||||||
|
value: "id",
|
||||||
|
label: "name",
|
||||||
|
url: "accessDict",
|
||||||
|
async getNodesByValues(ids: number[]) {
|
||||||
|
return await accessApi.GetDictByIds(ids);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const dnsProviderTypeDict = dict({
|
||||||
|
url: "pi/dnsProvider/dnsProviderTypeDict",
|
||||||
|
});
|
||||||
|
const statusDict = dict({
|
||||||
|
data: [
|
||||||
|
{ value: "pending", label: "待设置", color: "warning" },
|
||||||
|
{ value: "created", label: "已创建", color: "blue" },
|
||||||
|
{ value: "validating", label: "校验中", color: "blue" },
|
||||||
|
{ value: "valid", label: "有效", color: "green" },
|
||||||
|
{ value: "failed", label: "请重试", color: "red" },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const pageRequest = async (query: UserPageQuery): Promise<UserPageRes> => {
|
||||||
|
return await api.GetList(query);
|
||||||
|
};
|
||||||
|
const editRequest = async ({ form, row }: EditReq) => {
|
||||||
|
form.id = row.id;
|
||||||
|
return await api.UpdateObj(form);
|
||||||
|
};
|
||||||
|
const delRequest = async ({ row }: DelReq) => {
|
||||||
|
const res = await api.DelObj(row.id);
|
||||||
|
if (res?.message) {
|
||||||
|
notification.warning({
|
||||||
|
message: "请到供应商删除TXT记录",
|
||||||
|
description: res.message,
|
||||||
|
duration: 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
const addRequest = async ({ form }: AddReq) => {
|
||||||
|
return await api.AddObj(form);
|
||||||
|
};
|
||||||
|
|
||||||
|
async function fillRecord(form: any) {
|
||||||
|
if (!form.domain || !form.acmeAccountAccessId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const access: any = await accessApi.GetObj(form.acmeAccountAccessId);
|
||||||
|
const setting = JSON.parse(access.setting || "{}");
|
||||||
|
const account = parseAccount(setting.account);
|
||||||
|
if (!account?.accountUri) {
|
||||||
|
message.error("ACME账号授权缺少accountUri,请重新生成账号");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const record = await api.BuildRecord({
|
||||||
|
domain: form.domain,
|
||||||
|
accountUri: account.accountUri,
|
||||||
|
wildcard: true,
|
||||||
|
persistUntil: form.persistUntil,
|
||||||
|
});
|
||||||
|
form.caType = account.caType;
|
||||||
|
form.accountUri = account.accountUri;
|
||||||
|
form.hostRecord = record.hostRecord;
|
||||||
|
form.recordValue = record.recordValue;
|
||||||
|
form.status = "pending";
|
||||||
|
}
|
||||||
|
|
||||||
|
async function verifyRecord(row: any) {
|
||||||
|
const ok = await api.Verify(row.id);
|
||||||
|
message[ok ? "success" : "error"](ok ? "校验成功" : "未找到匹配的TXT记录,请稍后重试");
|
||||||
|
await crudExpose.doRefresh();
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
function showRecordHelp(row: any) {
|
||||||
|
openDnsPersistSettingDialog({
|
||||||
|
record: row,
|
||||||
|
async onDone() {
|
||||||
|
await crudExpose.doRefresh();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
crudOptions: {
|
||||||
|
request: {
|
||||||
|
pageRequest,
|
||||||
|
addRequest,
|
||||||
|
editRequest,
|
||||||
|
delRequest,
|
||||||
|
},
|
||||||
|
actionbar: {
|
||||||
|
buttons: {
|
||||||
|
add: {
|
||||||
|
icon: "ion:add-circle-outline",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rowHandle: {
|
||||||
|
minWidth: 120,
|
||||||
|
fixed: "right",
|
||||||
|
},
|
||||||
|
columns: {
|
||||||
|
id: {
|
||||||
|
title: "ID",
|
||||||
|
key: "id",
|
||||||
|
type: "number",
|
||||||
|
column: { width: 80, order: -999 },
|
||||||
|
form: { show: false },
|
||||||
|
},
|
||||||
|
domain: {
|
||||||
|
title: "域名",
|
||||||
|
type: "text",
|
||||||
|
search: { show: true },
|
||||||
|
form: {
|
||||||
|
required: true,
|
||||||
|
valueChange({ form }) {
|
||||||
|
fillRecord(form);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mainDomain: {
|
||||||
|
title: "主域名",
|
||||||
|
type: "text",
|
||||||
|
form: {
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
|
column: {
|
||||||
|
width: 160,
|
||||||
|
order: 901,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wildcard: {
|
||||||
|
title: "通配符",
|
||||||
|
type: "dict-switch",
|
||||||
|
form: {
|
||||||
|
show: false,
|
||||||
|
value: true,
|
||||||
|
},
|
||||||
|
column: { show: false },
|
||||||
|
},
|
||||||
|
acmeAccountAccessId: {
|
||||||
|
title: "ACME账号授权",
|
||||||
|
type: "dict-select",
|
||||||
|
dict: accessDict,
|
||||||
|
form: {
|
||||||
|
required: true,
|
||||||
|
order: -9,
|
||||||
|
component: {
|
||||||
|
name: "AccessSelector",
|
||||||
|
vModel: "modelValue",
|
||||||
|
type: "acmeAccount",
|
||||||
|
subtype: compute(({ form }) => {
|
||||||
|
return form.caType;
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
valueChange({ form }) {
|
||||||
|
fillRecord(form);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
column: {
|
||||||
|
width: 180,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
caType: {
|
||||||
|
title: "颁发机构",
|
||||||
|
type: "dict-select",
|
||||||
|
dict: Dicts.sslProviderDict,
|
||||||
|
form: {
|
||||||
|
required: true,
|
||||||
|
value: "letsencrypt",
|
||||||
|
order: -10,
|
||||||
|
valueChange({ form }) {
|
||||||
|
form.acmeAccountAccessId = null;
|
||||||
|
fillRecord(form);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
column: { width: 120 },
|
||||||
|
},
|
||||||
|
persistUntil: {
|
||||||
|
title: "有效期至",
|
||||||
|
type: "datetime",
|
||||||
|
form: {
|
||||||
|
helper: "可选;为空表示长期有效",
|
||||||
|
order: 20,
|
||||||
|
valueChange({ form }) {
|
||||||
|
fillRecord(form);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
column: { width: 180, order: 900 },
|
||||||
|
},
|
||||||
|
hostRecord: {
|
||||||
|
title: "TXT主机名",
|
||||||
|
type: "copyable",
|
||||||
|
form: {
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
|
column: {
|
||||||
|
width: 220,
|
||||||
|
cellRender({ value }) {
|
||||||
|
return (
|
||||||
|
<a-tooltip title={value}>
|
||||||
|
<fs-copyable modelValue={value}></fs-copyable>
|
||||||
|
</a-tooltip>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
recordValue: {
|
||||||
|
title: "请设置TXT记录",
|
||||||
|
type: "copyable",
|
||||||
|
form: {
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
|
column: {
|
||||||
|
width: 380,
|
||||||
|
cellRender({ value }) {
|
||||||
|
return (
|
||||||
|
<a-tooltip title={value}>
|
||||||
|
<fs-copyable modelValue={value}></fs-copyable>
|
||||||
|
</a-tooltip>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
dnsProviderType: {
|
||||||
|
title: "DNS服务商",
|
||||||
|
type: "dict-select",
|
||||||
|
dict: dnsProviderTypeDict,
|
||||||
|
form: {
|
||||||
|
show: false,
|
||||||
|
component: {
|
||||||
|
name: "DnsProviderSelector",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
column: { show: false },
|
||||||
|
},
|
||||||
|
dnsProviderAccess: {
|
||||||
|
title: "DNS授权",
|
||||||
|
type: "dict-select",
|
||||||
|
dict: accessDict,
|
||||||
|
form: {
|
||||||
|
show: false,
|
||||||
|
component: {
|
||||||
|
name: "AccessSelector",
|
||||||
|
vModel: "modelValue",
|
||||||
|
type: compute(({ form }) => {
|
||||||
|
const type = form.dnsProviderType || "aliyun";
|
||||||
|
return dnsProviderTypeDict?.dataMap[type]?.accessType || type;
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
column: { show: false },
|
||||||
|
},
|
||||||
|
status: {
|
||||||
|
title: "状态",
|
||||||
|
type: "dict-select",
|
||||||
|
dict: statusDict,
|
||||||
|
form: {
|
||||||
|
show: false,
|
||||||
|
value: "pending",
|
||||||
|
},
|
||||||
|
column: {
|
||||||
|
width: 120,
|
||||||
|
cellRender({ value, row }) {
|
||||||
|
async function resetStatus() {
|
||||||
|
Modal.confirm({
|
||||||
|
title: "重新校验",
|
||||||
|
content: "确认将该记录状态重置为待设置,并重新校验吗?",
|
||||||
|
onOk: async () => {
|
||||||
|
await api.UpdateObj({ id: row.id, status: "pending" });
|
||||||
|
await verifyRecord(row);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div class={"flex flex-left"}>
|
||||||
|
<fs-values-format modelValue={value} dict={statusDict}></fs-values-format>
|
||||||
|
{row.status === "valid" && (
|
||||||
|
<a-tooltip title="撤销并重新校验">
|
||||||
|
<fs-icon class={"ml-5 pointer color-yellow"} icon="solar:undo-left-square-bold" onClick={resetStatus}></fs-icon>
|
||||||
|
</a-tooltip>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
triggerValidate: {
|
||||||
|
title: "校验",
|
||||||
|
type: "text",
|
||||||
|
form: {
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
|
column: {
|
||||||
|
conditionalRenderDisabled: true,
|
||||||
|
width: 210,
|
||||||
|
align: "center",
|
||||||
|
cellRender({ row }) {
|
||||||
|
return (
|
||||||
|
<a-space>
|
||||||
|
{row.status === "valid" ? (
|
||||||
|
<span class="text-gray-500">请勿删除TXT记录</span>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<a-button type="primary" size="small" onClick={() => showRecordHelp(row)}>
|
||||||
|
设置TXT
|
||||||
|
</a-button>
|
||||||
|
<a-button type="primary" size="small" onClick={() => verifyRecord(row)}>
|
||||||
|
校验
|
||||||
|
</a-button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</a-space>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
accountUri: {
|
||||||
|
title: "Account URI",
|
||||||
|
type: "text",
|
||||||
|
form: { show: false },
|
||||||
|
column: { show: false },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
<template>
|
||||||
|
<fs-page class="page-cert-dns-persist">
|
||||||
|
<template #header>
|
||||||
|
<div>
|
||||||
|
<div class="title">DNS持久验证记录</div>
|
||||||
|
<div class="text-orange-500 mt-5">当前仅 Let's Encrypt 测试环境可以申请 DNS 持久验证证书。</div>
|
||||||
|
</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: "DnsPersistRecord",
|
||||||
|
});
|
||||||
|
|
||||||
|
const context: any = {
|
||||||
|
permission: { isProjectPermission: true },
|
||||||
|
};
|
||||||
|
const { crudBinding, crudRef, crudExpose } = useFs({ createCrudOptions, context });
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
crudExpose.doRefresh();
|
||||||
|
});
|
||||||
|
onActivated(async () => {
|
||||||
|
await crudExpose.doRefresh();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
import { message } from "ant-design-vue";
|
||||||
|
import { reactive } from "vue";
|
||||||
|
import AccessSelector from "/@/views/certd/access/access-selector/index.vue";
|
||||||
|
import DnsProviderSelector from "/@/components/plugins/cert/dns-provider-selector/index.vue";
|
||||||
|
import { useFormDialog } from "/@/use/use-dialog";
|
||||||
|
import { CreateTxt, TriggerVerify } from "./api";
|
||||||
|
|
||||||
|
export type DnsPersistSettingRecord = {
|
||||||
|
id?: number;
|
||||||
|
mainDomain?: string;
|
||||||
|
hostRecord?: string;
|
||||||
|
recordValue?: string;
|
||||||
|
dnsProviderType?: string;
|
||||||
|
dnsProviderAccess?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function useDnsPersistSettingDialog() {
|
||||||
|
const { openFormDialog } = useFormDialog();
|
||||||
|
|
||||||
|
function copyableRow(label: string, value?: string) {
|
||||||
|
return (
|
||||||
|
<div class="mb-10 flex items-center">
|
||||||
|
<div style={{ width: "90px", flexShrink: 0 }}>{label}</div>
|
||||||
|
<div style={{ flex: 1, minWidth: 0 }}>
|
||||||
|
<fs-copyable class="w-full" model-value={value || ""}></fs-copyable>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function openDnsPersistSettingDialog(req: { record: DnsPersistSettingRecord; onDone?: () => Promise<void> | void }) {
|
||||||
|
const record = req.record;
|
||||||
|
const form = reactive({
|
||||||
|
mode: "manual",
|
||||||
|
dnsProviderType: record.dnsProviderType || "",
|
||||||
|
dnsProviderAccessType: "",
|
||||||
|
dnsProviderAccess: record.dnsProviderAccess || null,
|
||||||
|
});
|
||||||
|
|
||||||
|
async function submit() {
|
||||||
|
if (!record.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (form.mode === "manual") {
|
||||||
|
await TriggerVerify(record.id);
|
||||||
|
message.success("已提交校验");
|
||||||
|
await req.onDone?.();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!form.dnsProviderType || !form.dnsProviderAccess) {
|
||||||
|
throw new Error("请选择DNS服务商和授权");
|
||||||
|
}
|
||||||
|
await CreateTxt({
|
||||||
|
id: record.id,
|
||||||
|
dnsProviderType: form.dnsProviderType,
|
||||||
|
dnsProviderAccess: form.dnsProviderAccess,
|
||||||
|
});
|
||||||
|
message.success("TXT记录已创建");
|
||||||
|
await req.onDone?.();
|
||||||
|
}
|
||||||
|
|
||||||
|
await openFormDialog({
|
||||||
|
title: "设置DNS TXT记录",
|
||||||
|
wrapper: {
|
||||||
|
width: 680,
|
||||||
|
buttons: {
|
||||||
|
reset: {
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
|
ok: {
|
||||||
|
show: true,
|
||||||
|
text: "确定",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
body: () => (
|
||||||
|
<div>
|
||||||
|
<a-radio-group value={form.mode} buttonStyle="solid" class="mb-10" onUpdate:value={(value: string) => (form.mode = value)}>
|
||||||
|
<a-radio-button value="manual">手动添加</a-radio-button>
|
||||||
|
<a-radio-button value="auto">选择授权添加</a-radio-button>
|
||||||
|
</a-radio-group>
|
||||||
|
{form.mode === "manual" ? (
|
||||||
|
<div>
|
||||||
|
<a-alert class="mb-10" type="info" show-icon message="请到DNS解析控制台添加以下TXT记录,添加后点击确定会立即校验。" />
|
||||||
|
{copyableRow("主域名", record.mainDomain)}
|
||||||
|
{copyableRow("TXT主机名", record.hostRecord)}
|
||||||
|
{copyableRow("TXT值", record.recordValue)}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div>
|
||||||
|
<a-alert class="mb-10" type="info" show-icon message="请选择DNS服务商和授权,系统会创建TXT记录,后续校验由后台完成。" />
|
||||||
|
{copyableRow("主域名", record.mainDomain)}
|
||||||
|
<div class="mb-10 flex items-center">
|
||||||
|
<div style={{ width: "90px", flexShrink: 0 }}>DNS服务商</div>
|
||||||
|
<div style={{ flex: 1, minWidth: 0 }}>
|
||||||
|
<DnsProviderSelector
|
||||||
|
class="w-full"
|
||||||
|
style={{ width: "100%" }}
|
||||||
|
modelValue={form.dnsProviderType}
|
||||||
|
onUpdate:modelValue={(value: string) => {
|
||||||
|
form.dnsProviderType = value;
|
||||||
|
form.dnsProviderAccess = null;
|
||||||
|
}}
|
||||||
|
onSelectedChange={(option: any) => {
|
||||||
|
form.dnsProviderAccessType = option?.accessType || form.dnsProviderType;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mb-10 flex items-center">
|
||||||
|
<div style={{ width: "90px", flexShrink: 0 }}>DNS授权</div>
|
||||||
|
<div style={{ flex: 1, minWidth: 0 }}>
|
||||||
|
<AccessSelector
|
||||||
|
modelValue={form.dnsProviderAccess}
|
||||||
|
type={form.dnsProviderAccessType || form.dnsProviderType || "aliyun"}
|
||||||
|
onUpdate:modelValue={(value: number) => {
|
||||||
|
form.dnsProviderAccess = value;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
onSubmit: submit,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
openDnsPersistSettingDialog,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -92,6 +92,9 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
|
|||||||
if (form.challengeType === "cname") {
|
if (form.challengeType === "cname") {
|
||||||
throw new Error(t("certd.domain.cnameManagedInCnamePage"));
|
throw new Error(t("certd.domain.cnameManagedInCnamePage"));
|
||||||
}
|
}
|
||||||
|
if (form.challengeType === "dns-persist") {
|
||||||
|
throw new Error("DNS持久验证记录请在DNS持久验证记录页面管理");
|
||||||
|
}
|
||||||
if (form.challengeType === "dns") {
|
if (form.challengeType === "dns") {
|
||||||
const isSubdomain = await api.IsSubdomain({ domain: form.domain });
|
const isSubdomain = await api.IsSubdomain({ domain: form.domain });
|
||||||
if (isSubdomain && !subdomainConfirmed.value) {
|
if (isSubdomain && !subdomainConfirmed.value) {
|
||||||
@@ -221,6 +224,17 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
|
|||||||
crudExpose.getFormWrapperRef().close();
|
crudExpose.getFormWrapperRef().close();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
} else if (value === "dns-persist") {
|
||||||
|
Modal.confirm({
|
||||||
|
title: "请前往DNS持久验证记录页面添加记录",
|
||||||
|
content: "DNS持久验证需要先配置ACME账号和_validation-persist持久TXT记录,续期时不再增删DNS记录;当前仅 Let's Encrypt 测试环境可以申请。",
|
||||||
|
async onOk() {
|
||||||
|
router.push({
|
||||||
|
path: "/certd/cert/dns-persist",
|
||||||
|
});
|
||||||
|
crudExpose.getFormWrapperRef().close();
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { request } from "/@/api/service";
|
||||||
|
|
||||||
|
export async function GetMyInvite() {
|
||||||
|
return await request({ url: "/invite/my", method: "post" });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function OpenInvitePlan() {
|
||||||
|
return await request({ url: "/invite/open", 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 });
|
||||||
|
}
|
||||||
@@ -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<UserPageRes> => {
|
||||||
|
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 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
import { CreateCrudOptionsRet, 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<UserPageRes> => {
|
||||||
|
return await api.GetCommissionLogs(query);
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
crudOptions: {
|
||||||
|
request: { pageRequest },
|
||||||
|
actionbar: { show: false },
|
||||||
|
toolbar: { show: false },
|
||||||
|
rowHandle: { show: false },
|
||||||
|
columns: {
|
||||||
|
amount: {
|
||||||
|
title: "收益金额",
|
||||||
|
type: "number",
|
||||||
|
column: {
|
||||||
|
width: 120,
|
||||||
|
component: { name: PriceInput, vModel: "modelValue", edit: false },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
simpleUser: {
|
||||||
|
title: "被推广用户",
|
||||||
|
type: "text",
|
||||||
|
column: {
|
||||||
|
width: 170,
|
||||||
|
cellRender({ row }) {
|
||||||
|
const simpleUser = row.simpleUser;
|
||||||
|
if (!simpleUser) {
|
||||||
|
return "-";
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div class="leading-5">
|
||||||
|
<div>
|
||||||
|
{simpleUser.username || "-"} ({simpleUser.id})
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
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 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,337 @@
|
|||||||
|
<template>
|
||||||
|
<fs-page class="page-invite">
|
||||||
|
<template #header>
|
||||||
|
<div class="title">激励计划</div>
|
||||||
|
</template>
|
||||||
|
<div v-if="loaded && enabled && inviteInfo.enabled" class="invite-body">
|
||||||
|
<div class="invite-summary">
|
||||||
|
<a-row :gutter="16">
|
||||||
|
<a-col :span="6">
|
||||||
|
<a-statistic title="累计收益" :value="amountToYuan(inviteInfo.summary.totalIncomeAmount)" suffix="元" />
|
||||||
|
</a-col>
|
||||||
|
<a-col :span="6">
|
||||||
|
<a-statistic title="本月收益" :value="amountToYuan(inviteInfo.summary.monthIncomeAmount)" suffix="元" />
|
||||||
|
</a-col>
|
||||||
|
<a-col :span="6">
|
||||||
|
<a-statistic title="推广人数" :value="inviteInfo.summary.inviteeCount || 0" suffix="人" />
|
||||||
|
</a-col>
|
||||||
|
<a-col :span="6">
|
||||||
|
<a-statistic title="累计推广金额" :value="amountToYuan(inviteInfo.summary.promotionAmount)" suffix="元" />
|
||||||
|
</a-col>
|
||||||
|
</a-row>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="invite-main">
|
||||||
|
<div class="invite-link-row flex-o">
|
||||||
|
<span class="label">邀请码:</span>
|
||||||
|
<fs-copyable v-model="inviteInfo.inviteCode" />
|
||||||
|
</div>
|
||||||
|
<div class="invite-link-row flex-o mt-10">
|
||||||
|
<span class="label">邀请链接:</span>
|
||||||
|
<fs-copyable v-model="inviteInfo.inviteLink" />
|
||||||
|
</div>
|
||||||
|
<div class="invite-link-row flex-o mt-10">
|
||||||
|
<span class="label">我的等级:</span>
|
||||||
|
<a-button type="link" class="level-button" @click="levelDialogOpen = true">
|
||||||
|
{{ inviteInfo.currentLevel?.name || "未设置" }}
|
||||||
|
<span v-if="inviteInfo.currentLevel">({{ inviteInfo.currentLevel.commissionRate }}%)</span>
|
||||||
|
</a-button>
|
||||||
|
<a-button size="small" @click="openAgreementDialog(false)">查看推广协议</a-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a-tabs v-model:active-key="activeTab" class="invite-tabs">
|
||||||
|
<a-tab-pane key="invitees" tab="推广成功">
|
||||||
|
<fs-crud v-if="activeTab === 'invitees'" ref="inviteesCrudRef" class="invite-crud" v-bind="inviteesCrudBinding" />
|
||||||
|
</a-tab-pane>
|
||||||
|
<a-tab-pane key="logs" tab="收益记录">
|
||||||
|
<fs-crud v-if="activeTab === 'logs'" ref="logsCrudRef" class="invite-crud" v-bind="logsCrudBinding" />
|
||||||
|
</a-tab-pane>
|
||||||
|
</a-tabs>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="loaded && enabled" class="invite-disabled">
|
||||||
|
<a-empty description="请先开通激励计划">
|
||||||
|
<a-button type="primary" @click="openAgreementDialog(true)">开通激励计划</a-button>
|
||||||
|
</a-empty>
|
||||||
|
</div>
|
||||||
|
<a-empty v-else-if="loaded" description="激励计划未开启" />
|
||||||
|
|
||||||
|
<a-modal v-model:open="levelDialogOpen" title="推广等级" width="720px" :footer="null">
|
||||||
|
<div class="level-list">
|
||||||
|
<div v-for="level in inviteInfo.levelList" :key="level.id" class="level-item" :class="{ active: level.id === inviteInfo.currentLevel?.id }">
|
||||||
|
<div class="level-title">
|
||||||
|
<span>{{ level.name }}</span>
|
||||||
|
<a-tag v-if="level.id === inviteInfo.currentLevel?.id" color="green">当前等级</a-tag>
|
||||||
|
<a-tag v-if="level.isHidden" color="orange">专属等级</a-tag>
|
||||||
|
</div>
|
||||||
|
<div class="level-meta">佣金比例 {{ level.commissionRate }}%,累计推广金额达到 {{ amountToYuan(level.minAmount) }} 元</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="inviteInfo.nextLevel" class="next-level">距离下一等级「{{ inviteInfo.nextLevel.name }}」还差 {{ amountToYuan(inviteInfo.nextLevel.gapAmount) }} 元推广金额</div>
|
||||||
|
<div v-else class="next-level">已达到当前可自动升级的最高等级</div>
|
||||||
|
</div>
|
||||||
|
</a-modal>
|
||||||
|
</fs-page>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { h, nextTick, onActivated, onMounted, reactive, ref } from "vue";
|
||||||
|
import { useFs } from "@fast-crud/fast-crud";
|
||||||
|
import { notification } from "ant-design-vue";
|
||||||
|
import * as api from "./api";
|
||||||
|
import createInviteesCrudOptions from "./crud-invitees";
|
||||||
|
import createLogsCrudOptions from "./crud-logs";
|
||||||
|
import { useSettingStore } from "/@/store/settings";
|
||||||
|
import { util } from "/@/utils";
|
||||||
|
import { useFormDialog } from "/@/use/use-dialog";
|
||||||
|
|
||||||
|
defineOptions({ name: "InviteCommission" });
|
||||||
|
|
||||||
|
const settingStore = useSettingStore();
|
||||||
|
const enabled = ref(false);
|
||||||
|
const activeTab = ref("invitees");
|
||||||
|
const loaded = ref(false);
|
||||||
|
const levelDialogOpen = ref(false);
|
||||||
|
const { openFormDialog } = useFormDialog();
|
||||||
|
|
||||||
|
const inviteInfo = reactive<any>({
|
||||||
|
enabled: false,
|
||||||
|
inviteCode: "",
|
||||||
|
inviteLink: "",
|
||||||
|
agreementContent: "",
|
||||||
|
summary: { totalIncomeAmount: 0, monthIncomeAmount: 0, promotionAmount: 0, inviteeCount: 0 },
|
||||||
|
currentLevel: null,
|
||||||
|
nextLevel: null,
|
||||||
|
levelList: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const { crudBinding: inviteesCrudBinding, crudExpose: inviteesCrudExpose, crudRef: inviteesCrudRef } = useFs({ createCrudOptions: createInviteesCrudOptions });
|
||||||
|
const { crudBinding: logsCrudBinding, crudExpose: logsCrudExpose, crudRef: logsCrudRef } = useFs({ createCrudOptions: createLogsCrudOptions });
|
||||||
|
|
||||||
|
function amountToYuan(amount: number) {
|
||||||
|
return util.amount.toYuan(amount || 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderAgreement() {
|
||||||
|
return h("div", { class: "invite-agreement-content" }, inviteInfo.agreementContent || "暂无推广协议内容");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function openAgreementDialog(needOpenPlan: boolean) {
|
||||||
|
await openFormDialog({
|
||||||
|
title: needOpenPlan ? "开通激励计划" : "推广协议",
|
||||||
|
wrapper: {
|
||||||
|
width: 720,
|
||||||
|
maskClosable: !needOpenPlan,
|
||||||
|
keyboard: !needOpenPlan,
|
||||||
|
},
|
||||||
|
initialForm: {
|
||||||
|
agree: false,
|
||||||
|
},
|
||||||
|
body: renderAgreement,
|
||||||
|
columns: needOpenPlan
|
||||||
|
? {
|
||||||
|
agree: {
|
||||||
|
title: "确认",
|
||||||
|
type: "text",
|
||||||
|
form: {
|
||||||
|
col: { span: 24 },
|
||||||
|
component: {
|
||||||
|
name: "a-checkbox",
|
||||||
|
vModel: "checked",
|
||||||
|
},
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
validator: async (_rule: any, value: boolean) => {
|
||||||
|
if (value === true) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
throw new Error("请勾选同意推广协议");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
helper: "我已阅读并同意推广协议",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: {},
|
||||||
|
async onSubmit(form: any) {
|
||||||
|
if (!needOpenPlan) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (form.agree !== true) {
|
||||||
|
throw new Error("请勾选同意推广协议");
|
||||||
|
}
|
||||||
|
await api.OpenInvitePlan();
|
||||||
|
notification.success({ message: "激励计划已开通" });
|
||||||
|
await refreshInvitePage(true, false);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadMyInvite(autoOpenAgreement = false) {
|
||||||
|
const res: any = await api.GetMyInvite();
|
||||||
|
Object.assign(inviteInfo, res || {});
|
||||||
|
if (autoOpenAgreement && !inviteInfo.enabled) {
|
||||||
|
await nextTick();
|
||||||
|
await openAgreementDialog(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refreshActiveList() {
|
||||||
|
if (!inviteInfo.enabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (activeTab.value === "invitees") {
|
||||||
|
await inviteesCrudExpose.doRefresh();
|
||||||
|
} else if (activeTab.value === "logs") {
|
||||||
|
await logsCrudExpose.doRefresh();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refreshInvitePage(refreshAll = false, autoOpenAgreement = true) {
|
||||||
|
await settingStore.initOnce();
|
||||||
|
enabled.value = settingStore.isInviteCommissionEnabled;
|
||||||
|
loaded.value = true;
|
||||||
|
if (!enabled.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await loadMyInvite(autoOpenAgreement);
|
||||||
|
if (!inviteInfo.enabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await nextTick();
|
||||||
|
if (refreshAll) {
|
||||||
|
await Promise.all([inviteesCrudExpose.doRefresh(), logsCrudExpose.doRefresh()]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await refreshActiveList();
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await refreshInvitePage(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
onActivated(async () => {
|
||||||
|
if (!loaded.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await refreshInvitePage();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less">
|
||||||
|
.page-invite {
|
||||||
|
display: flex;
|
||||||
|
min-height: 0;
|
||||||
|
|
||||||
|
.fs-page-content {
|
||||||
|
display: flex;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.invite-body {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 0;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.invite-disabled {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.invite-summary,
|
||||||
|
.invite-main {
|
||||||
|
flex: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.invite-summary {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.invite-main {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.invite-tabs {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-tabs-content-holder,
|
||||||
|
.ant-tabs-content,
|
||||||
|
.ant-tabs-tabpane {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-tabs-tabpane {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.invite-crud {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
width: 80px;
|
||||||
|
flex: none;
|
||||||
|
text-align: right;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.level-button {
|
||||||
|
padding-left: 0;
|
||||||
|
margin-right: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.invite-agreement-content {
|
||||||
|
max-height: 360px;
|
||||||
|
padding: 12px;
|
||||||
|
overflow: auto;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
border: 1px solid #eee;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: #fafafa;
|
||||||
|
line-height: 1.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.level-list {
|
||||||
|
.level-item {
|
||||||
|
padding: 12px 14px;
|
||||||
|
border: 1px solid #eee;
|
||||||
|
border-radius: 6px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.level-item.active {
|
||||||
|
border-color: #52c41a;
|
||||||
|
background: #f6ffed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.level-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.level-meta {
|
||||||
|
margin-top: 6px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.next-level {
|
||||||
|
margin-top: 12px;
|
||||||
|
color: #1677ff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
+4
@@ -213,6 +213,10 @@ function useStepForm() {
|
|||||||
const stepOpen = (step: any, emit: any) => {
|
const stepOpen = (step: any, emit: any) => {
|
||||||
callback.value = emit;
|
callback.value = emit;
|
||||||
currentStep.value = merge({ input: {}, strategy: {} }, step);
|
currentStep.value = merge({ input: {}, strategy: {} }, step);
|
||||||
|
// 旧版证书申请任务没有 version 字段,编辑时补成 1,保持旧任务继续走兼容逻辑。
|
||||||
|
if (mode.value === "edit" && currentStep.value.type === "CertApply" && currentStep.value.input?.version == null) {
|
||||||
|
currentStep.value.input.version = 1;
|
||||||
|
}
|
||||||
if (step.type) {
|
if (step.type) {
|
||||||
changeCurrentPlugin(currentStep.value);
|
changeCurrentPlugin(currentStep.value);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ export type TradeCreateReq = {
|
|||||||
duration: number;
|
duration: number;
|
||||||
num: number;
|
num: number;
|
||||||
payType: string;
|
payType: string;
|
||||||
|
useRebateBalance?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function TradeCreate(form: TradeCreateReq) {
|
export async function TradeCreate(form: TradeCreateReq) {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
<a-col :span="24">
|
<a-col :span="24">
|
||||||
<a-card>
|
<a-card>
|
||||||
<div class="suite-intro-box">
|
<div class="suite-intro-box">
|
||||||
<div>{{ buyHelperText }}</div>
|
<div>说明:① 同一时间只有最新购买的一个套餐生效;② 可以购买多个加量包,加量包立即生效;③ 套餐和加量包内的数量可以叠加</div>
|
||||||
<div v-if="suiteIntro" v-html="suiteIntro"></div>
|
<div v-if="suiteIntro" v-html="suiteIntro"></div>
|
||||||
</div>
|
</div>
|
||||||
</a-card>
|
</a-card>
|
||||||
@@ -31,7 +31,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, ref } from "vue";
|
import { ref } from "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";
|
||||||
@@ -55,17 +55,9 @@ async function doOrder(req: any) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const suiteIntro = ref("");
|
const suiteIntro = ref("");
|
||||||
const allowSuiteStack = ref(false);
|
|
||||||
const buyHelperText = computed(() => {
|
|
||||||
if (allowSuiteStack.value) {
|
|
||||||
return "说明:可以购买多个套餐和加量包,套餐和加量包内的数量可以叠加";
|
|
||||||
}
|
|
||||||
return "说明:① 同一时间只有最新购买的一个套餐生效;② 可以购买多个加量包,加量包立即生效;③ 套餐和加量包内的数量可以叠加";
|
|
||||||
});
|
|
||||||
async function loadSuiteIntro() {
|
async function loadSuiteIntro() {
|
||||||
const res = await api.GetSuiteSetting();
|
const res = await api.GetSuiteSetting();
|
||||||
suiteIntro.value = res.intro;
|
suiteIntro.value = res.intro;
|
||||||
allowSuiteStack.value = !!res.allowSuiteStack;
|
|
||||||
}
|
}
|
||||||
loadSuiteIntro();
|
loadSuiteIntro();
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -28,10 +28,19 @@
|
|||||||
<span class="label">{{ $t("certd.order.price") }}:</span>
|
<span class="label">{{ $t("certd.order.price") }}:</span>
|
||||||
<price-input :edit="false" :model-value="durationSelected.price"></price-input>
|
<price-input :edit="false" :model-value="durationSelected.price"></price-input>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="durationSelected.price > 0 && wallet.availableAmount > 0" class="flex-o mt-5">
|
||||||
|
<span class="label">返利抵扣:</span>
|
||||||
|
<a-switch v-model:checked="formRef.useRebateBalance" />
|
||||||
|
<span class="ml-10">可用 {{ amountToYuan(wallet.availableAmount) }} 元,预计抵扣 {{ amountToYuan(expectedRebateAmount) }} 元</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="durationSelected.price > 0 && formRef.useRebateBalance" class="flex-o mt-5">
|
||||||
|
<span class="label">还需支付:</span>
|
||||||
|
<span class="color-red">{{ amountToYuan(expectedThirdPartyAmount) }} 元</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="flex-o mt-5">
|
<div class="flex-o mt-5">
|
||||||
<span class="label">{{ $t("certd.order.paymentMethod") }}:</span>
|
<span class="label">{{ $t("certd.order.paymentMethod") }}:</span>
|
||||||
<div v-if="durationSelected.price === 0">{{ $t("certd.order.free") }}</div>
|
<div v-if="durationSelected.price === 0 || expectedThirdPartyAmount === 0">{{ $t("certd.order.free") }}</div>
|
||||||
<fs-dict-select v-else v-model:value="formRef.payType" :dict="paymentsDictRef" style="width: 200px"> </fs-dict-select>
|
<fs-dict-select v-else v-model:value="formRef.payType" :dict="paymentsDictRef" style="width: 200px"> </fs-dict-select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -39,7 +48,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="tsx">
|
<script setup lang="tsx">
|
||||||
import { ref } from "vue";
|
import { computed, ref } from "vue";
|
||||||
import { GetPaymentTypes, OrderModalOpenReq, TradeCreate } from "/@/views/certd/suite/api";
|
import { GetPaymentTypes, OrderModalOpenReq, TradeCreate } from "/@/views/certd/suite/api";
|
||||||
import SuiteValue from "/@/views/sys/suite/product/suite-value.vue";
|
import SuiteValue from "/@/views/sys/suite/product/suite-value.vue";
|
||||||
import PriceInput from "/@/views/sys/suite/product/price-input.vue";
|
import PriceInput from "/@/views/sys/suite/product/price-input.vue";
|
||||||
@@ -49,11 +58,14 @@ import DurationValue from "/@/views/sys/suite/product/duration-value.vue";
|
|||||||
import { useRouter } from "vue-router";
|
import { useRouter } from "vue-router";
|
||||||
import qrcode from "qrcode";
|
import qrcode from "qrcode";
|
||||||
import * as api from "/@/views/certd/suite/api";
|
import * as api from "/@/views/certd/suite/api";
|
||||||
|
import { GetMyInvite } from "/@/views/certd/invite/api";
|
||||||
|
import { util } from "/@/utils";
|
||||||
const openRef = ref(false);
|
const openRef = ref(false);
|
||||||
|
|
||||||
const product = ref<any>(null);
|
const product = ref<any>(null);
|
||||||
const formRef = ref<any>({});
|
const formRef = ref<any>({});
|
||||||
const durationSelected = ref<any>(null);
|
const durationSelected = ref<any>(null);
|
||||||
|
const wallet = ref<any>({ availableAmount: 0 });
|
||||||
async function open(opts: OrderModalOpenReq) {
|
async function open(opts: OrderModalOpenReq) {
|
||||||
openRef.value = true;
|
openRef.value = true;
|
||||||
|
|
||||||
@@ -63,6 +75,13 @@ async function open(opts: OrderModalOpenReq) {
|
|||||||
formRef.value.productId = opts.product.id;
|
formRef.value.productId = opts.product.id;
|
||||||
formRef.value.duration = opts.duration;
|
formRef.value.duration = opts.duration;
|
||||||
formRef.value.num = opts.num ?? 1;
|
formRef.value.num = opts.num ?? 1;
|
||||||
|
formRef.value.useRebateBalance = false;
|
||||||
|
try {
|
||||||
|
const inviteInfo: any = await GetMyInvite();
|
||||||
|
wallet.value = inviteInfo.wallet || { availableAmount: 0 };
|
||||||
|
} catch (e) {
|
||||||
|
wallet.value = { availableAmount: 0 };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
const paymentsDictRef = dict({
|
const paymentsDictRef = dict({
|
||||||
async getData() {
|
async getData() {
|
||||||
@@ -77,6 +96,21 @@ const paymentsDictRef = dict({
|
|||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
|
const expectedRebateAmount = computed(() => {
|
||||||
|
if (!formRef.value.useRebateBalance) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return Math.min(wallet.value.availableAmount || 0, durationSelected.value?.price || 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
const expectedThirdPartyAmount = computed(() => {
|
||||||
|
return Math.max(0, (durationSelected.value?.price || 0) - expectedRebateAmount.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
function amountToYuan(amount: number) {
|
||||||
|
return util.amount.toYuan(amount || 0);
|
||||||
|
}
|
||||||
|
|
||||||
async function orderCreate() {
|
async function orderCreate() {
|
||||||
if (durationSelected.value.price === 0) {
|
if (durationSelected.value.price === 0) {
|
||||||
//如果是0,直接请求创建订单
|
//如果是0,直接请求创建订单
|
||||||
@@ -93,7 +127,7 @@ async function orderCreate() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!formRef.value.payType) {
|
if (expectedThirdPartyAmount.value > 0 && !formRef.value.payType) {
|
||||||
notification.error({
|
notification.error({
|
||||||
message: "请选择支付方式",
|
message: "请选择支付方式",
|
||||||
});
|
});
|
||||||
@@ -104,8 +138,17 @@ async function orderCreate() {
|
|||||||
duration: formRef.value.duration,
|
duration: formRef.value.duration,
|
||||||
num: formRef.value.num ?? 1,
|
num: formRef.value.num ?? 1,
|
||||||
payType: formRef.value.payType,
|
payType: formRef.value.payType,
|
||||||
|
useRebateBalance: formRef.value.useRebateBalance,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (paymentReq.paid) {
|
||||||
|
notification.success({
|
||||||
|
message: "套餐购买成功",
|
||||||
|
});
|
||||||
|
openRef.value = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
async function onPaid() {
|
async function onPaid() {
|
||||||
openRef.value = false;
|
openRef.value = false;
|
||||||
router.push({
|
router.push({
|
||||||
|
|||||||
@@ -151,6 +151,30 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
rebateAmount: {
|
||||||
|
title: "返利抵扣",
|
||||||
|
type: "number",
|
||||||
|
column: {
|
||||||
|
width: 110,
|
||||||
|
component: {
|
||||||
|
name: PriceInput,
|
||||||
|
vModel: "modelValue",
|
||||||
|
edit: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
thirdPartyPayAmount: {
|
||||||
|
title: "实付金额",
|
||||||
|
type: "number",
|
||||||
|
column: {
|
||||||
|
width: 110,
|
||||||
|
component: {
|
||||||
|
name: PriceInput,
|
||||||
|
vModel: "modelValue",
|
||||||
|
edit: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
status: {
|
status: {
|
||||||
title: "状态",
|
title: "状态",
|
||||||
search: { show: true },
|
search: { show: true },
|
||||||
@@ -177,6 +201,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
|
|||||||
{ label: "支付宝", value: "alipay" },
|
{ label: "支付宝", value: "alipay" },
|
||||||
{ label: "微信", value: "wxpay" },
|
{ label: "微信", value: "wxpay" },
|
||||||
{ label: "免费", value: "free" },
|
{ label: "免费", value: "free" },
|
||||||
|
{ label: "返利余额", value: "rebate" },
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
column: {
|
column: {
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { request } from "/@/api/service";
|
||||||
|
|
||||||
|
export async function GetWalletSummary() {
|
||||||
|
return await request({ url: "/wallet/summary", method: "post" });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function GetWithdrawSetting() {
|
||||||
|
return await request({ url: "/wallet/withdraw/setting/get", method: "post" });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function SaveWithdrawSetting(data: any) {
|
||||||
|
return await request({ url: "/wallet/withdraw/setting/save", method: "post", data });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function ApplyWithdraw(amount: number) {
|
||||||
|
return await request({ url: "/wallet/withdraw/apply", method: "post", data: { amount } });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function GetWithdraws(query: any) {
|
||||||
|
return await request({ url: "/wallet/withdraw/page", method: "post", data: query });
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
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<UserPageRes> => {
|
||||||
|
return await api.GetWithdraws(query);
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
crudOptions: {
|
||||||
|
request: { pageRequest },
|
||||||
|
actionbar: { show: false },
|
||||||
|
toolbar: { show: false },
|
||||||
|
rowHandle: { show: false },
|
||||||
|
columns: {
|
||||||
|
amount: {
|
||||||
|
title: "金额",
|
||||||
|
type: "number",
|
||||||
|
column: {
|
||||||
|
width: 120,
|
||||||
|
component: { name: PriceInput, vModel: "modelValue", edit: false },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
status: {
|
||||||
|
title: "状态",
|
||||||
|
type: "dict-select",
|
||||||
|
dict: dict({
|
||||||
|
data: [
|
||||||
|
{ label: "待审核", value: "pending", color: "warning" },
|
||||||
|
{ label: "已通过", value: "approved", color: "success" },
|
||||||
|
{ label: "已拒绝", value: "rejected", color: "error" },
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
column: { width: 110 },
|
||||||
|
},
|
||||||
|
channel: {
|
||||||
|
title: "提现渠道",
|
||||||
|
type: "dict-select",
|
||||||
|
dict: dict({
|
||||||
|
data: [
|
||||||
|
{ label: "支付宝", value: "alipay" },
|
||||||
|
{ label: "银行卡", value: "bank" },
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
column: { width: 110 },
|
||||||
|
},
|
||||||
|
realName: { title: "真实姓名", type: "text", column: { width: 120 } },
|
||||||
|
account: { title: "收款账号", type: "text", column: { width: 180 } },
|
||||||
|
bankName: { title: "开户银行", type: "text", column: { width: 160 } },
|
||||||
|
qrCode: {
|
||||||
|
title: "收款二维码",
|
||||||
|
type: "text",
|
||||||
|
column: {
|
||||||
|
width: 120,
|
||||||
|
cellRender({ value }) {
|
||||||
|
if (!value) {
|
||||||
|
return "-";
|
||||||
|
}
|
||||||
|
return <a-image src={`/api/basic/file/download?key=${value}`} width={48} />;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
auditRemark: { title: "审核备注", type: "text", column: { minWidth: 180 } },
|
||||||
|
createTime: { title: "申请时间", type: "datetime", column: { width: 180 } },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,207 @@
|
|||||||
|
<template>
|
||||||
|
<fs-page class="page-wallet">
|
||||||
|
<template #header>
|
||||||
|
<div class="title">我的钱包</div>
|
||||||
|
</template>
|
||||||
|
<div class="wallet-body">
|
||||||
|
<a-row :gutter="16" class="wallet-summary">
|
||||||
|
<a-col :span="6">
|
||||||
|
<a-statistic title="可用余额" :value="amountToYuan(summary.availableAmount)" suffix="元" />
|
||||||
|
</a-col>
|
||||||
|
<a-col :span="6">
|
||||||
|
<a-statistic title="冻结余额" :value="amountToYuan(summary.frozenAmount)" suffix="元" />
|
||||||
|
</a-col>
|
||||||
|
<a-col :span="6">
|
||||||
|
<a-statistic title="累计收入" :value="amountToYuan(summary.totalIncomeAmount)" suffix="元" />
|
||||||
|
</a-col>
|
||||||
|
<a-col :span="6">
|
||||||
|
<a-statistic title="累计提现" :value="amountToYuan(summary.totalWithdrawAmount)" suffix="元" />
|
||||||
|
</a-col>
|
||||||
|
</a-row>
|
||||||
|
|
||||||
|
<div class="wallet-actions">
|
||||||
|
<a-space>
|
||||||
|
<a-button type="primary" @click="openWithdrawSetting">提现设置</a-button>
|
||||||
|
<a-input-number v-model:value="withdrawAmountYuan" :min="0" addon-before="提现金额" addon-after="元" />
|
||||||
|
<a-button @click="applyWithdraw">申请提现</a-button>
|
||||||
|
</a-space>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<fs-crud ref="withdrawCrudRef" class="wallet-crud" v-bind="withdrawCrudBinding" />
|
||||||
|
</div>
|
||||||
|
</fs-page>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { onActivated, onMounted, reactive, ref } from "vue";
|
||||||
|
import { compute, dict, useFs } from "@fast-crud/fast-crud";
|
||||||
|
import { notification } from "ant-design-vue";
|
||||||
|
import * as api from "./api";
|
||||||
|
import createWithdrawCrudOptions from "./crud-withdraw";
|
||||||
|
import { util } from "/@/utils";
|
||||||
|
import { useFormDialog } from "/@/use/use-dialog";
|
||||||
|
import { useUserStore } from "/@/store/user";
|
||||||
|
|
||||||
|
defineOptions({ name: "MyWallet" });
|
||||||
|
|
||||||
|
const summary = reactive<any>({ availableAmount: 0, frozenAmount: 0, totalIncomeAmount: 0, totalWithdrawAmount: 0 });
|
||||||
|
const withdrawAmountYuan = ref(0);
|
||||||
|
const loaded = ref(false);
|
||||||
|
const { openFormDialog } = useFormDialog();
|
||||||
|
const userStore = useUserStore();
|
||||||
|
const { crudBinding: withdrawCrudBinding, crudExpose: withdrawCrudExpose, crudRef: withdrawCrudRef } = useFs({ createCrudOptions: createWithdrawCrudOptions });
|
||||||
|
|
||||||
|
function amountToYuan(amount: number) {
|
||||||
|
return util.amount.toYuan(amount || 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadWalletSummary() {
|
||||||
|
const res: any = await api.GetWalletSummary();
|
||||||
|
Object.assign(summary, res || {});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function openWithdrawSetting() {
|
||||||
|
const setting: any = await api.GetWithdrawSetting();
|
||||||
|
const initialForm = Object.assign({ channel: "alipay", realName: "", account: "", bankName: "" }, setting || {});
|
||||||
|
await openFormDialog({
|
||||||
|
title: "提现设置",
|
||||||
|
wrapper: {
|
||||||
|
width: 560,
|
||||||
|
},
|
||||||
|
initialForm,
|
||||||
|
columns: {
|
||||||
|
channel: {
|
||||||
|
title: "提现渠道",
|
||||||
|
type: "dict-radio",
|
||||||
|
dict: dict({
|
||||||
|
data: [
|
||||||
|
{ label: "支付宝", value: "alipay" },
|
||||||
|
{ label: "银行卡", value: "bank" },
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
form: {
|
||||||
|
col: { span: 24 },
|
||||||
|
rules: [{ required: true, message: "请选择提现渠道" }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
realName: {
|
||||||
|
title: "真实姓名",
|
||||||
|
type: "text",
|
||||||
|
form: {
|
||||||
|
col: { span: 24 },
|
||||||
|
rules: [{ required: true, message: "请输入真实姓名" }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
account: {
|
||||||
|
title: "收款账号",
|
||||||
|
type: "text",
|
||||||
|
form: {
|
||||||
|
col: { span: 24 },
|
||||||
|
rules: [{ required: true, message: "请输入收款账号" }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
qrCode: {
|
||||||
|
title: "收款二维码",
|
||||||
|
type: "cropper-uploader",
|
||||||
|
form: {
|
||||||
|
col: { span: 24 },
|
||||||
|
component: {
|
||||||
|
vModel: "modelValue",
|
||||||
|
valueType: "key",
|
||||||
|
cropper: {
|
||||||
|
aspectRatio: 1,
|
||||||
|
autoCropArea: 1,
|
||||||
|
viewMode: 0,
|
||||||
|
},
|
||||||
|
onReady: null,
|
||||||
|
uploader: {
|
||||||
|
type: "form",
|
||||||
|
action: "/basic/file/upload?token=" + userStore.getToken,
|
||||||
|
name: "file",
|
||||||
|
headers: {
|
||||||
|
Authorization: "Bearer " + userStore.getToken,
|
||||||
|
},
|
||||||
|
successHandle(res: any) {
|
||||||
|
return res;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
buildUrl(key: string) {
|
||||||
|
return `/api/basic/file/download?key=` + key;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
bankName: {
|
||||||
|
title: "开户银行",
|
||||||
|
type: "text",
|
||||||
|
form: {
|
||||||
|
col: { span: 24 },
|
||||||
|
show: compute(({ form }) => form.channel === "bank"),
|
||||||
|
rules: [{ required: compute(({ form }) => form.channel === "bank"), message: "请输入开户银行" }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
async onSubmit(form: any) {
|
||||||
|
await api.SaveWithdrawSetting(form);
|
||||||
|
notification.success({ message: "保存成功" });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function applyWithdraw() {
|
||||||
|
await api.ApplyWithdraw(util.amount.toCent(withdrawAmountYuan.value || 0));
|
||||||
|
withdrawAmountYuan.value = 0;
|
||||||
|
await loadWalletSummary();
|
||||||
|
await withdrawCrudExpose.doRefresh();
|
||||||
|
notification.success({ message: "提现申请已提交" });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refreshWalletPage() {
|
||||||
|
await loadWalletSummary();
|
||||||
|
await withdrawCrudExpose.doRefresh();
|
||||||
|
loaded.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(refreshWalletPage);
|
||||||
|
|
||||||
|
onActivated(async () => {
|
||||||
|
if (!loaded.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await refreshWalletPage();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less">
|
||||||
|
.page-wallet {
|
||||||
|
display: flex;
|
||||||
|
min-height: 0;
|
||||||
|
|
||||||
|
.fs-page-content {
|
||||||
|
display: flex;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wallet-body {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 0;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wallet-summary,
|
||||||
|
.wallet-actions {
|
||||||
|
flex: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wallet-actions {
|
||||||
|
margin: 16px 0 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wallet-crud {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -4,14 +4,8 @@
|
|||||||
<a-popover>
|
<a-popover>
|
||||||
<template #content>
|
<template #content>
|
||||||
<div style="width: 300px">
|
<div style="width: 300px">
|
||||||
<div v-if="hiddenSuiteList.length > 0 || detail.addonList.length > 0" class="flex flex-wrap">
|
<div v-if="detail.addonList.length > 0" class="flex flex-wrap">
|
||||||
<a-tag v-for="(item, index) of hiddenSuiteList" :key="`suite-${index}`" color="green" class="pointer flex-o m-1">
|
<a-tag v-for="(item, index) of detail.addonList" :key="index" color="green" class="pointer flex-o m-1">
|
||||||
<span class="mr-5">
|
|
||||||
{{ item.title }}
|
|
||||||
</span>
|
|
||||||
<span>(<expires-time-text :value="item.expiresTime" />)</span>
|
|
||||||
</a-tag>
|
|
||||||
<a-tag v-for="(item, index) of detail.addonList" :key="`addon-${index}`" color="green" class="pointer flex-o m-1">
|
|
||||||
<span class="mr-5">
|
<span class="mr-5">
|
||||||
{{ item.title }}
|
{{ item.title }}
|
||||||
</span>
|
</span>
|
||||||
@@ -49,13 +43,13 @@
|
|||||||
</template>
|
</template>
|
||||||
<div class="flex-o">
|
<div class="flex-o">
|
||||||
<fs-icon icon="ant-design:gift-outlined" class="color-green mr-5" />
|
<fs-icon icon="ant-design:gift-outlined" class="color-green mr-5" />
|
||||||
<a-tag v-if="currentSuite" color="green" class="pointer flex-o">
|
<a-tag v-for="(item, index) of detail.suiteList" :key="index" color="green" class="pointer flex-o">
|
||||||
<span class="mr-5">
|
<span class="mr-5">
|
||||||
{{ currentSuite.title }}
|
{{ item.title }}
|
||||||
</span>
|
</span>
|
||||||
<span>(<expires-time-text :value="currentSuite.expiresTime" />)</span>
|
<span>(<expires-time-text :value="item.expiresTime" />)</span>
|
||||||
<span v-if="hiddenCount > 0" class="ml-5">+{{ hiddenCount }}</span>
|
|
||||||
</a-tag>
|
</a-tag>
|
||||||
|
<a-tag v-if="detail.addonList.length > 0" color="green" class="pointer flex-o">加量包+{{ detail.addonList.length }}</a-tag>
|
||||||
<div v-if="detail.suites?.length === 0" class="flex-o ml-5">暂无套餐 <a-button class="ml-5" type="primary" size="small" @click="goBuy">去购买</a-button></div>
|
<div v-if="detail.suites?.length === 0" class="flex-o ml-5">暂无套餐 <a-button class="ml-5" type="primary" size="small" @click="goBuy">去购买</a-button></div>
|
||||||
</div>
|
</div>
|
||||||
</a-popover>
|
</a-popover>
|
||||||
@@ -65,7 +59,7 @@
|
|||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import SuiteValue from "/@/views/sys/suite/product/suite-value.vue";
|
import SuiteValue from "/@/views/sys/suite/product/suite-value.vue";
|
||||||
import { computed, ref } from "vue";
|
import { ref } from "vue";
|
||||||
import ExpiresTimeText from "/@/components/expires-time-text.vue";
|
import ExpiresTimeText from "/@/components/expires-time-text.vue";
|
||||||
import { mySuiteApi, SuiteDetail } from "/@/views/certd/suite/mine/api";
|
import { mySuiteApi, SuiteDetail } from "/@/views/certd/suite/mine/api";
|
||||||
import { FsIcon } from "@fast-crud/fast-crud";
|
import { FsIcon } from "@fast-crud/fast-crud";
|
||||||
@@ -77,10 +71,6 @@ defineOptions({
|
|||||||
|
|
||||||
const detail = ref<SuiteDetail>({});
|
const detail = ref<SuiteDetail>({});
|
||||||
|
|
||||||
const currentSuite = computed(() => detail.value.suiteList?.[0]);
|
|
||||||
const hiddenSuiteList = computed(() => detail.value.suiteList?.slice(1) || []);
|
|
||||||
const hiddenCount = computed(() => hiddenSuiteList.value.length + (detail.value.addonList?.length || 0));
|
|
||||||
|
|
||||||
async function loadSuiteDetail() {
|
async function loadSuiteDetail() {
|
||||||
detail.value = await mySuiteApi.SuiteDetailGet();
|
detail.value = await mySuiteApi.SuiteDetailGet();
|
||||||
const suites = detail.value.suites.filter(item => item.productType === "suite");
|
const suites = detail.value.suites.filter(item => item.productType === "suite");
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
<!-- eslint-disable vue/multi-word-component-names -->
|
||||||
<template>
|
<template>
|
||||||
<div class="main login-page">
|
<div class="main login-page">
|
||||||
<a-form v-if="!twoFactor.loginId" ref="formRef" class="user-layout-login" name="custom-validation" :model="formState" v-bind="layout" @finish="handleFinish" @finish-failed="handleFinishFailed">
|
<a-form v-if="!twoFactor.loginId" ref="formRef" class="user-layout-login" name="custom-validation" :model="formState" v-bind="layout" @finish="handleFinish" @finish-failed="handleFinishFailed">
|
||||||
@@ -108,6 +109,7 @@ import * as oauthApi from "../oauth/api";
|
|||||||
import { notification } from "ant-design-vue";
|
import { notification } from "ant-design-vue";
|
||||||
import { request } from "/src/api/service";
|
import { request } from "/src/api/service";
|
||||||
import * as UserApi from "/src/store/user/api.user";
|
import * as UserApi from "/src/store/user/api.user";
|
||||||
|
import { inviteUtils } from "/@/utils/util.invite";
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
@@ -136,6 +138,7 @@ const formState = reactive({
|
|||||||
smsCode: "",
|
smsCode: "",
|
||||||
captcha: null,
|
captcha: null,
|
||||||
smsCaptcha: null,
|
smsCaptcha: null,
|
||||||
|
inviteCode: inviteUtils.get(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const rules = {
|
const rules = {
|
||||||
|
|||||||
@@ -78,6 +78,14 @@
|
|||||||
</a-tab-pane>
|
</a-tab-pane>
|
||||||
</a-tabs>
|
</a-tabs>
|
||||||
|
|
||||||
|
<a-form-item v-if="registerType !== 'mobile'" has-feedback name="inviteCode" label="邀请码">
|
||||||
|
<a-input v-model:value="formState.inviteCode" placeholder="邀请码(选填)" size="large" autocomplete="off">
|
||||||
|
<template #prefix>
|
||||||
|
<fs-icon icon="ion:gift-outline"></fs-icon>
|
||||||
|
</template>
|
||||||
|
</a-input>
|
||||||
|
</a-form-item>
|
||||||
|
|
||||||
<a-form-item v-if="registerType !== 'mobile'">
|
<a-form-item v-if="registerType !== 'mobile'">
|
||||||
<a-button type="primary" size="large" html-type="submit" class="login-button">注册</a-button>
|
<a-button type="primary" size="large" html-type="submit" class="login-button">注册</a-button>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
@@ -97,6 +105,7 @@ import { useSettingStore } from "/@/store/settings";
|
|||||||
import { notification } from "ant-design-vue";
|
import { notification } from "ant-design-vue";
|
||||||
import CaptchaInput from "/@/components/captcha/captcha-input.vue";
|
import CaptchaInput from "/@/components/captcha/captcha-input.vue";
|
||||||
import { useRouter } from "vue-router";
|
import { useRouter } from "vue-router";
|
||||||
|
import { inviteUtils } from "/@/utils/util.invite";
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: "RegisterPage",
|
name: "RegisterPage",
|
||||||
components: { CaptchaInput, EmailCode },
|
components: { CaptchaInput, EmailCode },
|
||||||
@@ -125,6 +134,7 @@ export default defineComponent({
|
|||||||
confirmPassword: "",
|
confirmPassword: "",
|
||||||
captcha: null,
|
captcha: null,
|
||||||
captchaForEmail: null,
|
captchaForEmail: null,
|
||||||
|
inviteCode: inviteUtils.get(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const rules = {
|
const rules = {
|
||||||
@@ -213,6 +223,7 @@ export default defineComponent({
|
|||||||
email: formState.email,
|
email: formState.email,
|
||||||
captcha: registerType.value === "email" ? formState.captchaForEmail : formState.captcha,
|
captcha: registerType.value === "email" ? formState.captchaForEmail : formState.captcha,
|
||||||
validateCode: formState.validateCode,
|
validateCode: formState.validateCode,
|
||||||
|
inviteCode: formState.inviteCode,
|
||||||
}) as any
|
}) as any
|
||||||
);
|
);
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
<div class="sys-plugin-config settings-form">
|
<div class="sys-plugin-config settings-form">
|
||||||
<a-form :model="formState" :label-col="{ span: 8 }" :wrapper-col="{ span: 16 }" autocomplete="off" @finish="onFinish" @finish-failed="onFinishFailed">
|
<a-form :model="formState" :label-col="{ span: 8 }" :wrapper-col="{ span: 16 }" autocomplete="off" @finish="onFinish" @finish-failed="onFinishFailed">
|
||||||
<a-form-item label="公共Google EAB授权" :name="['CertApply', 'sysSetting', 'input', 'googleCommonEabAccessId']">
|
<a-form-item v-show="false" label="公共Google EAB授权" :name="['CertApply', 'sysSetting', 'input', 'googleCommonEabAccessId']">
|
||||||
<access-selector v-model:model-value="formState.CertApply.sysSetting.input.googleCommonEabAccessId" type="eab" from="sys"></access-selector>
|
<access-selector v-model:model-value="formState.CertApply.sysSetting.input.googleCommonEabAccessId" type="eab" from="sys"></access-selector>
|
||||||
<div class="helper">
|
<div class="helper">
|
||||||
<div>设置公共Google EAB授权给用户使用,避免用户自己去翻墙获取Google EAB授权</div>
|
<div>设置公共Google EAB授权给用户使用,避免用户自己去翻墙获取Google EAB授权</div>
|
||||||
@@ -16,7 +16,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
|
|
||||||
<a-form-item label="公共ZeroSSL EAB授权" :name="['CertApply', 'sysSetting', 'input', 'zerosslCommonEabAccessId']">
|
<a-form-item label="公共Google ACME账号" :name="['CertApply', 'sysSetting', 'input', 'googleCommonAcmeAccountAccessId']">
|
||||||
|
<access-selector v-model:model-value="formState.CertApply.sysSetting.input.googleCommonAcmeAccountAccessId" type="acmeAccount" subtype="google" from="sys"></access-selector>
|
||||||
|
<div class="helper">
|
||||||
|
<div>优先推荐配置公共ACME账号。配置后普通用户申请Google证书时无需选择账号,也不会重复消费公共EAB。</div>
|
||||||
|
</div>
|
||||||
|
</a-form-item>
|
||||||
|
|
||||||
|
<a-form-item v-show="false" label="公共ZeroSSL EAB授权" :name="['CertApply', 'sysSetting', 'input', 'zerosslCommonEabAccessId']">
|
||||||
<access-selector v-model:model-value="formState.CertApply.sysSetting.input.zerosslCommonEabAccessId" type="eab" from="sys"></access-selector>
|
<access-selector v-model:model-value="formState.CertApply.sysSetting.input.zerosslCommonEabAccessId" type="eab" from="sys"></access-selector>
|
||||||
<div class="helper">
|
<div class="helper">
|
||||||
<div>设置公共ZeroSSL EAB授权给用户使用,避免用户自己去翻墙获取Zero EAB授权</div>
|
<div>设置公共ZeroSSL EAB授权给用户使用,避免用户自己去翻墙获取Zero EAB授权</div>
|
||||||
@@ -26,7 +33,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
|
|
||||||
<a-form-item label="公共litessl EAB授权" :name="['CertApply', 'sysSetting', 'input', 'litesslCommonEabAccessId']">
|
<a-form-item label="公共ZeroSSL ACME账号" :name="['CertApply', 'sysSetting', 'input', 'zerosslCommonAcmeAccountAccessId']">
|
||||||
|
<access-selector v-model:model-value="formState.CertApply.sysSetting.input.zerosslCommonAcmeAccountAccessId" type="acmeAccount" subtype="zerossl" from="sys"></access-selector>
|
||||||
|
</a-form-item>
|
||||||
|
|
||||||
|
<a-form-item v-show="false" label="公共litessl EAB授权" :name="['CertApply', 'sysSetting', 'input', 'litesslCommonEabAccessId']">
|
||||||
<access-selector v-model:model-value="formState.CertApply.sysSetting.input.litesslCommonEabAccessId" type="eab" from="sys"></access-selector>
|
<access-selector v-model:model-value="formState.CertApply.sysSetting.input.litesslCommonEabAccessId" type="eab" from="sys"></access-selector>
|
||||||
<div class="helper">
|
<div class="helper">
|
||||||
<div>设置公共litessl EAB授权给用户使用,避免用户自己获取litessl EAB授权</div>
|
<div>设置公共litessl EAB授权给用户使用,避免用户自己获取litessl EAB授权</div>
|
||||||
@@ -36,6 +47,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
|
|
||||||
|
<a-form-item label="公共litessl ACME账号" :name="['CertApply', 'sysSetting', 'input', 'litesslCommonAcmeAccountAccessId']">
|
||||||
|
<access-selector v-model:model-value="formState.CertApply.sysSetting.input.litesslCommonAcmeAccountAccessId" type="acmeAccount" subtype="litessl" from="sys"></access-selector>
|
||||||
|
</a-form-item>
|
||||||
|
|
||||||
<a-form-item label="其他配置">
|
<a-form-item label="其他配置">
|
||||||
<a-button type="primary" @click="doPluginConfig">证书申请插件默认值设置</a-button>
|
<a-button type="primary" @click="doPluginConfig">证书申请插件默认值设置</a-button>
|
||||||
<div class="helper">
|
<div class="helper">
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import { request } from "/@/api/service";
|
||||||
|
|
||||||
|
export async function GetSettings() {
|
||||||
|
return await request({ url: "/sys/invite/settings/get", method: "post" });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function SaveSettings(data: any) {
|
||||||
|
return await request({ url: "/sys/invite/settings/save", method: "post", data });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function GetLevels(query: any) {
|
||||||
|
return await request({ url: "/sys/invite/level/page", method: "post", data: query });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function GetLevelList() {
|
||||||
|
return await request({ url: "/sys/invite/level/list", method: "post", data: {} });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function AddLevel(data: any) {
|
||||||
|
return await request({ url: "/sys/invite/level/add", method: "post", data });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function UpdateLevel(data: any) {
|
||||||
|
return await request({ url: "/sys/invite/level/update", method: "post", data });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function GetUserLevels(query: any) {
|
||||||
|
return await request({ url: "/sys/invite/user/page", method: "post", data: query });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function SetUserLevel(data: any) {
|
||||||
|
return await request({ url: "/sys/invite/user/setLevel", method: "post", data });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function GetWithdraws(query: any) {
|
||||||
|
return await request({ url: "/sys/wallet/withdraw/page", method: "post", data: query });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function ApproveWithdraw(id: number, remark?: string) {
|
||||||
|
return await request({ url: "/sys/wallet/withdraw/approve", method: "post", data: { id, remark } });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function RejectWithdraw(id: number, remark: string) {
|
||||||
|
return await request({ url: "/sys/wallet/withdraw/reject", method: "post", data: { id, remark } });
|
||||||
|
}
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
import { AddReq, compute, CreateCrudOptionsRet, DelReq, EditReq, UserPageQuery, UserPageRes, dict } 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<UserPageRes> => {
|
||||||
|
return await api.GetLevels(query);
|
||||||
|
};
|
||||||
|
const addRequest = async ({ form }: AddReq) => {
|
||||||
|
return await api.AddLevel(form);
|
||||||
|
};
|
||||||
|
const editRequest = async ({ form, row }: EditReq) => {
|
||||||
|
form.id = row.id;
|
||||||
|
return await api.UpdateLevel(form);
|
||||||
|
};
|
||||||
|
const delRequest = async ({ row }: DelReq) => {
|
||||||
|
row.disabled = true;
|
||||||
|
return await api.UpdateLevel(row);
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
crudOptions: {
|
||||||
|
request: { pageRequest, addRequest, editRequest, delRequest },
|
||||||
|
rowHandle: {
|
||||||
|
width: 180,
|
||||||
|
fixed: "right",
|
||||||
|
buttons: {
|
||||||
|
view: { show: false },
|
||||||
|
copy: { show: false },
|
||||||
|
remove: {
|
||||||
|
text: "禁用",
|
||||||
|
show: compute(({ row }) => row.disabled !== true),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
columns: {
|
||||||
|
id: {
|
||||||
|
title: "ID",
|
||||||
|
type: "number",
|
||||||
|
form: { show: false },
|
||||||
|
column: { width: 90 },
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
title: "等级名称",
|
||||||
|
type: "text",
|
||||||
|
search: { show: true },
|
||||||
|
form: {
|
||||||
|
rules: [{ required: true, message: "请输入等级名称" }],
|
||||||
|
},
|
||||||
|
column: { width: 140 },
|
||||||
|
},
|
||||||
|
minAmount: {
|
||||||
|
title: "升级金额",
|
||||||
|
type: "number",
|
||||||
|
form: {
|
||||||
|
component: { name: PriceInput, vModel: "modelValue", edit: true },
|
||||||
|
rules: [{ required: true, message: "请输入升级金额" }],
|
||||||
|
},
|
||||||
|
column: {
|
||||||
|
width: 140,
|
||||||
|
component: { name: PriceInput, vModel: "modelValue", edit: false },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
commissionRate: {
|
||||||
|
title: "佣金比例",
|
||||||
|
type: "number",
|
||||||
|
form: {
|
||||||
|
component: { min: 0, max: 100, addonAfter: "%" },
|
||||||
|
rules: [{ required: true, message: "请输入佣金比例" }],
|
||||||
|
},
|
||||||
|
column: { width: 110, align: "center", cellRender: ({ value }) => `${value || 0}%` },
|
||||||
|
},
|
||||||
|
isHidden: {
|
||||||
|
title: "隐藏等级",
|
||||||
|
type: "dict-switch",
|
||||||
|
dict: dict({
|
||||||
|
data: [
|
||||||
|
{ label: "普通等级", value: false, color: "success" },
|
||||||
|
{ label: "隐藏等级", value: true, color: "warning" },
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
form: { value: false },
|
||||||
|
column: { width: 120, align: "center" },
|
||||||
|
},
|
||||||
|
sort: {
|
||||||
|
title: "排序",
|
||||||
|
type: "number",
|
||||||
|
form: { value: 10 },
|
||||||
|
column: { width: 90, align: "center" },
|
||||||
|
},
|
||||||
|
disabled: {
|
||||||
|
title: "状态",
|
||||||
|
type: "dict-switch",
|
||||||
|
dict: dict({
|
||||||
|
data: [
|
||||||
|
{ label: "启用", value: false, color: "success" },
|
||||||
|
{ label: "禁用", value: true, color: "error" },
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
form: { value: false },
|
||||||
|
column: { width: 100, align: "center" },
|
||||||
|
},
|
||||||
|
createTime: { title: "创建时间", type: "datetime", form: { show: false }, column: { width: 170 } },
|
||||||
|
updateTime: { title: "更新时间", type: "datetime", form: { show: false }, column: { width: 170 } },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,136 @@
|
|||||||
|
import { compute, CreateCrudOptionsProps, CreateCrudOptionsRet, dict, UserPageQuery, UserPageRes } from "@fast-crud/fast-crud";
|
||||||
|
import { notification } from "ant-design-vue";
|
||||||
|
import * as api from "./api";
|
||||||
|
import PriceInput from "/@/views/sys/suite/product/price-input.vue";
|
||||||
|
import { useFormDialog } from "/@/use/use-dialog";
|
||||||
|
|
||||||
|
export default function ({ crudExpose }: CreateCrudOptionsProps): CreateCrudOptionsRet {
|
||||||
|
const { openFormDialog } = useFormDialog();
|
||||||
|
const levelDict = dict({
|
||||||
|
url: "/sys/invite/level/list",
|
||||||
|
value: "id",
|
||||||
|
label: "name",
|
||||||
|
});
|
||||||
|
|
||||||
|
const pageRequest = async (query: UserPageQuery): Promise<UserPageRes> => {
|
||||||
|
return await api.GetUserLevels(query);
|
||||||
|
};
|
||||||
|
|
||||||
|
async function openSetLevel(row: any) {
|
||||||
|
await openFormDialog({
|
||||||
|
title: "指定推广等级",
|
||||||
|
wrapper: { width: 560 },
|
||||||
|
initialForm: {
|
||||||
|
userId: row.userId,
|
||||||
|
levelId: row.levelId,
|
||||||
|
levelLocked: row.levelLocked === true,
|
||||||
|
},
|
||||||
|
columns: {
|
||||||
|
levelId: {
|
||||||
|
title: "推广等级",
|
||||||
|
type: "dict-select",
|
||||||
|
dict: levelDict,
|
||||||
|
form: {
|
||||||
|
col: { span: 24 },
|
||||||
|
rules: [{ required: true, message: "请选择推广等级" }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
levelLocked: {
|
||||||
|
title: "锁定等级",
|
||||||
|
type: "dict-switch",
|
||||||
|
dict: dict({
|
||||||
|
data: [
|
||||||
|
{ label: "自动升级", value: false, color: "success" },
|
||||||
|
{ label: "锁定", value: true, color: "warning" },
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
form: {
|
||||||
|
col: { span: 24 },
|
||||||
|
helper: "隐藏等级会自动锁定,不参与自动升级。",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
async onSubmit(form: any) {
|
||||||
|
await api.SetUserLevel(form);
|
||||||
|
await crudExpose.doRefresh();
|
||||||
|
notification.success({ message: "推广等级已更新" });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
crudOptions: {
|
||||||
|
request: { pageRequest },
|
||||||
|
actionbar: { show: false },
|
||||||
|
toolbar: { show: false },
|
||||||
|
rowHandle: {
|
||||||
|
width: 130,
|
||||||
|
fixed: "right",
|
||||||
|
buttons: {
|
||||||
|
view: { show: false },
|
||||||
|
edit: { show: false },
|
||||||
|
copy: { show: false },
|
||||||
|
remove: { show: false },
|
||||||
|
setLevel: {
|
||||||
|
text: "指定等级",
|
||||||
|
type: "link",
|
||||||
|
click: ({ row }) => openSetLevel(row),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
columns: {
|
||||||
|
userId: { title: "用户ID", type: "number", search: { show: true }, column: { width: 100 } },
|
||||||
|
username: { title: "用户名", type: "text", search: { show: true }, column: { width: 160 } },
|
||||||
|
userDisplay: { title: "显示名称", type: "text", column: { width: 160 } },
|
||||||
|
enabled: {
|
||||||
|
title: "开通状态",
|
||||||
|
type: "dict-switch",
|
||||||
|
dict: dict({
|
||||||
|
data: [
|
||||||
|
{ label: "未开通", value: false, color: "default" },
|
||||||
|
{ label: "已开通", value: true, color: "success" },
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
column: { width: 110, align: "center" },
|
||||||
|
},
|
||||||
|
levelId: {
|
||||||
|
title: "当前等级",
|
||||||
|
type: "dict-select",
|
||||||
|
dict: levelDict,
|
||||||
|
search: { show: true },
|
||||||
|
column: { width: 130 },
|
||||||
|
},
|
||||||
|
levelLocked: {
|
||||||
|
title: "升级方式",
|
||||||
|
type: "dict-switch",
|
||||||
|
dict: dict({
|
||||||
|
data: [
|
||||||
|
{ label: "自动升级", value: false, color: "success" },
|
||||||
|
{ label: "锁定", value: true, color: "warning" },
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
column: { width: 110, align: "center" },
|
||||||
|
},
|
||||||
|
isHidden: {
|
||||||
|
title: "隐藏等级",
|
||||||
|
type: "dict-switch",
|
||||||
|
dict: dict({
|
||||||
|
data: [
|
||||||
|
{ label: "否", value: false, color: "default" },
|
||||||
|
{ label: "是", value: true, color: "warning" },
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
column: { width: 100, align: "center", show: compute(({ row }) => row.levelId) },
|
||||||
|
},
|
||||||
|
commissionRate: { title: "佣金比例", type: "number", column: { width: 110, align: "center", cellRender: ({ value }) => (value == null ? "-" : `${value}%`) } },
|
||||||
|
promotionAmount: {
|
||||||
|
title: "累计推广金额",
|
||||||
|
type: "number",
|
||||||
|
column: { width: 140, component: { name: PriceInput, vModel: "modelValue", edit: false } },
|
||||||
|
},
|
||||||
|
createTime: { title: "开通时间", type: "datetime", column: { width: 170 } },
|
||||||
|
updateTime: { title: "更新时间", type: "datetime", column: { width: 170 } },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,169 @@
|
|||||||
|
import { compute, CreateCrudOptionsProps, CreateCrudOptionsRet, dict, UserPageQuery, UserPageRes } from "@fast-crud/fast-crud";
|
||||||
|
import { notification } from "ant-design-vue";
|
||||||
|
import * as api from "./api";
|
||||||
|
import PriceInput from "/@/views/sys/suite/product/price-input.vue";
|
||||||
|
import { useFormDialog } from "/@/use/use-dialog";
|
||||||
|
|
||||||
|
export default function ({ crudExpose }: CreateCrudOptionsProps): CreateCrudOptionsRet {
|
||||||
|
const { openFormDialog } = useFormDialog();
|
||||||
|
|
||||||
|
const pageRequest = async (query: UserPageQuery): Promise<UserPageRes> => {
|
||||||
|
return await api.GetWithdraws(query);
|
||||||
|
};
|
||||||
|
|
||||||
|
function renderWithdrawDetail(row: any) {
|
||||||
|
return (
|
||||||
|
<a-descriptions class={"w-full"} bordered column={2} size={"small"}>
|
||||||
|
<a-descriptions-item label="渠道类型">{row.channel === "bank" ? "银行卡" : "支付宝"}</a-descriptions-item>
|
||||||
|
<a-descriptions-item label="用户名">{row.userDisplay || row.userId}</a-descriptions-item>
|
||||||
|
<a-descriptions-item label="账号">{row.account || "-"}</a-descriptions-item>
|
||||||
|
<a-descriptions-item label="开户行名称">{row.bankName || "-"}</a-descriptions-item>
|
||||||
|
<a-descriptions-item label="收款二维码" span={2}>
|
||||||
|
{row.qrCode ? <a-image src={`/api/basic/file/download?key=${row.qrCode}`} width={160} /> : <span>-</span>}
|
||||||
|
</a-descriptions-item>
|
||||||
|
<a-descriptions-item label="提现金额">{row.amount / 100} 元</a-descriptions-item>
|
||||||
|
</a-descriptions>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function approve(row: any) {
|
||||||
|
await openFormDialog({
|
||||||
|
title: "提现审核",
|
||||||
|
wrapper: {
|
||||||
|
width: 760,
|
||||||
|
},
|
||||||
|
body: () => renderWithdrawDetail(row),
|
||||||
|
onSubmit: async () => {
|
||||||
|
await api.ApproveWithdraw(row.id);
|
||||||
|
await crudExpose.doRefresh();
|
||||||
|
notification.success({ message: "已审核通过" });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function reject(row: any) {
|
||||||
|
await openFormDialog({
|
||||||
|
title: "提现审核",
|
||||||
|
wrapper: {
|
||||||
|
width: 760,
|
||||||
|
},
|
||||||
|
initialForm: {
|
||||||
|
remark: "",
|
||||||
|
},
|
||||||
|
body: () => renderWithdrawDetail(row),
|
||||||
|
columns: {
|
||||||
|
remark: {
|
||||||
|
title: "拒绝理由",
|
||||||
|
type: "textarea",
|
||||||
|
form: {
|
||||||
|
col: {
|
||||||
|
span: 24,
|
||||||
|
},
|
||||||
|
component: {
|
||||||
|
name: "a-textarea",
|
||||||
|
vModel: "value",
|
||||||
|
rows: 4,
|
||||||
|
placeholder: "请填写拒绝理由",
|
||||||
|
},
|
||||||
|
rules: [{ required: true, message: "请填写拒绝理由" }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
async onSubmit(form: any) {
|
||||||
|
const remark = form.remark.trim();
|
||||||
|
if (!remark) {
|
||||||
|
notification.error({ message: "请填写拒绝理由" });
|
||||||
|
throw new Error("请填写拒绝理由");
|
||||||
|
}
|
||||||
|
await api.RejectWithdraw(row.id, remark);
|
||||||
|
await crudExpose.doRefresh();
|
||||||
|
notification.success({ message: "已拒绝并退回余额" });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
crudOptions: {
|
||||||
|
request: { pageRequest },
|
||||||
|
actionbar: { show: false },
|
||||||
|
toolbar: { show: false },
|
||||||
|
rowHandle: {
|
||||||
|
width: 150,
|
||||||
|
fixed: "right",
|
||||||
|
buttons: {
|
||||||
|
view: { show: false },
|
||||||
|
edit: { show: false },
|
||||||
|
copy: { show: false },
|
||||||
|
remove: { show: false },
|
||||||
|
approve: {
|
||||||
|
text: "通过",
|
||||||
|
type: "link",
|
||||||
|
show: compute(({ row }) => row.status === "pending"),
|
||||||
|
click: ({ row }) => approve(row),
|
||||||
|
},
|
||||||
|
reject: {
|
||||||
|
text: "拒绝",
|
||||||
|
type: "link",
|
||||||
|
show: compute(({ row }) => row.status === "pending"),
|
||||||
|
click: ({ row }) => reject(row),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
columns: {
|
||||||
|
userId: { title: "用户ID", type: "number", search: { show: true }, column: { width: 100 } },
|
||||||
|
amount: {
|
||||||
|
title: "金额",
|
||||||
|
type: "number",
|
||||||
|
column: {
|
||||||
|
width: 120,
|
||||||
|
component: { name: PriceInput, vModel: "modelValue", edit: false },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
userDisplay: { title: "用户名", type: "text", search: { show: true }, column: { width: 140 } },
|
||||||
|
status: {
|
||||||
|
title: "状态",
|
||||||
|
type: "dict-select",
|
||||||
|
search: { show: true },
|
||||||
|
dict: dict({
|
||||||
|
data: [
|
||||||
|
{ label: "待审核", value: "pending", color: "warning" },
|
||||||
|
{ label: "已通过", value: "approved", color: "success" },
|
||||||
|
{ label: "已拒绝", value: "rejected", color: "error" },
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
column: { width: 110 },
|
||||||
|
},
|
||||||
|
channel: {
|
||||||
|
title: "提现渠道",
|
||||||
|
type: "dict-select",
|
||||||
|
search: { show: true },
|
||||||
|
dict: dict({
|
||||||
|
data: [
|
||||||
|
{ label: "支付宝", value: "alipay" },
|
||||||
|
{ label: "银行卡", value: "bank" },
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
column: { width: 110 },
|
||||||
|
},
|
||||||
|
realName: { title: "真实姓名", type: "text", search: { show: true }, column: { width: 120 } },
|
||||||
|
account: { title: "收款账号", type: "text", column: { width: 180 } },
|
||||||
|
bankName: { title: "开户银行", type: "text", column: { width: 160 } },
|
||||||
|
qrCode: {
|
||||||
|
title: "收款二维码",
|
||||||
|
type: "text",
|
||||||
|
column: {
|
||||||
|
width: 120,
|
||||||
|
cellRender({ value }) {
|
||||||
|
if (!value) {
|
||||||
|
return "-";
|
||||||
|
}
|
||||||
|
return <a-image src={`/api/basic/file/download?key=${value}`} width={48} />;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
auditRemark: { title: "审核备注", type: "text", column: { minWidth: 180 } },
|
||||||
|
createTime: { title: "申请时间", type: "datetime", column: { width: 180 } },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
<template>
|
||||||
|
<fs-page class="page-sys-invite-level">
|
||||||
|
<template #header>
|
||||||
|
<div class="title">推广等级</div>
|
||||||
|
</template>
|
||||||
|
<fs-crud ref="crudRef" v-bind="crudBinding" />
|
||||||
|
</fs-page>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { onActivated, onMounted } from "vue";
|
||||||
|
import { useFs } from "@fast-crud/fast-crud";
|
||||||
|
import createCrudOptions from "./crud-level";
|
||||||
|
|
||||||
|
defineOptions({ name: "SysInviteLevel" });
|
||||||
|
|
||||||
|
const { crudBinding, crudRef, crudExpose } = useFs({ createCrudOptions });
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
crudExpose.doRefresh();
|
||||||
|
});
|
||||||
|
onActivated(() => {
|
||||||
|
crudExpose.doRefresh();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
<template>
|
||||||
|
<fs-page class="page-sys-invite-setting">
|
||||||
|
<template #header>
|
||||||
|
<div class="title">激励计划设置</div>
|
||||||
|
</template>
|
||||||
|
<div class="page-body">
|
||||||
|
<a-form ref="formRef" :model="settings" :label-col="{ style: { width: '140px' } }" class="settings-form">
|
||||||
|
<a-form-item label="开启激励计划" name="enabled">
|
||||||
|
<a-switch v-model:checked="settings.enabled" />
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="推广协议" name="agreementContent">
|
||||||
|
<a-textarea v-model:value="settings.agreementContent" :rows="10" placeholder="请输入用户开通激励计划前需要确认的推广协议内容" />
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="最低提现金额" name="minWithdrawAmountYuan">
|
||||||
|
<a-input-number v-model:value="settings.minWithdrawAmountYuan" :min="0" addon-after="元" />
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="提现渠道" name="withdrawChannels">
|
||||||
|
<a-checkbox-group v-model:value="settings.withdrawChannels" :options="withdrawChannelOptions" />
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label=" ">
|
||||||
|
<a-button type="primary" @click="saveSettings">保存设置</a-button>
|
||||||
|
</a-form-item>
|
||||||
|
</a-form>
|
||||||
|
</div>
|
||||||
|
</fs-page>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { onMounted, reactive } from "vue";
|
||||||
|
import { notification } from "ant-design-vue";
|
||||||
|
import * as api from "./api";
|
||||||
|
import { util } from "/@/utils";
|
||||||
|
import { useSettingStore } from "/@/store/settings";
|
||||||
|
|
||||||
|
defineOptions({ name: "SysInviteCommissionSetting" });
|
||||||
|
|
||||||
|
const defaultAgreement = "请遵守平台推广规则,不得通过虚假注册、刷单、恶意诱导等方式获取收益。平台有权对异常推广行为进行核查,并根据实际情况暂停结算或关闭激励计划资格。";
|
||||||
|
const settings = reactive<any>({ enabled: false, agreementContent: "", minWithdrawAmountYuan: 0, withdrawChannels: ["alipay", "bank"] });
|
||||||
|
const withdrawChannelOptions = [
|
||||||
|
{ label: "支付宝", value: "alipay" },
|
||||||
|
{ label: "银行卡", value: "bank" },
|
||||||
|
];
|
||||||
|
|
||||||
|
async function loadSettings() {
|
||||||
|
const data: any = await api.GetSettings();
|
||||||
|
settings.enabled = !!data?.enabled;
|
||||||
|
settings.agreementContent = data?.agreementContent || defaultAgreement;
|
||||||
|
settings.minWithdrawAmountYuan = util.amount.toYuan(data?.minWithdrawAmount || 0);
|
||||||
|
settings.withdrawChannels = data?.withdrawChannels?.length ? data.withdrawChannels : ["alipay", "bank"];
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveSettings() {
|
||||||
|
await api.SaveSettings({
|
||||||
|
enabled: settings.enabled,
|
||||||
|
agreementContent: settings.agreementContent || "",
|
||||||
|
minWithdrawAmount: util.amount.toCent(settings.minWithdrawAmountYuan || 0),
|
||||||
|
withdrawChannels: settings.withdrawChannels || [],
|
||||||
|
});
|
||||||
|
await useSettingStore().loadSysSettings();
|
||||||
|
notification.success({ message: "保存成功" });
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(loadSettings);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less">
|
||||||
|
.page-sys-invite-setting {
|
||||||
|
.page-body {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
.settings-form {
|
||||||
|
max-width: 860px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
<template>
|
||||||
|
<fs-page class="page-sys-invite-user-level">
|
||||||
|
<template #header>
|
||||||
|
<div class="title">用户推广等级</div>
|
||||||
|
</template>
|
||||||
|
<fs-crud ref="crudRef" v-bind="crudBinding" />
|
||||||
|
</fs-page>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { onActivated, onMounted } from "vue";
|
||||||
|
import { useFs } from "@fast-crud/fast-crud";
|
||||||
|
import createCrudOptions from "./crud-user-level";
|
||||||
|
|
||||||
|
defineOptions({ name: "SysInviteUserLevel" });
|
||||||
|
|
||||||
|
const { crudBinding, crudRef, crudExpose } = useFs({ createCrudOptions });
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
crudExpose.doRefresh();
|
||||||
|
});
|
||||||
|
onActivated(() => {
|
||||||
|
crudExpose.doRefresh();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
<template>
|
||||||
|
<fs-page class="page-sys-invite-withdraw">
|
||||||
|
<template #header>
|
||||||
|
<div class="title">提现申请记录</div>
|
||||||
|
</template>
|
||||||
|
<fs-crud ref="crudRef" v-bind="crudBinding" />
|
||||||
|
</fs-page>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { onActivated, onMounted } from "vue";
|
||||||
|
import { useFs } from "@fast-crud/fast-crud";
|
||||||
|
import createCrudOptions from "./crud-withdraw";
|
||||||
|
|
||||||
|
defineOptions({ name: "SysInviteWithdraw" });
|
||||||
|
|
||||||
|
const { crudBinding, crudRef, crudExpose } = useFs({ createCrudOptions });
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
crudExpose.doRefresh();
|
||||||
|
});
|
||||||
|
onActivated(() => {
|
||||||
|
crudExpose.doRefresh();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
@@ -21,11 +21,6 @@
|
|||||||
<div class="helper">不建议设置免费套餐,可以在下方配置注册赠送套餐,或者在用户套餐管理中手动赠送套餐</div>
|
<div class="helper">不建议设置免费套餐,可以在下方配置注册赠送套餐,或者在用户套餐管理中手动赠送套餐</div>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
|
|
||||||
<a-form-item label="套餐支持叠加" name="allowSuiteStack">
|
|
||||||
<a-switch v-model:checked="formState.allowSuiteStack" />
|
|
||||||
<div class="helper">默认只有一个套餐生效。开启此开关,可以让多个套餐可以叠加;加量包无论是否开启此开关都可以叠加。</div>
|
|
||||||
</a-form-item>
|
|
||||||
|
|
||||||
<a-form-item label="注册赠送套餐" name="registerGift">
|
<a-form-item label="注册赠送套餐" name="registerGift">
|
||||||
<suite-duration-selector ref="suiteDurationSelectedRef" v-model="formState.registerGift"></suite-duration-selector>
|
<suite-duration-selector ref="suiteDurationSelectedRef" v-model="formState.registerGift"></suite-duration-selector>
|
||||||
<div class="helper">添加套餐后再选择</div>
|
<div class="helper">添加套餐后再选择</div>
|
||||||
@@ -78,7 +73,6 @@ const formRef = ref<any>(null);
|
|||||||
const formState = reactive<
|
const formState = reactive<
|
||||||
Partial<{
|
Partial<{
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
allowSuiteStack: boolean;
|
|
||||||
registerGift?: {
|
registerGift?: {
|
||||||
productId?: number;
|
productId?: number;
|
||||||
duration?: number;
|
duration?: number;
|
||||||
|
|||||||
@@ -174,6 +174,30 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
rebateAmount: {
|
||||||
|
title: "返利抵扣",
|
||||||
|
type: "number",
|
||||||
|
column: {
|
||||||
|
width: 110,
|
||||||
|
component: {
|
||||||
|
name: PriceInput,
|
||||||
|
vModel: "modelValue",
|
||||||
|
edit: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
thirdPartyPayAmount: {
|
||||||
|
title: "实付金额",
|
||||||
|
type: "number",
|
||||||
|
column: {
|
||||||
|
width: 110,
|
||||||
|
component: {
|
||||||
|
name: PriceInput,
|
||||||
|
vModel: "modelValue",
|
||||||
|
edit: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
status: {
|
status: {
|
||||||
title: "状态",
|
title: "状态",
|
||||||
search: { show: true },
|
search: { show: true },
|
||||||
@@ -200,6 +224,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
|
|||||||
{ label: "支付宝", value: "alipay" },
|
{ label: "支付宝", value: "alipay" },
|
||||||
{ label: "微信", value: "wxpay" },
|
{ label: "微信", value: "wxpay" },
|
||||||
{ label: "免费", value: "free" },
|
{ label: "免费", value: "free" },
|
||||||
|
{ label: "返利余额", value: "rebate" },
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
column: {
|
column: {
|
||||||
|
|||||||
@@ -3,16 +3,6 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
|
|
||||||
## [1.40.4](https://github.com/certd/certd/compare/v1.40.3...v1.40.4) (2026-05-24)
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
* **pipeline-service:** 修复流水线运行时超过套餐部署次数仍然能够正常运行的bug ([5e59651](https://github.com/certd/certd/commit/5e59651d45bc91919629e35995ff1b3cff6b87ea))
|
|
||||||
|
|
||||||
### Performance Improvements
|
|
||||||
|
|
||||||
* 新增阿里云直播证书部署插件 ([8edb6f8](https://github.com/certd/certd/commit/8edb6f8727bd148f106801bef25567880fd35e9e))
|
|
||||||
|
|
||||||
## [1.40.3](https://github.com/certd/certd/compare/v1.40.2...v1.40.3) (2026-05-21)
|
## [1.40.3](https://github.com/certd/certd/compare/v1.40.2...v1.40.3) (2026-05-21)
|
||||||
|
|
||||||
### Bug Fixes
|
### Bug Fixes
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
ALTER TABLE cd_trade ADD COLUMN rebate_amount bigint NOT NULL DEFAULT 0;
|
||||||
|
ALTER TABLE cd_trade ADD COLUMN third_party_pay_amount bigint NOT NULL DEFAULT 0;
|
||||||
|
|
||||||
|
CREATE TABLE `cd_invite_code`
|
||||||
|
(
|
||||||
|
`id` bigint PRIMARY KEY AUTO_INCREMENT NOT NULL,
|
||||||
|
`user_id` bigint,
|
||||||
|
`code` varchar(50),
|
||||||
|
`disabled` boolean NOT NULL DEFAULT false,
|
||||||
|
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
CREATE UNIQUE INDEX `index_invite_code_user_id` ON `cd_invite_code` (`user_id`);
|
||||||
|
CREATE UNIQUE INDEX `index_invite_code_code` ON `cd_invite_code` (`code`);
|
||||||
|
|
||||||
|
CREATE TABLE `cd_invite_relation`
|
||||||
|
(
|
||||||
|
`id` bigint PRIMARY KEY AUTO_INCREMENT NOT NULL,
|
||||||
|
`inviter_user_id` bigint,
|
||||||
|
`invitee_user_id` bigint,
|
||||||
|
`invite_code` varchar(50),
|
||||||
|
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
CREATE INDEX `index_invite_relation_inviter` ON `cd_invite_relation` (`inviter_user_id`);
|
||||||
|
CREATE UNIQUE INDEX `index_invite_relation_invitee` ON `cd_invite_relation` (`invitee_user_id`);
|
||||||
|
|
||||||
|
CREATE TABLE `cd_user_wallet`
|
||||||
|
(
|
||||||
|
`id` bigint PRIMARY KEY AUTO_INCREMENT NOT NULL,
|
||||||
|
`user_id` bigint,
|
||||||
|
`available_amount` bigint NOT NULL DEFAULT 0,
|
||||||
|
`frozen_amount` bigint NOT NULL DEFAULT 0,
|
||||||
|
`total_income_amount` bigint NOT NULL DEFAULT 0,
|
||||||
|
`total_consumed_amount` bigint NOT NULL DEFAULT 0,
|
||||||
|
`total_withdraw_amount` bigint NOT NULL DEFAULT 0,
|
||||||
|
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
CREATE UNIQUE INDEX `index_user_wallet_user_id` ON `cd_user_wallet` (`user_id`);
|
||||||
|
|
||||||
|
CREATE TABLE `cd_invite_commission_log`
|
||||||
|
(
|
||||||
|
`id` bigint PRIMARY KEY AUTO_INCREMENT NOT NULL,
|
||||||
|
`user_id` bigint,
|
||||||
|
`amount` bigint,
|
||||||
|
`trade_id` bigint,
|
||||||
|
`invitee_user_id` bigint,
|
||||||
|
`consume_amount` bigint NOT NULL DEFAULT 0,
|
||||||
|
`remark` varchar(2048),
|
||||||
|
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
CREATE INDEX `index_invite_log_user_id` ON `cd_invite_commission_log` (`user_id`);
|
||||||
|
|
||||||
|
CREATE TABLE `cd_user_wallet_log`
|
||||||
|
(
|
||||||
|
`id` bigint PRIMARY KEY AUTO_INCREMENT NOT NULL,
|
||||||
|
`user_id` bigint,
|
||||||
|
`type` varchar(50),
|
||||||
|
`amount` bigint,
|
||||||
|
`balance_after` bigint,
|
||||||
|
`trade_id` bigint,
|
||||||
|
`withdraw_id` bigint,
|
||||||
|
`remark` varchar(2048),
|
||||||
|
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
CREATE INDEX `index_user_wallet_log_user_id` ON `cd_user_wallet_log` (`user_id`);
|
||||||
|
|
||||||
|
CREATE TABLE `cd_user_wallet_withdraw`
|
||||||
|
(
|
||||||
|
`id` bigint PRIMARY KEY AUTO_INCREMENT NOT NULL,
|
||||||
|
`user_id` bigint,
|
||||||
|
`amount` bigint,
|
||||||
|
`status` varchar(50),
|
||||||
|
`channel` varchar(50),
|
||||||
|
`real_name` varchar(100),
|
||||||
|
`account` varchar(200),
|
||||||
|
`bank_name` varchar(200),
|
||||||
|
`qr_code` varchar(512),
|
||||||
|
`audit_user_id` bigint,
|
||||||
|
`audit_remark` varchar(2048),
|
||||||
|
`audit_time` bigint,
|
||||||
|
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
CREATE INDEX `index_user_wallet_withdraw_user_id` ON `cd_user_wallet_withdraw` (`user_id`);
|
||||||
|
CREATE INDEX `index_user_wallet_withdraw_status` ON `cd_user_wallet_withdraw` (`status`);
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
ALTER TABLE cd_invite_commission_log ADD COLUMN level_id bigint NOT NULL DEFAULT 0;
|
||||||
|
ALTER TABLE cd_invite_commission_log ADD COLUMN commission_rate bigint NOT NULL DEFAULT 0;
|
||||||
|
|
||||||
|
CREATE TABLE `cd_invite_level`
|
||||||
|
(
|
||||||
|
`id` bigint PRIMARY KEY AUTO_INCREMENT NOT NULL,
|
||||||
|
`name` varchar(100),
|
||||||
|
`sort` bigint NOT NULL DEFAULT 0,
|
||||||
|
`min_amount` bigint NOT NULL DEFAULT 0,
|
||||||
|
`commission_rate` bigint NOT NULL DEFAULT 0,
|
||||||
|
`is_hidden` boolean NOT NULL DEFAULT false,
|
||||||
|
`disabled` boolean NOT NULL DEFAULT false,
|
||||||
|
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
CREATE INDEX `index_invite_level_sort` ON `cd_invite_level` (`sort`);
|
||||||
|
|
||||||
|
CREATE TABLE `cd_invite_user_plan`
|
||||||
|
(
|
||||||
|
`id` bigint PRIMARY KEY AUTO_INCREMENT NOT NULL,
|
||||||
|
`user_id` bigint,
|
||||||
|
`enabled` boolean NOT NULL DEFAULT false,
|
||||||
|
`level_id` bigint NOT NULL DEFAULT 0,
|
||||||
|
`level_locked` boolean NOT NULL DEFAULT false,
|
||||||
|
`agreement_time` bigint NOT NULL DEFAULT 0,
|
||||||
|
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
CREATE UNIQUE INDEX `index_invite_user_plan_user_id` ON `cd_invite_user_plan` (`user_id`);
|
||||||
|
|
||||||
|
INSERT INTO `cd_invite_level` (`name`, `sort`, `min_amount`, `commission_rate`, `is_hidden`, `disabled`)
|
||||||
|
VALUES ('青铜', 10, 0, 10, false, false),
|
||||||
|
('白银', 20, 100000, 15, false, false),
|
||||||
|
('黄金', 30, 500000, 20, false, false),
|
||||||
|
('钻石', 40, 1000000, 30, false, false);
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
ALTER TABLE cd_trade ADD COLUMN rebate_amount bigint NOT NULL DEFAULT 0;
|
||||||
|
ALTER TABLE cd_trade ADD COLUMN third_party_pay_amount bigint NOT NULL DEFAULT 0;
|
||||||
|
|
||||||
|
CREATE TABLE "cd_invite_code"
|
||||||
|
(
|
||||||
|
"id" bigint PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY NOT NULL,
|
||||||
|
"user_id" bigint,
|
||||||
|
"code" varchar(50),
|
||||||
|
"disabled" boolean NOT NULL DEFAULT (false),
|
||||||
|
"create_time" timestamp NOT NULL DEFAULT (CURRENT_TIMESTAMP),
|
||||||
|
"update_time" timestamp NOT NULL DEFAULT (CURRENT_TIMESTAMP)
|
||||||
|
);
|
||||||
|
CREATE UNIQUE INDEX "index_invite_code_user_id" ON "cd_invite_code" ("user_id");
|
||||||
|
CREATE UNIQUE INDEX "index_invite_code_code" ON "cd_invite_code" ("code");
|
||||||
|
|
||||||
|
CREATE TABLE "cd_invite_relation"
|
||||||
|
(
|
||||||
|
"id" bigint PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY NOT NULL,
|
||||||
|
"inviter_user_id" bigint,
|
||||||
|
"invitee_user_id" bigint,
|
||||||
|
"invite_code" varchar(50),
|
||||||
|
"create_time" timestamp NOT NULL DEFAULT (CURRENT_TIMESTAMP),
|
||||||
|
"update_time" timestamp NOT NULL DEFAULT (CURRENT_TIMESTAMP)
|
||||||
|
);
|
||||||
|
CREATE INDEX "index_invite_relation_inviter" ON "cd_invite_relation" ("inviter_user_id");
|
||||||
|
CREATE UNIQUE INDEX "index_invite_relation_invitee" ON "cd_invite_relation" ("invitee_user_id");
|
||||||
|
|
||||||
|
CREATE TABLE "cd_user_wallet"
|
||||||
|
(
|
||||||
|
"id" bigint PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY NOT NULL,
|
||||||
|
"user_id" bigint,
|
||||||
|
"available_amount" bigint NOT NULL DEFAULT 0,
|
||||||
|
"frozen_amount" bigint NOT NULL DEFAULT 0,
|
||||||
|
"total_income_amount" bigint NOT NULL DEFAULT 0,
|
||||||
|
"total_consumed_amount" bigint NOT NULL DEFAULT 0,
|
||||||
|
"total_withdraw_amount" bigint NOT NULL DEFAULT 0,
|
||||||
|
"create_time" timestamp NOT NULL DEFAULT (CURRENT_TIMESTAMP),
|
||||||
|
"update_time" timestamp NOT NULL DEFAULT (CURRENT_TIMESTAMP)
|
||||||
|
);
|
||||||
|
CREATE UNIQUE INDEX "index_user_wallet_user_id" ON "cd_user_wallet" ("user_id");
|
||||||
|
|
||||||
|
CREATE TABLE "cd_invite_commission_log"
|
||||||
|
(
|
||||||
|
"id" bigint PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY NOT NULL,
|
||||||
|
"user_id" bigint,
|
||||||
|
"amount" bigint,
|
||||||
|
"trade_id" bigint,
|
||||||
|
"invitee_user_id" bigint,
|
||||||
|
"consume_amount" bigint NOT NULL DEFAULT 0,
|
||||||
|
"remark" varchar(2048),
|
||||||
|
"create_time" timestamp NOT NULL DEFAULT (CURRENT_TIMESTAMP),
|
||||||
|
"update_time" timestamp NOT NULL DEFAULT (CURRENT_TIMESTAMP)
|
||||||
|
);
|
||||||
|
CREATE INDEX "index_invite_log_user_id" ON "cd_invite_commission_log" ("user_id");
|
||||||
|
|
||||||
|
CREATE TABLE "cd_user_wallet_log"
|
||||||
|
(
|
||||||
|
"id" bigint PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY NOT NULL,
|
||||||
|
"user_id" bigint,
|
||||||
|
"type" varchar(50),
|
||||||
|
"amount" bigint,
|
||||||
|
"balance_after" bigint,
|
||||||
|
"trade_id" bigint,
|
||||||
|
"withdraw_id" bigint,
|
||||||
|
"remark" varchar(2048),
|
||||||
|
"create_time" timestamp NOT NULL DEFAULT (CURRENT_TIMESTAMP),
|
||||||
|
"update_time" timestamp NOT NULL DEFAULT (CURRENT_TIMESTAMP)
|
||||||
|
);
|
||||||
|
CREATE INDEX "index_user_wallet_log_user_id" ON "cd_user_wallet_log" ("user_id");
|
||||||
|
|
||||||
|
CREATE TABLE "cd_user_wallet_withdraw"
|
||||||
|
(
|
||||||
|
"id" bigint PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY NOT NULL,
|
||||||
|
"user_id" bigint,
|
||||||
|
"amount" bigint,
|
||||||
|
"status" varchar(50),
|
||||||
|
"channel" varchar(50),
|
||||||
|
"real_name" varchar(100),
|
||||||
|
"account" varchar(200),
|
||||||
|
"bank_name" varchar(200),
|
||||||
|
"qr_code" varchar(512),
|
||||||
|
"audit_user_id" bigint,
|
||||||
|
"audit_remark" varchar(2048),
|
||||||
|
"audit_time" bigint,
|
||||||
|
"create_time" timestamp NOT NULL DEFAULT (CURRENT_TIMESTAMP),
|
||||||
|
"update_time" timestamp NOT NULL DEFAULT (CURRENT_TIMESTAMP)
|
||||||
|
);
|
||||||
|
CREATE INDEX "index_user_wallet_withdraw_user_id" ON "cd_user_wallet_withdraw" ("user_id");
|
||||||
|
CREATE INDEX "index_user_wallet_withdraw_status" ON "cd_user_wallet_withdraw" ("status");
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
ALTER TABLE cd_invite_commission_log ADD COLUMN level_id bigint NOT NULL DEFAULT 0;
|
||||||
|
ALTER TABLE cd_invite_commission_log ADD COLUMN commission_rate bigint NOT NULL DEFAULT 0;
|
||||||
|
|
||||||
|
CREATE TABLE "cd_invite_level"
|
||||||
|
(
|
||||||
|
"id" bigint PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY NOT NULL,
|
||||||
|
"name" varchar(100),
|
||||||
|
"sort" bigint NOT NULL DEFAULT 0,
|
||||||
|
"min_amount" bigint NOT NULL DEFAULT 0,
|
||||||
|
"commission_rate" bigint NOT NULL DEFAULT 0,
|
||||||
|
"is_hidden" boolean NOT NULL DEFAULT (false),
|
||||||
|
"disabled" boolean NOT NULL DEFAULT (false),
|
||||||
|
"create_time" timestamp NOT NULL DEFAULT (CURRENT_TIMESTAMP),
|
||||||
|
"update_time" timestamp NOT NULL DEFAULT (CURRENT_TIMESTAMP)
|
||||||
|
);
|
||||||
|
CREATE INDEX "index_invite_level_sort" ON "cd_invite_level" ("sort");
|
||||||
|
|
||||||
|
CREATE TABLE "cd_invite_user_plan"
|
||||||
|
(
|
||||||
|
"id" bigint PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY NOT NULL,
|
||||||
|
"user_id" bigint,
|
||||||
|
"enabled" boolean NOT NULL DEFAULT (false),
|
||||||
|
"level_id" bigint NOT NULL DEFAULT 0,
|
||||||
|
"level_locked" boolean NOT NULL DEFAULT (false),
|
||||||
|
"agreement_time" bigint NOT NULL DEFAULT 0,
|
||||||
|
"create_time" timestamp NOT NULL DEFAULT (CURRENT_TIMESTAMP),
|
||||||
|
"update_time" timestamp NOT NULL DEFAULT (CURRENT_TIMESTAMP)
|
||||||
|
);
|
||||||
|
CREATE UNIQUE INDEX "index_invite_user_plan_user_id" ON "cd_invite_user_plan" ("user_id");
|
||||||
|
|
||||||
|
INSERT INTO "cd_invite_level" ("name", "sort", "min_amount", "commission_rate", "is_hidden", "disabled")
|
||||||
|
VALUES ('青铜', 10, 0, 10, false, false),
|
||||||
|
('白银', 20, 100000, 15, false, false),
|
||||||
|
('黄金', 30, 500000, 20, false, false),
|
||||||
|
('钻石', 40, 1000000, 30, false, false);
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
ALTER TABLE cd_trade ADD COLUMN rebate_amount integer NOT NULL DEFAULT 0;
|
||||||
|
ALTER TABLE cd_trade ADD COLUMN third_party_pay_amount integer NOT NULL DEFAULT 0;
|
||||||
|
|
||||||
|
CREATE TABLE "cd_invite_code"
|
||||||
|
(
|
||||||
|
"id" integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||||
|
"user_id" integer,
|
||||||
|
"code" varchar(50),
|
||||||
|
"disabled" boolean NOT NULL DEFAULT (false),
|
||||||
|
"create_time" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP),
|
||||||
|
"update_time" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP)
|
||||||
|
);
|
||||||
|
CREATE UNIQUE INDEX "index_invite_code_user_id" ON "cd_invite_code" ("user_id");
|
||||||
|
CREATE UNIQUE INDEX "index_invite_code_code" ON "cd_invite_code" ("code");
|
||||||
|
|
||||||
|
CREATE TABLE "cd_invite_relation"
|
||||||
|
(
|
||||||
|
"id" integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||||
|
"inviter_user_id" integer,
|
||||||
|
"invitee_user_id" integer,
|
||||||
|
"invite_code" varchar(50),
|
||||||
|
"create_time" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP),
|
||||||
|
"update_time" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP)
|
||||||
|
);
|
||||||
|
CREATE INDEX "index_invite_relation_inviter" ON "cd_invite_relation" ("inviter_user_id");
|
||||||
|
CREATE UNIQUE INDEX "index_invite_relation_invitee" ON "cd_invite_relation" ("invitee_user_id");
|
||||||
|
|
||||||
|
CREATE TABLE "cd_user_wallet"
|
||||||
|
(
|
||||||
|
"id" integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||||
|
"user_id" integer,
|
||||||
|
"available_amount" integer NOT NULL DEFAULT 0,
|
||||||
|
"frozen_amount" integer NOT NULL DEFAULT 0,
|
||||||
|
"total_income_amount" integer NOT NULL DEFAULT 0,
|
||||||
|
"total_consumed_amount" integer NOT NULL DEFAULT 0,
|
||||||
|
"total_withdraw_amount" integer NOT NULL DEFAULT 0,
|
||||||
|
"create_time" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP),
|
||||||
|
"update_time" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP)
|
||||||
|
);
|
||||||
|
CREATE UNIQUE INDEX "index_user_wallet_user_id" ON "cd_user_wallet" ("user_id");
|
||||||
|
|
||||||
|
CREATE TABLE "cd_invite_commission_log"
|
||||||
|
(
|
||||||
|
"id" integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||||
|
"user_id" integer,
|
||||||
|
"amount" integer,
|
||||||
|
"trade_id" integer,
|
||||||
|
"invitee_user_id" integer,
|
||||||
|
"consume_amount" integer NOT NULL DEFAULT 0,
|
||||||
|
"remark" varchar(2048),
|
||||||
|
"create_time" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP),
|
||||||
|
"update_time" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP)
|
||||||
|
);
|
||||||
|
CREATE INDEX "index_invite_log_user_id" ON "cd_invite_commission_log" ("user_id");
|
||||||
|
|
||||||
|
CREATE TABLE "cd_user_wallet_log"
|
||||||
|
(
|
||||||
|
"id" integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||||
|
"user_id" integer,
|
||||||
|
"type" varchar(50),
|
||||||
|
"amount" integer,
|
||||||
|
"balance_after" integer,
|
||||||
|
"trade_id" integer,
|
||||||
|
"withdraw_id" integer,
|
||||||
|
"remark" varchar(2048),
|
||||||
|
"create_time" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP),
|
||||||
|
"update_time" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP)
|
||||||
|
);
|
||||||
|
CREATE INDEX "index_user_wallet_log_user_id" ON "cd_user_wallet_log" ("user_id");
|
||||||
|
|
||||||
|
CREATE TABLE "cd_user_wallet_withdraw"
|
||||||
|
(
|
||||||
|
"id" integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||||
|
"user_id" integer,
|
||||||
|
"amount" integer,
|
||||||
|
"status" varchar(50),
|
||||||
|
"channel" varchar(50),
|
||||||
|
"real_name" varchar(100),
|
||||||
|
"account" varchar(200),
|
||||||
|
"bank_name" varchar(200),
|
||||||
|
"qr_code" varchar(512),
|
||||||
|
"audit_user_id" integer,
|
||||||
|
"audit_remark" varchar(2048),
|
||||||
|
"audit_time" integer,
|
||||||
|
"create_time" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP),
|
||||||
|
"update_time" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP)
|
||||||
|
);
|
||||||
|
CREATE INDEX "index_user_wallet_withdraw_user_id" ON "cd_user_wallet_withdraw" ("user_id");
|
||||||
|
CREATE INDEX "index_user_wallet_withdraw_status" ON "cd_user_wallet_withdraw" ("status");
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
ALTER TABLE cd_invite_commission_log ADD COLUMN level_id integer NOT NULL DEFAULT 0;
|
||||||
|
ALTER TABLE cd_invite_commission_log ADD COLUMN commission_rate integer NOT NULL DEFAULT 0;
|
||||||
|
|
||||||
|
CREATE TABLE "cd_invite_level"
|
||||||
|
(
|
||||||
|
"id" integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||||
|
"name" varchar(100),
|
||||||
|
"sort" integer NOT NULL DEFAULT 0,
|
||||||
|
"min_amount" integer NOT NULL DEFAULT 0,
|
||||||
|
"commission_rate" integer NOT NULL DEFAULT 0,
|
||||||
|
"is_hidden" boolean NOT NULL DEFAULT (false),
|
||||||
|
"disabled" boolean NOT NULL DEFAULT (false),
|
||||||
|
"create_time" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP),
|
||||||
|
"update_time" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP)
|
||||||
|
);
|
||||||
|
CREATE INDEX "index_invite_level_sort" ON "cd_invite_level" ("sort");
|
||||||
|
|
||||||
|
CREATE TABLE "cd_invite_user_plan"
|
||||||
|
(
|
||||||
|
"id" integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||||
|
"user_id" integer,
|
||||||
|
"enabled" boolean NOT NULL DEFAULT (false),
|
||||||
|
"level_id" integer NOT NULL DEFAULT 0,
|
||||||
|
"level_locked" boolean NOT NULL DEFAULT (false),
|
||||||
|
"agreement_time" integer NOT NULL DEFAULT 0,
|
||||||
|
"create_time" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP),
|
||||||
|
"update_time" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP)
|
||||||
|
);
|
||||||
|
CREATE UNIQUE INDEX "index_invite_user_plan_user_id" ON "cd_invite_user_plan" ("user_id");
|
||||||
|
|
||||||
|
INSERT INTO "cd_invite_level" ("name", "sort", "min_amount", "commission_rate", "is_hidden", "disabled")
|
||||||
|
VALUES ('青铜', 10, 0, 10, false, false),
|
||||||
|
('白银', 20, 100000, 15, false, false),
|
||||||
|
('黄金', 30, 500000, 20, false, false),
|
||||||
|
('钻石', 40, 1000000, 30, false, false);
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user