mirror of
https://github.com/coaidev/coai.git
synced 2025-05-20 05:20:15 +09:00
update subscription migration feature
This commit is contained in:
parent
a0392c041a
commit
324cc1b8f1
@ -1,7 +1,7 @@
|
|||||||
import React, {useMemo} from "react";
|
import React, { useMemo } from "react";
|
||||||
import { buySubscription } from "@/api/addition.ts";
|
import { buySubscription } from "@/api/addition.ts";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useDispatch } from "react-redux";
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
import { useToast } from "@/components/ui/use-toast.ts";
|
import { useToast } from "@/components/ui/use-toast.ts";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
@ -21,12 +21,12 @@ import {
|
|||||||
import { Badge } from "@/components/ui/badge.tsx";
|
import { Badge } from "@/components/ui/badge.tsx";
|
||||||
import { DialogClose } from "@radix-ui/react-dialog";
|
import { DialogClose } from "@radix-ui/react-dialog";
|
||||||
import { Button } from "@/components/ui/button.tsx";
|
import { Button } from "@/components/ui/button.tsx";
|
||||||
import { refreshSubscription } from "@/store/subscription.ts";
|
import { expiredSelector, refreshSubscription } from "@/store/subscription.ts";
|
||||||
import { Plus } from "lucide-react";
|
import { Plus } from "lucide-react";
|
||||||
import {subscriptionPrize} from "@/conf.ts";
|
import { subscriptionPrize } from "@/conf.ts";
|
||||||
|
|
||||||
function countPrize(base: number, month: number): number {
|
function countPrize(base: number, month: number): number {
|
||||||
const prize = base * month;
|
const prize = subscriptionPrize[base] * month;
|
||||||
if (month >= 36) {
|
if (month >= 36) {
|
||||||
return prize * 0.7;
|
return prize * 0.7;
|
||||||
} else if (month >= 12) {
|
} else if (month >= 12) {
|
||||||
@ -38,6 +38,15 @@ function countPrize(base: number, month: number): number {
|
|||||||
return prize;
|
return prize;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function countUpgradePrize(
|
||||||
|
level: number,
|
||||||
|
target: number,
|
||||||
|
days: number,
|
||||||
|
): number {
|
||||||
|
const bias = subscriptionPrize[target] - subscriptionPrize[level];
|
||||||
|
return (bias / 30) * days;
|
||||||
|
}
|
||||||
|
|
||||||
type UpgradeProps = {
|
type UpgradeProps = {
|
||||||
base: number;
|
base: number;
|
||||||
level: number;
|
level: number;
|
||||||
@ -91,6 +100,7 @@ async function callMigrateAction(
|
|||||||
|
|
||||||
export function Upgrade({ base, level }: UpgradeProps) {
|
export function Upgrade({ base, level }: UpgradeProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const expired = useSelector(expiredSelector);
|
||||||
const [open, setOpen] = React.useState(false);
|
const [open, setOpen] = React.useState(false);
|
||||||
const [month, setMonth] = React.useState(1);
|
const [month, setMonth] = React.useState(1);
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
@ -99,11 +109,11 @@ export function Upgrade({ base, level }: UpgradeProps) {
|
|||||||
const isCurrent = useMemo(() => level === base, [level, base]);
|
const isCurrent = useMemo(() => level === base, [level, base]);
|
||||||
const isUpgrade = useMemo(() => level < base, [level, base]);
|
const isUpgrade = useMemo(() => level < base, [level, base]);
|
||||||
|
|
||||||
return (level === 0 || level === base) ? (
|
return level === 0 || level === base ? (
|
||||||
<Dialog open={open} onOpenChange={setOpen}>
|
<Dialog open={open} onOpenChange={setOpen}>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button className={`action`} variant={`default`}>
|
<Button className={`action`} variant={`default`}>
|
||||||
{isCurrent ? t("sub.renew") : t("sub.upgrade")}
|
{isCurrent ? t("sub.renew") : t("sub.subscribe")}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className={`flex-dialog`}>
|
<DialogContent className={`flex-dialog`}>
|
||||||
@ -139,7 +149,7 @@ export function Upgrade({ base, level }: UpgradeProps) {
|
|||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
<p className={`price`}>
|
<p className={`price`}>
|
||||||
{t("sub.price", { price: countPrize(subscriptionPrize[base], month).toFixed(2) })}
|
{t("sub.price", { price: countPrize(base, month).toFixed(2) })}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<DialogFooter className={`translate-y-1.5`}>
|
<DialogFooter className={`translate-y-1.5`}>
|
||||||
@ -165,7 +175,10 @@ export function Upgrade({ base, level }: UpgradeProps) {
|
|||||||
) : (
|
) : (
|
||||||
<Dialog open={open} onOpenChange={setOpen}>
|
<Dialog open={open} onOpenChange={setOpen}>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button className={`action`} variant={isUpgrade ? `default` : `outline`}>
|
<Button
|
||||||
|
className={`action`}
|
||||||
|
variant={isUpgrade ? `default` : `outline`}
|
||||||
|
>
|
||||||
{isUpgrade ? t("sub.upgrade") : t("sub.downgrade")}
|
{isUpgrade ? t("sub.upgrade") : t("sub.downgrade")}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
@ -173,8 +186,16 @@ export function Upgrade({ base, level }: UpgradeProps) {
|
|||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>{t("sub.migrate-plan")}</DialogTitle>
|
<DialogTitle>{t("sub.migrate-plan")}</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div className={`pt-2`}>
|
<div className={`upgrade-wrapper`}>
|
||||||
{t('sub.migrate-plan-desc')}
|
{t("sub.migrate-plan-desc")}
|
||||||
|
|
||||||
|
{isUpgrade && (
|
||||||
|
<p className={`price`}>
|
||||||
|
{t("sub.upgrade-price", {
|
||||||
|
price: countUpgradePrize(level, base, expired).toFixed(2),
|
||||||
|
})}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<DialogFooter className={`translate-y-1.5`}>
|
<DialogFooter className={`translate-y-1.5`}>
|
||||||
<DialogClose asChild>
|
<DialogClose asChild>
|
||||||
|
@ -37,7 +37,7 @@ import { selectAuthenticated } from "@/store/auth.ts";
|
|||||||
import SubscriptionUsage from "@/components/home/subscription/SubscriptionUsage.tsx";
|
import SubscriptionUsage from "@/components/home/subscription/SubscriptionUsage.tsx";
|
||||||
import Tips from "@/components/Tips.tsx";
|
import Tips from "@/components/Tips.tsx";
|
||||||
import { subscriptionPrize } from "@/conf.ts";
|
import { subscriptionPrize } from "@/conf.ts";
|
||||||
import {Upgrade} from "@/components/home/subscription/BuyDialog.tsx";
|
import { Upgrade } from "@/components/home/subscription/BuyDialog.tsx";
|
||||||
|
|
||||||
function SubscriptionDialog() {
|
function SubscriptionDialog() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
@ -162,14 +162,17 @@ const resources = {
|
|||||||
"enterprise-deploy": "支持私有化部署",
|
"enterprise-deploy": "支持私有化部署",
|
||||||
"contact-sale": "联系销售",
|
"contact-sale": "联系销售",
|
||||||
current: "当前计划",
|
current: "当前计划",
|
||||||
|
subscribe: "订阅",
|
||||||
upgrade: "升级",
|
upgrade: "升级",
|
||||||
downgrade: "降级",
|
downgrade: "降级",
|
||||||
renew: "续费",
|
renew: "续费",
|
||||||
"cannot-select": "无法选择",
|
"cannot-select": "无法选择",
|
||||||
"select-time": "选择订阅时间",
|
"select-time": "选择订阅时间",
|
||||||
"migrate-plan": "变更订阅计划",
|
"migrate-plan": "变更订阅计划",
|
||||||
"migrate-plan-desc": "变更订阅后,您的订阅时间将会根据剩余天数价格计算,重新计算订阅时间。(如降级会时间翻倍,升级会时间减半)",
|
"migrate-plan-desc":
|
||||||
|
"变更订阅后,您的订阅时间将会根据剩余天数价格计算,重新计算订阅时间。(如降级会时间翻倍,升级会补齐差价)",
|
||||||
price: "价格 {{price}} 元",
|
price: "价格 {{price}} 元",
|
||||||
|
"upgrade-price": "升级费用 {{price}} 元 (仅供参考)",
|
||||||
expired: "订阅剩余天数",
|
expired: "订阅剩余天数",
|
||||||
time: {
|
time: {
|
||||||
1: "1个月",
|
1: "1个月",
|
||||||
@ -517,13 +520,17 @@ const resources = {
|
|||||||
"enterprise-deploy": "Support Private Cloud Deployment",
|
"enterprise-deploy": "Support Private Cloud Deployment",
|
||||||
"contact-sale": "Contact Sales",
|
"contact-sale": "Contact Sales",
|
||||||
current: "Current Plan",
|
current: "Current Plan",
|
||||||
|
subscribe: "Subscribe",
|
||||||
upgrade: "Upgrade",
|
upgrade: "Upgrade",
|
||||||
downgrade: "Downgrade",
|
downgrade: "Downgrade",
|
||||||
renew: "Renew",
|
renew: "Renew",
|
||||||
"cannot-select": "Cannot Select",
|
"cannot-select": "Cannot Select",
|
||||||
"select-time": "Select Subscription Time",
|
"select-time": "Select Subscription Time",
|
||||||
"migrate-plan": "Migrate Subscription Plan",
|
"migrate-plan": "Migrate Subscription Plan",
|
||||||
|
"migrate-plan-desc":
|
||||||
|
"After changing the subscription, your subscription time will be calculated based on the remaining days price, and the subscription time will be recalculated. (For example, downgrading will double the time, and upgrading will make up the difference)",
|
||||||
price: "Price {{price}} CNY",
|
price: "Price {{price}} CNY",
|
||||||
|
"upgrade-price": "Upgrade Fee {{price}} CNY (for reference only)",
|
||||||
expired: "Subscription Remaining Days",
|
expired: "Subscription Remaining Days",
|
||||||
time: {
|
time: {
|
||||||
1: "1 Month",
|
1: "1 Month",
|
||||||
@ -536,7 +543,8 @@ const resources = {
|
|||||||
"success-prompt":
|
"success-prompt":
|
||||||
"You have successfully subscribed to {{month}} months subscription.",
|
"You have successfully subscribed to {{month}} months subscription.",
|
||||||
"migrate-success": "Migrate success",
|
"migrate-success": "Migrate success",
|
||||||
"migrate-success-prompt": "You have successfully migrated subscription.",
|
"migrate-success-prompt":
|
||||||
|
"You have successfully migrated subscription.",
|
||||||
failed: "Subscribe failed",
|
failed: "Subscribe failed",
|
||||||
"failed-prompt":
|
"failed-prompt":
|
||||||
"Failed to subscribe, please make sure you have enough balance, you will soon jump to deeptrain wallet to pay balance.",
|
"Failed to subscribe, please make sure you have enough balance, you will soon jump to deeptrain wallet to pay balance.",
|
||||||
@ -883,13 +891,17 @@ const resources = {
|
|||||||
"enterprise-deploy": "Поддержка частной облачной инфраструктуры",
|
"enterprise-deploy": "Поддержка частной облачной инфраструктуры",
|
||||||
"contact-sale": "Связаться с отделом продаж",
|
"contact-sale": "Связаться с отделом продаж",
|
||||||
current: "Текущая подписка",
|
current: "Текущая подписка",
|
||||||
|
subscribe: "Подписаться",
|
||||||
upgrade: "Обновить",
|
upgrade: "Обновить",
|
||||||
downgrade: "Понизить",
|
downgrade: "Понизить",
|
||||||
renew: "Продлить",
|
renew: "Продлить",
|
||||||
"cannot-select": "Невозможно выбрать",
|
"cannot-select": "Невозможно выбрать",
|
||||||
"select-time": "Выберите время подписки",
|
"select-time": "Выберите время подписки",
|
||||||
"migrate-plan": "Перенести подписку",
|
"migrate-plan": "Перенести подписку",
|
||||||
|
"migrate-plan-desc":
|
||||||
|
"После изменения подписки ваше время подписки будет рассчитываться на основе цены оставшихся дней, и время подписки будет пересчитано. (Например, понижение удваивает время, а повышение компенсирует разницу)",
|
||||||
price: "Цена {{price}} CNY",
|
price: "Цена {{price}} CNY",
|
||||||
|
"upgrade-price": "Плата за обновление {{price}} CNY (для справки)",
|
||||||
expired: "Осталось дней подписки",
|
expired: "Осталось дней подписки",
|
||||||
time: {
|
time: {
|
||||||
1: "1 месяц",
|
1: "1 месяц",
|
||||||
|
@ -82,17 +82,35 @@ func (u *User) AddSubscription(db *sql.DB, month int, level int) bool {
|
|||||||
return err == nil
|
return err == nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *User) MigratePlan(db *sql.DB, level int) error {
|
func (u *User) DowngradePlan(db *sql.DB, target int) error {
|
||||||
current := u.GetSubscriptionLevel(db)
|
expired, current := u.GetSubscription(db)
|
||||||
if current == 0 || current == level {
|
if current == 0 || current == target {
|
||||||
return fmt.Errorf("invalid plan level")
|
return fmt.Errorf("invalid plan level")
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err := db.Exec(`CALL migrate_plan(?, ?)`, u.GetID(db), level)
|
now := time.Now()
|
||||||
|
weight := GetLevel(current).Price / GetLevel(target).Price
|
||||||
|
stamp := float32(expired.Unix()-now.Unix()) * weight
|
||||||
|
|
||||||
|
// ceil expired time
|
||||||
|
expiredAt := now.Add(time.Duration(stamp)*time.Second).AddDate(0, 0, -1)
|
||||||
|
date := utils.ConvertSqlTime(expiredAt)
|
||||||
|
_, err := db.Exec("UPDATE subscription SET level = ?, expired_at = ? WHERE user_id = ?", target, date, u.GetID(db))
|
||||||
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (u *User) CountUpgradePrice(db *sql.DB, target int) float32 {
|
||||||
|
expired := u.GetSubscriptionExpiredAt(db)
|
||||||
|
weight := GetLevel(target).Price - u.GetPlan(db).Price
|
||||||
|
if weight < 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
days := expired.Sub(time.Now()).Hours() / 24
|
||||||
|
return float32(days) * weight / 30
|
||||||
|
}
|
||||||
|
|
||||||
func (u *User) SetSubscriptionLevel(db *sql.DB, level int) bool {
|
func (u *User) SetSubscriptionLevel(db *sql.DB, level int) bool {
|
||||||
_, err := db.Exec("UPDATE subscription SET level = ? WHERE user_id = ?", level, u.GetID(db))
|
_, err := db.Exec("UPDATE subscription SET level = ? WHERE user_id = ?", level, u.GetID(db))
|
||||||
return err == nil
|
return err == nil
|
||||||
@ -120,19 +138,25 @@ func BuySubscription(db *sql.DB, cache *redis.Client, user *User, level int, mon
|
|||||||
if before == 0 || before == level {
|
if before == 0 || before == level {
|
||||||
// buy new subscription or renew subscription
|
// buy new subscription or renew subscription
|
||||||
money := CountSubscriptionPrize(level, month)
|
money := CountSubscriptionPrize(level, month)
|
||||||
fmt.Println(money)
|
|
||||||
if user.Pay(cache, money) {
|
if user.Pay(cache, money) {
|
||||||
user.AddSubscription(db, month, level)
|
user.AddSubscription(db, month, level)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
} else {
|
} else if before > level {
|
||||||
// upgrade or downgrade subscription
|
// downgrade subscription
|
||||||
err := user.MigratePlan(db, level)
|
err := user.DowngradePlan(db, level)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return err == nil
|
return err == nil
|
||||||
|
} else {
|
||||||
|
// upgrade subscription
|
||||||
|
money := user.CountUpgradePrice(db, level)
|
||||||
|
if user.Pay(cache, money) {
|
||||||
|
user.SetSubscriptionLevel(db, level)
|
||||||
|
return true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return false
|
return false
|
||||||
|
@ -152,30 +152,6 @@ func CreateSubscriptionTable(db *sql.DB) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = db.Exec(`
|
|
||||||
CREATE PROCEDURE migrate_plan(IN id INT, IN target_level INT)
|
|
||||||
BEGIN
|
|
||||||
DECLARE current_level INT;
|
|
||||||
DECLARE expired DATETIME;
|
|
||||||
|
|
||||||
SELECT level, expired_at INTO current_level, expired FROM subscription WHERE user_id = id;
|
|
||||||
SET @current = UNIX_TIMESTAMP();
|
|
||||||
SET @stamp = UNIX_TIMESTAMP(expired) - @current;
|
|
||||||
SET @offset = target_level - current_level;
|
|
||||||
|
|
||||||
IF @offset > 0 THEN
|
|
||||||
SET @stamp = @stamp / 2 * @offset;
|
|
||||||
ELSEIF @offset < 0 THEN
|
|
||||||
SET @stamp = @stamp * 2 * (-@offset);
|
|
||||||
END IF;
|
|
||||||
UPDATE subscription SET level = target_level, expired_at = FROM_UNIXTIME(@current + @stamp) WHERE user_id = id;
|
|
||||||
END;
|
|
||||||
`)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println(err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func CreateApiKeyTable(db *sql.DB) {
|
func CreateApiKeyTable(db *sql.DB) {
|
||||||
|
Loading…
Reference in New Issue
Block a user