mirror of
https://github.com/certd/certd.git
synced 2026-08-03 20:05:54 +08:00
feat: 新增推广等级激励功能
This commit is contained in:
@@ -117,6 +117,7 @@ Certd 是一个支持私有化部署的 SSL/TLS 证书自动化管理平台。
|
|||||||
- 开发或重构这类页面前,先读取 `.trae/skills/fast-crud-page-dev/SKILL.md`,按仓库内 Fast Crud 页面拆分与验证方式实现。
|
- 开发或重构这类页面前,先读取 `.trae/skills/fast-crud-page-dev/SKILL.md`,按仓库内 Fast Crud 页面拆分与验证方式实现。
|
||||||
- 前端对话框里只做纯确认时可以使用 `Modal.confirm`;只要对话框里有字段输入、表单校验或提交字段,统一使用 `useFormDialog` / `openFormDialog`,不要在 `Modal.confirm` 的 `content` 里手写输入框。
|
- 前端对话框里只做纯确认时可以使用 `Modal.confirm`;只要对话框里有字段输入、表单校验或提交字段,统一使用 `useFormDialog` / `openFormDialog`,不要在 `Modal.confirm` 的 `content` 里手写输入框。
|
||||||
- 页面内嵌 Fast Crud 表格时,要显式给外层容器稳定高度或 `flex: 1; min-height: 0` 的撑满链路;Fast Crud 依赖外部元素高度,不能只依赖表格默认高度。
|
- 页面内嵌 Fast Crud 表格时,要显式给外层容器稳定高度或 `flex: 1; min-height: 0` 的撑满链路;Fast Crud 依赖外部元素高度,不能只依赖表格默认高度。
|
||||||
|
- 后台管理列表里展示或筛选用户字段时,优先参考 `packages/ui/certd-client/src/views/sys/suite/user-suite/crud.tsx` 的 `userId` 字段模式:前端使用 `table-select` + `/sys/authority/user/getSimpleUserByIds` 字典回显和搜索;不要为了展示用户名让后端列表接口额外 `fillSimpleUser` / `userDisplay`,除非该接口本身就是用户端业务列表且已有明确模式。
|
||||||
|
|
||||||
## 流水线与插件模型
|
## 流水线与插件模型
|
||||||
|
|
||||||
@@ -180,6 +181,11 @@ Certd 是一个支持私有化部署的 SSL/TLS 证书自动化管理平台。
|
|||||||
- 使用 `/basic/file/upload` 上传文件后,接口返回的是临时缓存 key。业务保存表单或设置时,后端必须调用 `FileService.saveFile(userId, key, "public" | "private")` 转成永久文件 key 后再入库/入设置;不要直接保存 `tmpfile_key_...`,否则后续回显或下载会失效。
|
- 使用 `/basic/file/upload` 上传文件后,接口返回的是临时缓存 key。业务保存表单或设置时,后端必须调用 `FileService.saveFile(userId, key, "public" | "private")` 转成永久文件 key 后再入库/入设置;不要直接保存 `tmpfile_key_...`,否则后续回显或下载会失效。
|
||||||
- 本仓库代码注释优先使用中文,尤其是解释业务规则、兼容逻辑、协议细节和隐藏风险时;除非文件已有明确英文注释风格或引用外部英文术语,否则不要新增英文说明性注释。
|
- 本仓库代码注释优先使用中文,尤其是解释业务规则、兼容逻辑、协议细节和隐藏风险时;除非文件已有明确英文注释风格或引用外部英文术语,否则不要新增英文说明性注释。
|
||||||
- 代码可读性优先于短写法。遇到包含业务分支的复杂三元表达式、内联对象、链式调用或条件组合时,优先拆成命名清晰的中间变量、独立分支或小函数,让读代码的人能一眼看出业务意图;不要为了少写几行把逻辑压成难读的一坨。
|
- 代码可读性优先于短写法。遇到包含业务分支的复杂三元表达式、内联对象、链式调用或条件组合时,优先拆成命名清晰的中间变量、独立分支或小函数,让读代码的人能一眼看出业务意图;不要为了少写几行把逻辑压成难读的一坨。
|
||||||
|
- 遵守 DRY 原则:同一业务规则、字段转换、权限判断、Repository 选择、事务传播、金额计算等逻辑不要在多个地方复制粘贴。第二次出现时可以先保持清晰,第三次出现前应优先抽成局部 helper、service 方法或已有公共工具;抽象要服务于减少真实重复和降低修改风险,不要为了形式上的“复用”制造过度设计。
|
||||||
|
- 遵守单一职责原则:一个方法只负责一个清晰的业务步骤或技术步骤。流程编排方法可以串联多个步骤,但具体的校验、计算、持久化、状态变更、展示数据组装应尽量拆到命名明确的小方法中;不要让一个方法同时承担查询、校验、计算、写库、格式化返回等过多职责。
|
||||||
|
- 后端方法参数超过 3 个时,尽量改为对象参数传入;需要传入 `manager` / `EntityManager` 做事务传播的方法,必须使用对象参数,不要把 `manager` 作为位置参数藏在参数列表末尾。
|
||||||
|
- 后端 service 层只有存在事务链路传播需求时才定义 `ctx`,不要为了将来可能需要而提前给普通方法加 `ctx`。事务链路方法统一采用 `method(ctx, req)` 形式,`ctx` 放第一位并承载 `manager?: EntityManager` 等横切上下文,业务参数放在 `req` 对象里,例如 `settleCommission({ manager }, { tradeId, userId, amount })`。无事务链路需求的普通查询、纯函数和简单私有方法继续使用明确参数。
|
||||||
|
- service 内部需要根据事务上下文选择 Repository 时,优先使用 `BaseService.getRepo(ctx, entity)`;不要在业务方法里反复写 `ctx.manager?.getRepository(Entity) || this.xxxRepository`。`ctx` 类型统一从 `BaseService` 导出的 `ServiceContext` 复用,不要在每个 service 里重复定义。
|
||||||
|
|
||||||
## 插件开发技能
|
## 插件开发技能
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 92 KiB |
@@ -8,7 +8,7 @@
|
|||||||

|

|
||||||

|

|
||||||
3. 填写域名、端口和密码
|
3. 填写域名、端口和密码
|
||||||

|

|
||||||
|
|
||||||
## QQ邮箱配置
|
## QQ邮箱配置
|
||||||
1. smtp配置
|
1. smtp配置
|
||||||
@@ -20,4 +20,6 @@ smtp端口: 465
|
|||||||
```
|
```
|
||||||
|
|
||||||
2. 获取授权码
|
2. 获取授权码
|
||||||
|
登录qq邮箱,点击账号与安全
|
||||||
|
|
||||||

|

|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { PermissionException, ValidateException } from './exception/index.js';
|
import { PermissionException, ValidateException } from './exception/index.js';
|
||||||
import { FindOneOptions, In, Repository, SelectQueryBuilder } from 'typeorm';
|
import { EntityTarget, FindOneOptions, In, Repository, SelectQueryBuilder } from 'typeorm';
|
||||||
import { Inject } from '@midwayjs/core';
|
import { Inject } from '@midwayjs/core';
|
||||||
import { TypeORMDataSourceManager } from '@midwayjs/typeorm';
|
import { TypeORMDataSourceManager } from '@midwayjs/typeorm';
|
||||||
import { EntityManager } from 'typeorm/entity-manager/EntityManager.js';
|
import { EntityManager } from 'typeorm/entity-manager/EntityManager.js';
|
||||||
@@ -20,6 +20,10 @@ export type ListReq<T = any> = {
|
|||||||
select?: any;
|
select?: any;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ServiceContext = {
|
||||||
|
manager?: EntityManager;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 服务基类
|
* 服务基类
|
||||||
*/
|
*/
|
||||||
@@ -34,6 +38,14 @@ export abstract class BaseService<T> {
|
|||||||
return await dataSource.transaction(callback as any);
|
return await dataSource.transaction(callback as any);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected getRepo<E>(ctx: ServiceContext, entity: EntityTarget<E>): Repository<E> {
|
||||||
|
if (ctx.manager) {
|
||||||
|
return ctx.manager.getRepository(entity);
|
||||||
|
}
|
||||||
|
const dataSource = this.dataSourceManager.getDataSource('default');
|
||||||
|
return dataSource.getRepository(entity);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获得单个ID
|
* 获得单个ID
|
||||||
* @param id ID
|
* @param id ID
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
import { useSettingStore } from "/@/store/settings";
|
import { useSettingStore } from "/@/store/settings";
|
||||||
|
|
||||||
|
function isInviteLevelEnabled() {
|
||||||
|
const settingStore = useSettingStore();
|
||||||
|
const levelEnabled = settingStore.inviteSetting?.levelEnabled;
|
||||||
|
return settingStore.isComm && levelEnabled === true;
|
||||||
|
}
|
||||||
|
|
||||||
export const sysResources = [
|
export const sysResources = [
|
||||||
{
|
{
|
||||||
title: "certd.sysResources.sysRoot",
|
title: "certd.sysResources.sysRoot",
|
||||||
@@ -309,8 +315,7 @@ export const sysResources = [
|
|||||||
component: "/sys/suite/invite/level.vue",
|
component: "/sys/suite/invite/level.vue",
|
||||||
meta: {
|
meta: {
|
||||||
show: () => {
|
show: () => {
|
||||||
const settingStore = useSettingStore();
|
return isInviteLevelEnabled();
|
||||||
return settingStore.isComm;
|
|
||||||
},
|
},
|
||||||
icon: "ion:ribbon-outline",
|
icon: "ion:ribbon-outline",
|
||||||
permission: "sys:settings:edit",
|
permission: "sys:settings:edit",
|
||||||
@@ -325,8 +330,7 @@ export const sysResources = [
|
|||||||
component: "/sys/suite/invite/user-level.vue",
|
component: "/sys/suite/invite/user-level.vue",
|
||||||
meta: {
|
meta: {
|
||||||
show: () => {
|
show: () => {
|
||||||
const settingStore = useSettingStore();
|
return isInviteLevelEnabled();
|
||||||
return settingStore.isComm;
|
|
||||||
},
|
},
|
||||||
icon: "ion:people-outline",
|
icon: "ion:people-outline",
|
||||||
permission: "sys:settings:edit",
|
permission: "sys:settings:edit",
|
||||||
|
|||||||
@@ -99,6 +99,8 @@ export type SuiteSetting = {
|
|||||||
};
|
};
|
||||||
export type InviteSetting = {
|
export type InviteSetting = {
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
|
levelEnabled?: boolean;
|
||||||
|
fixedCommissionRate?: number;
|
||||||
};
|
};
|
||||||
export type SysPrivateSetting = {
|
export type SysPrivateSetting = {
|
||||||
httpProxy?: string;
|
httpProxy?: string;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { defineStore } from "pinia";
|
import { defineStore } from "pinia";
|
||||||
import { notification } from "ant-design-vue";
|
import { notification } from "ant-design-vue";
|
||||||
import * as basicApi from "./api.basic";
|
import * as basicApi from "./api.basic";
|
||||||
import { AppInfo, HeaderMenus, PlusInfo, SiteEnv, SiteInfo, SuiteSetting, SysInstallInfo, SysPublicSetting } from "./api.basic";
|
import { AppInfo, HeaderMenus, InviteSetting, PlusInfo, SiteEnv, SiteInfo, SuiteSetting, SysInstallInfo, SysPublicSetting } from "./api.basic";
|
||||||
import { useUserStore } from "../user";
|
import { useUserStore } from "../user";
|
||||||
import { mitter } from "/@/utils/util.mitt";
|
import { mitter } from "/@/utils/util.mitt";
|
||||||
import { env } from "/@/utils/util.env";
|
import { env } from "/@/utils/util.env";
|
||||||
@@ -30,9 +30,7 @@ export interface SettingState {
|
|||||||
headerMenus?: HeaderMenus;
|
headerMenus?: HeaderMenus;
|
||||||
inited?: boolean;
|
inited?: boolean;
|
||||||
suiteSetting?: SuiteSetting;
|
suiteSetting?: SuiteSetting;
|
||||||
inviteSetting?: {
|
inviteSetting?: InviteSetting;
|
||||||
enabled?: boolean;
|
|
||||||
};
|
|
||||||
app: {
|
app: {
|
||||||
version?: string;
|
version?: string;
|
||||||
time?: number;
|
time?: number;
|
||||||
@@ -105,7 +103,7 @@ export const useSettingStore = defineStore({
|
|||||||
menus: [],
|
menus: [],
|
||||||
},
|
},
|
||||||
suiteSetting: { enabled: false },
|
suiteSetting: { enabled: false },
|
||||||
inviteSetting: { enabled: false },
|
inviteSetting: { enabled: false, levelEnabled: false, fixedCommissionRate: 10 },
|
||||||
inited: false,
|
inited: false,
|
||||||
app: {
|
app: {
|
||||||
version: "",
|
version: "",
|
||||||
|
|||||||
@@ -45,7 +45,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="invite-info-row invite-highlight-row level-highlight-row" @click="levelDialogOpen = true">
|
<div v-if="inviteInfo.levelEnabled" class="invite-info-row invite-highlight-row level-highlight-row" @click="levelDialogOpen = true">
|
||||||
<div class="info-icon level-info-icon">
|
<div class="info-icon level-info-icon">
|
||||||
<fs-icon v-if="inviteInfo.currentLevel" :icon="levelIcon(inviteInfo.currentLevel)" />
|
<fs-icon v-if="inviteInfo.currentLevel" :icon="levelIcon(inviteInfo.currentLevel)" />
|
||||||
<fs-icon v-else icon="ion:ribbon-outline" />
|
<fs-icon v-else icon="ion:ribbon-outline" />
|
||||||
@@ -61,6 +61,19 @@
|
|||||||
</div>
|
</div>
|
||||||
<fs-icon class="level-open-icon" icon="ion:chevron-forward-outline" />
|
<fs-icon class="level-open-icon" icon="ion:chevron-forward-outline" />
|
||||||
</div>
|
</div>
|
||||||
|
<div v-else class="invite-info-row invite-highlight-row">
|
||||||
|
<div class="info-icon level-info-icon">
|
||||||
|
<fs-icon icon="ion:cash-outline" />
|
||||||
|
</div>
|
||||||
|
<span class="info-label">返佣比例</span>
|
||||||
|
<div class="info-content level-info-content">
|
||||||
|
<span class="current-level-rate fixed-rate-tag">
|
||||||
|
<span class="current-level-rate-label">比例</span>
|
||||||
|
<span class="current-level-rate-value">{{ inviteInfo.fixedCommissionRate || 0 }}%</span>
|
||||||
|
</span>
|
||||||
|
<span class="level-rate-desc">好友付费后按此比例计算佣金</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<a-tabs v-model:active-key="activeTab" class="invite-tabs" @change="handleTabChange">
|
<a-tabs v-model:active-key="activeTab" class="invite-tabs" @change="handleTabChange">
|
||||||
@@ -80,7 +93,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<a-empty v-else-if="loaded" description="激励计划未开启" />
|
<a-empty v-else-if="loaded" description="激励计划未开启" />
|
||||||
|
|
||||||
<a-modal v-model:open="levelDialogOpen" title="推广等级" width="820px" wrap-class-name="invite-level-modal" :footer="null">
|
<a-modal v-if="inviteInfo.levelEnabled" v-model:open="levelDialogOpen" title="推广等级" width="820px" wrap-class-name="invite-level-modal" :footer="null">
|
||||||
<div class="level-modal-subtitle">推广越多,等级越高,返佣比例越高</div>
|
<div class="level-modal-subtitle">推广越多,等级越高,返佣比例越高</div>
|
||||||
<div class="level-progress-box">
|
<div class="level-progress-box">
|
||||||
<div>
|
<div>
|
||||||
@@ -88,13 +101,13 @@
|
|||||||
<div class="level-progress-value">¥ {{ amountToYuan(inviteInfo.summary.promotionAmount) }}</div>
|
<div class="level-progress-value">¥ {{ amountToYuan(inviteInfo.summary.promotionAmount) }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="level-progress-desc">
|
<div class="level-progress-desc">
|
||||||
<template v-if="inviteInfo.nextLevel">距离下一等级「{{ inviteInfo.nextLevel.name }}」还差 {{ amountToYuan(inviteInfo.nextLevel.gapAmount) }} 元</template>
|
<template v-if="inviteInfo.currentLevel?.levelType === 'exclusive'">当前为专属等级,不参与自动升级</template>
|
||||||
<template v-else-if="inviteInfo.currentLevel?.levelType === 'exclusive'">当前为专属等级,不参与自动升级</template>
|
<template v-else-if="inviteInfo.nextLevel">距离下一等级「{{ inviteInfo.nextLevel.name }}」还差 {{ amountToYuan(inviteInfo.nextLevel.gapAmount) }} 元</template>
|
||||||
<template v-else>已达到当前可自动升级的最高等级</template>
|
<template v-else>已达到当前可自动升级的最高等级</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="level-card-grid modal-level-grid">
|
<div class="level-card-grid modal-level-grid">
|
||||||
<div v-for="level in visibleLevels" :key="level.id" class="level-card" :class="{ active: level.id === inviteInfo.currentLevel?.id }">
|
<div v-for="level in visibleLevels" :key="level.id" class="level-card" :class="{ active: level.id === inviteInfo.currentLevel?.id, exclusive: level.levelType === 'exclusive' }">
|
||||||
<div class="level-name">
|
<div class="level-name">
|
||||||
<span class="level-medal">
|
<span class="level-medal">
|
||||||
<fs-icon :icon="levelIcon(level)" />
|
<fs-icon :icon="levelIcon(level)" />
|
||||||
@@ -110,8 +123,6 @@
|
|||||||
<div v-else-if="level.id === inviteInfo.nextLevel?.id" class="next-gap">还差 {{ amountToYuan(inviteInfo.nextLevel.gapAmount) }}</div>
|
<div v-else-if="level.id === inviteInfo.nextLevel?.id" class="next-gap">还差 {{ amountToYuan(inviteInfo.nextLevel.gapAmount) }}</div>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
|
||||||
</a-modal>
|
</a-modal>
|
||||||
|
|
||||||
<a-modal
|
<a-modal
|
||||||
@@ -169,6 +180,8 @@ const inviteInfo = reactive<any>({
|
|||||||
agreementContent: "",
|
agreementContent: "",
|
||||||
summary: { totalIncomeAmount: 0, monthIncomeAmount: 0, promotionAmount: 0, inviteeCount: 0 },
|
summary: { totalIncomeAmount: 0, monthIncomeAmount: 0, promotionAmount: 0, inviteeCount: 0 },
|
||||||
wallet: { availableAmount: 0 },
|
wallet: { availableAmount: 0 },
|
||||||
|
levelEnabled: false,
|
||||||
|
fixedCommissionRate: 10,
|
||||||
currentLevel: null,
|
currentLevel: null,
|
||||||
nextLevel: null,
|
nextLevel: null,
|
||||||
levelList: [],
|
levelList: [],
|
||||||
@@ -213,7 +226,12 @@ const summaryCards = computed(() => [
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
const visibleLevels = computed(() => {
|
const visibleLevels = computed(() => {
|
||||||
return (inviteInfo.levelList || []).filter((level: any) => !level.disabled);
|
return (inviteInfo.levelList || []).filter((level: any) => {
|
||||||
|
if (level.disabled) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return level.levelType !== "exclusive" || level.id === inviteInfo.currentLevel?.id;
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const agreementText = computed(() => inviteInfo.agreementContent?.trim() || defaultAgreementContent);
|
const agreementText = computed(() => inviteInfo.agreementContent?.trim() || defaultAgreementContent);
|
||||||
@@ -546,6 +564,10 @@ onActivated(async () => {
|
|||||||
line-height: 24px;
|
line-height: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.fixed-rate-tag {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.level-info-content {
|
.level-info-content {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -666,6 +688,18 @@ onActivated(async () => {
|
|||||||
background: linear-gradient(145deg, rgba(236, 244, 255, 0.92), rgba(248, 250, 252, 0.88)), hsl(var(--primary) / 10%);
|
background: linear-gradient(145deg, rgba(236, 244, 255, 0.92), rgba(248, 250, 252, 0.88)), hsl(var(--primary) / 10%);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.level-card.exclusive {
|
||||||
|
border-color: rgba(147, 51, 234, 0.36);
|
||||||
|
background: linear-gradient(145deg, rgba(255, 255, 255, 0.94), rgba(250, 245, 255, 0.86)), linear-gradient(135deg, rgba(147, 51, 234, 0.24), rgba(245, 158, 11, 0.2));
|
||||||
|
box-shadow: 0 10px 28px rgba(88, 28, 135, 0.14);
|
||||||
|
}
|
||||||
|
|
||||||
|
.level-card.exclusive.active {
|
||||||
|
border-color: rgba(147, 51, 234, 0.64);
|
||||||
|
background: linear-gradient(145deg, rgba(250, 245, 255, 0.96), rgba(255, 251, 235, 0.9)), linear-gradient(135deg, rgba(147, 51, 234, 0.28), rgba(245, 158, 11, 0.24));
|
||||||
|
box-shadow: 0 14px 34px rgba(88, 28, 135, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
.level-name {
|
.level-name {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -47,3 +47,11 @@ export async function ApproveWithdraw(id: number, remark?: string) {
|
|||||||
export async function RejectWithdraw(id: number, remark: string) {
|
export async function RejectWithdraw(id: number, remark: string) {
|
||||||
return await request({ url: "/sys/wallet/withdraw/reject", method: "post", data: { id, remark } });
|
return await request({ url: "/sys/wallet/withdraw/reject", method: "post", data: { id, remark } });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function GetSimpleUserByIds(ids: number[]) {
|
||||||
|
return await request({
|
||||||
|
url: "/sys/authority/user/getSimpleUserByIds",
|
||||||
|
method: "post",
|
||||||
|
data: { ids },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { notification } from "ant-design-vue";
|
|||||||
import * as api from "./api";
|
import * as api from "./api";
|
||||||
import PriceInput from "/@/views/sys/suite/product/price-input.vue";
|
import PriceInput from "/@/views/sys/suite/product/price-input.vue";
|
||||||
import { useFormDialog } from "/@/use/use-dialog";
|
import { useFormDialog } from "/@/use/use-dialog";
|
||||||
|
import createCrudOptionsUser from "/@/views/sys/authority/user/crud";
|
||||||
|
|
||||||
export default function ({ crudExpose }: CreateCrudOptionsProps): CreateCrudOptionsRet {
|
export default function ({ crudExpose }: CreateCrudOptionsProps): CreateCrudOptionsRet {
|
||||||
const { openFormDialog } = useFormDialog();
|
const { openFormDialog } = useFormDialog();
|
||||||
@@ -11,6 +12,13 @@ export default function ({ crudExpose }: CreateCrudOptionsProps): CreateCrudOpti
|
|||||||
value: "id",
|
value: "id",
|
||||||
label: "name",
|
label: "name",
|
||||||
});
|
});
|
||||||
|
const userDict = dict({
|
||||||
|
async getNodesByValues(ids: number[]) {
|
||||||
|
return await api.GetSimpleUserByIds(ids);
|
||||||
|
},
|
||||||
|
value: "id",
|
||||||
|
label: "nickName",
|
||||||
|
});
|
||||||
|
|
||||||
const pageRequest = async (query: UserPageQuery): Promise<UserPageRes> => {
|
const pageRequest = async (query: UserPageQuery): Promise<UserPageRes> => {
|
||||||
return await api.GetUserLevels(query);
|
return await api.GetUserLevels(query);
|
||||||
@@ -65,15 +73,20 @@ export default function ({ crudExpose }: CreateCrudOptionsProps): CreateCrudOpti
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
columns: {
|
columns: {
|
||||||
userId: { title: "用户ID", type: "number", search: { show: true }, column: { width: 100 } },
|
userId: {
|
||||||
username: {
|
title: "用户",
|
||||||
title: "用户名",
|
type: "table-select",
|
||||||
type: "text",
|
|
||||||
search: { show: true },
|
search: { show: true },
|
||||||
column: {
|
dict: userDict,
|
||||||
width: 180,
|
form: {
|
||||||
cellRender({ row }) {
|
show: false,
|
||||||
return row.simpleUser?.displayName || row.userDisplay || row.username || row.userId;
|
component: {
|
||||||
|
crossPage: true,
|
||||||
|
multiple: false,
|
||||||
|
select: {
|
||||||
|
placeholder: "点击选择用户",
|
||||||
|
},
|
||||||
|
createCrudOptions: createCrudOptionsUser,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import * as api from "./api";
|
|||||||
import PriceInput from "/@/views/sys/suite/product/price-input.vue";
|
import PriceInput from "/@/views/sys/suite/product/price-input.vue";
|
||||||
import { useFormDialog } from "/@/use/use-dialog";
|
import { useFormDialog } from "/@/use/use-dialog";
|
||||||
import { useUserStore } from "/@/store/user";
|
import { useUserStore } from "/@/store/user";
|
||||||
|
import createCrudOptionsUser from "/@/views/sys/authority/user/crud";
|
||||||
|
|
||||||
function buildPrivateFileUrl(key: string) {
|
function buildPrivateFileUrl(key: string) {
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
@@ -12,6 +13,13 @@ function buildPrivateFileUrl(key: string) {
|
|||||||
|
|
||||||
export default function ({ crudExpose }: CreateCrudOptionsProps): CreateCrudOptionsRet {
|
export default function ({ crudExpose }: CreateCrudOptionsProps): CreateCrudOptionsRet {
|
||||||
const { openFormDialog } = useFormDialog();
|
const { openFormDialog } = useFormDialog();
|
||||||
|
const userDict = dict({
|
||||||
|
async getNodesByValues(ids: number[]) {
|
||||||
|
return await api.GetSimpleUserByIds(ids);
|
||||||
|
},
|
||||||
|
value: "id",
|
||||||
|
label: "nickName",
|
||||||
|
});
|
||||||
|
|
||||||
const pageRequest = async (query: UserPageQuery): Promise<UserPageRes> => {
|
const pageRequest = async (query: UserPageQuery): Promise<UserPageRes> => {
|
||||||
return await api.GetWithdraws(query);
|
return await api.GetWithdraws(query);
|
||||||
@@ -25,7 +33,7 @@ export default function ({ crudExpose }: CreateCrudOptionsProps): CreateCrudOpti
|
|||||||
<span class={"text-red-500"}>{row.amount / 100} 元</span>
|
<span class={"text-red-500"}>{row.amount / 100} 元</span>
|
||||||
</a-descriptions-item>
|
</a-descriptions-item>
|
||||||
<a-descriptions-item label="渠道类型">{row.channel === "bank" ? "银行卡" : "支付宝"}</a-descriptions-item>
|
<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="用户ID">{row.userId}</a-descriptions-item>
|
||||||
<a-descriptions-item label="账号">{row.account || "-"}</a-descriptions-item>
|
<a-descriptions-item label="账号">{row.account || "-"}</a-descriptions-item>
|
||||||
{isBank ? <a-descriptions-item label="开户行名称">{row.bankName || "-"}</a-descriptions-item> : null}
|
{isBank ? <a-descriptions-item label="开户行名称">{row.bankName || "-"}</a-descriptions-item> : null}
|
||||||
{!isBank ? <a-descriptions-item label="收款二维码">{row.qrCode ? <a-image src={buildPrivateFileUrl(row.qrCode)} width={160} /> : <span>-</span>}</a-descriptions-item> : null}
|
{!isBank ? <a-descriptions-item label="收款二维码">{row.qrCode ? <a-image src={buildPrivateFileUrl(row.qrCode)} width={160} /> : <span>-</span>}</a-descriptions-item> : null}
|
||||||
@@ -122,7 +130,24 @@ export default function ({ crudExpose }: CreateCrudOptionsProps): CreateCrudOpti
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
columns: {
|
columns: {
|
||||||
userId: { title: "用户ID", type: "number", search: { show: true }, column: { width: 100 } },
|
createTime: { title: "申请时间", type: "datetime", column: { width: 180 } },
|
||||||
|
userId: {
|
||||||
|
title: "用户",
|
||||||
|
type: "table-select",
|
||||||
|
search: { show: true },
|
||||||
|
dict: userDict,
|
||||||
|
form: {
|
||||||
|
show: false,
|
||||||
|
component: {
|
||||||
|
crossPage: true,
|
||||||
|
multiple: false,
|
||||||
|
select: {
|
||||||
|
placeholder: "点击选择用户",
|
||||||
|
},
|
||||||
|
createCrudOptions: createCrudOptionsUser,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
amount: {
|
amount: {
|
||||||
title: "金额",
|
title: "金额",
|
||||||
type: "number",
|
type: "number",
|
||||||
@@ -131,7 +156,6 @@ export default function ({ crudExpose }: CreateCrudOptionsProps): CreateCrudOpti
|
|||||||
component: { name: PriceInput, vModel: "modelValue", edit: false },
|
component: { name: PriceInput, vModel: "modelValue", edit: false },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
userDisplay: { title: "用户名", type: "text", search: { show: true }, column: { width: 140 } },
|
|
||||||
status: {
|
status: {
|
||||||
title: "状态",
|
title: "状态",
|
||||||
type: "dict-select",
|
type: "dict-select",
|
||||||
@@ -173,7 +197,6 @@ export default function ({ crudExpose }: CreateCrudOptionsProps): CreateCrudOpti
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
auditRemark: { title: "审核备注", type: "text", column: { minWidth: 180 } },
|
auditRemark: { title: "审核备注", type: "text", column: { minWidth: 180 } },
|
||||||
createTime: { title: "申请时间", type: "datetime", column: { width: 180 } },
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
<fs-crud ref="crudRef" v-bind="crudBinding">
|
<fs-crud ref="crudRef" v-bind="crudBinding">
|
||||||
<a-empty v-if="levelList.length === 0" class="level-empty" />
|
<a-empty v-if="levelList.length === 0" class="level-empty" />
|
||||||
<div v-else class="level-card-grid">
|
<div v-else class="level-card-grid">
|
||||||
<div v-for="(item, index) of levelList" :key="item.id" class="level-card" :class="{ disabled: item.disabled }">
|
<div v-for="(item, index) of levelList" :key="item.id" class="level-card" :class="{ disabled: item.disabled, exclusive: item.levelType === 'exclusive' }">
|
||||||
<div class="level-card-actions">
|
<div class="level-card-actions">
|
||||||
<a-tooltip title="编辑">
|
<a-tooltip title="编辑">
|
||||||
<a-button type="text" size="small" @click="openEdit({ index, row: item })">
|
<a-button type="text" size="small" @click="openEdit({ index, row: item })">
|
||||||
@@ -153,6 +153,17 @@ onActivated(() => {
|
|||||||
transform: translateY(-3px);
|
transform: translateY(-3px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.level-card.exclusive {
|
||||||
|
border-color: rgba(147, 51, 234, 0.34);
|
||||||
|
background: linear-gradient(145deg, rgba(255, 255, 255, 0.92), rgba(250, 245, 255, 0.86)), linear-gradient(135deg, rgba(147, 51, 234, 0.24), rgba(245, 158, 11, 0.2));
|
||||||
|
box-shadow: 0 12px 32px rgba(88, 28, 135, 0.14);
|
||||||
|
}
|
||||||
|
|
||||||
|
.level-card.exclusive:hover {
|
||||||
|
border-color: rgba(147, 51, 234, 0.52);
|
||||||
|
box-shadow: 0 18px 42px rgba(88, 28, 135, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
.level-card.disabled {
|
.level-card.disabled {
|
||||||
opacity: 0.66;
|
opacity: 0.66;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,15 @@
|
|||||||
<a-form-item label="开启激励计划" name="enabled">
|
<a-form-item label="开启激励计划" name="enabled">
|
||||||
<a-switch v-model:checked="settings.enabled" />
|
<a-switch v-model:checked="settings.enabled" />
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
|
<a-form-item label="启用推广等级" name="levelEnabled">
|
||||||
|
<a-space>
|
||||||
|
<a-switch v-model:checked="settings.levelEnabled" />
|
||||||
|
<a-button v-if="levelEnabled" type="link" @click="gotoInviteLevel">推广等级设置</a-button>
|
||||||
|
</a-space>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item v-if="!settings.levelEnabled" label="佣金比例" name="fixedCommissionRate">
|
||||||
|
<a-input-number v-model:value="settings.fixedCommissionRate" :min="0" :max="100" addon-after="%" />
|
||||||
|
</a-form-item>
|
||||||
<a-form-item label="最低提现金额" name="minWithdrawAmountYuan">
|
<a-form-item label="最低提现金额" name="minWithdrawAmountYuan">
|
||||||
<a-input-number v-model:value="settings.minWithdrawAmountYuan" :min="0" addon-after="元" />
|
<a-input-number v-model:value="settings.minWithdrawAmountYuan" :min="0" addon-after="元" />
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
@@ -38,13 +47,19 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, onMounted, reactive } from "vue";
|
import { computed, onMounted, reactive } from "vue";
|
||||||
import { notification } from "ant-design-vue";
|
import { notification } from "ant-design-vue";
|
||||||
|
import { useRouter } from "vue-router";
|
||||||
import * as api from "./api";
|
import * as api from "./api";
|
||||||
import { util } from "/@/utils";
|
import { util } from "/@/utils";
|
||||||
import { useSettingStore } from "/@/store/settings";
|
import { useSettingStore } from "/@/store/settings";
|
||||||
import { useUserStore } from "/@/store/user";
|
import { useUserStore } from "/@/store/user";
|
||||||
|
import { useAccessStore } from "/@/vben/stores";
|
||||||
|
import { frameworkRoutes } from "/@/router/resolve";
|
||||||
|
import { generateMenus } from "/@/vben/utils";
|
||||||
|
import utilPermission from "/@/plugin/permission/util.permission";
|
||||||
|
|
||||||
defineOptions({ name: "SysInviteCommissionSetting" });
|
defineOptions({ name: "SysInviteCommissionSetting" });
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
const defaultAgreement = "<p>请遵守平台推广规则,不得通过虚假注册、刷单、恶意诱导等方式获取收益。平台有权对异常推广行为进行核查,并根据实际情况暂停结算或关闭激励计划资格。</p>";
|
const defaultAgreement = "<p>请遵守平台推广规则,不得通过虚假注册、刷单、恶意诱导等方式获取收益。平台有权对异常推广行为进行核查,并根据实际情况暂停结算或关闭激励计划资格。</p>";
|
||||||
const defaultWithdrawBanks = [
|
const defaultWithdrawBanks = [
|
||||||
"中国工商银行",
|
"中国工商银行",
|
||||||
@@ -63,13 +78,22 @@ const defaultWithdrawBanks = [
|
|||||||
"兴业银行",
|
"兴业银行",
|
||||||
"浦发银行",
|
"浦发银行",
|
||||||
];
|
];
|
||||||
const settings = reactive<any>({ enabled: false, agreementContent: "", minWithdrawAmountYuan: 0, withdrawChannels: ["alipay", "bank"], withdrawBanks: defaultWithdrawBanks });
|
const settings = reactive<any>({
|
||||||
|
enabled: false,
|
||||||
|
levelEnabled: false,
|
||||||
|
fixedCommissionRate: 10,
|
||||||
|
agreementContent: "",
|
||||||
|
minWithdrawAmountYuan: 0,
|
||||||
|
withdrawChannels: ["alipay", "bank"],
|
||||||
|
withdrawBanks: defaultWithdrawBanks,
|
||||||
|
});
|
||||||
const withdrawChannelOptions = [
|
const withdrawChannelOptions = [
|
||||||
{ label: "支付宝", value: "alipay" },
|
{ label: "支付宝", value: "alipay" },
|
||||||
{ label: "银行卡", value: "bank" },
|
{ label: "银行卡", value: "bank" },
|
||||||
];
|
];
|
||||||
const bankOptions = computed(() => defaultWithdrawBanks.map(item => ({ label: item, value: item })));
|
const bankOptions = computed(() => defaultWithdrawBanks.map(item => ({ label: item, value: item })));
|
||||||
const bankChannelEnabled = computed(() => settings.withdrawChannels?.includes("bank"));
|
const bankChannelEnabled = computed(() => settings.withdrawChannels?.includes("bank"));
|
||||||
|
const levelEnabled = computed(() => settings.levelEnabled === true);
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
const editorUploader = {
|
const editorUploader = {
|
||||||
type: "form",
|
type: "form",
|
||||||
@@ -89,6 +113,8 @@ const editorUploader = {
|
|||||||
async function loadSettings() {
|
async function loadSettings() {
|
||||||
const data: any = await api.GetSettings();
|
const data: any = await api.GetSettings();
|
||||||
settings.enabled = !!data?.enabled;
|
settings.enabled = !!data?.enabled;
|
||||||
|
settings.levelEnabled = data?.levelEnabled === true;
|
||||||
|
settings.fixedCommissionRate = Number(data?.fixedCommissionRate) || 10;
|
||||||
settings.agreementContent = data?.agreementContent || defaultAgreement;
|
settings.agreementContent = data?.agreementContent || defaultAgreement;
|
||||||
settings.minWithdrawAmountYuan = util.amount.toYuan(data?.minWithdrawAmount || 0);
|
settings.minWithdrawAmountYuan = util.amount.toYuan(data?.minWithdrawAmount || 0);
|
||||||
settings.withdrawChannels = data?.withdrawChannels?.length ? data.withdrawChannels : ["alipay", "bank"];
|
settings.withdrawChannels = data?.withdrawChannels?.length ? data.withdrawChannels : ["alipay", "bank"];
|
||||||
@@ -101,17 +127,57 @@ async function saveSettings() {
|
|||||||
notification.warning({ message: "请填写推广协议内容" });
|
notification.warning({ message: "请填写推广协议内容" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (!levelEnabled.value && (!settings.fixedCommissionRate || settings.fixedCommissionRate <= 0)) {
|
||||||
|
notification.warning({ message: "关闭推广等级时,请设置佣金比例" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
await api.SaveSettings({
|
await api.SaveSettings({
|
||||||
enabled: settings.enabled,
|
enabled: settings.enabled,
|
||||||
|
levelEnabled: levelEnabled.value,
|
||||||
|
fixedCommissionRate: settings.fixedCommissionRate || 0,
|
||||||
agreementContent: settings.agreementContent || "",
|
agreementContent: settings.agreementContent || "",
|
||||||
minWithdrawAmount: util.amount.toCent(settings.minWithdrawAmountYuan || 0),
|
minWithdrawAmount: util.amount.toCent(settings.minWithdrawAmountYuan || 0),
|
||||||
withdrawChannels: settings.withdrawChannels || [],
|
withdrawChannels: settings.withdrawChannels || [],
|
||||||
withdrawBanks,
|
withdrawBanks,
|
||||||
});
|
});
|
||||||
await useSettingStore().loadSysSettings();
|
await useSettingStore().loadSysSettings();
|
||||||
|
await refreshMenus();
|
||||||
notification.success({ message: "保存成功" });
|
notification.success({ message: "保存成功" });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function gotoInviteLevel() {
|
||||||
|
router.push({ path: "/sys/suite/invite/level" });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refreshMenus() {
|
||||||
|
const accessStore = useAccessStore();
|
||||||
|
const settingStore = useSettingStore();
|
||||||
|
let allMenus = await generateMenus(frameworkRoutes[0].children, router);
|
||||||
|
allMenus = allMenus.concat(settingStore.getHeaderMenus);
|
||||||
|
allMenus = buildAccessedMenus(allMenus);
|
||||||
|
accessStore.setAccessMenus(allMenus);
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildAccessedMenus(menus: any) {
|
||||||
|
if (menus == null) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const list: any = [];
|
||||||
|
for (const sub of menus) {
|
||||||
|
if (sub.meta?.permission != null && !utilPermission.hasPermissions(sub.meta.permission)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const item: any = {
|
||||||
|
...sub,
|
||||||
|
};
|
||||||
|
list.push(item);
|
||||||
|
if (sub.children && sub.children.length > 0) {
|
||||||
|
item.children = buildAccessedMenus(sub.children);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
function isBlankAgreement(content: string) {
|
function isBlankAgreement(content: string) {
|
||||||
const text = `${content || ""}`
|
const text = `${content || ""}`
|
||||||
.replace(/<[^>]*>/g, "")
|
.replace(/<[^>]*>/g, "")
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ CREATE TABLE `cd_invite_user_plan`
|
|||||||
`enabled` boolean NOT NULL DEFAULT false,
|
`enabled` boolean NOT NULL DEFAULT false,
|
||||||
`level_id` bigint NOT NULL DEFAULT 0,
|
`level_id` bigint NOT NULL DEFAULT 0,
|
||||||
`level_locked` boolean NOT NULL DEFAULT false,
|
`level_locked` boolean NOT NULL DEFAULT false,
|
||||||
|
`promotion_amount` bigint NOT NULL DEFAULT 0,
|
||||||
`agreement_time` bigint NOT NULL DEFAULT 0,
|
`agreement_time` bigint NOT NULL DEFAULT 0,
|
||||||
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP
|
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
@@ -31,6 +32,6 @@ CREATE UNIQUE INDEX `index_invite_user_plan_user_id` ON `cd_invite_user_plan` (`
|
|||||||
|
|
||||||
INSERT INTO `cd_invite_level` (`name`, `icon`, `sort`, `min_amount`, `commission_rate`, `level_type`, `disabled`)
|
INSERT INTO `cd_invite_level` (`name`, `icon`, `sort`, `min_amount`, `commission_rate`, `level_type`, `disabled`)
|
||||||
VALUES ('青铜', 'fluent-emoji-flat:2nd-place-medal', 10, 0, 10, 'normal', false),
|
VALUES ('青铜', 'fluent-emoji-flat:2nd-place-medal', 10, 0, 10, 'normal', false),
|
||||||
('白银', 'fluent-emoji-flat:1st-place-medal', 20, 100000, 15, 'normal', false),
|
('白银', 'fluent-emoji-flat:1st-place-medal', 20, 10000, 15, 'normal', false),
|
||||||
('黄金', 'fluent-emoji-flat:3rd-place-medal', 30, 500000, 20, 'normal', false),
|
('黄金', 'fluent-emoji-flat:3rd-place-medal', 30, 50000, 20, 'normal', false),
|
||||||
('钻石', 'streamline-color:diamond-2', 40, 1000000, 30, 'normal', false);
|
('钻石', 'streamline-color:diamond-2', 40, 300000, 30, 'normal', false);
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ CREATE TABLE "cd_invite_user_plan"
|
|||||||
"enabled" boolean NOT NULL DEFAULT (false),
|
"enabled" boolean NOT NULL DEFAULT (false),
|
||||||
"level_id" bigint NOT NULL DEFAULT 0,
|
"level_id" bigint NOT NULL DEFAULT 0,
|
||||||
"level_locked" boolean NOT NULL DEFAULT (false),
|
"level_locked" boolean NOT NULL DEFAULT (false),
|
||||||
|
"promotion_amount" bigint NOT NULL DEFAULT 0,
|
||||||
"agreement_time" bigint NOT NULL DEFAULT 0,
|
"agreement_time" bigint NOT NULL DEFAULT 0,
|
||||||
"create_time" timestamp NOT NULL DEFAULT (CURRENT_TIMESTAMP),
|
"create_time" timestamp NOT NULL DEFAULT (CURRENT_TIMESTAMP),
|
||||||
"update_time" timestamp NOT NULL DEFAULT (CURRENT_TIMESTAMP)
|
"update_time" timestamp NOT NULL DEFAULT (CURRENT_TIMESTAMP)
|
||||||
@@ -31,6 +32,6 @@ CREATE UNIQUE INDEX "index_invite_user_plan_user_id" ON "cd_invite_user_plan" ("
|
|||||||
|
|
||||||
INSERT INTO "cd_invite_level" ("name", "icon", "sort", "min_amount", "commission_rate", "level_type", "disabled")
|
INSERT INTO "cd_invite_level" ("name", "icon", "sort", "min_amount", "commission_rate", "level_type", "disabled")
|
||||||
VALUES ('青铜', 'fluent-emoji-flat:2nd-place-medal', 10, 0, 10, 'normal', false),
|
VALUES ('青铜', 'fluent-emoji-flat:2nd-place-medal', 10, 0, 10, 'normal', false),
|
||||||
('白银', 'fluent-emoji-flat:1st-place-medal', 20, 100000, 15, 'normal', false),
|
('白银', 'fluent-emoji-flat:1st-place-medal', 20, 10000, 15, 'normal', false),
|
||||||
('黄金', 'fluent-emoji-flat:3rd-place-medal', 30, 500000, 20, 'normal', false),
|
('黄金', 'fluent-emoji-flat:3rd-place-medal', 30, 50000, 20, 'normal', false),
|
||||||
('钻石', 'streamline-color:diamond-2', 40, 1000000, 30, 'normal', false);
|
('钻石', 'streamline-color:diamond-2', 40, 300000, 30, 'normal', false);
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ CREATE TABLE "cd_invite_user_plan"
|
|||||||
"enabled" boolean NOT NULL DEFAULT (false),
|
"enabled" boolean NOT NULL DEFAULT (false),
|
||||||
"level_id" integer NOT NULL DEFAULT 0,
|
"level_id" integer NOT NULL DEFAULT 0,
|
||||||
"level_locked" boolean NOT NULL DEFAULT (false),
|
"level_locked" boolean NOT NULL DEFAULT (false),
|
||||||
|
"promotion_amount" integer NOT NULL DEFAULT 0,
|
||||||
"agreement_time" integer NOT NULL DEFAULT 0,
|
"agreement_time" integer NOT NULL DEFAULT 0,
|
||||||
"create_time" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP),
|
"create_time" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP),
|
||||||
"update_time" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP)
|
"update_time" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP)
|
||||||
@@ -31,6 +32,6 @@ CREATE UNIQUE INDEX "index_invite_user_plan_user_id" ON "cd_invite_user_plan" ("
|
|||||||
|
|
||||||
INSERT INTO "cd_invite_level" ("name", "icon", "sort", "min_amount", "commission_rate", "level_type", "disabled")
|
INSERT INTO "cd_invite_level" ("name", "icon", "sort", "min_amount", "commission_rate", "level_type", "disabled")
|
||||||
VALUES ('青铜', 'fluent-emoji-flat:2nd-place-medal', 10, 0, 10, 'normal', false),
|
VALUES ('青铜', 'fluent-emoji-flat:2nd-place-medal', 10, 0, 10, 'normal', false),
|
||||||
('白银', 'fluent-emoji-flat:1st-place-medal', 20, 100000, 15, 'normal', false),
|
('白银', 'fluent-emoji-flat:1st-place-medal', 20, 10000, 15, 'normal', false),
|
||||||
('黄金', 'fluent-emoji-flat:3rd-place-medal', 30, 500000, 20, 'normal', false),
|
('黄金', 'fluent-emoji-flat:3rd-place-medal', 30, 50000, 20, 'normal', false),
|
||||||
('钻石', 'streamline-color:diamond-2', 40, 1000000, 30, 'normal', false);
|
('钻石', 'streamline-color:diamond-2', 40, 300000, 30, 'normal', false);
|
||||||
|
|||||||
@@ -137,6 +137,5 @@ export class MainConfiguration {
|
|||||||
});
|
});
|
||||||
|
|
||||||
logger.info('当前环境:', this.app.getEnv()); // prod
|
logger.info('当前环境:', this.app.getEnv()); // prod
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ export class RegisterController extends BaseController {
|
|||||||
password: body.password,
|
password: body.password,
|
||||||
} as any;
|
} as any;
|
||||||
const newUser = await this.userService.register(body.type, registerUser, async txManager => {
|
const newUser = await this.userService.register(body.type, registerUser, async txManager => {
|
||||||
await this.inviteService.bindInvitee(registerUser.id, body.inviteCode, txManager);
|
await this.inviteService.bindInvitee({ manager: txManager }, { inviteeUserId: registerUser.id, inviteCode: body.inviteCode });
|
||||||
});
|
});
|
||||||
return this.ok(newUser);
|
return this.ok(newUser);
|
||||||
} else if (body.type === 'mobile') {
|
} else if (body.type === 'mobile') {
|
||||||
@@ -85,7 +85,7 @@ export class RegisterController extends BaseController {
|
|||||||
password: body.password,
|
password: body.password,
|
||||||
} as any;
|
} as any;
|
||||||
const newUser = await this.userService.register(body.type, registerUser, async txManager => {
|
const newUser = await this.userService.register(body.type, registerUser, async txManager => {
|
||||||
await this.inviteService.bindInvitee(registerUser.id, body.inviteCode, txManager);
|
await this.inviteService.bindInvitee({ manager: txManager }, { inviteeUserId: registerUser.id, inviteCode: body.inviteCode });
|
||||||
});
|
});
|
||||||
return this.ok(newUser);
|
return this.ok(newUser);
|
||||||
} else if (body.type === 'email') {
|
} else if (body.type === 'email') {
|
||||||
@@ -104,7 +104,7 @@ export class RegisterController extends BaseController {
|
|||||||
password: body.password,
|
password: body.password,
|
||||||
} as any;
|
} as any;
|
||||||
const newUser = await this.userService.register(body.type, registerUser, async txManager => {
|
const newUser = await this.userService.register(body.type, registerUser, async txManager => {
|
||||||
await this.inviteService.bindInvitee(registerUser.id, body.inviteCode, txManager);
|
await this.inviteService.bindInvitee({ manager: txManager }, { inviteeUserId: registerUser.id, inviteCode: body.inviteCode });
|
||||||
});
|
});
|
||||||
return this.ok(newUser);
|
return this.ok(newUser);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,6 +65,8 @@ export class BasicSettingsController extends BaseController {
|
|||||||
const setting = await this.sysSettingsService.getSetting<SysInviteCommissionSetting>(SysInviteCommissionSetting);
|
const setting = await this.sysSettingsService.getSetting<SysInviteCommissionSetting>(SysInviteCommissionSetting);
|
||||||
return {
|
return {
|
||||||
enabled: setting.enabled,
|
enabled: setting.enabled,
|
||||||
|
levelEnabled: setting.levelEnabled === true,
|
||||||
|
fixedCommissionRate: setting.fixedCommissionRate || 10,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -139,7 +139,7 @@ export class LoginService {
|
|||||||
password: '',
|
password: '',
|
||||||
} as any;
|
} as any;
|
||||||
info = await this.userService.register('mobile', registerUser, async txManager => {
|
info = await this.userService.register('mobile', registerUser, async txManager => {
|
||||||
await this.inviteService.bindInvitee(registerUser.id, req.inviteCode, txManager);
|
await this.inviteService.bindInvitee({ manager: txManager }, { inviteeUserId: registerUser.id, inviteCode: req.inviteCode });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
this.clearCacheOnSuccess(mobile);
|
this.clearCacheOnSuccess(mobile);
|
||||||
|
|||||||
Reference in New Issue
Block a user