update subscription migration feature

This commit is contained in:
Zhang Minghan 2023-11-27 11:37:06 +08:00
parent a0392c041a
commit 324cc1b8f1
5 changed files with 79 additions and 46 deletions

View File

@ -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>

View File

@ -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();

View File

@ -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 месяц",

View File

@ -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

View File

@ -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) {