Skip to content

Commit

Permalink
feat: add proxy node occupancy feature
Browse files Browse the repository at this point in the history
  • Loading branch information
Ehco1996 committed Dec 22, 2023
1 parent 4af759b commit e4e6430
Show file tree
Hide file tree
Showing 7 changed files with 310 additions and 17 deletions.
17 changes: 17 additions & 0 deletions apps/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,23 @@
("superhero", "superhero"),
)

BULMA_COLOR_EMPTY = ""
BULMA_COLOR_PRIMARY = "is-primary"
BULMA_COLOR_LINK = "is-link"
BULMA_COLOR_INFO = "is-info"
BULMA_COLOR_DANGER = "is-danger"
BULMA_COLOR_WARNING = "is-warning"
BULMA_COLOR_SUCCESS = "is-success"
BULMA_COLOR_CHOICES = (
(BULMA_COLOR_EMPTY, "empty"),
(BULMA_COLOR_INFO, "is-info"),
(BULMA_COLOR_LINK, "is-link"),
(BULMA_COLOR_PRIMARY, "is-primary"),
(BULMA_COLOR_DANGER, "is-danger"),
(BULMA_COLOR_WARNING, "is-warning"),
(BULMA_COLOR_SUCCESS, "is-success"),
)


# 判断节点在线时间间隔
NODE_TIME_OUT = 75
Expand Down
12 changes: 12 additions & 0 deletions apps/proxy/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ class OccupancyConfigInline(admin.StackedInline):
"occupancy_price",
"occupancy_traffic",
"occupancy_user_limit",
"color",
"status",
"remark",
]

def get_formset(self, request, obj=None, **kwargs):
Expand Down Expand Up @@ -236,6 +239,15 @@ def traffic_info(self, instance):
def out_of_usage(self, instance):
return instance.out_of_usage()

def get_form(self, request, obj=None, **kwargs):
if obj:
help_texts = {
"total_traffic": f"={traffic_format(obj.total_traffic)}",
"used_traffic": f"={traffic_format(obj.used_traffic)}",
}
kwargs.update({"help_texts": help_texts})
return super().get_form(request, obj, **kwargs)


# Register your models here.
admin.site.register(models.ProxyNode, ProxyNodeAdmin)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Generated by Django 4.2.6 on 2023-12-22 06:12

from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
("proxy", "0023_alter_userproxynodeoccupancy_index_together_and_more"),
]

operations = [
migrations.AddField(
model_name="occupancyconfig",
name="color",
field=models.CharField(
blank=True,
choices=[
("", "empty"),
("is-info", "is-info"),
("is-link", "is-link"),
("is-primary", "is-primary"),
("is-danger", "is-danger"),
("is-warning", "is-warning"),
("is-uccess", "is-success"),
],
default="",
max_length=32,
verbose_name="颜色",
),
),
migrations.AddField(
model_name="occupancyconfig",
name="remark",
field=models.CharField(
blank=True, default="", max_length=64, verbose_name="备注"
),
),
migrations.AddField(
model_name="occupancyconfig",
name="status",
field=models.CharField(
choices=[("active", "active"), ("normal", "normal")],
default="normal",
max_length=32,
verbose_name="状态",
),
),
]
109 changes: 93 additions & 16 deletions apps/proxy/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import random
from collections import defaultdict
from copy import deepcopy
from datetime import timedelta
from decimal import Decimal
from functools import cached_property
from urllib.parse import quote, urlencode
Expand Down Expand Up @@ -836,6 +837,12 @@ def total_traffic(self):


class OccupancyConfig(BaseModel):
STATUS_ACTIVE = "active"
STATUS_NORMAL = "normal"
STATUS_CHOICES = (
(STATUS_ACTIVE, "active"),
(STATUS_NORMAL, "normal"),
)
proxy_node = models.OneToOneField(
ProxyNode,
on_delete=models.CASCADE,
Expand All @@ -848,6 +855,17 @@ class OccupancyConfig(BaseModel):
)
occupancy_traffic = models.BigIntegerField(default=0, verbose_name="流量(单位字节)")
occupancy_user_limit = models.PositiveIntegerField(verbose_name="用户数", default=0)
color = models.CharField(
choices=c.BULMA_COLOR_CHOICES,
max_length=32,
default=c.BULMA_COLOR_EMPTY,
blank=True,
verbose_name="颜色",
)
status = models.CharField(
"状态", max_length=32, choices=STATUS_CHOICES, default=STATUS_NORMAL
)
remark = models.CharField("备注", max_length=64, blank=True, default="")

class Meta:
verbose_name = "占用配置"
Expand All @@ -857,16 +875,47 @@ def __str__(self) -> str:
return f"占用配置:{self.id}"

@classmethod
def get_by_proxy_node(cls, node: ProxyNode):
return cls.objects.filter(proxy_node=node).first()
def get_purchasable_proxy_nodes(cls, user: User):
# 1. get all proxy nodes that have occupancy config
query = cls.objects.filter(occupancy_user_limit__gt=0)
# 2. filter out nodes that has been occupied by other users
occupied_node_ids = UserProxyNodeOccupancy.get_occupied_node_ids()
not_occupied_node_ids = query.exclude(
proxy_node_id__in=occupied_node_ids
).values("proxy_node_id")
# 3. add nodes that has been occupied by user but not exceed limit
not_reach_limit_node_ids = []
for node_query in occupied_node_ids:
node_id = node_query["proxy_node_id"]
cfg = cls.objects.get(proxy_node_id=node_id)
if not cfg.reach_limit(user):
not_reach_limit_node_ids.append(node_id)
return ProxyNode.objects.filter(id__in=not_occupied_node_ids).select_related(
"occupancy_config"
) | ProxyNode.objects.filter(id__in=not_reach_limit_node_ids).select_related(
"occupancy_config"
)

def to_snapshot(self):
return {
"proxy_node_id": self.proxy_node.id,
"occupancy_price": self.occupancy_price,
"occupancy_traffic": self.occupancy_traffic,
"occupancy_user_limit": self.occupancy_user_limit,
}
def reach_limit(self, user: User):
node = self.proxy_node
# 1. check if node has been occupied by user, if yes, return False because user can occupy same node multiple times
node_user_ids = [
i["user_id"]
for i in UserProxyNodeOccupancy.get_node_occupancy_user_ids(node)
]
if user.id in node_user_ids:
return False
else:
return len(node_user_ids) >= self.occupancy_user_limit

@property
def human_occupancy_traffic(self):
return utils.traffic_format(self.occupancy_traffic)

@property
def bulma_is_active(self):
if self.status == self.STATUS_ACTIVE:
return "is-active"


class UserProxyNodeOccupancy(BaseModel):
Expand Down Expand Up @@ -899,15 +948,14 @@ def _valid_occupancy_query(cls):

@classmethod
@transaction.atomic
def create_occupancy(
cls, user: User, proxy_node: ProxyNode, occupancy_config: OccupancyConfig
):
def create_occupancy(cls, user: User, node: ProxyNode):
occupancy_config = node.occupancy_config
# check user limit first
if occupancy_config.occupancy_user_limit <= 0:
raise Exception("not allow to create occupancy record with user limit 0")
if occupancy_config.occupancy_user_limit > 0:
if (
cls.get_node_occupancies(proxy_node).count()
cls.get_node_occupancies(node).count()
>= occupancy_config.occupancy_user_limit
):
raise Exception("occupancy user limit exceed")
Expand All @@ -916,8 +964,10 @@ def create_occupancy(
if user.balance < occupancy_config.occupancy_price:
raise Exception("user balance not enough")

user.balance -= occupancy_config.occupancy_price
user.save()
# check if user already occupied this node
o = cls.objects.filter(user=user, proxy_node=proxy_node).first()
o = cls.objects.filter(user=user, proxy_node=node).first()
if o:
if o.out_of_usage():
# reset traffic and time when out of usage
Expand All @@ -928,13 +978,13 @@ def create_occupancy(
o.save()
else:
# incr traffic and time
o.end_time = o.end_time.add(days=30)
o.end_time = o.end_time + timedelta(days=30)
o.total_traffic += occupancy_config.occupancy_traffic
o.save()
else:
return cls.objects.create(
user=user,
proxy_node=proxy_node,
proxy_node=node,
start_time=utils.get_current_datetime(),
end_time=utils.get_current_datetime().add(days=30),
total_traffic=occupancy_config.occupancy_traffic,
Expand Down Expand Up @@ -964,6 +1014,33 @@ def check_and_incr_traffic(cls, user_id, proxy_node_id, traffic):
r.used_traffic += traffic
r.save()

@classmethod
def get_user_occupancies(cls, user: User):
return cls._valid_occupancy_query().filter(user=user)

def human_total_traffic(self):
return utils.traffic_format(self.total_traffic)

def human_used_traffic(self):
return utils.traffic_format(self.used_traffic)

def used_percentage(self):
return round(self.used_traffic / self.total_traffic, 2) * 100

@property
def progress_color(self):
percentage = self.used_percentage()
if percentage < 20:
return c.BULMA_COLOR_SUCCESS
elif percentage < 40:
return c.BULMA_COLOR_INFO
elif percentage < 60:
return c.BULMA_COLOR_LINK
elif percentage < 80:
return c.BULMA_COLOR_WARNING
else:
return c.BULMA_COLOR_DANGER

def out_of_usage(self):
return (
self.used_traffic >= self.total_traffic
Expand Down
4 changes: 4 additions & 0 deletions apps/sspanel/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@
# 捐赠/充值
path("shop/", views.ShopView.as_view(), name="shop"),
path("chargecenter/", views.ChargeView.as_view(), name="chargecenter"),
# 独享节点
path(
"node_occupancy/", views.ProxyNodeOccupancyView.as_view(), name="node_occupancy"
),
# 公告
path("announcement/", views.AnnouncementView.as_view(), name="announcement"),
# 工单
Expand Down
31 changes: 30 additions & 1 deletion apps/sspanel/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
)

from apps.constants import THEME_CHOICES
from apps.proxy.models import ProxyNode
from apps.proxy.models import OccupancyConfig, ProxyNode, UserProxyNodeOccupancy
from apps.sspanel.forms import LoginForm, RegisterForm, TGLoginForm
from apps.sspanel.models import (
Announcement,
Expand Down Expand Up @@ -344,3 +344,32 @@ def get(self, request, pk):
else:
messages.error(request, "该工单不存在", extra_tags="删除失败")
return HttpResponseRedirect(reverse("sspanel:tickets"))


class ProxyNodeOccupancyView(LoginRequiredMixin, View):
def get(self, request):
purchasable_proxy_nodes = OccupancyConfig.get_purchasable_proxy_nodes(
request.user
)
occupies = UserProxyNodeOccupancy.get_user_occupancies(request.user)
context = {
"user": request.user,
"purchasable_proxy_nodes": purchasable_proxy_nodes,
"occupies": occupies,
}
return render(request, "web/node_occupancy.html", context=context)

def post(self, request):
node_id = request.POST.get("node_id")
node = ProxyNode.get_by_id(node_id)
if not node:
messages.error(request, "节点不存在", extra_tags="购买失败")
return HttpResponseRedirect(reverse("sspanel:node_occupancy"))
try:
UserProxyNodeOccupancy.create_occupancy(user=request.user, node=node)
except Exception as e:
messages.error(request, str(e), extra_tags="购买失败")
return HttpResponseRedirect(reverse("sspanel:node_occupancy"))
else:
messages.success(request, "更新订阅后使用", extra_tags=f"{node.name} 购买成功!")
return HttpResponseRedirect(reverse("sspanel:node_occupancy"))
Loading

0 comments on commit e4e6430

Please sign in to comment.