Merge branch 'v2-dev-buy' into v2-dev

pull/576/head
xiaojunnuo 2025-11-09 02:06:57 +08:00
commit c3637e731f
13 changed files with 216 additions and 89 deletions

View File

@ -130,12 +130,15 @@ export class PlusService {
return res.accessToken;
}
async getVipTrial() {
async getVipTrial(vipType= "plus") {
await this.register();
const plusRequestService = await this.getPlusRequestService();
const res = await plusRequestService.request({
url: '/activation/subject/vip/trialGet',
method: 'POST',
data:{
vipType
}
});
if (res.license) {
await this.updateLicense(res.license);

View File

@ -10,4 +10,4 @@ VITE_APP_LOGO=static/images/logo/logo.svg
VITE_APP_LOGIN_LOGO=static/images/logo/rect-black.svg
VITE_APP_PROJECT_PATH=https://github.com/certd/certd
VITE_APP_NAMESPACE=fs
VITE_APP_VIP_PRODUCT_URL=http://localhost:1017/subject#/product/list
VITE_APP_VIP_PRODUCT_URL="http://localhost:1017/subject#/app/certd/product"

View File

@ -1,3 +1,6 @@
VITE_APP_API=api
#登录与权限开启
VITE_APP_PM_ENABLED=true
VITE_APP_VIP_PRODUCT_URL="https://app.handfree.work/subject#/app/certd/product"

View File

@ -8,10 +8,10 @@ export async function doActive(form: any) {
});
}
export async function getVipTrial() {
export async function getVipTrial(vipType: string) {
return await request({
url: "/sys/plus/getVipTrial",
method: "post",
data: {},
data: { vipType },
});
}

View File

@ -1,5 +1,5 @@
<template>
<div v-if="!settingStore.isComm || userStore.isAdmin" class="layout-vip isPlus" @click="openUpgrade">
<div v-if="!settingStore.isComm || userStore.isAdmin" class="layout-vip isPlus" :class="{ isForever: settingStore.isForever }" @click="openUpgrade">
<contextHolder />
<fs-icon icon="mingcute:vip-1-line" :title="text.title" />
@ -12,7 +12,7 @@
</div>
</template>
<script lang="tsx" setup>
import { computed, onMounted, reactive } from "vue";
import { computed, onMounted, reactive, ref } from "vue";
import dayjs from "dayjs";
import { message, Modal } from "ant-design-vue";
import * as api from "./api";
@ -21,7 +21,7 @@ import { useRouter } from "vue-router";
import { useUserStore } from "/@/store/user";
import { mitter } from "/@/utils/util.mitt";
import { useI18n } from "vue-i18n";
import { env } from "/@/utils/util.env";
const { t } = useI18n();
const settingStore = useSettingStore();
@ -106,7 +106,7 @@ const text = computed<Text>(() => {
const expireTime = computed(() => {
if (settingStore.isPlus) {
return dayjs(settingStore.plusInfo.expireTime).format("YYYY-MM-DD");
return settingStore.expiresText;
}
return "";
});
@ -165,34 +165,38 @@ function goAccount() {
router.push("/sys/account");
}
async function getVipTrial() {
const res = await api.getVipTrial();
async function getVipTrial(vipType = "plus") {
const res = await api.getVipTrial(vipType);
message.success(t("vip.congratulations_vip_trial", { duration: res.duration }));
await settingStore.init();
}
function openTrialModal() {
function openTrialModal(vipType = "plus") {
Modal.destroyAll();
modal.confirm({
title: t("vip.trial_modal_title"),
okText: t("vip.trial_modal_ok_text"),
onOk() {
getVipTrial();
getVipTrial(vipType);
},
width: 600,
content: () => {
return (
<div class="flex-col mt-10 mb-10">
<div>{t("vip.trial_modal_thanks")}</div>
<div>{t("vip.trial_modal_click_confirm")}</div>
<div>{t("vip.trial_modal_click_confirm", { vipType })}</div>
</div>
);
},
});
}
function openStarModal() {
function openStarModal(vipType: string) {
if (settingStore.isPlus) {
message.error(t("vip.already_vip"));
return;
}
Modal.destroyAll();
const goGithub = () => {
window.open("https://github.com/certd/certd/");
@ -203,7 +207,7 @@ function openStarModal() {
okText: t("vip.star_now"),
onOk() {
goGithub();
openTrialModal();
openTrialModal(vipType);
},
width: 600,
content: () => {
@ -231,7 +235,49 @@ function openUpgrade() {
title = t("vip.renew_pro_upgrade_business");
}
// const goBuyUrl = "https://afdian.com/a/greper"
const subjectId = settingStore.installInfo.siteId;
const appKey = settingStore.installInfo.appKey;
const location = window.location;
const callbackUrl = encodeURIComponent(`${location.origin}${location.pathname}#/sys/account`);
const goBuyUrl = `${env.VIP_PRODUCT_URL}?appKey=${appKey}&subjectId=${subjectId}&callback=${callbackUrl}`;
const goBuyCommUrl = `${goBuyUrl}&vipType=comm`;
const productInfo = settingStore.productInfo;
function checkPerpetualPlus() {
if (settingStore.isPerpetual) {
Modal.warn({
title: t("vip.already_perpetual_plus"),
okText: t("vip.confirm"),
});
throw new Error(t("vip.already_perpetual_plus"));
}
}
function goBuyPlusPage() {
checkPerpetualPlus();
if (settingStore.isComm) {
Modal.warn({
title: t("vip.already_comm"),
okText: t("vip.confirm"),
});
return;
}
window.open(goBuyUrl);
}
function goBuyCommPage() {
checkPerpetualPlus();
if (settingStore.isPlus && !settingStore.isComm) {
Modal.confirm({
title: t("vip.already_plus"),
okText: t("vip.confirm"),
onOk() {
window.open(goBuyCommUrl);
},
});
return;
}
window.open(goBuyCommUrl);
}
const vipTypeDefine = {
free: {
title: t("vip.basic_edition"),
@ -248,7 +294,7 @@ function openUpgrade() {
trial: {
title: t("vip.click_to_get_7_day_trial"),
click: () => {
openStarModal();
openStarModal("plus");
},
},
icon: "stash:thumb-up",
@ -258,7 +304,7 @@ function openUpgrade() {
get() {
return (
<a-tooltip title={t("vip.afdian_support_vip")}>
<a-button size="small" type="primary" href="https://afdian.com/a/greper" target="_blank">
<a-button size="small" type="primary" onClick={goBuyPlusPage}>
{t("vip.get_after_support")}
</a-button>
</a-tooltip>
@ -274,30 +320,57 @@ function openUpgrade() {
price: productInfo.comm.price,
price3: `¥${productInfo.comm.price3}/3${t("vip.years")}`,
tooltip: productInfo.comm.tooltip,
trial: {
title: t("vip.click_to_get_7_day_trial"),
click: () => {
openStarModal("comm");
},
},
get() {
return <a-button size="small">{t("vip.contact_author_for_trial")}</a-button>;
return (
<a-button size="small" type="primary" onClick={goBuyCommPage}>
{t("vip.buy")}
</a-button>
);
},
},
};
const modalRef = modal.confirm({
const manualActiveFlag = ref();
function showManualActivation() {
manualActiveFlag.value = true;
}
const modalRef = modal.success({
title,
async onOk() {
return await doActive();
},
maskClosable: true,
okText: t("vip.activate"),
okText: t("vip.close"),
width: 1100,
content: () => {
let activationCodeGetWay = (
<span>
<a href="https://afdian.com/a/greper" target="_blank">
{t("vip.get_pro_code_after_support")}
</a>
<span> {t("vip.business_contact_author")}</span>
</span>
let manualActiveBlock: any = "";
if (manualActiveFlag.value) {
manualActiveBlock = (
<div>
<div class="mt-10">
<a-input-search class="w-2/6" v-model:value={formState.code} placeholder={placeholder} enter-button={t("vip.activate")} onSearch={doActive}></a-input-search>
</div>
<div class="mt-10">
{t("vip.activation_code_one_use")}
<a onClick={goAccount}>{t("vip.bind_account")}</a>{t("vip.transfer_vip")}
</div>
</div>
);
}
const vipLabel = settingStore.vipLabel;
let plusInfo: any = "";
if (isPlus) {
plusInfo = (
<div class="mt-10">
{t("vip.current")} {vipLabel} {t("vip.activated_expire_time")}
{settingStore.expiresText}
</div>
);
}
const slots = [];
for (const key in vipTypeDefine) {
// @ts-ignore
@ -362,27 +435,20 @@ function openUpgrade() {
<div class="vip-type-vs">
<a-row gutter={20}>{slots}</a-row>
</div>
<div class="mt-10">
<h3 class="block-header">{isPlus ? t("vip.renew") : t("vip.activate_immediately")}</h3>
<div>{isPlus ? `${t("vip.current")} ${vipLabel} ${t("vip.activated_expire_time")}` + dayjs(settingStore.plusInfo.expireTime).format("YYYY-MM-DD") : ""}</div>
<div class="mt-10">
<div class="flex-o w-100">
<span>{t("vip.site_id")}</span>
<fs-copyable class="flex-1" v-model={computedSiteId.value}></fs-copyable>
<fs-copyable v-model={computedSiteId.value}></fs-copyable>
</div>
<a-input class="mt-10" v-model:value={formState.code} placeholder={placeholder} />
<a-input class="mt-10" v-model:value={formState.inviteCode} placeholder={t("vip.invite_code_optional")} />
</div>
{plusInfo}
<div class="mt-10">
{t("vip.no_activation_code")}
{activationCodeGetWay}
</div>
<div class="mt-10">
{t("vip.activation_code_one_use")}
<a onClick={goAccount}>{t("vip.bind_account")}</a>{t("vip.transfer_vip")}
</div>
{t("vip.have_activation_code")}
<span>
<a onClick={showManualActivation}>{t("vip.manual_activation")}</a>
</span>
</div>
<div class="mt-10">{manualActiveBlock}</div>
</div>
);
},
@ -406,6 +472,10 @@ onMounted(() => {
&.isPlus {
color: #c5913f;
&.isForever {
color: #ff2e83;
}
}
.text {
@ -420,6 +490,11 @@ onMounted(() => {
border: 1px solid #eee;
border-radius: 5px;
height: 275px;
line-height: 24px;
.privilege {
margin-top: 5px;
}
//background-color: rgba(250, 237, 167, 0.79);
&.current {

View File

@ -32,13 +32,14 @@ export default {
successContent: "You have successfully activated {vipLabel}, valid until: {expireDate}",
bindAccountTitle: "Bind Your Account",
bindAccountContent: "Binding your account helps prevent license loss. Strongly recommended.",
congratulations_vip_trial: "Congratulations, you have received a Pro version {duration} days trial",
trial_modal_title: "7-day Pro version trial acquisition",
congratulations_vip_trial: "Congratulations, you have received a VIP version {duration} days trial",
trial_modal_title: "7-day VIP version trial acquisition",
trial_modal_ok_text: "Get now",
trial_modal_thanks: "Thank you for supporting the open source project",
trial_modal_click_confirm: "Click confirm to get a 7-day Pro version trial",
get_7_day_pro_trial: "7-day professional version trial",
trial_modal_click_confirm: "Click confirm to get a 7-day VIP({vipType}) version trial",
get_7_day_pro_trial: "7-day VIP version trial",
star_now: "Star Now",
already_vip: "Already VIP version, can't trial ",
please_help_star: "Could you please help by starring? Thanks a lot!",
admin_only_operation: "Admin operation only",
enter_activation_code: "Please enter the activation code",
@ -61,7 +62,7 @@ export default {
plugins_fully_open: "All plugins open, including Synology and more",
click_to_get_7_day_trial: "Click to get 7-day trial",
years: "years",
afdian_support_vip: 'Get a one-year professional activation code after supporting "VIP membership" on Afdian, open source needs your support',
afdian_support_vip: "Obtain the permanent professional version coupon",
get_after_support: "Get after sponsoring",
business_edition: "Business Edition",
@ -72,9 +73,9 @@ export default {
plugin_management: "Plugin management",
unlimited_multi_users: "Unlimited multi-users",
support_user_payment: "Supports user payments",
contact_author_for_trial: "Please contact the author for trial",
contact_author_for_trial: "Buy It Now",
activate: "Activate",
get_pro_code_after_support: 'Get a one-year professional activation code after supporting "VIP membership" on Afdian',
get_pro_code_after_support: "Go to sponsoring",
business_contact_author: "Business edition please contact the author directly",
year: "year",
freee: "Free",
@ -89,4 +90,12 @@ export default {
bind_account: "bind account",
transfer_vip: ' then "Transfer VIP"',
needVipTip: "This feature requires a professional version, please upgrade to a professional version first.",
manual_activation: "Manual activation use code",
close: "Close",
have_activation_code: "Already have activation code?",
buy: "Buy",
already_plus: "Already Professional Edition, will upgrade to Business Edition, Professional Edition time will be lost",
already_comm: "Already Business Edition, can't change to Professional Edition",
already_perpetual_plus: "You already have a perpetual Professional Edition, can't upgrade",
confirm: "Confirm",
};

View File

@ -32,13 +32,14 @@ export default {
successContent: "您已成功激活{vipLabel},有效期至:{expireDate}",
bindAccountTitle: "是否绑定袖手账号",
bindAccountContent: "绑定账号后可以避免License丢失强烈建议绑定",
congratulations_vip_trial: "恭喜,您已获得专业版{duration}天试用",
trial_modal_title: "7天专业版试用获取",
congratulations_vip_trial: "恭喜,您已获得VIP{duration}天试用",
trial_modal_title: "7天VIP试用获取",
trial_modal_ok_text: "立即获取",
trial_modal_thanks: "感谢您对开源项目的支持",
trial_modal_click_confirm: "点击确认即可获取7天专业版试用",
get_7_day_pro_trial: "7天专业版试用获取",
trial_modal_click_confirm: "点击确认即可获取7天VIP({vipType})试用",
get_7_day_pro_trial: "7天VIP试用获取",
star_now: "立即去Star",
already_vip: "您已经是VIP了不能试用",
please_help_star: "可以先请您帮忙点个star吗感谢感谢",
admin_only_operation: "仅限管理员操作",
enter_activation_code: "请输入激活码",
@ -61,8 +62,8 @@ export default {
plugins_fully_open: "插件全开放,群辉等更多插件",
click_to_get_7_day_trial: "点击获取7天试用",
years: "年",
afdian_support_vip: "爱发电赞助“VIP会员”后获取一年期专业版激活码开源需要您的支持",
get_after_support: "爱发电赞助后获取",
afdian_support_vip: "新用户开通永久专业版立享50优惠券",
get_after_support: "立即赞助",
business_edition: "商业版",
commercial_license: "商业授权,可对外运营",
@ -72,10 +73,9 @@ export default {
plugin_management: "插件管理",
unlimited_multi_users: "多用户无限制",
support_user_payment: "支持用户支付",
contact_author_for_trial: "请联系作者获取试用",
activate: "激活",
get_pro_code_after_support: "爱发电赞助“VIP会员”后获取一年期专业版激活码",
business_contact_author: "商业版请直接联系作者",
get_pro_code_after_support: "前往获取",
business_contact_author: "",
year: "年",
freee: "免费",
renew: "续期",
@ -89,4 +89,12 @@ export default {
bind_account: "绑定账号",
transfer_vip: ',然后"转移VIP"即可',
needVipTip: "此为专业版功能,请先开通专业版",
manual_activation: "激活码手动激活",
close: "关闭",
have_activation_code: "已经有激活码了?",
buy: "立即购买",
already_plus: "已经是专业版了,是否升级为商业版?注意:专业版时长将被覆盖",
already_comm: "已经是商业版了,不能降级为专业版",
already_perpetual_plus: "您已经是永久专业版了,无法继续升级",
confirm: "确认",
};

View File

@ -10,6 +10,7 @@ import { useTitle } from "@vueuse/core";
import { utils } from "/@/utils";
import { cloneDeep, merge } from "lodash-es";
import { useI18n } from "/src/locales";
import dayjs from "dayjs";
import { $t } from "/src/locales";
export interface SettingState {
skipReset?: boolean; // 注销登录时不清空此store的状态
@ -126,12 +127,14 @@ export const useSettingStore = defineStore({
getInstallInfo(): SysInstallInfo {
return this.installInfo;
},
isPerpetual(): boolean {
return this.plusInfo?.isPlus && this.plusInfo?.expireTime === -1;
},
isPlus(): boolean {
return this.plusInfo?.isPlus && this.plusInfo?.expireTime > new Date().getTime();
// return false;
return this.plusInfo?.isPlus && (this.plusInfo?.expireTime === -1 || this.plusInfo?.expireTime > new Date().getTime());
},
isComm(): boolean {
return this.plusInfo?.isComm && this.plusInfo?.expireTime > new Date().getTime();
return this.plusInfo?.isComm && (this.plusInfo?.expireTime === -1 || this.plusInfo?.expireTime > new Date().getTime());
},
isAgent(): boolean {
return this.siteEnv?.agent?.enabled === true;
@ -139,6 +142,18 @@ export const useSettingStore = defineStore({
isCommOrAgent() {
return this.isComm || this.isAgent;
},
expiresText() {
if (this.plusInfo?.expireTime == null) {
return "";
}
if (this.plusInfo?.expireTime === -1) {
return "永久";
}
return dayjs(this.plusInfo?.expireTime).format("YYYY-MM-DD");
},
isForever() {
return this.isPlus && this.plusInfo?.expireTime === -1;
},
vipLabel(): string {
const { t } = useI18n();
const vipLabelMap: any = {

View File

@ -6,22 +6,28 @@ export function getEnvValue(key: string) {
export class EnvConfig {
MODE: string = import.meta.env.MODE;
API: string = import.meta.env.VITE_APP_API;
STORAGE: string = import.meta.env.VITE_APP_STORAGE;
TITLE: string = import.meta.env.VITE_APP_TITLE;
SLOGAN: string = import.meta.env.VITE_APP_SLOGAN;
LOGO: string = import.meta.env.VITE_APP_LOGO;
LOGIN_LOGO: string = import.meta.env.VITE_APP_LOGIN_LOGO;
ICP_NO: string = import.meta.env.VITE_APP_ICP_NO;
COPYRIGHT_YEAR: string = import.meta.env.VITE_APP_COPYRIGHT_YEAR;
COPYRIGHT_NAME: string = import.meta.env.VITE_APP_COPYRIGHT_NAME;
COPYRIGHT_URL: string = import.meta.env.VITE_APP_COPYRIGHT_URL;
PM_ENABLED: string = import.meta.env.VITE_APP_PM_ENABLED;
API: string;
STORAGE: string;
TITLE: string;
SLOGAN: string;
LOGO: string;
LOGIN_LOGO: string;
ICP_NO: string;
COPYRIGHT_YEAR: string;
COPYRIGHT_NAME: string;
COPYRIGHT_URL: string;
PM_ENABLED: string;
VIP_PRODUCT_URL: string;
init(env: any) {
constructor() {
this.init();
}
init() {
const env = import.meta.env;
for (const key in this) {
if (this.hasOwnProperty(key)) {
this[key] = env[key];
const metaKey = "VITE_APP_" + key;
if (this.hasOwnProperty(key) && env.hasOwnProperty(metaKey)) {
this[key] = env[metaKey];
}
}
}

View File

@ -375,7 +375,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
},
},
createTime: {
title: t("certd.create_time"),
title: t("certd.createTime"),
type: "datetime",
form: {
show: false,
@ -387,7 +387,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
},
},
updateTime: {
title: t("certd.update_time"),
title: t("certd.updateTime"),
type: "datetime",
form: {
show: false,

View File

@ -20,7 +20,7 @@
typeorm:
dataSource:
default:
database: './data/db-plus-dev-1.sqlite'
database: './data/db-plus-dev.sqlite'
# plus server: 'http://127.0.0.1:11007'
account:

View File

@ -1,5 +1,6 @@
import { ALL, Body, Controller, Inject, Post, Provide } from '@midwayjs/core';
import { BaseController, PlusService, SysInstallInfo, SysSettingsService } from '@certd/lib-server';
import { logger } from '@certd/basic';
/**
*/
@ -31,15 +32,20 @@ export class SysPlusController extends BaseController {
installInfo.bindUrl = url;
await this.sysSettingsService.saveSetting(installInfo);
//重新验证配置
//重新验证vip
try{
await this.plusService.verify();
}catch(e){
logger.error(`验证配置失败:${e}`);
}
return this.ok(true);
}
@Post('/getVipTrial', { summary: 'sys:settings:edit' })
async getVipTrial(@Body(ALL) body) {
const res = await this.plusService.getVipTrial();
async getVipTrial(@Body("vipType") vipType?:string) {
const res = await this.plusService.getVipTrial(vipType);
return this.ok(res);
}
//

View File

@ -134,6 +134,8 @@ export class CnameRecordService extends BaseService<CnameRecordEntity> {
if (!param.id) {
throw new ValidateException("id不能为空");
}
//hostRecord包含所有权校验信息不允许用户修改hostRecord
delete param.hostRecord
const old = await this.info(param.id);
if (!old) {