diff --git a/.gitignore b/.gitignore index b3030329fd..79b571e448 100644 --- a/.gitignore +++ b/.gitignore @@ -105,4 +105,9 @@ apps/Alipay_pub.pem *.dat #PyCharm -.idea/ \ No newline at end of file +.idea/ + +static/admin/ +static/jet/ +static/range_filter/ +*.log \ No newline at end of file diff --git a/apps/api/urls.py b/apps/api/urls.py index cf84f8801b..ad07039fdd 100644 --- a/apps/api/urls.py +++ b/apps/api/urls.py @@ -3,27 +3,31 @@ app_name = "api" urlpatterns = [ - path("user/data/", views.userData, name="userdata"), - path("node/data/", views.nodeData, name="nodedata"), - path("donate/data/", views.donateData, name="donatedata"), - path("random/port/", views.change_ss_port, name="changessport"), + path("system_status/", views.SystemStatusView.as_view(), name="system_status"), + path("user/settings/", views.UserSettingsView.as_view(), name="user_settings"), + path("subscribe/", views.SubscribeView.as_view(), name="subscribe"), + path("reset_ss_port/", views.ReSetSSPortView.as_view(), name="reset_ss_port"), path("gen/invitecode/", views.gen_invite_code, name="geninvitecode"), path("shop/", views.purchase, name="purchase"), - path("traffic/query/", views.traffic_query, name="traffic_query"), path("change/theme/", views.change_theme, name="change_theme"), path("change/sub_type/", views.change_sub_type, name="change_sub_type"), - path("checkin/", views.checkin, name="checkin"), - # 邀请码接口 - path("get/invitecode/", views.get_invitecode, name="get_invitecode"), + path("checkin/", views.UserCheckInView.as_view(), name="checkin"), # web api 接口 - path("nodes/", views.node_api, name="get_node_info"), - path("nodes/online", views.node_online_api, name="post_onlineip"), path( - "users/nodes/", views.node_user_configs, name="node_user_configs" + "user_ss_config//", + views.UserSSConfigView.as_view(), + name="user_ss_config", ), - path("traffic/upload", views.traffic_api, name="post_traffic"), - path("nodes/aliveip", views.alive_ip_api, name="post_aliveip"), # 支付 path("orders", views.OrderView.as_view(), name="order"), path("callback/alipay", views.ailpay_callback, name="alipay_callback"), + # user stats + path( + "user/stats/ref_chart", views.UserRefChartView.as_view(), name="user_ref_chart" + ), + path( + "user/stats/traffic_chart", + views.UserTrafficChartView.as_view(), + name="user_traffic_chart", + ), ] diff --git a/apps/api/views.py b/apps/api/views.py index 82ba2aaecb..5728b6d2fb 100644 --- a/apps/api/views.py +++ b/apps/api/views.py @@ -1,85 +1,237 @@ -import time +import base64 import pendulum -from django.views import View -from django.db.models import F from django.conf import settings -from django.http import JsonResponse +from django.contrib.auth.decorators import login_required, permission_required +from django.http import HttpResponseNotFound, JsonResponse, StreamingHttpResponse from django.shortcuts import HttpResponse -from ratelimit.decorators import ratelimit +from django.utils.decorators import method_decorator +from django.views import View from django.views.decorators.csrf import csrf_exempt from django.views.decorators.http import require_http_methods -from django.contrib.auth.decorators import login_required, permission_required +from ratelimit.decorators import ratelimit +from apps.encoder import encoder from apps.payments import pay -from apps.utils import traffic_format, simple_cached_view, authorized -from apps.ssserver.models import Suser, TrafficLog, Node, NodeOnlineLog, AliveIp -from apps.sspanel.models import InviteCode, Goods, User, Donate, UserOrder +from apps.sspanel.models import ( + Donate, + Goods, + InviteCode, + User, + UserOrder, + UserRefLog, + UserOnLineIpLog, + UserTrafficLog, + SSNodeOnlineLog, + SSNode, + UserCheckInLog, + UserTraffic, + UserSSConfig, +) +from apps.utils import api_authorized, handle_json_post, traffic_format + + +class SystemStatusView(View): + @method_decorator(permission_required("sspanel")) + def get(self, request): + user_status = [ + SSNodeOnlineLog.get_all_node_online_user_count(), + User.get_today_register_user().count(), + UserCheckInLog.get_today_checkin_user_count(), + UserTraffic.get_never_used_user_count(), + ] + donate_status = [ + Donate.get_donate_count_by_date(), + Donate.get_donate_money_by_date(), + Donate.get_donate_count_by_date(date=pendulum.today()), + Donate.get_donate_money_by_date(date=pendulum.today()), + ] + + active_nodes = SSNode.get_active_nodes() + node_status = { + "names": [node.name for node in active_nodes], + "traffics": [ + round(node.used_traffic / settings.GB, 2) for node in active_nodes + ], + } + data = { + "user_status": user_status, + "donate_status": donate_status, + "node_status": node_status, + } + return JsonResponse(data) -@permission_required("sspanel") -def userData(request): - """ - 返回用户信息: - 在线人数、今日签到、从未签到、从未使用 - """ - - data = [ - NodeOnlineLog.totalOnlineUser(), - User.get_today_register_user().count(), - Suser.get_today_checked_user_num(), - Suser.get_never_checked_user_num(), - Suser.get_never_used_num(), - ] - return JsonResponse({"data": data}) +class UserSettingsView(View): + @csrf_exempt + def dispatch(self, *args, **kwargs): + return super(UserSettingsView, self).dispatch(*args, **kwargs) + @method_decorator(handle_json_post) + @method_decorator(login_required) + def post(self, request): + config = request.user.user_ss_config -@permission_required("sspanel") -def nodeData(request): - """ - 返回节点信息 - 所有节点名 - 各自消耗的流量 - """ - nodeName = [node.name for node in Node.objects.filter(show=1)] + success = config.update_from_dict(data=request.json) + if success: + data = {"title": "修改成功!", "status": "success", "subtitle": "请及时更换客户端配置!"} + else: + data = {"title": "修改失败!", "status": "error", "subtitle": "配置更新失败!"} + return JsonResponse(data) - nodeTraffic = [ - round(node.used_traffic / settings.GB, 2) - for node in Node.objects.filter(show=1) - ] - data = {"nodeName": nodeName, "nodeTraffic": nodeTraffic} - return JsonResponse(data) +class SubscribeView(View): + def get(self, request): + token = request.GET.get("token") + if not token: + return HttpResponseNotFound() + user = User.get_by_pk(encoder.string2int(token)) + sub_links = user.get_sub_links() + sub_links = base64.b64encode(bytes(sub_links, "utf8")).decode("ascii") + resp = StreamingHttpResponse(sub_links) + resp["Content-Type"] = "application/octet-stream; charset=utf-8" + resp["Content-Disposition"] = "attachment; filename={}.txt".format(token) + resp["Cache-Control"] = "no-store, no-cache, must-revalidate" + resp["Content-Length"] = len(sub_links) + return resp + + +class UserRefChartView(View): + @method_decorator(login_required) + def get(self, request): + # 最近10天的 + date = request.GET.get("date") + t = pendulum.parse(date) if date else pendulum.now() + date_list = [t.add(days=i).date() for i in range(-7, 3)] + bar_configs = UserRefLog.gen_bar_chart_configs(request.user.id, date_list) + return JsonResponse(bar_configs) -@permission_required("sspanel") -def donateData(request): - """ - 返回捐赠信息 - 捐赠笔数 - 捐赠总金额 - """ - data = [Donate.totalDonateNums(), int(Donate.totalDonateMoney())] - return JsonResponse({"data": data}) +class UserTrafficChartView(View): + @method_decorator(login_required) + def get(self, request): + node_id = request.GET.get("node_id", 0) + user_id = request.user.pk + now = pendulum.now() + last_week = [now.subtract(days=i).date() for i in range(6, -1, -1)] + configs = UserTrafficLog.gen_line_chart_configs(user_id, node_id, last_week) + return JsonResponse(configs) + + +class UserSSConfigView(View): + @csrf_exempt + def dispatch(self, *args, **kwargs): + return super(UserSSConfigView, self).dispatch(*args, **kwargs) + + @method_decorator(api_authorized) + def get(self, request, node_id): + configs = SSNode.get_user_ss_configs_by_node_id(node_id) + return JsonResponse(configs) + + @method_decorator(handle_json_post) + @method_decorator(api_authorized) + def post(self, request, node_id): + """ + 这个接口操作比较重,所以为了避免发信号 + 所有写操作都需要用BULK的方式 + 1 更新节点流量 + 2 更新用户流量 + 3 记录节点在线IP + 4 关闭超出流量的用户 + 5 关闭超出流量的节点 + """ + data = request.json["data"] + log_time = pendulum.now() + node_total_traffic = 0 + trafficlog_model_list = [] + user_traffic_model_list = [] + online_ip_log_model_list = [] + user_ss_config_model_list = [] + + for user_data in data: + user_id = user_data["user_id"] + u = user_data["upload_traffic"] + d = user_data["download_traffic"] + + # 个人流量增量 + user_traffic = UserTraffic.get_by_user_id(user_id) + user_traffic.download_traffic += d + user_traffic.upload_traffic += u + user_traffic.last_use_time = log_time + user_traffic_model_list.append(user_traffic) + if user_traffic.overflow: + user_ss_config = UserSSConfig.get_by_user_id(user_id) + user_ss_config.enable = False + user_ss_config_model_list.append(user_ss_config) + # 个人流量记录 + trafficlog_model_list.append( + UserTrafficLog( + node_id=node_id, + user_id=user_id, + download_traffic=u, + upload_traffic=d, + ) + ) + # 节点流量增量 + node_total_traffic += u + d + # online ip log + for ip in user_data.get("ip_list", []): + online_ip_log_model_list.append( + UserOnLineIpLog(user_id=user_id, node_id=node_id, ip=ip) + ) + + # 节点流量记录 + SSNode.increase_used_traffic(node_id, node_total_traffic) + # 流量记录 + UserTrafficLog.objects.bulk_create(trafficlog_model_list) + # 在线IP + UserOnLineIpLog.objects.bulk_create(online_ip_log_model_list) + # 个人流量记录 + UserTraffic.objects.bulk_update( + user_traffic_model_list, + ["download_traffic", "upload_traffic", "last_use_time"], + ) + # 用户开关 + UserSSConfig.objects.bulk_update(user_ss_config_model_list, ["enable"]) + # 节点在线人数 + SSNodeOnlineLog.add_log(node_id, len(data)) + # check node && user traffic + ss_node = SSNode.get_or_none_by_node_id(node_id) + if ss_node.overflow: + ss_node.enable = False + if user_ss_config_model_list or ss_node.overflow: + # NOTE save for clear cache + ss_node.save() + return JsonResponse({"ret": 1, "data": []}) + + +class UserCheckInView(View): + @method_decorator(login_required) + def post(self, request): + user = request.user + if not user.today_is_checkin: + log = UserCheckInLog.checkin(user.pk) + data = { + "title": "签到成功!", + "subtitle": f"获得{traffic_format(log.increased_traffic)}流量!", + "status": "success", + } + else: + data = {"title": "签到失败!", "subtitle": "今天已经签到过了", "status": "error"} + return JsonResponse(data) -@login_required -def change_ss_port(request): - """ - 随机重置用户用端口 - 返回是否成功 - """ - user = request.user.ss_user - # 找到端口池中最大的端口 - port = Suser.get_random_port() - user.port = port - user.save() - registerinfo = { - "title": "修改成功!", - "subtitle": "端口修改为:{}!".format(port), - "status": "success", - } - return JsonResponse(registerinfo) +class ReSetSSPortView(View): + @method_decorator(login_required) + def post(self, request): + user_ss_config = request.user.user_ss_config + port = user_ss_config.reset_random_port() + data = { + "title": "修改成功!", + "subtitle": "端口修改为:{}!".format(port), + "status": "success", + } + return JsonResponse(data) @login_required @@ -88,16 +240,8 @@ def gen_invite_code(request): 生成用户的邀请码 返回是否成功 """ - u = request.user - if u.is_superuser is True: - # 针对管理员特出处理,每次生成5个邀请码 - num = 5 - else: - num = u.invitecode_num - len(InviteCode.objects.filter(code_id=u.pk)) + num = InviteCode.create_by_user(request.user) if num > 0: - for i in range(num): - code = InviteCode(code_type=0, code_id=u.pk) - code.save() registerinfo = { "title": "成功", "subtitle": "添加邀请码{}个,请刷新页面".format(num), @@ -123,34 +267,6 @@ def purchase(request): ) -@login_required -def traffic_query(request): - """ - 流量查请求 - """ - node_id = request.POST.get("node_id", 0) - node_name = request.POST.get("node_name", "") - user_id = request.user.pk - now = pendulum.now() - last_week = [now.subtract(days=i).date() for i in range(6, -1, -1)] - labels = ["{}-{}".format(t.month, t.day) for t in last_week] - traffic_data = [ - TrafficLog.get_traffic_by_date(node_id, user_id, t) for t in last_week - ] - total = TrafficLog.get_user_traffic(node_id, user_id) - title = "节点 {} 当月共消耗:{}".format(node_name, total) - - configs = { - "title": title, - "labels": labels, - "data": traffic_data, - "data_title": node_name, - "x_label": "日期 最近七天", - "y_label": "流量 单位:MB", - } - return JsonResponse(configs) - - @login_required def change_theme(request): """ @@ -174,146 +290,6 @@ def change_sub_type(request): return JsonResponse(res) -@authorized -@csrf_exempt -@require_http_methods(["POST"]) -def get_invitecode(request): - """ - 获取邀请码接口 - 只开放给管理员账号 - 返回一个没用过的邀请码 - 需要验证token - """ - admin_user = User.objects.filter(is_superuser=True).first() - code = InviteCode.objects.filter(code_id=admin_user.pk, isused=False).first() - if code: - return JsonResponse({"msg": code.code}) - else: - return JsonResponse({"msg": "邀请码用光啦"}) - - -@authorized -@simple_cached_view() -@require_http_methods(["GET"]) -def node_api(request, node_id): - """ - 返回节点信息 - 筛选节点是否用光 - """ - node = Node.objects.filter(node_id=node_id).first() - if node and node.used_traffic < node.total_traffic: - data = (node.traffic_rate,) - else: - data = None - res = {"ret": 1, "data": data} - return JsonResponse(res) - - -@authorized -@csrf_exempt -@require_http_methods(["POST"]) -def node_online_api(request): - """ - 接受节点在线人数上报 - """ - data = request.json - node = Node.objects.filter(node_id=data["node_id"]).first() - if node: - NodeOnlineLog.objects.create( - node_id=data["node_id"], - online_user=data["online_user"], - log_time=int(time.time()), - ) - res = {"ret": 1, "data": []} - return JsonResponse(res) - - -@authorized -@require_http_methods(["GET"]) -def node_user_configs(request, node_id): - res = {"ret": 1, "data": Suser.get_user_configs_by_node_id(node_id)} - return JsonResponse(res) - - -@authorized -@csrf_exempt -@require_http_methods(["POST"]) -def traffic_api(request): - """ - 接受服务端的用户流量上报 - """ - data = request.json - node_id = data["node_id"] - traffic_list = data["data"] - log_time = int(time.time()) - - node_total_traffic = 0 - trafficlog_model_list = [] - - for rec in traffic_list: - user_id = rec["user_id"] - u = rec["u"] - d = rec["d"] - # 个人流量增量 - Suser.objects.filter(user_id=user_id).update( - download_traffic=F("download_traffic") + d, - upload_traffic=F("upload_traffic") + u, - last_use_time=log_time, - ) - # 个人流量记录 - trafficlog_model_list.append( - TrafficLog( - node_id=node_id, - user_id=user_id, - traffic=traffic_format(u + d), - download_traffic=u, - upload_traffic=d, - log_time=log_time, - ) - ) - # 节点流量增量 - node_total_traffic += u + d - # 节点流量记录 - Node.objects.filter(node_id=node_id).update( - used_traffic=F("used_traffic") + node_total_traffic - ) - # 流量记录 - TrafficLog.objects.bulk_create(trafficlog_model_list) - return JsonResponse({"ret": 1, "data": []}) - - -@authorized -@csrf_exempt -@require_http_methods(["POST"]) -def alive_ip_api(request): - data = request.json - node_id = data["node_id"] - model_list = [] - for user_id, ip_list in data["data"].items(): - user = User.objects.get(id=user_id) - for ip in ip_list: - model_list.append(AliveIp(node_id=node_id, user=user.username, ip=ip)) - AliveIp.objects.bulk_create(model_list) - res = {"ret": 1, "data": []} - return JsonResponse(res) - - -@login_required -def checkin(request): - """用户签到""" - ss_user = request.user.ss_user - res, traffic = ss_user.checkin() - if res: - data = { - "title": "签到成功!", - "subtitle": "获得{}流量!".format(traffic_format(traffic)), - "status": "success", - } - else: - data = {"title": "签到失败!", "subtitle": "距离上次签到不足一天", "status": "error"} - return JsonResponse(data) - - @csrf_exempt @require_http_methods(["POST"]) def ailpay_callback(request): @@ -329,6 +305,7 @@ def ailpay_callback(request): class OrderView(View): + @method_decorator(login_required) def get(self, request): user = request.user order = UserOrder.get_recent_created_order(user) @@ -339,10 +316,10 @@ def get(self, request): info = {"title": "支付查询失败!", "subtitle": "亲,确认支付了么?", "status": "error"} return JsonResponse({"info": info}) - @ratelimit(key="user", rate="1/1s") + @method_decorator(login_required) + @ratelimit(key="user", rate="1/1s", block=True) def post(self, request): amount = int(request.POST.get("num")) - if amount < 1: info = {"title": "失败", "subtitle": "请保证金额大于1元", "status": "error"} else: diff --git a/apps/connector.py b/apps/connector.py new file mode 100644 index 0000000000..5984b56abf --- /dev/null +++ b/apps/connector.py @@ -0,0 +1,44 @@ +from django.db.models.signals import pre_delete, post_save +from apps.sspanel import models as m +from apps.utils import cache + + +def clear_get_user_ss_configs_by_node_id_cache(sender, instance, *args, **kwargs): + + if isinstance(instance, (m.UserSSConfig, m.UserTraffic)): + user = m.User.get_by_pk(instance.user_id) + node_ids = m.SSNode.get_node_ids_by_level(user.level) + elif isinstance(instance, m.SSNode): + node_ids = [instance.node_id] + elif isinstance(instance, m.User): + node_ids = m.SSNode.get_node_ids_by_level(instance.level) + else: + return + + keys = [ + m.SSNode.get_user_ss_configs_by_node_id.make_cache_key(m.SSNode, node_id) + for node_id in node_ids + ] + cache.delete_many(keys) + + +def clear_user_get_by_pk_cache(sender, instance, *args, **kwargs): + key = m.User.get_by_pk.make_cache_key(m.User, instance.pk) + cache.delete(key) + + +def register_connectors(): + + post_save.connect(clear_get_user_ss_configs_by_node_id_cache, sender=m.SSNode) + pre_delete.connect(clear_get_user_ss_configs_by_node_id_cache, sender=m.SSNode) + + post_save.connect(clear_get_user_ss_configs_by_node_id_cache, sender=m.UserSSConfig) + pre_delete.connect( + clear_get_user_ss_configs_by_node_id_cache, sender=m.UserSSConfig + ) + + post_save.connect(clear_user_get_by_pk_cache, sender=m.User) + pre_delete.connect(clear_user_get_by_pk_cache, sender=m.User) + + post_save.connect(clear_user_get_by_pk_cache, sender=m.UserTraffic) + pre_delete.connect(clear_user_get_by_pk_cache, sender=m.UserTraffic) diff --git a/apps/constants.py b/apps/constants.py index ce7f917925..29c81bbd80 100644 --- a/apps/constants.py +++ b/apps/constants.py @@ -75,5 +75,4 @@ # 判断节点在线时间间隔 NODE_TIME_OUT = 75 - DEFAULT_CACHE_TTL = 60 * 60 * 2 diff --git a/apps/custom_views.py b/apps/custom_views.py index f0936e64c1..f2cab88336 100644 --- a/apps/custom_views.py +++ b/apps/custom_views.py @@ -1,16 +1,16 @@ -from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger +from django.core.paginator import Paginator, EmptyPage -class Page_List_View(object): +class PageListView: """ 拥有翻页功能的通用类 Args: request : django request - obj: 等待分分页的列表,例如 User.objects.all() + obj_list: 等待分分页的列表 page_num: 分页的页数 """ - def __init__(self, request, obj_list, page_num): + def __init__(self, request, obj_list, page_num=10): self.request = request self.obj_list = obj_list self.page_num = page_num @@ -20,14 +20,10 @@ def get_page_context(self): # 每页显示10条记录 paginator = Paginator(self.obj_list, self.page_num) # 构造分页.获取当前页码数量 - page = self.request.GET.get("page") + page = int(self.request.GET.get("page", 1)) # 页码为1时,防止异常 try: contacts = paginator.page(page) - page = int(page) - except PageNotAnInteger: - contacts = paginator.page(1) - page = 1 except EmptyPage: contacts = paginator.page(paginator.num_pages) # 获得整个分页页码列表 diff --git a/apps/encoder.py b/apps/encoder.py new file mode 100644 index 0000000000..e8b6da7931 --- /dev/null +++ b/apps/encoder.py @@ -0,0 +1,16 @@ +from short_url import UrlEncoder +from django.conf import settings + + +class Encoder: + def __init__(self, alphabet): + self.encoder = UrlEncoder(alphabet=alphabet) + + def int2string(self, value): + return self.encoder.encode_url(value) + + def string2int(self, value): + return self.encoder.decode_url(value) + + +encoder = Encoder(alphabet=settings.DEFAULT_ALPHABET) diff --git a/apps/mixin.py b/apps/mixin.py new file mode 100644 index 0000000000..78a515d43c --- /dev/null +++ b/apps/mixin.py @@ -0,0 +1,17 @@ +from django.contrib.auth.mixins import LoginRequiredMixin +from django.http import HttpResponseForbidden +from django.utils.decorators import method_decorator +from django.views.decorators.csrf import csrf_exempt + + +class CSRFExemptMixin: + @method_decorator(csrf_exempt) + def dispatch(self, *args, **kwargs): + return super(CSRFExemptMixin, self).dispatch(*args, **kwargs) + + +class StaffRequiredMixin(LoginRequiredMixin): + def dispatch(self, request, *args, **kwargs): + if not request.user.is_staff: + return HttpResponseForbidden() + return super().dispatch(request, *args, **kwargs) diff --git a/apps/payments.py b/apps/payments.py index cdf348fcef..8c3b5c1cd3 100644 --- a/apps/payments.py +++ b/apps/payments.py @@ -26,7 +26,7 @@ def __init__(self, app_id, pub_key_path, pri_key_path): def init_payment(self): self.alipay = AliPay( appid=APPID, - app_notify_url="", + app_notify_url=settings.ALIPAY_CALLBACK_URL, app_private_key_path=PRIVATE_KEY_PATH, # 支付宝的公钥,验证支付宝回传消息使用,不是你自己的公钥, alipay_public_key_path=PUBLIC_KEY_PATH, diff --git a/apps/sspanel/admin.py b/apps/sspanel/admin.py index cc35e5ac45..48d87b099b 100644 --- a/apps/sspanel/admin.py +++ b/apps/sspanel/admin.py @@ -10,6 +10,88 @@ class UserAdmin(admin.ModelAdmin): list_filter = ["level"] +class UserOrderAdmin(admin.ModelAdmin): + list_display = [ + "user", + "status", + "out_trade_no", + "amount", + "created_at", + "expired_at", + ] + search_fields = ["user__username", "user__id"] + list_filter = ["user", "amount", "status", "created_at"] + ordering = ("-created_at",) + + +class UserOnLineIpLogAdmin(admin.ModelAdmin): + list_display = ["user", "user_id", "node_id", "ip", "created_at"] + + search_fields = ["user_id"] + + +class UserTrafficLogAdmin(admin.ModelAdmin): + + list_display = ["user", "user_id", "node_id", "total_traffic", "date"] + search_fields = ["user_id", "node_id"] + list_filter = ["date", "user_id", "node_id"] + + +class UserSSConfigAdmin(admin.ModelAdmin): + list_display = [ + "user", + "user_id", + "port", + "password", + "method", + "speed_limit", + "human_used_traffic", + "human_total_traffic", + "enable", + ] + search_fields = ["user_id", "port"] + list_filter = ["enable"] + + +class UserCheckInAdmin(admin.ModelAdmin): + list_display = ["user", "user_id", "increased_traffic", "date"] + search_fields = ["user_id", "date"] + list_filter = ["date"] + + +class UserRefLogAdmin(admin.ModelAdmin): + list_display = ["user", "user_id", "register_count", "date"] + search_fields = ["user_id", "date"] + list_filter = ["date"] + + +class UserTrafficAdmin(admin.ModelAdmin): + list_display = [ + "user", + "user_id", + "human_used_traffic", + "used_percentage", + "overflow", + ] + search_fields = ["user_id", "last_use_time"] + + +class SSNodeOnlineLogAdmin(admin.ModelAdmin): + list_display = ["node_id", "online_user_count", "created_at"] + + +class SSNodeAdmin(admin.ModelAdmin): + list_display = [ + "name", + "node_id", + "level", + "server", + "human_used_traffic", + "human_total_traffic", + "enable", + ] + + class PurchaseHistoryAdmin(admin.ModelAdmin): list_display = ["good", "user", "money", "purchtime"] search_fields = ["user"] @@ -17,7 +99,7 @@ class PurchaseHistoryAdmin(admin.ModelAdmin): class InviteCodeAdmin(admin.ModelAdmin): - list_display = ["code", "time_created", "isused", "code_type"] + list_display = ["code", "created_at", "used", "code_type"] search_fields = ["code"] @@ -34,22 +116,18 @@ class GoodsAdmin(admin.ModelAdmin): list_display = ["name", "transfer", "money", "level"] -class UserOrderAdmin(admin.ModelAdmin): - list_display = [ - "user", - "status", - "out_trade_no", - "amount", - "created_at", - "expired_at", - ] - search_fields = ["user__username", "user__id"] - list_filter = ["user", "amount", "status"] - ordering = ("-created_at",) - - # Register your models here. admin.site.register(models.User, UserAdmin) +admin.site.register(models.UserOrder, UserOrderAdmin) +admin.site.register(models.UserOnLineIpLog, UserOnLineIpLogAdmin) +admin.site.register(models.UserTrafficLog, UserTrafficLogAdmin) +admin.site.register(models.UserSSConfig, UserSSConfigAdmin) +admin.site.register(models.UserCheckInLog, UserCheckInAdmin) +admin.site.register(models.UserRefLog, UserRefLogAdmin) +admin.site.register(models.UserTraffic, UserTrafficAdmin) +admin.site.register(models.SSNodeOnlineLog, SSNodeOnlineLogAdmin) +admin.site.register(models.SSNode, SSNodeAdmin) + admin.site.register(models.InviteCode, InviteCodeAdmin) admin.site.register(models.Donate, DonateAdmin) admin.site.register(models.MoneyCode, MoneyCodeAdmin) @@ -57,6 +135,6 @@ class UserOrderAdmin(admin.ModelAdmin): admin.site.register(models.PurchaseHistory, PurchaseHistoryAdmin) admin.site.register(models.Announcement) admin.site.register(models.Ticket) -admin.site.register(models.UserOrder, UserOrderAdmin) + admin.site.unregister(Group) diff --git a/apps/sspanel/admin_views.py b/apps/sspanel/admin_views.py new file mode 100644 index 0000000000..e5877ee495 --- /dev/null +++ b/apps/sspanel/admin_views.py @@ -0,0 +1,368 @@ +import tomd +from django.conf import settings +from django.contrib import messages +from django.contrib.auth.decorators import permission_required +from django.db.models import Q +from django.http import HttpResponseRedirect +from django.shortcuts import render +from django.urls import reverse +from django.views import View + +from apps.custom_views import PageListView +from apps.mixin import StaffRequiredMixin +from apps.sspanel.forms import AnnoForm, GoodsForm, SSNodeForm, UserSSConfigForm +from apps.sspanel.models import ( + Announcement, + Donate, + Goods, + InviteCode, + MoneyCode, + PurchaseHistory, + SSNode, + SSNodeOnlineLog, + Ticket, + User, + UserOnLineIpLog, + UserCheckInLog, + UserTraffic, + UserSSConfig, +) + + +class UserOnlineIpLogView(StaffRequiredMixin, View): + def get(self, request): + data = [] + for node in SSNode.get_active_nodes(): + data.extend(UserOnLineIpLog.get_recent_log_by_node_id(node.node_id)) + context = PageListView(request, data).get_page_context() + return render(request, "backend/user_online_ip_log.html", context=context) + + +class SSNodeView(StaffRequiredMixin, View): + def get(self, request): + form = SSNodeForm() + return render(request, "backend/ss_node_detail.html", context={"form": form}) + + def post(self, request): + form = SSNodeForm(request.POST) + if form.is_valid(): + form.save() + messages.success(request, "数据更新成功!", extra_tags="添加成功") + return HttpResponseRedirect(reverse("sspanel:backend_ss_node_list")) + else: + messages.error(request, "数据填写错误", extra_tags="错误") + context = {"form": form} + return render(request, "backend/ss_node_detail.html", context=context) + + +class SSNodeListView(StaffRequiredMixin, View): + def get(self, request): + context = {"ss_node_list": SSNode.objects.all()} + return render(request, "backend/ss_node_list.html", context=context) + + +class SSNodeDetailView(StaffRequiredMixin, View): + def get(self, request, node_id): + ss_node = SSNode.objects.get(node_id=node_id) + form = SSNodeForm(instance=ss_node) + return render(request, "backend/ss_node_detail.html", context={"form": form}) + + def post(self, request, node_id): + ss_node = SSNode.objects.get(node_id=node_id) + form = SSNodeForm(request.POST, instance=ss_node) + if form.is_valid(): + form.save() + messages.success(request, "数据更新成功", extra_tags="修改成功") + return HttpResponseRedirect(reverse("sspanel:backend_ss_node_list")) + else: + messages.error(request, "数据填写错误", extra_tags="错误") + context = {"form": form, "ss_node": ss_node} + return render(request, "backend/ss_node_detail.html", context=context) + + +class SSNodeDeleteView(StaffRequiredMixin, View): + def get(self, request, node_id): + ss_node = SSNode.get_or_none_by_node_id(node_id) + node_id and ss_node.delete() + messages.success(request, "成功啦", extra_tags="删除节点") + return HttpResponseRedirect(reverse("sspanel:backend_ss_node_list")) + + +class UserSSConfigListView(StaffRequiredMixin, View): + def get(self, request): + context = PageListView( + request, User.objects.all().order_by("-date_joined") + ).get_page_context() + + return render(request, "backend/user_ss_config_list.html", context) + + +class UserSSConfigDeleteView(StaffRequiredMixin, View): + def get(self, request, user_id): + user = User.get_by_pk(user_id) + user.delete() + messages.success(request, "成功啦", extra_tags="删除用户") + return HttpResponseRedirect(reverse("sspanel:backend_user_ss_config_list")) + + +class UserSSConfigSearchView(StaffRequiredMixin, View): + def get(self, request): + q = request.GET.get("q") + contacts = User.objects.filter( + Q(username__icontains=q) | Q(email__icontains=q) | Q(pk__icontains=q) + ) + context = {"contacts": contacts} + return render(request, "backend/user_ss_config_list.html", context=context) + + +class UserSSConfigDetailView(StaffRequiredMixin, View): + def get(self, request, user_id): + user_ss_config = UserSSConfig.get_by_user_id(user_id) + form = UserSSConfigForm(instance=user_ss_config) + return render( + request, "backend/user_ss_config_detail.html", context={"form": form} + ) + + def post(self, request, user_id): + user_ss_config = UserSSConfig.get_by_user_id(user_id) + form = UserSSConfigForm(request.POST, instance=user_ss_config) + if form.is_valid(): + form.save() + messages.success(request, "数据更新成功", extra_tags="修改成功") + return HttpResponseRedirect(reverse("sspanel:backend_user_ss_config_list")) + else: + messages.error(request, "数据填写错误", extra_tags="错误") + context = {"form": form, "user_ss_config": user_ss_config} + return render( + request, "backend/user_ss_config_detail.html", context=context + ) + + +class UserStatusView(StaffRequiredMixin, View): + def get(self, request): + today_register_user = User.get_today_register_user().values()[:10] + # find inviter + for u in today_register_user: + try: + u["inviter"] = User.objects.get(pk=u["inviter_id"]) + except User.DoesNotExist: + u["inviter"] = "None" + + context = { + "total_user_num": User.get_total_user_num(), + "alive_user_count": SSNodeOnlineLog.get_all_node_online_user_count(), + "today_checked_user_count": UserCheckInLog.get_today_checkin_user_count(), + "today_register_user_count": len(today_register_user), + "traffic_users": UserTraffic.get_user_order_by_traffic(count=10), + "rich_users_data": Donate.get_most_donated_user_by_count(10), + "today_register_user": today_register_user, + } + return render(request, "backend/user_status.html", context=context) + + +@permission_required("sspanel") +def system_status(request): + """跳转到后台界面""" + context = {"total_user_num": User.get_total_user_num()} + return render(request, "backend/index.html", context=context) + + +@permission_required("sspanel") +def backend_invite(request): + """邀请码生成""" + # TODO 这里加入一些统计功能 + code_list = InviteCode.objects.filter(code_type=0, used=False, user_id=1) + return render(request, "backend/invitecode.html", {"code_list": code_list}) + + +@permission_required("sspanel") +def gen_invite_code(request): + + Num = request.GET.get("num") + code_type = request.GET.get("type") + for i in range(int(Num)): + code = InviteCode(code_type=code_type) + code.save() + messages.success(request, "添加邀请码{}个".format(Num), extra_tags="成功") + return HttpResponseRedirect(reverse("sspanel:backend_invite")) + + +@permission_required("sspanel") +def backend_charge(request): + """后台充值码界面""" + # 获取所有充值码记录 + obj = MoneyCode.objects.all() + page_num = 10 + context = PageListView(request, obj, page_num).get_page_context() + # 获取充值的金额和数量 + Num = request.GET.get("num") + money = request.GET.get("money") + if Num and money: + for i in range(int(Num)): + code = MoneyCode(number=money) + code.save() + messages.success(request, "添加{}元充值码{}个".format(money, Num), extra_tags="成功") + return HttpResponseRedirect(reverse("sspanel:backend_charge")) + return render(request, "backend/charge.html", context=context) + + +@permission_required("sspanel") +def backend_shop(request): + """商品管理界面""" + + goods = Goods.objects.all() + context = {"goods": goods} + return render(request, "backend/shop.html", context=context) + + +@permission_required("sspanel") +def good_delete(request, pk): + """删除商品""" + good = Goods.objects.filter(pk=pk) + good.delete() + messages.success(request, "成功啦", extra_tags="删除商品") + return HttpResponseRedirect(reverse("sspanel:backend_shop")) + + +@permission_required("sspanel") +def good_edit(request, pk): + """商品编辑""" + + good = Goods.objects.get(pk=pk) + # 当为post请求时,修改数据 + if request.method == "POST": + # 转换为GB + data = request.POST.copy() + data["transfer"] = eval(data["transfer"]) * settings.GB + form = GoodsForm(data, instance=good) + if form.is_valid(): + form.save() + messages.success(request, "数据更新成功", extra_tags="修改成功") + return HttpResponseRedirect(reverse("sspanel:backend_shop")) + else: + messages.error(request, "数据填写错误", extra_tags="错误") + context = {"form": form, "good": good} + return render(request, "backend/goodedit.html", context=context) + # 当请求不是post时,渲染form + else: + data = {"transfer": round(good.transfer / settings.GB)} + form = GoodsForm(initial=data, instance=good) + context = {"form": form, "good": good} + return render(request, "backend/goodedit.html", context=context) + + +@permission_required("sspanel") +def good_create(request): + """商品创建""" + if request.method == "POST": + # 转换为GB + data = request.POST.copy() + data["transfer"] = eval(data["transfer"]) * settings.GB + form = GoodsForm(data) + if form.is_valid(): + form.save() + messages.success(request, "数据更新成功!", extra_tags="添加成功") + return HttpResponseRedirect(reverse("sspanel:backend_shop")) + else: + messages.error(request, "数据填写错误", extra_tags="错误") + context = {"form": form} + return render(request, "backend/goodcreate.html", context=context) + else: + form = GoodsForm() + return render(request, "backend/goodcreate.html", context={"form": form}) + + +@permission_required("sspanel") +def purchase_history(request): + """购买历史""" + obj = PurchaseHistory.objects.all() + page_num = 10 + context = PageListView(request, obj, page_num).get_page_context() + return render(request, "backend/purchasehistory.html", context=context) + + +@permission_required("sspanel") +def backend_anno(request): + """公告管理界面""" + anno = Announcement.objects.all() + context = {"anno": anno} + return render(request, "backend/annolist.html", context=context) + + +@permission_required("sspanel") +def anno_delete(request, pk): + """删除公告""" + anno = Announcement.objects.filter(pk=pk) + anno.delete() + messages.success(request, "成功啦", extra_tags="删除公告") + return HttpResponseRedirect(reverse("sspanel:backend_anno")) + + +@permission_required("sspanel") +def anno_create(request): + """公告创建""" + if request.method == "POST": + form = AnnoForm(request.POST) + if form.is_valid(): + form.save() + messages.success(request, "数据更新成功", extra_tags="添加成功") + return HttpResponseRedirect(reverse("sspanel:backend_anno")) + else: + messages.error(request, "数据填写错误", extra_tags="错误") + context = {"form": form} + return render(request, "backend/annocreate.html", context=context) + else: + form = AnnoForm() + return render(request, "backend/annocreate.html", context={"form": form}) + + +@permission_required("sspanel") +def anno_edit(request, pk): + """公告编辑""" + anno = Announcement.objects.get(pk=pk) + # 当为post请求时,修改数据 + if request.method == "POST": + form = AnnoForm(request.POST, instance=anno) + if form.is_valid(): + form.save() + messages.success(request, "数据更新成功", extra_tags="修改成功") + return HttpResponseRedirect(reverse("sspanel:backend_anno")) + else: + messages.error(request, "数据填写错误", extra_tags="错误") + context = {"form": form, "anno": anno} + return render(request, "backend/annoedit.html", context=context) + # 当请求不是post时,渲染form + else: + anno.body = tomd.convert(anno.body) + context = {"anno": anno} + return render(request, "backend/annoedit.html", context=context) + + +@permission_required("sspanel") +def backend_ticket(request): + """工单系统""" + ticket = Ticket.objects.filter(status=1) + context = {"ticket": ticket} + return render(request, "backend/ticket.html", context=context) + + +@permission_required("sspanel") +def backend_ticketedit(request, pk): + """后台工单编辑""" + ticket = Ticket.objects.get(pk=pk) + # 当为post请求时,修改数据 + if request.method == "POST": + title = request.POST.get("title", "") + body = request.POST.get("body", "") + status = request.POST.get("status", 1) + ticket.title = title + ticket.body = body + ticket.status = status + ticket.save() + + messages.success(request, "数据更新成功", extra_tags="修改成功") + return HttpResponseRedirect(reverse("sspanel:backend_ticket")) + # 当请求不是post时,渲染 + else: + context = {"ticket": ticket} + return render(request, "backend/ticketedit.html", context=context) diff --git a/apps/sspanel/forms.py b/apps/sspanel/forms.py index 5d8ab7d38a..39f7381b72 100644 --- a/apps/sspanel/forms.py +++ b/apps/sspanel/forms.py @@ -1,10 +1,16 @@ from django import forms -from django.conf import settings -from django.forms import ModelForm from django.contrib.auth.forms import UserCreationForm +from django.forms import ModelForm -from apps.ssserver.models import Node -from apps.sspanel.models import Announcement, Goods, User, InviteCode +from apps.encoder import encoder +from apps.sspanel.models import ( + Announcement, + Goods, + InviteCode, + User, + SSNode, + UserSSConfig, +) class RegisterForm(UserCreationForm): @@ -19,11 +25,6 @@ class RegisterForm(UserCreationForm): email = forms.EmailField( label="邮箱", widget=forms.TextInput(attrs={"class": "input is-warning"}) ) - invitecode = forms.CharField( - label="邀请码", - help_text="邀请码必须填写", - widget=forms.TextInput(attrs={"class": "input is-success"}), - ) password1 = forms.CharField( label="密码", help_text="""你的密码不能与其他个人信息太相似。 @@ -37,6 +38,23 @@ class RegisterForm(UserCreationForm): widget=forms.TextInput(attrs={"class": "input is-danger", "type": "password"}), ) + invitecode = forms.CharField( + label="邀请码", + help_text="邀请码必须填写", + widget=forms.TextInput(attrs={"class": "input is-success"}), + ) + + ref = forms.CharField( + label="邀请", widget=forms.TextInput(attrs={"class": "input is-success"}) + ) + + def __init__(self, *args, **kw): + super().__init__(*args, **kw) + if "ref" in self.data or "ref" in self.initial.keys(): + self.fields.pop("invitecode") + else: + self.fields.pop("ref") + def clean_email(self): email = self.cleaned_data.get("email") if User.objects.filter(email=email).first(): @@ -44,16 +62,41 @@ def clean_email(self): else: return email - def clean_invitecode(self): + def _post_clean(self): + super()._post_clean() + if "ref" in self.fields: + try: + self._clean_ref() + except forms.ValidationError as error: + self.add_error("ref", error) + if "invitecode" in self.fields: + try: + self._clean_invitecode() + except forms.ValidationError as error: + self.add_error("invitecode", error) + + def _clean_invitecode(self): code = self.cleaned_data.get("invitecode") - if InviteCode.objects.filter(code=code, isused=False).first(): + if InviteCode.objects.filter(code=code, used=False).exists(): return code else: raise forms.ValidationError("该邀请码失效") + def _clean_ref(self): + ref = self.cleaned_data.get("ref") + try: + user_id = encoder.string2int(ref) + except ValueError: + raise forms.ValidationError("ref不正确") + + if User.objects.filter(id=user_id).exists(): + return ref + else: + raise forms.ValidationError("ref不正确") + class Meta(UserCreationForm.Meta): model = User - fields = ("username", "email", "password1", "password2", "invitecode") + fields = ("username", "email", "password1", "password2", "invitecode", "ref") class LoginForm(forms.Form): @@ -81,18 +124,23 @@ def clean(self): self.cleaned_data = super(LoginForm, self).clean() -class NodeForm(ModelForm): - total_traffic = forms.IntegerField(label="节点总流量(GB)") - - def clean(self): - data = self.cleaned_data - data["total_traffic"] = data["total_traffic"] * settings.GB - return data - +class SSNodeForm(ModelForm): class Meta: - model = Node + model = SSNode fields = "__all__" - exclude = ["used_traffic"] + widgets = { + "node_id": forms.NumberInput(attrs={"class": "input"}), + "level": forms.NumberInput(attrs={"class": "input"}), + "name": forms.TextInput(attrs={"class": "input"}), + "info": forms.TextInput(attrs={"class": "input"}), + "server": forms.TextInput(attrs={"class": "input"}), + "method": forms.Select(attrs={"class": "input"}), + "country": forms.Select(attrs={"class": "input"}), + "used_traffic": forms.NumberInput(attrs={"class": "input"}), + "total_traffic": forms.NumberInput(attrs={"class": "input"}), + "enable": forms.CheckboxInput(attrs={"class": "checkbox"}), + "custom_method": forms.CheckboxInput(attrs={"class": "checkbox"}), + } class GoodsForm(ModelForm): @@ -116,3 +164,16 @@ class Meta: "level": forms.NumberInput(attrs={"class": "input"}), "level_expire_time": forms.DateTimeInput(attrs={"class": "input"}), } + + +class UserSSConfigForm(ModelForm): + class Meta: + model = UserSSConfig + fields = ["port", "password", "speed_limit", "method", "enable"] + widgets = { + "port": forms.NumberInput(attrs={"class": "input"}), + "speed_limit": forms.NumberInput(attrs={"class": "input"}), + "password": forms.TextInput(attrs={"class": "input"}), + "method": forms.Select(attrs={"class": "input"}), + "enable": forms.CheckboxInput(attrs={"class": "checkbox"}), + } diff --git a/apps/sspanel/migrations/0007_auto_20190420_2043.py b/apps/sspanel/migrations/0007_auto_20190420_2043.py new file mode 100644 index 0000000000..e20e452bff --- /dev/null +++ b/apps/sspanel/migrations/0007_auto_20190420_2043.py @@ -0,0 +1,31 @@ +# Generated by Django 2.1.7 on 2019-04-20 12:43 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [("sspanel", "0006_user_sub_type")] + + operations = [ + migrations.AlterField( + model_name="donate", + name="money", + field=models.DecimalField( + blank=True, + db_index=True, + decimal_places=2, + default=0, + max_digits=10, + null=True, + verbose_name="捐赠金额", + ), + ), + migrations.AlterField( + model_name="donate", + name="time", + field=models.DateTimeField( + auto_now_add=True, db_index=True, verbose_name="捐赠时间" + ), + ), + ] diff --git a/apps/sspanel/migrations/0008_auto_20190520_1639.py b/apps/sspanel/migrations/0008_auto_20190520_1639.py new file mode 100644 index 0000000000..aaf577d496 --- /dev/null +++ b/apps/sspanel/migrations/0008_auto_20190520_1639.py @@ -0,0 +1,31 @@ +# Generated by Django 2.1.7 on 2019-05-20 08:39 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [("sspanel", "0007_auto_20190420_2043")] + + operations = [ + migrations.AlterModelOptions( + name="invitecode", + options={"ordering": ("used", "-created_at"), "verbose_name_plural": "邀请码"}, + ), + migrations.RenameField( + model_name="invitecode", old_name="isused", new_name="used" + ), + migrations.RenameField( + model_name="invitecode", old_name="code_id", new_name="user_id" + ), + migrations.RenameField( + model_name="invitecode", old_name="time_created", new_name="created_at" + ), + migrations.RenameField( + model_name="user", old_name="invited_by", new_name="inviter_id" + ), + migrations.RenameField( + model_name="rebaterecord", old_name="rebatetime", new_name="created_at" + ), + migrations.RemoveField(model_name="user", name="invitecode"), + ] diff --git a/apps/sspanel/migrations/0009_auto_20190521_1332.py b/apps/sspanel/migrations/0009_auto_20190521_1332.py new file mode 100644 index 0000000000..e3f1e6396e --- /dev/null +++ b/apps/sspanel/migrations/0009_auto_20190521_1332.py @@ -0,0 +1,31 @@ +# Generated by Django 2.1.7 on 2019-05-21 05:32 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [("sspanel", "0008_auto_20190520_1639")] + + operations = [ + migrations.AlterModelOptions( + name="rebaterecord", options={"ordering": ("-created_at",)} + ), + migrations.AddField( + model_name="rebaterecord", + name="consumer_id", + field=models.PositiveIntegerField( + blank=True, null=True, verbose_name="消费者ID" + ), + ), + migrations.AlterField( + model_name="invitecode", + name="created_at", + field=models.DateTimeField(auto_now_add=True), + ), + migrations.AlterField( + model_name="rebaterecord", + name="created_at", + field=models.DateTimeField(auto_now_add=True), + ), + ] diff --git a/apps/sspanel/migrations/0010_auto_20190522_1326.py b/apps/sspanel/migrations/0010_auto_20190522_1326.py new file mode 100644 index 0000000000..3cc86684e2 --- /dev/null +++ b/apps/sspanel/migrations/0010_auto_20190522_1326.py @@ -0,0 +1,37 @@ +# Generated by Django 2.1.7 on 2019-05-22 05:26 + +from django.db import migrations, models +import pendulum + + +class Migration(migrations.Migration): + + dependencies = [("sspanel", "0009_auto_20190521_1332")] + + operations = [ + migrations.CreateModel( + name="UserRefLog", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("user_id", models.PositiveIntegerField()), + ("register_count", models.IntegerField(default=0)), + ( + "date", + models.DateField( + db_index=True, default=pendulum.today, verbose_name="记录日期" + ), + ), + ], + ), + migrations.AlterUniqueTogether( + name="userreflog", unique_together={("user_id", "date")} + ), + ] diff --git a/apps/sspanel/migrations/0011_useronlineiplog.py b/apps/sspanel/migrations/0011_useronlineiplog.py new file mode 100644 index 0000000000..73a10bc479 --- /dev/null +++ b/apps/sspanel/migrations/0011_useronlineiplog.py @@ -0,0 +1,34 @@ +# Generated by Django 2.2.1 on 2019-05-27 04:56 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [("sspanel", "0010_auto_20190522_1326")] + + operations = [ + migrations.CreateModel( + name="UserOnLineIpLog", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("user_id", models.IntegerField(db_index=True)), + ("node_id", models.IntegerField()), + ("ip", models.CharField(max_length=128)), + ("created_at", models.DateTimeField(auto_now_add=True, db_index=True)), + ], + options={ + "verbose_name_plural": "用户在线IP", + "ordering": ["-created_at"], + "unique_together": {("node_id", "created_at")}, + }, + ) + ] diff --git a/apps/sspanel/migrations/0012_auto_20190528_1407.py b/apps/sspanel/migrations/0012_auto_20190528_1407.py new file mode 100644 index 0000000000..c8b1d09119 --- /dev/null +++ b/apps/sspanel/migrations/0012_auto_20190528_1407.py @@ -0,0 +1,45 @@ +# Generated by Django 2.2.1 on 2019-05-28 06:07 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [("sspanel", "0011_useronlineiplog")] + + operations = [ + migrations.AlterUniqueTogether(name="useronlineiplog", unique_together=set()), + migrations.AlterIndexTogether( + name="useronlineiplog", index_together={("node_id", "created_at")} + ), + migrations.CreateModel( + name="UserTrafficLog", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("user_id", models.IntegerField()), + ("node_id", models.IntegerField()), + ("date", models.DateField(auto_now_add=True, db_index=True)), + ( + "upload_traffic", + models.BigIntegerField(default=0, verbose_name="上传流量"), + ), + ( + "download_traffic", + models.BigIntegerField(default=0, verbose_name="下载流量"), + ), + ], + options={ + "verbose_name_plural": "流量记录", + "ordering": ["-date"], + "index_together": {("user_id", "node_id", "date")}, + }, + ), + ] diff --git a/apps/sspanel/migrations/0013_ssnodeonlinelog.py b/apps/sspanel/migrations/0013_ssnodeonlinelog.py new file mode 100644 index 0000000000..6410b6ab77 --- /dev/null +++ b/apps/sspanel/migrations/0013_ssnodeonlinelog.py @@ -0,0 +1,33 @@ +# Generated by Django 2.2.1 on 2019-05-28 06:33 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [("sspanel", "0012_auto_20190528_1407")] + + operations = [ + migrations.CreateModel( + name="SSNodeOnlineLog", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("node_id", models.IntegerField()), + ("online_user_count", models.IntegerField(default=0)), + ("created_at", models.DateTimeField(auto_now_add=True, db_index=True)), + ], + options={ + "verbose_name_plural": "节点在线记录", + "ordering": ["-created_at"], + "index_together": {("node_id", "created_at")}, + }, + ) + ] diff --git a/apps/sspanel/migrations/0014_ssnode.py b/apps/sspanel/migrations/0014_ssnode.py new file mode 100644 index 0000000000..535ff4cd9f --- /dev/null +++ b/apps/sspanel/migrations/0014_ssnode.py @@ -0,0 +1,103 @@ +# Generated by Django 2.2.1 on 2019-05-29 02:51 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [("sspanel", "0013_ssnodeonlinelog")] + + operations = [ + migrations.CreateModel( + name="SSNode", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("node_id", models.IntegerField(unique=True)), + ("enable", models.BooleanField(db_index=True, default=True)), + ("level", models.PositiveIntegerField(default=0)), + ( + "custom_method", + models.BooleanField(default=False, verbose_name="自定义加密"), + ), + ("name", models.CharField(max_length=32, verbose_name="名字")), + ("info", models.CharField(max_length=1024, verbose_name="节点说明")), + ( + "country", + models.CharField( + choices=[ + ("US", "美国"), + ("CN", "中国"), + ("GB", "英国"), + ("SG", "新加坡"), + ("TW", "台湾"), + ("HK", "香港"), + ("JP", "日本"), + ("FR", "法国"), + ("DE", "德国"), + ("KR", "韩国"), + ("JE", "泽西岛"), + ("NZ", "新西兰"), + ("MX", "墨西哥"), + ("CA", "加拿大"), + ("BR", "巴西"), + ("CU", "古巴"), + ("CZ", "捷克"), + ("EG", "埃及"), + ("FI", "芬兰"), + ("GR", "希腊"), + ("GU", "关岛"), + ("IS", "冰岛"), + ("MO", "澳门"), + ("NL", "荷兰"), + ("NO", "挪威"), + ("PL", "波兰"), + ("IT", "意大利"), + ("IE", "爱尔兰"), + ("AR", "阿根廷"), + ("PT", "葡萄牙"), + ("AU", "澳大利亚"), + ("RU", "俄罗斯联邦"), + ("CF", "中非共和国"), + ], + default="CN", + max_length=5, + verbose_name="国家", + ), + ), + ("server", models.CharField(max_length=128, verbose_name="服务器地址")), + ( + "method", + models.CharField( + choices=[ + ("aes-256-cfb", "aes-256-cfb"), + ("aes-128-ctr", "aes-128-ctr"), + ("rc4-md5", "rc4-md5"), + ("salsa20", "salsa20"), + ("chacha20", "chacha20"), + ("none", "none"), + ], + default="aes-128-ctr", + max_length=32, + verbose_name="加密类型", + ), + ), + ( + "used_traffic", + models.BigIntegerField(default=0, verbose_name="已用流量"), + ), + ( + "total_traffic", + models.BigIntegerField(default=1073741824, verbose_name="总流量"), + ), + ], + options={"verbose_name_plural": "SS节点"}, + ) + ] diff --git a/apps/sspanel/migrations/0015_auto_20190601_1013.py b/apps/sspanel/migrations/0015_auto_20190601_1013.py new file mode 100644 index 0000000000..8a30063464 --- /dev/null +++ b/apps/sspanel/migrations/0015_auto_20190601_1013.py @@ -0,0 +1,43 @@ +# Generated by Django 2.2.1 on 2019-06-01 02:13 + +from django.db import migrations, models +import pendulum + + +class Migration(migrations.Migration): + + dependencies = [("sspanel", "0014_ssnode")] + + operations = [ + migrations.AlterField( + model_name="ssnode", + name="enable", + field=models.BooleanField(db_index=True, default=True, verbose_name="是否开启"), + ), + migrations.CreateModel( + name="UserCheckInLog", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("user_id", models.PositiveIntegerField()), + ( + "date", + models.DateField( + db_index=True, default=pendulum.today, verbose_name="记录日期" + ), + ), + ( + "increased_traffic", + models.BigIntegerField(default=0, verbose_name="增加的流量"), + ), + ], + options={"unique_together": {("user_id", "date")}}, + ), + ] diff --git a/apps/sspanel/migrations/0016_userssconfig_usertraffic.py b/apps/sspanel/migrations/0016_userssconfig_usertraffic.py new file mode 100644 index 0000000000..bd51d6cf5e --- /dev/null +++ b/apps/sspanel/migrations/0016_userssconfig_usertraffic.py @@ -0,0 +1,90 @@ +# Generated by Django 2.2.1 on 2019-06-01 07:24 + +import apps.utils +import datetime +from django.db import migrations, models +from django.utils.timezone import utc + + +class Migration(migrations.Migration): + + dependencies = [("sspanel", "0015_auto_20190601_1013")] + + operations = [ + migrations.CreateModel( + name="UserSSConfig", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("user_id", models.IntegerField(db_index=True, unique=True)), + ("port", models.IntegerField(default=1025, unique=True)), + ( + "password", + models.CharField( + default=apps.utils.get_short_random_string, max_length=32 + ), + ), + ("enable", models.BooleanField(default=True)), + ("speed_limit", models.IntegerField(default=0)), + ( + "method", + models.CharField( + choices=[ + ("aes-256-cfb", "aes-256-cfb"), + ("aes-128-ctr", "aes-128-ctr"), + ("rc4-md5", "rc4-md5"), + ("salsa20", "salsa20"), + ("chacha20", "chacha20"), + ("none", "none"), + ], + default="aes-128-ctr", + max_length=32, + ), + ), + ], + options={"verbose_name_plural": "用户SS配置"}, + ), + migrations.CreateModel( + name="UserTraffic", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("user_id", models.IntegerField(db_index=True, unique=True)), + ( + "upload_traffic", + models.BigIntegerField(default=0, verbose_name="上传流量"), + ), + ( + "download_traffic", + models.BigIntegerField(default=0, verbose_name="下载流量"), + ), + ( + "total_traffic", + models.BigIntegerField(default=5368709120, verbose_name="总流量"), + ), + ( + "last_use_time", + models.DateTimeField( + db_index=True, + default=datetime.datetime(1996, 2, 2, 0, 0, 0), + verbose_name="上次使用时间", + ), + ), + ], + options={"verbose_name_plural": "用户流量"}, + ), + ] diff --git a/apps/sspanel/migrations/0017_auto_20190807_1150.py b/apps/sspanel/migrations/0017_auto_20190807_1150.py new file mode 100644 index 0000000000..cc9aded0a5 --- /dev/null +++ b/apps/sspanel/migrations/0017_auto_20190807_1150.py @@ -0,0 +1,71 @@ +# Generated by Django 2.2.1 on 2019-08-07 03:50 +import pendulum + +import apps.utils +import datetime +from django.db import migrations, models +from django.utils.timezone import utc + + +class Migration(migrations.Migration): + + dependencies = [("sspanel", "0016_userssconfig_usertraffic")] + + operations = [ + migrations.AlterModelOptions( + name="usercheckinlog", options={"verbose_name_plural": "用户签到记录"} + ), + migrations.AlterModelOptions( + name="userreflog", options={"verbose_name_plural": "用户推荐记录"} + ), + migrations.AlterField( + model_name="userssconfig", + name="enable", + field=models.BooleanField(default=True, verbose_name="是否开启"), + ), + migrations.AlterField( + model_name="userssconfig", + name="method", + field=models.CharField( + choices=[ + ("aes-256-cfb", "aes-256-cfb"), + ("aes-128-ctr", "aes-128-ctr"), + ("rc4-md5", "rc4-md5"), + ("salsa20", "salsa20"), + ("chacha20", "chacha20"), + ("none", "none"), + ], + default="aes-128-ctr", + max_length=32, + verbose_name="加密", + ), + ), + migrations.AlterField( + model_name="userssconfig", + name="password", + field=models.CharField( + default=apps.utils.get_short_random_string, + max_length=32, + verbose_name="密码", + ), + ), + migrations.AlterField( + model_name="userssconfig", + name="port", + field=models.IntegerField(default=1025, unique=True, verbose_name="端口"), + ), + migrations.AlterField( + model_name="userssconfig", + name="speed_limit", + field=models.IntegerField(default=0, verbose_name="限速"), + ), + migrations.AlterField( + model_name="usertraffic", + name="last_use_time", + field=models.DateTimeField( + db_index=True, + default=pendulum.datetime(1996, 2, 2, 0, 0, 0), + verbose_name="上次使用时间", + ), + ), + ] diff --git a/apps/sspanel/models.py b/apps/sspanel/models.py index 2983613dcb..fc3b188564 100644 --- a/apps/sspanel/models.py +++ b/apps/sspanel/models.py @@ -1,25 +1,38 @@ -import time import base64 import datetime +import random +import time +from decimal import Decimal from urllib.parse import urlencode -import pendulum import markdown -from decimal import Decimal -from django.db import models +import pendulum from django.conf import settings -from django.utils import timezone -from django.db import transaction from django.contrib.auth.models import AbstractUser +from django.core.exceptions import ValidationError +from django.core.mail import send_mail from django.core.validators import MaxValueValidator, MinValueValidator - +from django.db import connection, models, transaction +from django.forms.models import model_to_dict +from django.utils import functional, timezone + +from apps.constants import ( + COUNTRIES_CHOICES, + METHOD_CHOICES, + NODE_TIME_OUT, + THEME_CHOICES, +) +from apps.encoder import encoder from apps.payments import pay -from apps.constants import THEME_CHOICES -from apps.utils import get_long_random_string, traffic_format +from apps.utils import ( + cache, + get_long_random_string, + get_short_random_string, + traffic_format, +) class User(AbstractUser): - """SS账户模型""" SUB_TYPE_SS = 0 SUB_TYPE_SSR = 1 @@ -31,8 +44,6 @@ class User(AbstractUser): (SUB_TYPE_ALL, "订阅所有"), ) - invitecode = models.CharField(verbose_name="邀请码", max_length=40) - invited_by = models.PositiveIntegerField(verbose_name="邀请人id", default=1) balance = models.DecimalField( verbose_name="余额", decimal_places=2, @@ -57,15 +68,17 @@ class User(AbstractUser): default=settings.DEFAULT_THEME, max_length=10, ) + # TODO Move To UserSsConfig sub_type = models.SmallIntegerField( verbose_name="订阅类型", choices=SUB_TYPES, default=SUB_TYPE_ALL ) + inviter_id = models.PositiveIntegerField(verbose_name="邀请人id", default=1) class Meta(AbstractUser.Meta): verbose_name = "用户" def delete(self): - self.ss_user.delete() + self.user_ss_config.delete() return super(User, self).delete() def __str__(self): @@ -79,62 +92,696 @@ def get_total_user_num(cls): @classmethod def get_today_register_user(cls): """返回今日注册的用户""" - today = datetime.datetime.combine(datetime.date.today(), datetime.time.min) - return cls.objects.filter(date_joined__gt=today) + return cls.objects.filter(date_joined__gt=pendulum.today()) @classmethod + @transaction.atomic def add_new_user(cls, cleaned_data): - from apps.ssserver.models import Suser - - with transaction.atomic(): - username = cleaned_data["username"] - email = cleaned_data["email"] - password = cleaned_data["password1"] - invitecode = cleaned_data["invitecode"] - user = cls.objects.create_user(username, email, password) - code = InviteCode.objects.get(code=invitecode) - code.isused = True - code.save() - # 将user和ssuser关联 - Suser.objects.create(user_id=user.id, port=Suser.get_random_port()) - # 绑定邀请人 - user.invited_by = code.code_id - user.invitecode = invitecode - user.save() - return user + user = cls.objects.create_user( + cleaned_data["username"], cleaned_data["email"], cleaned_data["password1"] + ) + if "invitecode" in cleaned_data: + code = InviteCode.objects.get(code=cleaned_data["invitecode"]) + code.consume() + inviter_id = code.user_id + elif "ref" in cleaned_data: + inviter_id = encoder.string2int(cleaned_data["ref"]) + # 绑定邀请人 + UserRefLog.log_ref(inviter_id, pendulum.today()) + user.inviter_id = inviter_id + user.save() + # 添加SSconfig + UserSSConfig.create_by_user_id(user.id) + return user @classmethod def get_by_user_name(cls, username): return cls.objects.get(username=username) - @property - def expire_time(self): - """返回等级到期时间""" - return self.level_expire_time + @classmethod + @cache.cached(ttl=60 * 60 * 24) + def get_by_pk(cls, pk): + return cls.objects.get(pk=pk) + + @classmethod + def check_and_disable_expired_users(cls): + now = pendulum.now() + expired_user_emails = [] + expired_users = cls.objects.filter(level__gt=0, level_expire_time__lte=now) + for user in expired_users: + user.level = 0 + user.save() + print(f"time: {now} user: {user} level timeout!") + expired_user_emails.append(user.email) + if expired_user_emails and settings.EXPIRE_EMAIL_NOTICE: + send_mail( + f"您的{settings.TITLE}账号已到期", + f"您的账号现被暂停使用。如需继续使用请前往 {settings.HOST} 充值", + settings.DEFAULT_FROM_EMAIL, + expired_user_emails, + ) @property def sub_link(self): - """生成该用户的订阅地址""" - token = base64.urlsafe_b64encode(self.username.encode()).decode() - params = {"token": token} - return settings.HOST + f"/server/subscribe/?{urlencode(params)}" + """订阅地址""" + params = {"token": self.token} + return settings.HOST + f"/api/subscribe/?{urlencode(params)}" + + @property + def ref_link(self): + """ref地址""" + params = {"ref": self.token} + return settings.HOST + f"/sspanel/register/?{urlencode(params)}" + + @functional.cached_property + def user_ss_config(self): + return UserSSConfig.objects.get(user_id=self.id) + + @property + def today_is_checkin(self): + return UserCheckInLog.get_today_is_checkin_by_user_id(self.pk) + + @property + def token(self): + return encoder.int2string(self.pk) + + def get_sub_links(self): + + node_list = SSNode.get_active_nodes() + sub_links = "MAX={}\n".format(node_list.count()) + for node in node_list: + sub_links += node.get_ss_link(self.user_ss_config) + "\n" + return sub_links + + +class UserPropertyMixin: + @functional.cached_property + def user(self): + return User.get_by_pk(self.user_id) + + +class UserOrder(models.Model, UserPropertyMixin): + + DEFAULT_ORDER_TIME_OUT = "24h" + STATUS_CREATED = 0 + STATUS_PAID = 1 + STATUS_FINISHED = 2 + STATUS_CHOICES = ( + (STATUS_CREATED, "created"), + (STATUS_PAID, "paid"), + (STATUS_FINISHED, "finished"), + ) + + user = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name="用户") + status = models.SmallIntegerField( + verbose_name="订单状态", db_index=True, choices=STATUS_CHOICES + ) + out_trade_no = models.CharField( + verbose_name="流水号", max_length=64, unique=True, db_index=True + ) + qrcode_url = models.CharField(verbose_name="支付连接", max_length=64, null=True) + amount = models.DecimalField( + verbose_name="金额", decimal_places=2, max_digits=10, default=0 + ) + created_at = models.DateTimeField( + verbose_name="时间", auto_now_add=True, db_index=True + ) + expired_at = models.DateTimeField(verbose_name="过期时间", db_index=True) + + def __str__(self): + return f"<{self.id,self.user}>:{self.amount}" + + class Meta: + verbose_name_plural = "用户订单" + index_together = ["user", "status"] + + @classmethod + def gen_out_trade_no(cls): + return datetime.datetime.fromtimestamp(time.time()).strftime("%Y%m%d%H%M%S%s") + + @classmethod + def get_not_paid_order(cls, user, amount): + return ( + cls.objects.filter(user=user, status=cls.STATUS_CREATED, amount=amount) + .order_by("-created_at") + .first() + ) + + @classmethod + def get_recent_created_order(cls, user): + return cls.objects.filter(user=user).order_by("-created_at").first() + + @classmethod + def make_up_lost_orders(cls): + now = pendulum.now() + for order in cls.objects.filter(status=cls.STATUS_CREATED, expired_at__gte=now): + changed = order.check_order_status() + if changed: + print(f"补单:{order.user,order.amount}") + + @classmethod + def get_or_create_order(cls, user, amount): + now = pendulum.now() + order = cls.get_not_paid_order(user, amount) + if order and order.expired_at > now: + return order + with transaction.atomic(): + out_trade_no = cls.gen_out_trade_no() + trade = pay.alipay.api_alipay_trade_precreate( + subject=settings.ALIPAY_TRADE_INFO.format(amount), + out_trade_no=out_trade_no, + total_amount=amount, + timeout_express=cls.DEFAULT_ORDER_TIME_OUT, + notify_url=settings.ALIPAY_CALLBACK_URL, + ) + qrcode_url = trade.get("qr_code") + order = cls.objects.create( + user=user, + status=cls.STATUS_CREATED, + out_trade_no=out_trade_no, + amount=amount, + qrcode_url=qrcode_url, + expired_at=now.add(hours=24), + ) + return order + + def handle_paid(self): + if self.status != self.STATUS_PAID: + return + with transaction.atomic(): + self.user.balance += self.amount + self.user.save() + self.status = self.STATUS_FINISHED + self.save() + # 将充值记录和捐赠绑定 + Donate.objects.create(user=self.user, money=self.amount) + + def check_order_status(self): + changed = False + if self.status != self.STATUS_CREATED: + return + with transaction.atomic(): + res = pay.alipay.api_alipay_trade_query(out_trade_no=self.out_trade_no) + if res.get("trade_status", "") == "TRADE_SUCCESS": + self.status = self.STATUS_PAID + self.save() + changed = True + self.handle_paid() + return changed + + +class UserRefLog(models.Model, UserPropertyMixin): + user_id = models.PositiveIntegerField() + register_count = models.IntegerField(default=0) + date = models.DateField("记录日期", default=pendulum.today, db_index=True) + + class Meta: + verbose_name_plural = "用户推荐记录" + unique_together = [["user_id", "date"]] + + @classmethod + def log_ref(cls, user_id, date): + log, _ = cls.objects.get_or_create(user_id=user_id, date=date) + log.register_count += 1 + log.save() + + @classmethod + def list_by_user_id_and_date_list(cls, user_id, date_list): + return cls.objects.filter(user_id=user_id, date__in=date_list) + + @classmethod + def gen_bar_chart_configs(cls, user_id, date_list): + """set register_count to 0 if the query date log not exists""" + date_list = sorted(date_list) + logs = { + log.date: log.register_count + for log in cls.list_by_user_id_and_date_list(user_id, date_list) + } + bar_config = { + "labels": [f"{date.month}-{date.day}" for date in date_list], + "data": [logs.get(date, 0) for date in date_list], + "data_title": "每日邀请注册人数", + } + return bar_config + + +class UserOnLineIpLog(models.Model, UserPropertyMixin): + + user_id = models.IntegerField(db_index=True) + node_id = models.IntegerField() + ip = models.CharField(max_length=128) + created_at = models.DateTimeField(auto_now_add=True, db_index=True) + + class Meta: + verbose_name_plural = "用户在线IP" + ordering = ["-created_at"] + index_together = ["node_id", "created_at"] + + @classmethod + def get_recent_log_by_node_id(cls, node_id): + now = pendulum.now() + ip_set = set() + ret = [] + for log in cls.objects.filter( + node_id=node_id, + created_at__range=[now.subtract(seconds=NODE_TIME_OUT), now], + ): + if log.ip not in ip_set: + ret.append(log) + ip_set.add(log.ip) + return ret + + @classmethod + def truncate(cls): + with connection.cursor() as cursor: + cursor.execute("TRUNCATE TABLE {}".format(cls._meta.db_table)) + + +class UserTrafficLog(models.Model, UserPropertyMixin): + + user_id = models.IntegerField() + node_id = models.IntegerField() + date = models.DateField(auto_now_add=True, db_index=True) + upload_traffic = models.BigIntegerField("上传流量", default=0) + download_traffic = models.BigIntegerField("下载流量", default=0) + + class Meta: + verbose_name_plural = "流量记录" + ordering = ["-date"] + index_together = ["user_id", "node_id", "date"] @property - def ss_user(self): - from apps.ssserver.models import Suser + def total_traffic(self): + return traffic_format(self.download_traffic + self.upload_traffic) + + @classmethod + def truncate(cls): + with connection.cursor() as cursor: + cursor.execute("TRUNCATE TABLE {}".format(cls._meta.db_table)) + + @classmethod + def calc_user_total_traffic(cls, node_id, user_id): + logs = cls.objects.filter(user_id=user_id, node_id=node_id) + aggs = logs.aggregate( + u=models.Sum("upload_traffic"), d=models.Sum("download_traffic") + ) + ut = aggs["u"] if aggs["u"] else 0 + dt = aggs["d"] if aggs["d"] else 0 + return traffic_format(ut + dt) - return Suser.objects.get(user_id=self.id) + @classmethod + def calc_user_traffic_by_date(cls, user_id, node_id, date): + logs = cls.objects.filter(node_id=node_id, user_id=user_id, date=date) + aggs = logs.aggregate( + u=models.Sum("upload_traffic"), d=models.Sum("download_traffic") + ) + ut = aggs["u"] if aggs["u"] else 0 + dt = aggs["d"] if aggs["d"] else 0 + return (ut + dt) // settings.MB + + @classmethod + def gen_line_chart_configs(cls, user_id, node_id, date_list): + + ss_node = SSNode.get_or_none_by_node_id(node_id) + user_total_traffic = cls.calc_user_total_traffic(node_id, user_id) + date_list = sorted(date_list) + line_config = { + "title": "节点 {} 当月共消耗:{}".format(ss_node.name, user_total_traffic), + "labels": ["{}-{}".format(t.month, t.day) for t in date_list], + "data": [ + cls.calc_user_traffic_by_date(user_id, node_id, date) + for date in date_list + ], + "data_title": ss_node.name, + "x_label": "日期 最近七天", + "y_label": "流量 单位:MB", + } + return line_config + + +class UserCheckInLog(models.Model, UserPropertyMixin): + user_id = models.PositiveIntegerField() + date = models.DateField("记录日期", default=pendulum.today, db_index=True) + increased_traffic = models.BigIntegerField("增加的流量", default=0) + + class Meta: + verbose_name_plural = "用户签到记录" + unique_together = [["user_id", "date"]] + + @classmethod + def add_log(cls, user_id, traffic): + return cls.objects.create(user_id=user_id, increased_traffic=traffic) + + @classmethod + @transaction.atomic + def checkin(cls, user_id): + traffic = random.randint( + settings.MIN_CHECKIN_TRAFFIC, settings.MAX_CHECKIN_TRAFFIC + ) + user_traffic = UserTraffic.get_by_user_id(user_id) + user_traffic.total_traffic += traffic + user_traffic.save() + return cls.add_log(user_id, traffic) + + @classmethod + def get_today_is_checkin_by_user_id(cls, user_id): + return cls.objects.filter(user_id=user_id, date=pendulum.today()).exists() + + @classmethod + def get_today_checkin_user_count(cls): + return cls.objects.filter(date=pendulum.today()).count() + + @property + def human_increased_traffic(self): + return traffic_format(self.increased_traffic) + + +class UserSSConfig(models.Model, UserPropertyMixin): + + MIN_PORT = 1025 + + user_id = models.IntegerField(unique=True, db_index=True) + port = models.IntegerField("端口", unique=True, default=MIN_PORT) + password = models.CharField("密码", max_length=32, default=get_short_random_string) + enable = models.BooleanField("是否开启", default=True) + speed_limit = models.IntegerField("限速", default=0) + method = models.CharField( + "加密", default=settings.DEFAULT_METHOD, max_length=32, choices=METHOD_CHOICES + ) + + class Meta: + verbose_name_plural = "用户SS配置" + + @classmethod + @transaction.atomic + def create_by_user_id(cls, user_id): + config = cls.objects.create(user_id=user_id, port=cls.get_not_used_port()) + UserTraffic.objects.create(user_id=user_id) + return config + + @classmethod + def get_not_used_port(cls): + port_set = {log[0] for log in cls.objects.all().values_list("port")} + if not port_set: + return cls.MIN_PORT + max_port = max(port_set) + 1 + return random.choice( + list({i for i in range(cls.MIN_PORT, max_port + 1)}.difference(port_set)) + ) + + @classmethod + def get_by_user_id(cls, user_id): + return cls.objects.get(user_id=user_id) + + @classmethod + def get_configs_by_user_level(cls, level): + user_ids = [ + d[0] for d in User.objects.filter(level__gte=level).values_list("id") + ] + return UserSSConfig.objects.filter(user_id__in=user_ids) + + @functional.cached_property + def user_traffic(self): + return UserTraffic.get_by_user_id(self.user_id) + + @property + def human_total_traffic(self): + return traffic_format(self.user_traffic.total_traffic) + + @property + def human_used_traffic(self): + return traffic_format(self.user_traffic.used_traffic) + + def reset_random_port(self): + cls = type(self) + port = cls.get_not_used_port() + self.port = port + self.save() + return port + + def update_from_dict(self, data): + clean_fields = ["password", "method"] + for k, v in data.items(): + if k in clean_fields: + setattr(self, k, v) + try: + self.full_clean() + self.save() + return True + except ValidationError: + return False + + def get_import_links(self): + links = [node.get_ss_link(self) for node in SSNode.get_active_nodes()] + return "\n".join(links) + + +class UserTraffic(models.Model, UserPropertyMixin): + + DEFAULT_USE_TIME = pendulum.datetime(year=1996, month=2, day=2) + + user_id = models.IntegerField(unique=True, db_index=True) + upload_traffic = models.BigIntegerField("上传流量", default=0) + download_traffic = models.BigIntegerField("下载流量", default=0) + total_traffic = models.BigIntegerField("总流量", default=settings.DEFAULT_TRAFFIC) + last_use_time = models.DateTimeField( + "上次使用时间", default=DEFAULT_USE_TIME, db_index=True + ) + + class Meta: + verbose_name_plural = "用户流量" + + @classmethod + def get_never_used_user_count(cls): + return cls.objects.filter(last_use_time=cls.DEFAULT_USE_TIME).count() + + @classmethod + def get_by_user_id(cls, user_id): + return cls.objects.get(user_id=user_id) + + @classmethod + def get_overflow_user_ids(cls): + # NOTE cronjob用 先不加索引 + uts = cls.objects.filter( + total_traffic__lte=( + models.F("upload_traffic") + models.F("download_traffic") + ) + ).values_list("user_id") + return [ut[0] for ut in uts] + + @classmethod + def check_and_disable_out_of_traffic_user(cls): + need_disable_user_ids = UserTraffic.get_overflow_user_ids() + user_ss_configs = UserSSConfig.objects.filter(user_id__in=need_disable_user_ids) + need_set_user_ids = [c.user_id for c in user_ss_configs if c.enable] + UserSSConfig.objects.filter(user_id__in=need_set_user_ids).update(enable=False) + user_list = User.objects.filter(id__in=need_set_user_ids) + emails = [user.email for user in user_list] + if emails and settings.EXPIRE_EMAIL_NOTICE: + send_mail( + f"您的{settings.TITLE}账号流量已全部用完", + f"您的账号现被暂停使用。如需继续使用请前往 {settings.HOST} 充值", + settings.DEFAULT_FROM_EMAIL, + emails, + ) + print(f"共有{len(emails)}个用户流量用超啦") + + @property + def overflow(self): + return (self.upload_traffic + self.download_traffic) > self.total_traffic + + @property + def used_traffic(self): + return self.upload_traffic + self.download_traffic + + @property + def used_percentage(self): + try: + return round(self.used_traffic / self.total_traffic * 100, 2) + except ZeroDivisionError: + return 100.00 + + @property + def human_used_traffic(self): + return traffic_format(self.used_traffic) + + @property + def user(self): + return User.get_by_pk(self.user_id) + + @classmethod + def get_user_order_by_traffic(cls, count=10): + # NOTE 后台展示用 暂时不加索引 + return cls.objects.all().order_by("-download_traffic")[:count] + + def reset_traffic(self, new_traffic): + self.total_traffic = new_traffic + self.upload_traffic = 0 + self.download_traffic = 0 + + def reset_to_fresh(self): + self.enable = False + self.reset_traffic(settings.DEFAULT_TRAFFIC) + self.save() + + +class SSNodeOnlineLog(models.Model): + + node_id = models.IntegerField() + online_user_count = models.IntegerField(default=0) + created_at = models.DateTimeField(auto_now_add=True, db_index=True) + + class Meta: + verbose_name_plural = "节点在线记录" + ordering = ["-created_at"] + index_together = ["node_id", "created_at"] + + @property + def is_online(self): + return pendulum.now().subtract(seconds=NODE_TIME_OUT) < self.created_at + + @classmethod + def truncate(cls): + with connection.cursor() as cursor: + cursor.execute("TRUNCATE TABLE {}".format(cls._meta.db_table)) + + @classmethod + def add_log(cls, node_id, num): + return cls.objects.create(node_id=node_id, online_user_count=num) + + @classmethod + def get_latest_log_by_node_id(cls, node_id): + return cls.objects.filter(node_id=node_id).order_by("-created_at").first() + + @classmethod + def get_all_node_online_user_count(cls): + + ss_node_ids = [node.node_id for node in SSNode.get_active_nodes()] + count = 0 + for node_id in ss_node_ids: + log = cls.get_latest_log_by_node_id(node_id) + if log: + count += log.online_user_count + return count + + @classmethod + def get_latest_online_log_info(cls, node_id): + data = {"online": False, "online_user_count": 0} + log = cls.get_latest_log_by_node_id(node_id) + if log: + data["online"] = log.is_online + data["online_user_count"] = log.online_user_count if log.is_online else 0 + return data + + +class SSNode(models.Model): + + node_id = models.IntegerField(unique=True) + level = models.PositiveIntegerField(default=0) + name = models.CharField("名字", max_length=32) + info = models.CharField("节点说明", max_length=1024) + country = models.CharField( + "国家", default="CN", max_length=5, choices=COUNTRIES_CHOICES + ) + server = models.CharField("服务器地址", max_length=128) + method = models.CharField( + "加密类型", default=settings.DEFAULT_METHOD, max_length=32, choices=METHOD_CHOICES + ) + used_traffic = models.BigIntegerField("已用流量", default=0) + total_traffic = models.BigIntegerField("总流量", default=settings.GB) + enable = models.BooleanField("是否开启", default=True, db_index=True) + custom_method = models.BooleanField("自定义加密", default=False) + + class Meta: + verbose_name_plural = "SS节点" + + @classmethod + def get_or_none_by_node_id(cls, node_id): + return cls.objects.filter(node_id=node_id).first() + + @classmethod + def get_active_nodes(cls): + return cls.objects.filter(enable=True).order_by("level", "country") + + @classmethod + def get_node_ids_by_level(cls, level): + node_list = cls.objects.filter(level__lte=level).values_list("node_id") + return [node[0] for node in node_list] + + @classmethod + def increase_used_traffic(cls, node_id, used_traffic): + cls.objects.filter(node_id=node_id).update( + used_traffic=models.F("used_traffic") + used_traffic + ) + + @classmethod + def get_user_active_nodes(cls, user): + return cls.objects.filter(enable=True, level__lte=user.level) + + @classmethod + @cache.cached(ttl=60 * 60 * 24) + def get_user_ss_configs_by_node_id(cls, node_id): + ss_node = cls.get_or_none_by_node_id(node_id) + configs = {"users": []} + if ss_node: + configs["users"] = [ + ss_node.to_dict_with_user_ss_config(config) + for config in UserSSConfig.get_configs_by_user_level(ss_node.level) + ] + if not ss_node.enable: + for config in configs["users"]: + config["enable"] = False + return configs + + @property + def api_endpoint(self): + params = {"token": settings.TOKEN} + return ( + settings.HOST + f"/api/user_ss_config/{self.node_id}/?{urlencode(params)}" + ) + + @property + def human_total_traffic(self): + return traffic_format(self.total_traffic) + + @property + def human_used_traffic(self): + return traffic_format(self.used_traffic) + + @property + def overflow(self): + return (self.used_traffic) > self.total_traffic + + def get_ss_link(self, user_ss_config): + method = user_ss_config.method if self.custom_method else self.method + code = f"{method}:{user_ss_config.password}@{self.server}:{user_ss_config.port}" + b64_code = base64.urlsafe_b64encode(code.encode()).decode() + ss_link = "ss://{}#{}".format(b64_code, self.name) + return ss_link + + def to_dict_with_user_ss_config(self, user_ss_config): + data = model_to_dict(self) + data.update(model_to_dict(user_ss_config)) + if not self.custom_method: + data["method"] = self.method + return data + + def to_dict_with_extra_info(self, user_ss_config): + data = self.to_dict_with_user_ss_config(user_ss_config) + data.update(SSNodeOnlineLog.get_latest_online_log_info(self.node_id)) + data["country"] = self.country.lower() + data["ss_link"] = self.get_ss_link(user_ss_config) + data["api_point"] = self.api_endpoint + return data class InviteCode(models.Model): """邀请码""" - INVITE_CODE_TYPE = ((1, "公开"), (0, "不公开")) + TYPE_PUBLIC = 1 + TYPE_PRIVATE = 0 + INVITE_CODE_TYPE = ((TYPE_PUBLIC, "公开"), (TYPE_PRIVATE, "不公开")) - code_type = models.IntegerField( - verbose_name="类型", choices=INVITE_CODE_TYPE, default=0 - ) - code_id = models.PositiveIntegerField(verbose_name="邀请人ID", default=1) code = models.CharField( verbose_name="邀请码", primary_key=True, @@ -142,25 +789,55 @@ class InviteCode(models.Model): max_length=40, default=get_long_random_string, ) - time_created = models.DateTimeField( - verbose_name="创建时间", editable=False, auto_now_add=True + code_type = models.IntegerField( + verbose_name="类型", choices=INVITE_CODE_TYPE, default=TYPE_PRIVATE ) - isused = models.BooleanField(verbose_name="是否使用", default=False) + user_id = models.PositiveIntegerField(verbose_name="邀请人ID", default=1) + used = models.BooleanField(verbose_name="是否使用", default=False) + created_at = models.DateTimeField(editable=False, auto_now_add=True) def __str__(self): - return str(self.code) + return f"<{self.user_id}>-<{self.code}>" class Meta: verbose_name_plural = "邀请码" - ordering = ("isused", "-time_created") + ordering = ("used", "-created_at") + + @classmethod + def calc_num_by_user(cls, user): + return user.invitecode_num - cls.list_not_used_by_user_id(user.pk).count() + + @classmethod + def create_by_user(cls, user): + num = cls.calc_num_by_user(user) + if num > 0: + models = [cls(code_type=0, user_id=user.pk) for _ in range(num)] + cls.objects.bulk_create(models) + return num + + @classmethod + def list_by_code_type(cls, code_type, num=20): + return cls.objects.filter(code_type=code_type, used=False)[:num] + + @classmethod + def list_by_user_id(cls, user_id, num=10): + return cls.objects.filter(user_id=user_id)[:num] + + @classmethod + def list_not_used_by_user_id(cls, user_id): + return cls.objects.filter(user_id=user_id, used=False) + + def consume(self): + self.used = True + self.save() class RebateRecord(models.Model): """返利记录""" user_id = models.PositiveIntegerField(verbose_name="返利人ID", default=1) - rebatetime = models.DateTimeField( - verbose_name="返利时间", editable=False, auto_now_add=True + consumer_id = models.PositiveIntegerField( + verbose_name="消费者ID", null=True, blank=True ) money = models.DecimalField( verbose_name="金额", @@ -170,36 +847,27 @@ class RebateRecord(models.Model): max_digits=10, blank=True, ) + created_at = models.DateTimeField(editable=False, auto_now_add=True) class Meta: - ordering = ("-rebatetime",) - + ordering = ("-created_at",) -class Donate(models.Model): @classmethod - def totalDonateMoney(cls): - """返回捐赠总金额""" - return sum([d.money for d in cls.objects.all()]) + def list_by_user_id_with_consumer_username(cls, user_id, num=10): + logs = cls.objects.filter(user_id=user_id)[:num] + user_ids = [log.consumer_id for log in logs] + username_map = {u.id: u.username for u in User.objects.filter(id__in=user_ids)} + for log in logs: + setattr(log, "consumer_username", username_map.get(log.consumer_id, "")) + return logs - @classmethod - def totalDonateNums(cls): - """返回捐赠总数量""" - return len(cls.objects.all()) - @classmethod - def richPeople(cls): - """返回捐赠金额最多的前10名""" - rec = {} - for d in cls.objects.all(): - if d.user not in rec.keys(): - rec[d.user] = d.money - else: - rec[d.user] += d.money - return sorted(rec.items(), key=lambda rec: rec[1], reverse=True)[:10] +class Donate(models.Model): - """捐赠记录""" user = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name="捐赠人") - time = models.DateTimeField("捐赠时间", editable=False, auto_now_add=True) + time = models.DateTimeField( + "捐赠时间", editable=False, auto_now_add=True, db_index=True + ) money = models.DecimalField( verbose_name="捐赠金额", decimal_places=2, @@ -207,6 +875,7 @@ def richPeople(cls): default=0, null=True, blank=True, + db_index=True, ) def __str__(self): @@ -216,6 +885,28 @@ class Meta: verbose_name_plural = "捐赠记录" ordering = ("-time",) + @classmethod + def get_donate_money_by_date(cls, date=None): + qs = cls.objects.filter() + if date: + qs = qs.filter(time__gte=date, time__lte=date.add(days=1)) + res = qs.aggregate(amount=models.Sum("money"))["amount"] + return int(res) if res else 0 + + @classmethod + def get_donate_count_by_date(cls, date=None): + if date: + return cls.objects.filter(time__gte=date).count() + return cls.objects.all().count() + + @classmethod + def get_most_donated_user_by_count(cls, count): + return ( + cls.objects.values("user__username") + .annotate(amount=models.Sum("money")) + .order_by("-amount")[:count] + ) + class MoneyCode(models.Model): """充值码""" @@ -300,35 +991,39 @@ def get_days(self): """返回增加的天数""" return "{}".format(self.days) + @transaction.atomic def purchase_by_user(self, user): """购买商品 返回是否成功""" if user.balance < self.money: return False # 验证成功进行提权操作 - ss_user = user.ss_user + user_traffic = UserTraffic.get_by_user_id(user.pk) user.balance -= self.money now = pendulum.now() days = pendulum.duration(days=self.days) if user.level == self.level and user.level_expire_time > now: user.level_expire_time += days - ss_user.increase_transfer(self.transfer) + user_traffic.total_traffic += self.transfer else: user.level_expire_time = now + days - ss_user.reset_traffic(self.transfer) - ss_user.enable = True + user_traffic.reset_traffic(self.transfer) + user_ss_config = user.user_ss_config + user_ss_config.enable = True user.level = self.level - ss_user.save() user.save() - ss_user.clear_get_user_configs_by_node_id_cache() + user_traffic.save() + user_ss_config.save() # 增加购买记录 PurchaseHistory.objects.create( good=self, user=user, money=self.money, purchtime=now ) - # 增加返利记录 - inviter = User.objects.filter(pk=user.invited_by).first() - if inviter: + inviter = User.get_by_pk(user.inviter_id) + if inviter != user: + # 增加返利记录 rebaterecord = RebateRecord( - user_id=inviter.pk, money=self.money * Decimal(settings.INVITE_PERCENT) + user_id=inviter.pk, + consumer_id=user.pk, + money=self.money * Decimal(settings.INVITE_PERCENT), ) inviter.balance += rebaterecord.money inviter.save() @@ -416,113 +1111,3 @@ def __str__(self): class Meta: verbose_name_plural = "工单" ordering = ("-time",) - - -class UserOrder(models.Model): - - DEFAULT_ORDER_TIME_OUT = "24h" - STATUS_CREATED = 0 - STATUS_PAID = 1 - STATUS_FINISHED = 2 - STATUS_CHOICES = ( - (STATUS_CREATED, "created"), - (STATUS_PAID, "paid"), - (STATUS_FINISHED, "finished"), - ) - - user = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name="用户") - status = models.SmallIntegerField( - verbose_name="订单状态", db_index=True, choices=STATUS_CHOICES - ) - out_trade_no = models.CharField( - verbose_name="流水号", max_length=64, unique=True, db_index=True - ) - qrcode_url = models.CharField(verbose_name="支付连接", max_length=64, null=True) - amount = models.DecimalField( - verbose_name="金额", decimal_places=2, max_digits=10, default=0 - ) - created_at = models.DateTimeField( - verbose_name="时间", auto_now_add=True, db_index=True - ) - expired_at = models.DateTimeField(verbose_name="过期时间", db_index=True) - - def __str__(self): - return f"<{self.id,self.user}>:{self.amount}" - - class Meta: - verbose_name_plural = "用户订单" - index_together = ["user", "status"] - - @classmethod - def gen_out_trade_no(cls): - return datetime.datetime.fromtimestamp(time.time()).strftime("%Y%m%d%H%M%S%s") - - @classmethod - def get_not_paid_order(cls, user, amount): - return ( - cls.objects.filter(user=user, status=cls.STATUS_CREATED, amount=amount) - .order_by("-created_at") - .first() - ) - - @classmethod - def get_recent_created_order(cls, user): - return cls.objects.filter(user=user).order_by("-created_at").first() - - @classmethod - def make_up_lost_orders(cls): - now = pendulum.now() - for order in cls.objects.filter(status=cls.STATUS_CREATED, expired_at__gte=now): - changed = order.check_order_status() - if changed: - print(f"补单:{order.user,order.amount}") - - @classmethod - def get_or_create_order(cls, user, amount): - now = pendulum.now() - order = cls.get_not_paid_order(user, amount) - if order and order.expired_at > now: - return order - with transaction.atomic(): - out_trade_no = cls.gen_out_trade_no() - trade = pay.alipay.api_alipay_trade_precreate( - subject=settings.ALIPAY_TRADE_INFO.format(amount), - out_trade_no=out_trade_no, - total_amount=amount, - timeout_express=cls.DEFAULT_ORDER_TIME_OUT, - notify_url=settings.ALIPAY_CALLBACK_URL, - ) - qrcode_url = trade.get("qr_code") - order = cls.objects.create( - user=user, - status=cls.STATUS_CREATED, - out_trade_no=out_trade_no, - amount=amount, - qrcode_url=qrcode_url, - expired_at=now.add(hours=24), - ) - return order - - def handle_paid(self): - if self.status != self.STATUS_PAID: - return - with transaction.atomic(): - self.user.balance += self.amount - self.user.save() - self.status = self.STATUS_FINISHED - self.save() - # 将充值记录和捐赠绑定 - Donate.objects.create(user=self.user, money=self.amount) - - def check_order_status(self): - changed = False - if self.status != self.STATUS_CREATED: - return - with transaction.atomic(): - res = pay.alipay.api_alipay_trade_query(out_trade_no=self.out_trade_no) - if res.get("trade_status", "") == "TRADE_SUCCESS": - self.status = self.STATUS_PAID - self.save() - changed = True - self.handle_paid() - return changed diff --git a/apps/sspanel/static/img/alipay.png b/apps/sspanel/static/img/alipay.png deleted file mode 100644 index 673c9dea20..0000000000 Binary files a/apps/sspanel/static/img/alipay.png and /dev/null differ diff --git a/apps/sspanel/static/img/wechat.jpg b/apps/sspanel/static/img/wechat.jpg deleted file mode 100644 index d49482f9b5..0000000000 Binary files a/apps/sspanel/static/img/wechat.jpg and /dev/null differ diff --git a/apps/sspanel/static/sspanel/css/ehco.css b/apps/sspanel/static/sspanel/css/sspanel.css similarity index 100% rename from apps/sspanel/static/sspanel/css/ehco.css rename to apps/sspanel/static/sspanel/css/sspanel.css diff --git a/apps/sspanel/static/sspanel/js/ehcov1.js b/apps/sspanel/static/sspanel/js/sspanel.js similarity index 82% rename from apps/sspanel/static/sspanel/js/ehcov1.js rename to apps/sspanel/static/sspanel/js/sspanel.js index 866a0a3218..c5b74d832f 100644 --- a/apps/sspanel/static/sspanel/js/ehcov1.js +++ b/apps/sspanel/static/sspanel/js/sspanel.js @@ -128,6 +128,21 @@ document.addEventListener('DOMContentLoaded', function () { +function genRandomRgbaSet(num) { + colorData = [] + for (var i = 0; i < num; i++) { + var r = Math.floor(Math.random() * 256); // Random between 0-255 + var g = Math.floor(Math.random() * 256); // Random between 0-255 + var b = Math.floor(Math.random() * 256); // Random between 0-255 + var rgba = 'rgba(' + r + ',' + g + ',' + b + ',' + 0.2 + ')'; // Collect all to a string + colorData.push(rgba) + } + return colorData + +} + + + var getRandomColor = function () { var letters = '0123456789ABCDEF'; var color = '#'; @@ -230,3 +245,39 @@ var genLineChart = function (chartId, config) { } }) } +var genBarChart = function (chartId, config) { + /** + charId : 元素id 定位canvas用 + config : 配置信息 dict类型 + { + title: 图表名字 + labels :data对应的label + data_title: data的标题 + data: 数据 + } + **/ + var ctx = $('#' + chartId) + var myChart = new Chart(ctx, { + type: 'bar', + data: { + labels: config.labels, + datasets: [{ + label: config.data_title, + data: config.data, + backgroundColor: genRandomRgbaSet(config.data.length), + }] + }, + options: { + scales: { + yAxes: [{ + ticks: { + beginAtZero: true, + stepSize: 1, + suggestedMax: 7 + } + }] + } + } + }) +} + diff --git a/apps/sspanel/templatetags/ehcofilter.py b/apps/sspanel/templatetags/ehcofilter.py index 629a37a570..07838b64e9 100644 --- a/apps/sspanel/templatetags/ehcofilter.py +++ b/apps/sspanel/templatetags/ehcofilter.py @@ -11,12 +11,13 @@ def add_class(value, arg): # 捐赠名混淆 - - @register.filter(name="mix_name") def mix_name(value, arg): - value = str(value) - mix_name = value[0] + "***" + value[-1] + if value: + value = str(value) + mix_name = value[0] + "***" + value[-1] + else: + mix_name = "***" return mix_name diff --git a/apps/sspanel/urls.py b/apps/sspanel/urls.py index 23c239b194..87a5db7686 100644 --- a/apps/sspanel/urls.py +++ b/apps/sspanel/urls.py @@ -1,5 +1,5 @@ from django.urls import path -from . import views +from apps.sspanel import views, admin_views app_name = "sspanel" @@ -7,21 +7,25 @@ # 网站用户面板 path("sshelp/", views.sshelp, name="sshelp"), path("ssclient/", views.ssclient, name="ssclient"), - path("ssinvite/", views.ssinvite, name="ssinvite"), - path("passinvite/()/", views.pass_invitecode, name="passinvitecode"), + path("invitecode/", views.InviteCodeView.as_view(), name="invite_code"), # 注册/登录 - path("register/", views.register, name="register"), + path("register/", views.RegisterView.as_view(), name="register"), path("login/", views.user_login, name="login"), path("logout/", views.user_logout, name="logout"), # 节点 - path("nodeinfo/", views.nodeinfo, name="nodeinfo"), - path("trafficlog/", views.trafficlog, name="trafficlog"), + path("nodeinfo/", views.NodeInfoView.as_view(), name="nodeinfo"), + path("user_traffic_log/", views.UserTrafficLog.as_view(), name="user_traffic_log"), # 用户信息 - path("users/userinfo/", views.userinfo, name="userinfo"), - path("users/userinfoedit/", views.userinfo_edit, name="userinfo_edit"), + path("users/userinfo/", views.UserInfoView.as_view(), name="userinfo"), + path("users/settings/", views.UserSettingView.as_view(), name="user_settings"), + path( + "users/ss_node_config/", + views.UserSSNodeConfigView.as_view(), + name="ss_node_config", + ), # 捐赠/充值 path("donate/", views.donate, name="donate"), - path("shop/", views.shop, name="shop"), + path("shop/", views.ShopView.as_view(), name="shop"), path("purchaselog/", views.purchaselog, name="purchaselog"), path("chargecenter/", views.chargecenter, name="chargecenter"), path("charge/", views.charge, name="charge"), @@ -33,42 +37,84 @@ path("ticket/edit/()/", views.ticket_edit, name="ticket_edit"), path("ticket/delete/)/", views.ticket_delete, name="ticket_delete"), # 推广相关 - path("affiliate/", views.affiliate, name="affiliate"), - path("rebate/record/", views.rebate_record, name="rebate"), - # 网站后台面板 - path("backend/", views.backend_index, name="backend_index"), + path("aff/invite/", views.AffInviteView.as_view(), name="aff_invite"), + path("aff/status/", views.AffStatusView.as_view(), name="aff_status"), + # ==================================================================== + # 网站后台界面 + # ==================================================================== + path( + "backend/user_online_ip_log/", + admin_views.UserOnlineIpLogView.as_view(), + name="user_online_ip_log", + ), + path("backend/", admin_views.system_status, name="system_status"), # 邀请码相关 - path("backend/invite/", views.backend_invite, name="backend_invite"), - path("invite_gen_code/", views.gen_invite_code, name="geninvitecode"), + path("backend/invite/", admin_views.backend_invite, name="backend_invite"), + path("invite_gen_code/", admin_views.gen_invite_code, name="geninvitecode"), # 节点相关 - path("backend/nodeinfo/", views.backend_node_info, name="backend_node_info"), - path("backend/node/delete//", views.node_delete, name="node_delete"), - path("backend/node/edit//", views.node_edit, name="node_edit"), - path("backend/node/create/", views.node_create, name="node_create"), + path( + "backend/ss_node_list/", + admin_views.SSNodeListView.as_view(), + name="backend_ss_node_list", + ), + path("backend/ss_node/", admin_views.SSNodeView.as_view(), name="backend_ss_node"), + path( + "backend/ss_node_delete//", + admin_views.SSNodeDeleteView.as_view(), + name="backend_ss_node_delete", + ), + path( + "backend/ss_node//", + admin_views.SSNodeDetailView.as_view(), + name="backend_ss_node_detail", + ), # 用户相关 - path("backend/userlist/", views.backend_userlist, name="user_list"), - path("backend/user/delete//", views.user_delete, name="user_delete"), - path("backend/user/search/", views.user_search, name="user_search"), - path("backend/user/status/", views.user_status, name="user_status"), + path( + "backend/user_ss_config_list/", + admin_views.UserSSConfigListView.as_view(), + name="backend_user_ss_config_list", + ), + path( + "backend/user_ss_config/delete//", + admin_views.UserSSConfigDeleteView.as_view(), + name="backend_user_ss_config_delete", + ), + path( + "backend/user_ss_config/search/", + admin_views.UserSSConfigSearchView.as_view(), + name="backend_user_ss_config_search", + ), + path( + "backend/user_ss_config//", + admin_views.UserSSConfigDetailView.as_view(), + name="backend_user_ss_config_detail", + ), + path( + "backend/user_status/", + admin_views.UserStatusView.as_view(), + name="backend_user_status", + ), # 商品充值相关 - path("backend/charge/", views.backend_charge, name="backend_charge"), - path("backend/shop/", views.backend_shop, name="backend_shop"), - path("backend/shop/delete//", views.good_delete, name="good_delete"), - path("backend/good/create/", views.good_create, name="good_create"), - path("backend/good/edit//", views.good_edit, name="good_edit"), - path("backend/purchase/history/", views.purchase_history, name="purchase_history"), + path("backend/charge/", admin_views.backend_charge, name="backend_charge"), + path("backend/shop/", admin_views.backend_shop, name="backend_shop"), + path("backend/shop/delete//", admin_views.good_delete, name="good_delete"), + path("backend/good/create/", admin_views.good_create, name="good_create"), + path("backend/good/edit//", admin_views.good_edit, name="good_edit"), + path( + "backend/purchase/history/", + admin_views.purchase_history, + name="purchase_history", + ), # 公告管理相关 - path("backend/anno/", views.backend_anno, name="backend_anno"), - path("backend/anno/delete//", views.anno_delete, name="anno_delete"), - path("backend/anno/create/", views.anno_create, name="anno_create"), - path("backend/anno/edit//", views.anno_edit, name="anno_edit"), + path("backend/anno/", admin_views.backend_anno, name="backend_anno"), + path("backend/anno/delete//", admin_views.anno_delete, name="anno_delete"), + path("backend/anno/create/", admin_views.anno_create, name="anno_create"), + path("backend/anno/edit//", admin_views.anno_edit, name="anno_edit"), # 工单相关 - path("backend/ticket/", views.backend_ticket, name="backend_ticket"), + path("backend/ticket/", admin_views.backend_ticket, name="backend_ticket"), path( "backend/ticket/edit//", - views.backend_ticketedit, + admin_views.backend_ticketedit, name="backend_ticketedit", ), - # 在线ip - path("backend/aliveuser/", views.backend_alive_user, name="alive_user"), ] diff --git a/apps/sspanel/views.py b/apps/sspanel/views.py index e89bfa0797..9b0cf8afd0 100644 --- a/apps/sspanel/views.py +++ b/apps/sspanel/views.py @@ -1,37 +1,177 @@ -import tomd - -from django.db.models import Q -from django.urls import reverse from django.conf import settings -from django.shortcuts import render from django.contrib import messages -from django.http import HttpResponse, HttpResponseRedirect from django.contrib.auth import authenticate, login, logout -from django.contrib.auth.decorators import login_required, permission_required +from django.contrib.auth.decorators import login_required +from django.http import HttpResponse, HttpResponseRedirect, JsonResponse +from django.shortcuts import render +from django.urls import reverse +from django.utils.decorators import method_decorator +from django.views import View -from apps.utils import traffic_format -from apps.custom_views import Page_List_View -from .forms import RegisterForm, LoginForm, NodeForm, GoodsForm, AnnoForm -from apps.ssserver.models import Suser, Node, NodeOnlineLog, AliveIp -from .models import ( - InviteCode, - User, +from apps.constants import METHOD_CHOICES, THEME_CHOICES +from apps.sspanel.forms import LoginForm, RegisterForm +from apps.sspanel.models import ( + Announcement, Donate, Goods, + InviteCode, MoneyCode, PurchaseHistory, - Announcement, - Ticket, RebateRecord, + Ticket, + User, + SSNode, ) -from apps.constants import METHOD_CHOICES, PROTOCOL_CHOICES, OBFS_CHOICES, THEME_CHOICES +from apps.utils import traffic_format + + +class RegisterView(View): + def get(self, request): + if request.user.is_authenticated: + return HttpResponseRedirect(reverse("sspanel:userinfo")) + ref = request.GET.get("ref") + if ref: + form = RegisterForm(initial={"ref": ref}) + else: + form = RegisterForm(initial={"invitecode": request.GET.get("invitecode")}) + return render(request, "sspanel/register.html", {"form": form}) + + def post(self, request): + if settings.ALLOW_REGISTER is False: + return HttpResponse("已经关闭注册了喵") + + form = RegisterForm(data=request.POST) + if form.is_valid(): + user = User.add_new_user(form.cleaned_data) + if not user: + messages.error(request, "服务出现了点小问题", extra_tags="请尝试或者联系站长~") + return render(request, "sspanel/register.html", {"form": form}) + else: + messages.success(request, "自动跳转到用户中心", extra_tags="注册成功!") + user = authenticate( + username=form.cleaned_data["username"], + password=form.cleaned_data["password1"], + ) + login(request, user) + return HttpResponseRedirect(reverse("sspanel:userinfo")) + return render(request, "sspanel/register.html", {"form": form}) + + +class InviteCodeView(View): + def get(self, request): + code_list = InviteCode.list_by_code_type(InviteCode.TYPE_PUBLIC) + return render(request, "sspanel/invite.html", context={"code_list": code_list}) + + +class AffInviteView(View): + @method_decorator(login_required) + def get(self, request): + user = request.user + context = { + "code_list": InviteCode.list_by_user_id(user.pk), + "invite_percent": settings.INVITE_PERCENT * 100, + "invitecode_num": InviteCode.calc_num_by_user(user), + "ref_link": user.ref_link, + } + return render(request, "sspanel/aff_invite.html", context=context) + + +class AffStatusView(View): + @method_decorator(login_required) + def get(self, request): + user = request.user + rebate_logs = RebateRecord.list_by_user_id_with_consumer_username(user.pk) + bar_config = { + "labels": ["z", "v", "x", "x", "z", "v", "x", "x", "z", "v"], + "data": [1, 2, 3, 4, 1, 1, 1, 1, 1, 2], + "data_title": "每日邀请注册人数", + } + context = {"rebate_logs": rebate_logs, "user": user, "bar_config": bar_config} + return render(request, "sspanel/aff_status.html", context=context) + + +class UserInfoView(View): + @method_decorator(login_required) + def get(self, request): + user = request.user + user_ss_config = user.user_ss_config + user_traffic = user_ss_config.user_traffic + # 获取公告 + anno = Announcement.objects.first() + min_traffic = traffic_format(settings.MIN_CHECKIN_TRAFFIC) + max_traffic = traffic_format(settings.MAX_CHECKIN_TRAFFIC) + remain_traffic = "{:.2f}".format(100 - user_traffic.used_percentage) + context = { + "user": user, + "user_traffic": user_traffic, + "anno": anno, + "remain_traffic": remain_traffic, + "min_traffic": min_traffic, + "max_traffic": max_traffic, + "import_links": user_ss_config.get_import_links(), + "themes": THEME_CHOICES, + "sub_link": user.sub_link, + "sub_types": User.SUB_TYPES, + "user_sub_type": user.get_sub_type_display(), + } + return render(request, "sspanel/userinfo.html", context=context) + + +class NodeInfoView(View): + @method_decorator(login_required) + def get(self, request): + user = request.user + user_ss_config = user.user_ss_config + node_list = [ + node.to_dict_with_extra_info(user_ss_config) + for node in SSNode.get_active_nodes() + ] + context = {"node_list": node_list, "user": user, "sub_link": user.sub_link} + return render(request, "sspanel/nodeinfo.html", context=context) + + +class UserTrafficLog(View): + @method_decorator(login_required) + def get(self, request): + node_list = SSNode.get_active_nodes() + context = {"user": request.user, "node_list": node_list} + return render(request, "sspanel/user_traffic_log.html", context=context) + + +class UserSSNodeConfigView(View): + @method_decorator(login_required) + def get(self, request): + user = request.user + user_ss_config = user.user_ss_config + configs = [ + node.to_dict_with_user_ss_config(user_ss_config) + for node in SSNode.get_user_active_nodes(user) + ] + return JsonResponse({"configs": configs}) + + +class UserSettingView(View): + @method_decorator(login_required) + def get(self, request): + methods = [m[0] for m in METHOD_CHOICES] + context = {"user_ss_config": request.user.user_ss_config, "methods": methods} + return render(request, "sspanel/user_settings.html", context=context) + + +class ShopView(View): + @method_decorator(login_required) + def get(self, request): + user = request.user + goods = Goods.objects.filter(status=1) + context = {"user": user, "goods": goods} + return render(request, "sspanel/shop.html", context=context) def index(request): """跳转到首页""" return render( - request, "sspanel/index.html", {"allow_register": settings.ALLOW_REGISET} + request, "sspanel/index.html", {"allow_register": settings.ALLOW_REGISTER} ) @@ -46,42 +186,6 @@ def ssclient(request): return render(request, "sspanel/client.html") -def ssinvite(request): - """跳转到邀请码界面""" - codelist = InviteCode.objects.filter(code_type=1, isused=False, code_id=1)[:20] - - context = {"codelist": codelist} - - return render(request, "sspanel/invite.html", context=context) - - -def pass_invitecode(request, invitecode): - """提供点击邀请码连接之后自动填写邀请码""" - form = RegisterForm(initial={"invitecode": invitecode}) - return render(request, "sspanel/register.html", {"form": form}) - - -def register(request): - """用户注册时的函数""" - if settings.ALLOW_REGISET is False: - return HttpResponse("已经关闭注册了喵") - if request.method == "POST": - form = RegisterForm(request.POST) - if form.is_valid(): - user = User.add_new_user(form.cleaned_data) - if not user: - messages.error(request, "服务出现了点小问题", extra_tags="请尝试或者联系站长~") - return render(request, "sspanel/register.html", {"form": form}) - else: - messages.success(request, "请登录使用吧!", extra_tags="注册成功!") - - return HttpResponseRedirect(reverse("index")) - else: - form = RegisterForm() - - return render(request, "sspanel/register.html", {"form": form}) - - def user_login(request): """用户登录函数""" if request.method == "POST": @@ -97,7 +201,7 @@ def user_login(request): return HttpResponseRedirect(reverse("sspanel:userinfo")) else: messages.error(request, "请重新填写信息!", extra_tags="登录失败!") - context = {"form": LoginForm()} + context = {"form": LoginForm(), "USE_SMTP": settings.USE_SMTP} return render(request, "sspanel/login.html", context=context) @@ -108,47 +212,6 @@ def user_logout(request): return HttpResponseRedirect(reverse("index")) -@login_required -def userinfo(request): - """用户中心""" - user = request.user - # 获取公告 - anno = Announcement.objects.first() - min_traffic = traffic_format(settings.MIN_CHECKIN_TRAFFIC) - max_traffic = traffic_format(settings.MAX_CHECKIN_TRAFFIC) - remain_traffic = "{:.2f}".format(100 - user.ss_user.used_percentage) - context = { - "user": user, - "user_sub_type": user.get_sub_type_display(), - "anno": anno, - "remain_traffic": remain_traffic, - "min_traffic": min_traffic, - "max_traffic": max_traffic, - "sub_link": user.sub_link, - "import_code": Node.get_import_code(user), - "themes": THEME_CHOICES, - "sub_types": User.SUB_TYPES, - } - return render(request, "sspanel/userinfo.html", context=context) - - -@login_required -def userinfo_edit(request): - """跳转到资料编辑界面""" - ss_user = request.user.ss_user - methods = [m[0] for m in METHOD_CHOICES] - protocols = [p[0] for p in PROTOCOL_CHOICES] - obfss = [o[0] for o in OBFS_CHOICES] - - context = { - "ss_user": ss_user, - "methods": methods, - "protocols": protocols, - "obfss": obfss, - } - return render(request, "sspanel/userinfoedit.html", context=context) - - @login_required def donate(request): """捐赠界面和支付宝当面付功能""" @@ -162,81 +225,6 @@ def donate(request): return render(request, "sspanel/donate.html", context=context) -@login_required -def nodeinfo(request): - """跳转到节点信息的页面""" - - nodelists = [] - ss_user = request.user.ss_user - user = request.user - # 加入等级的判断 - nodes = Node.objects.filter(show=1).values() - # 循环遍历每一条线路的在线人数 - for node in nodes: - # 生成SSR和SS的链接 - obj = Node.objects.get(node_id=node["node_id"]) - node["ssrlink"] = obj.get_ssr_link(ss_user) - node["sslink"] = obj.get_ss_link(ss_user) - node["country"] = obj.country.lower() - node["node_type"] = obj.get_node_type_display()[:-3] - if obj.node_type == 1: - # 单端口的情况下 - node["port"] = obj.port - node["method"] = obj.method - node["password"] = obj.password - node["protocol"] = obj.protocol - node["node_color"] = "warning" - node["protocol_param"] = "{}:{}".format(ss_user.port, ss_user.password) - node["obfs"] = obj.obfs - node["obfs_param"] = obj.obfs_param - else: - node["port"] = ss_user.port - node["method"] = ss_user.method - node["password"] = ss_user.password - node["protocol"] = ss_user.protocol - node["node_color"] = "info" - node["protocol_param"] = ss_user.protocol_param - node["obfs"] = ss_user.obfs - node["obfs_param"] = ss_user.obfs_param - # 得到在线人数 - log = NodeOnlineLog.objects.filter(node_id=node["node_id"]).last() - if log: - node["online"] = log.get_oneline_status() - node["count"] = log.get_online_user() - else: - node["online"] = False - node["count"] = 0 - # 添加ss_type - node["ss_type_info"] = obj.get_ss_type_display() - nodelists.append(node) - context = { - "nodelists": nodelists, - "ss_user": ss_user, - "user": user, - "sub_link": user.sub_link, - } - return render(request, "sspanel/nodeinfo.html", context=context) - - -@login_required -def trafficlog(request): - """跳转到流量记录的页面""" - - ss_user = request.user.ss_user - nodes = Node.objects.filter(show=1) - context = {"ss_user": ss_user, "nodes": nodes} - return render(request, "sspanel/trafficlog.html", context=context) - - -@login_required -def shop(request): - """跳转到商品界面""" - ss_user = request.user - goods = Goods.objects.filter(status=1) - context = {"ss_user": ss_user, "goods": goods} - return render(request, "sspanel/shop.html", context=context) - - @login_required def purchaselog(request): """用户购买记录页面""" @@ -340,372 +328,3 @@ def ticket_edit(request, pk): else: context = {"ticket": ticket} return render(request, "sspanel/ticketedit.html", context=context) - - -@login_required -def affiliate(request): - """推广页面""" - if request.user.is_superuser is not True: - invidecodes = InviteCode.objects.filter(code_id=request.user.pk, code_type=0) - inviteNum = request.user.invitecode_num - len(invidecodes) - else: - # 如果是管理员,特殊处理 - invidecodes = InviteCode.objects.filter( - code_id=request.user.pk, code_type=0, isused=False - ) - inviteNum = 5 - context = { - "invitecodes": invidecodes, - "invitePercent": settings.INVITE_PERCENT * 100, - "inviteNumn": inviteNum, - } - return render(request, "sspanel/affiliate.html", context=context) - - -@login_required -def rebate_record(request): - """返利记录""" - u = request.user - records = RebateRecord.objects.filter(user_id=u.pk)[:10] - context = {"records": records, "user": request.user} - return render(request, "sspanel/rebaterecord.html", context=context) - - -# ================================== -# 网站后台界面 -# ================================== - - -@permission_required("sspanel") -def backend_index(request): - """跳转到后台界面""" - context = {"userNum": User.get_total_user_num()} - - return render(request, "backend/index.html", context=context) - - -@permission_required("sspanel") -def backend_node_info(request): - """节点编辑界面""" - nodes = Node.objects.all() - context = {"nodes": nodes} - return render(request, "backend/nodeinfo.html", context=context) - - -@permission_required("sspanel") -def node_delete(request, node_id): - """删除节点""" - node = Node.objects.filter(node_id=node_id) - node.delete() - messages.success(request, "成功啦", extra_tags="删除节点") - return HttpResponseRedirect(reverse("sspanel:backend_node_info")) - - -@permission_required("sspanel") -def node_edit(request, node_id): - """编辑节点""" - node = Node.objects.get(node_id=node_id) - # 当为post请求时,修改数据 - if request.method == "POST": - form = NodeForm(request.POST, instance=node) - if form.is_valid(): - form.save() - messages.success(request, "数据更新成功", extra_tags="修改成功") - return HttpResponseRedirect(reverse("sspanel:backend_node_info")) - else: - messages.error(request, "数据填写错误", extra_tags="错误") - context = {"form": form, "node": node} - return render(request, "backend/nodeedit.html", context=context) - # 当请求不是post时,渲染form - else: - form = NodeForm( - instance=node, initial={"total_traffic": node.total_traffic // settings.GB} - ) - context = {"form": form, "node": node} - return render(request, "backend/nodeedit.html", context=context) - - -@permission_required("sspanel") -def node_create(request): - """创建节点""" - if request.method == "POST": - form = NodeForm(request.POST) - if form.is_valid(): - form.save() - messages.success(request, "数据更新成功!", extra_tags="添加成功") - return HttpResponseRedirect(reverse("sspanel:backend_node_info")) - else: - messages.error(request, "数据填写错误", extra_tags="错误") - context = {"form": form} - return render(request, "backend/nodecreate.html", context=context) - - else: - form = NodeForm() - return render(request, "backend/nodecreate.html", context={"form": form}) - - -@permission_required("sspanel") -def backend_userlist(request): - """返回所有用户的View""" - obj = User.objects.all().order_by("-date_joined") - page_num = 15 - context = Page_List_View(request, obj, page_num).get_page_context() - return render(request, "backend/userlist.html", context) - - -@permission_required("sspanel") -def user_delete(request, pk): - """删除user""" - user = User.objects.get(pk=pk) - user.delete() - messages.success(request, "成功啦", extra_tags="删除用户") - return HttpResponseRedirect(reverse("sspanel:user_list")) - - -@permission_required("sspanel") -def user_search(request): - """用户搜索结果""" - q = request.GET.get("q") - contacts = User.objects.filter( - Q(username__icontains=q) | Q(email__icontains=q) | Q(pk__icontains=q) - ) - context = {"contacts": contacts} - return render(request, "backend/userlist.html", context=context) - - -@permission_required("sspanel") -def user_status(request): - """站内用户分析""" - # 查询今日注册的用户 - todayRegistered = User.get_today_register_user().values() - for t in todayRegistered: - try: - t["inviter"] = User.objects.get(pk=t["invited_by"]) - except User.DoesNotExist: - t["inviter"] = "ehco" - todayRegisteredNum = len(todayRegistered) - # 查询消费水平前十的用户 - richUser = Donate.richPeople() - # 查询流量用的最多的用户 - coreUser = Suser.get_user_by_traffic(num=10) - context = { - "userNum": User.get_total_user_num(), - "todayChecked": Suser.get_today_checked_user_num(), - "aliveUser": NodeOnlineLog.totalOnlineUser(), - "todayRegistered": todayRegistered[:10], - "todayRegisteredNum": todayRegisteredNum, - "richUser": richUser, - "coreUser": coreUser, - } - return render(request, "backend/userstatus.html", context=context) - - -@permission_required("sspanel") -def backend_invite(request): - """邀请码生成""" - code_list = InviteCode.objects.filter(code_type=0, isused=False, code_id=1) - return render(request, "backend/invitecode.html", {"code_list": code_list}) - - -@permission_required("sspanel") -def gen_invite_code(request): - - Num = request.GET.get("num") - code_type = request.GET.get("type") - for i in range(int(Num)): - code = InviteCode(code_type=code_type) - code.save() - messages.success(request, "添加邀请码{}个".format(Num), extra_tags="成功") - return HttpResponseRedirect(reverse("sspanel:backend_invite")) - - -@permission_required("sspanel") -def backend_charge(request): - """后台充值码界面""" - # 获取所有充值码记录 - obj = MoneyCode.objects.all() - page_num = 10 - context = Page_List_View(request, obj, page_num).get_page_context() - # 获取充值的金额和数量 - Num = request.GET.get("num") - money = request.GET.get("money") - if Num and money: - for i in range(int(Num)): - code = MoneyCode(number=money) - code.save() - messages.success(request, "添加{}元充值码{}个".format(money, Num), extra_tags="成功") - return HttpResponseRedirect(reverse("sspanel:backend_charge")) - return render(request, "backend/charge.html", context=context) - - -@permission_required("sspanel") -def backend_shop(request): - """商品管理界面""" - - goods = Goods.objects.all() - context = {"goods": goods} - return render(request, "backend/shop.html", context=context) - - -@permission_required("sspanel") -def good_delete(request, pk): - """删除商品""" - good = Goods.objects.filter(pk=pk) - good.delete() - messages.success(request, "成功啦", extra_tags="删除商品") - return HttpResponseRedirect(reverse("sspanel:backend_shop")) - - -@permission_required("sspanel") -def good_edit(request, pk): - """商品编辑""" - - good = Goods.objects.get(pk=pk) - # 当为post请求时,修改数据 - if request.method == "POST": - # 转换为GB - data = request.POST.copy() - data["transfer"] = eval(data["transfer"]) * settings.GB - form = GoodsForm(data, instance=good) - if form.is_valid(): - form.save() - messages.success(request, "数据更新成功", extra_tags="修改成功") - return HttpResponseRedirect(reverse("sspanel:backend_shop")) - else: - messages.error(request, "数据填写错误", extra_tags="错误") - context = {"form": form, "good": good} - return render(request, "backend/goodedit.html", context=context) - # 当请求不是post时,渲染form - else: - data = {"transfer": round(good.transfer / settings.GB)} - form = GoodsForm(initial=data, instance=good) - context = {"form": form, "good": good} - return render(request, "backend/goodedit.html", context=context) - - -@permission_required("sspanel") -def good_create(request): - """商品创建""" - if request.method == "POST": - # 转换为GB - data = request.POST.copy() - data["transfer"] = eval(data["transfer"]) * settings.GB - form = GoodsForm(data) - if form.is_valid(): - form.save() - messages.success(request, "数据更新成功!", extra_tags="添加成功") - return HttpResponseRedirect(reverse("sspanel:backend_shop")) - else: - messages.error(request, "数据填写错误", extra_tags="错误") - context = {"form": form} - return render(request, "backend/goodcreate.html", context=context) - else: - form = GoodsForm() - return render(request, "backend/goodcreate.html", context={"form": form}) - - -@permission_required("sspanel") -def purchase_history(request): - """购买历史""" - obj = PurchaseHistory.objects.all() - page_num = 10 - context = Page_List_View(request, obj, page_num).get_page_context() - return render(request, "backend/purchasehistory.html", context=context) - - -@permission_required("sspanel") -def backend_anno(request): - """公告管理界面""" - anno = Announcement.objects.all() - context = {"anno": anno} - return render(request, "backend/annolist.html", context=context) - - -@permission_required("sspanel") -def anno_delete(request, pk): - """删除公告""" - anno = Announcement.objects.filter(pk=pk) - anno.delete() - messages.success(request, "成功啦", extra_tags="删除公告") - return HttpResponseRedirect(reverse("sspanel:backend_anno")) - - -@permission_required("sspanel") -def anno_create(request): - """公告创建""" - if request.method == "POST": - form = AnnoForm(request.POST) - if form.is_valid(): - form.save() - messages.success(request, "数据更新成功", extra_tags="添加成功") - return HttpResponseRedirect(reverse("sspanel:backend_anno")) - else: - messages.error(request, "数据填写错误", extra_tags="错误") - context = {"form": form} - return render(request, "backend/annocreate.html", context=context) - else: - form = AnnoForm() - return render(request, "backend/annocreate.html", context={"form": form}) - - -@permission_required("sspanel") -def anno_edit(request, pk): - """公告编辑""" - anno = Announcement.objects.get(pk=pk) - # 当为post请求时,修改数据 - if request.method == "POST": - form = AnnoForm(request.POST, instance=anno) - if form.is_valid(): - form.save() - messages.success(request, "数据更新成功", extra_tags="修改成功") - return HttpResponseRedirect(reverse("sspanel:backend_anno")) - else: - messages.error(request, "数据填写错误", extra_tags="错误") - context = {"form": form, "anno": anno} - return render(request, "backend/annoedit.html", context=context) - # 当请求不是post时,渲染form - else: - anno.body = tomd.convert(anno.body) - context = {"anno": anno} - return render(request, "backend/annoedit.html", context=context) - - -@permission_required("sspanel") -def backend_ticket(request): - """工单系统""" - ticket = Ticket.objects.filter(status=1) - context = {"ticket": ticket} - return render(request, "backend/ticket.html", context=context) - - -@permission_required("sspanel") -def backend_ticketedit(request, pk): - """后台工单编辑""" - ticket = Ticket.objects.get(pk=pk) - # 当为post请求时,修改数据 - if request.method == "POST": - title = request.POST.get("title", "") - body = request.POST.get("body", "") - status = request.POST.get("status", 1) - ticket.title = title - ticket.body = body - ticket.status = status - ticket.save() - - messages.success(request, "数据更新成功", extra_tags="修改成功") - return HttpResponseRedirect(reverse("sspanel:backend_ticket")) - # 当请求不是post时,渲染 - else: - context = {"ticket": ticket} - return render(request, "backend/ticketedit.html", context=context) - - -@permission_required("ssserver") -def backend_alive_user(request): - user_list = [] - for node_id in Node.get_node_ids_by_show(): - user_list.extend(AliveIp.recent_alive(node_id)) - page_num = 15 - context = Page_List_View(request, user_list, page_num).get_page_context() - - return render(request, "backend/aliveuser.html", context=context) diff --git a/apps/ssserver/__init__.py b/apps/ssserver/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/apps/ssserver/admin.py b/apps/ssserver/admin.py deleted file mode 100644 index c7b8b92aaa..0000000000 --- a/apps/ssserver/admin.py +++ /dev/null @@ -1,43 +0,0 @@ -from django.contrib import admin -from . import models - - -class SUserAdmin(admin.ModelAdmin): - list_display = ["user", "user_id", "port", "used_traffic", "totla_transfer"] - search_fields = ["user_id", "port"] - list_filter = ["enable"] - - -class TrafficLogAdmin(admin.ModelAdmin): - search_fields = ["user_id", "node_id"] - list_display = ["user", "user_id", "node_id", "traffic", "log_date"] - - -class NodeAdmin(admin.ModelAdmin): - list_display = [ - "name", - "node_id", - "level", - "traffic_rate", - "order", - "human_used_traffic", - "human_total_traffic", - "show", - ] - - -class NodeOnlineAdmin(admin.ModelAdmin): - list_display = ["node_id", "online_user"] - - -class AliveIpAdmin(admin.ModelAdmin): - list_display = ["node_id", "user", "ip", "log_time"] - list_filter = ["node_id", "log_time"] - - -# Register your models here. -admin.site.register(models.Node, NodeAdmin) -admin.site.register(models.Suser, SUserAdmin) -admin.site.register(models.AliveIp, AliveIpAdmin) -admin.site.register(models.TrafficLog, TrafficLogAdmin) -admin.site.register(models.NodeOnlineLog, NodeOnlineAdmin) diff --git a/apps/ssserver/apps.py b/apps/ssserver/apps.py deleted file mode 100644 index d60c8048c3..0000000000 --- a/apps/ssserver/apps.py +++ /dev/null @@ -1,5 +0,0 @@ -from django.apps import AppConfig - - -class SsserverConfig(AppConfig): - name = "ssserver" diff --git a/apps/ssserver/forms.py b/apps/ssserver/forms.py deleted file mode 100644 index 1cb00f0e49..0000000000 --- a/apps/ssserver/forms.py +++ /dev/null @@ -1,42 +0,0 @@ -from django import forms -from django.forms import ModelForm -from .models import Suser - - -class ChangeSsPassForm(forms.Form): - - password = forms.CharField( - required=True, - label="连接密码", - error_messages={"required": "请输入密码"}, - widget=forms.PasswordInput( - attrs={"class": "input is-danger", "placeholder": "密码", "type": "text"} - ), - ) - - def clean(self): - if not self.is_valid(): - raise forms.ValidationError("太短啦!") - else: - self.cleaned_data = super(ChangeSsPassForm, self).clean() - - -class SuserForm(ModelForm): - class Meta: - model = Suser - fields = [ - "port", - "password", - "upload_traffic", - "download_traffic", - "transfer_enable", - "enable", - ] - widgets = { - "enable": forms.CheckboxInput(attrs={"class": "checkbox"}), - "port": forms.NumberInput(attrs={"class": "input"}), - "password": forms.TextInput(attrs={"class": "input"}), - "upload_traffic": forms.NumberInput(attrs={"class": "input"}), - "download_traffic": forms.NumberInput(attrs={"class": "input"}), - "transfer_enable": forms.NumberInput(attrs={"class": "input"}), - } diff --git a/apps/ssserver/migrations/0001_initial.py b/apps/ssserver/migrations/0001_initial.py deleted file mode 100644 index e405882133..0000000000 --- a/apps/ssserver/migrations/0001_initial.py +++ /dev/null @@ -1,642 +0,0 @@ -# Generated by Django 2.0.5 on 2018-06-18 21:17 - -import apps.utils -import datetime -from django.conf import settings -import django.core.validators -from django.db import migrations, models -import django.db.models.deletion -import django.utils.timezone - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [migrations.swappable_dependency(settings.AUTH_USER_MODEL)] - - operations = [ - migrations.CreateModel( - name="AliveIp", - fields=[ - ( - "id", - models.AutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ("node_id", models.IntegerField(verbose_name="节点id")), - ("ip", models.CharField(max_length=128, verbose_name="设备ip")), - ("user", models.CharField(max_length=128, verbose_name="用户名")), - ("log_time", models.DateTimeField(auto_now=True, verbose_name="日志时间")), - ], - options={"verbose_name_plural": "节点在线IP", "ordering": ["-log_time"]}, - ), - migrations.CreateModel( - name="Node", - fields=[ - ( - "id", - models.AutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ("node_id", models.IntegerField(unique=True, verbose_name="节点id")), - ( - "port", - models.IntegerField( - blank=True, - help_text="单端口多用户时需要", - null=True, - verbose_name="节点端口", - ), - ), - ( - "password", - models.CharField( - default="password", - help_text="单端口多用户时需要", - max_length=32, - verbose_name="节点密码", - ), - ), - ( - "country", - models.CharField( - choices=[ - ("AF", "阿富汗"), - ("AX", "奥兰群岛"), - ("AL", "阿尔巴尼亚"), - ("DZ", "阿尔及利亚"), - ("AS", "美属萨摩亚"), - ("AD", "安道尔"), - ("AO", "安哥拉"), - ("AI", "安圭拉"), - ("AQ", "南极洲"), - ("AG", "安提瓜和巴布达"), - ("AR", "阿根廷"), - ("AM", "亚美尼亚"), - ("AW", "阿鲁巴"), - ("AU", "澳大利亚"), - ("AT", "奥地利"), - ("AZ", "阿塞拜疆"), - ("BS", "巴哈马"), - ("BH", "巴林"), - ("BD", "孟加拉国"), - ("BB", "巴巴多斯"), - ("BY", "白俄罗斯"), - ("BE", "比利时"), - ("BZ", "伯利兹"), - ("BJ", "贝宁"), - ("BM", "百慕大"), - ("BT", "不丹"), - ("BO", "玻利维亚(多民族国)"), - ("BA", "波斯尼亚和黑塞哥维那"), - ("BW", "博茨瓦纳"), - ("BV", "布维岛"), - ("BR", "巴西"), - ("IO", "英属印度洋领地"), - ("BN", "文莱达鲁萨兰国"), - ("BG", "保加利亚"), - ("BF", "布基纳法索"), - ("BI", "布隆迪"), - ("CV", "Cabo Verde"), - ("KH", "柬埔寨"), - ("CM", "喀麦隆"), - ("CA", "加拿大"), - ("KY", "开曼群岛"), - ("CF", "中非共和国"), - ("TD", "乍得"), - ("CL", "智利"), - ("CN", "中国"), - ("CX", "圣诞岛"), - ("CC", "科科斯(基林)群岛"), - ("CO", "哥伦比亚"), - ("KM", "科摩罗"), - ("CD", "刚果(该民主共和国)"), - ("CG", "刚果"), - ("CK", "库克群岛"), - ("CR", "哥斯达黎加"), - ("CI", "科特迪瓦"), - ("HR", "克罗地亚"), - ("CU", "古巴"), - ("CW", "库拉索"), - ("CY", "塞浦路斯"), - ("CZ", "捷克"), - ("DK", "丹麦"), - ("DJ", "吉布提"), - ("DM", "多米尼克"), - ("DO", "多米尼加共和国"), - ("EC", "厄瓜多尔"), - ("EG", "埃及"), - ("SV", "萨尔瓦多"), - ("GQ", "赤道几内亚"), - ("ER", "厄立特里亚"), - ("EE", "爱沙尼亚"), - ("ET", "埃塞俄比亚"), - ("FK", "福克兰群岛[马尔维纳斯]"), - ("FO", "法罗群岛"), - ("FJ", "斐济"), - ("FI", "芬兰"), - ("FR", "法国"), - ("GF", "法属圭亚那"), - ("PF", "法属波利尼西亚"), - ("TF", "法国南部领土"), - ("GA", "加蓬"), - ("GM", "冈比亚"), - ("GE", "格鲁吉亚"), - ("DE", "德国"), - ("GH", "加纳"), - ("GI", "直布罗陀"), - ("GR", "希腊"), - ("GL", "格陵兰"), - ("GD", "格林纳达"), - ("GP", "Guadeloupe"), - ("GU", "关岛"), - ("GT", "危地马拉"), - ("GG", "根西岛"), - ("GN", "几内亚"), - ("GW", "几内亚比绍"), - ("GY", "圭亚那"), - ("HT", "海地"), - ("HM", "赫德岛和麦克唐纳群岛"), - ("VA", "罗马教廷"), - ("HN", "洪都拉斯"), - ("HK", "香港"), - ("HU", "匈牙利"), - ("IS", "冰岛"), - ("ID", "印度尼西亚"), - ("IR", "伊朗(伊斯兰共和国)"), - ("IQ", "伊拉克"), - ("IE", "爱尔兰"), - ("IM", "马恩岛"), - ("IL", "以色列"), - ("IT", "意大利"), - ("JM", "牙买加"), - ("JP", "日本"), - ("JE", "泽西岛"), - ("JO", "约旦"), - ("KZ", "哈萨克斯坦"), - ("KE", "肯尼亚"), - ("KI", "基里巴斯"), - ("KP", "韩国(朝鲜民主主义人民共和国)"), - ("KR", "韩国(共和国)"), - ("KW", "科威特"), - ("KG", "吉尔吉斯斯坦"), - ("LA", "老挝人民民主共和国"), - ("LV", "拉脱维亚"), - ("LB", "黎巴嫩"), - ("LS", "莱索托"), - ("LR", "利比里亚"), - ("LY", "利比亚"), - ("LI", "列支敦士登"), - ("LT", "立陶宛"), - ("LU", "卢森堡"), - ("MO", "澳门"), - ("MK", "马其顿(前南斯拉夫共和国)"), - ("MG", "马达加斯加"), - ("MW", "马拉维"), - ("MY", "马来西亚"), - ("MV", "马尔代夫"), - ("ML", "Mali"), - ("MT", "马耳他"), - ("MH", "马绍尔群岛"), - ("MQ", "马提尼克岛"), - ("MR", "毛里塔尼亚"), - ("MU", "毛里求斯"), - ("YT", "马约特岛"), - ("MX", "墨西哥"), - ("FM", "密克罗尼西亚联邦"), - ("MD", "摩尔多瓦共和国"), - ("MC", "摩纳哥"), - ("MN", "蒙古"), - ("ME", "黑山"), - ("MS", "蒙特塞拉特"), - ("MA", "摩洛哥"), - ("MZ", "莫桑比克"), - ("MM", "缅甸"), - ("NA", "纳米比亚"), - ("NR", "瑙鲁"), - ("NP", "尼泊尔"), - ("NL", "荷兰"), - ("NC", "新喀里多尼亚"), - ("NZ", "新西兰"), - ("NI", "尼加拉瓜"), - ("NE", "尼日尔"), - ("NG", "尼日利亚"), - ("NU", "纽埃"), - ("NF", "诺福克岛"), - ("MP", "北马里亚纳群岛"), - ("NO", "挪威"), - ("OM", "阿曼"), - ("PK", "巴基斯坦"), - ("PW", "帕劳"), - ("PS", "巴勒斯坦,国家"), - ("PA", "巴拿马"), - ("PG", "巴布亚新几内亚"), - ("PY", "巴拉圭"), - ("PE", "秘鲁"), - ("PH", "菲律宾"), - ("PN", "皮特凯恩"), - ("PL", "波兰"), - ("PT", "葡萄牙"), - ("PR", "波多黎各"), - ("QA", "卡塔尔"), - ("RE", "留尼汪"), - ("RO", "罗马尼亚"), - ("RU", "俄罗斯联邦"), - ("RW", "卢旺达"), - ("BL", "圣巴泰勒米"), - ("SH", "圣赫勒拿,阿森松和特里斯坦达库尼亚"), - ("KN", "圣基茨和尼维斯"), - ], - default="CN", - max_length=2, - verbose_name="国家", - ), - ), - ( - "custom_method", - models.SmallIntegerField( - choices=[(0, "否"), (1, "是")], default=0, verbose_name="自定义加密" - ), - ), - ( - "show", - models.SmallIntegerField( - choices=[(1, "显示"), (-1, "不显示")], default=1, verbose_name="是否显示" - ), - ), - ( - "node_type", - models.SmallIntegerField( - choices=[(0, "多端口多用户"), (1, "单端口多用户")], - default=0, - verbose_name="节点类型", - ), - ), - ("name", models.CharField(max_length=32, verbose_name="名字")), - ( - "info", - models.CharField( - blank=True, max_length=1024, null=True, verbose_name="节点说明" - ), - ), - ("server", models.CharField(max_length=128, verbose_name="服务器IP")), - ( - "method", - models.CharField( - choices=[ - ("aes-256-cfb", "aes-256-cfb"), - ("aes-128-ctr", "aes-128-ctr"), - ("rc4-md5", "rc4-md5"), - ("salsa20", "salsa20"), - ("chacha20", "chacha20"), - ("none", "none"), - ], - default="aes-128-ctr", - max_length=32, - verbose_name="加密类型", - ), - ), - ("traffic_rate", models.FloatField(default=1.0, verbose_name="流量比例")), - ( - "protocol", - models.CharField( - choices=[ - ("auth_sha1_v4", "auth_sha1_v4"), - ("auth_aes128_md5", "auth_aes128_md5"), - ("auth_aes128_sha1", "auth_aes128_sha1"), - ("auth_chain_a", "auth_chain_a"), - ("origin", "origin"), - ], - default="auth_chain_a", - max_length=32, - verbose_name="协议", - ), - ), - ( - "protocol_param", - models.CharField( - blank=True, max_length=128, null=True, verbose_name="协议参数" - ), - ), - ( - "obfs", - models.CharField( - choices=[ - ("plain", "plain"), - ("http_simple", "http_simple"), - ("http_simple_compatible", "http_simple_compatible"), - ("http_post", "http_post"), - ("tls1.2_ticket_auth", "tls1.2_ticket_auth"), - ], - default="http_simple", - max_length=32, - verbose_name="混淆", - ), - ), - ( - "obfs_param", - models.CharField( - blank=True, - default="", - max_length=128, - null=True, - verbose_name="混淆参数", - ), - ), - ( - "level", - models.PositiveIntegerField( - default=0, - validators=[ - django.core.validators.MaxValueValidator(9), - django.core.validators.MinValueValidator(0), - ], - verbose_name="节点等级", - ), - ), - ( - "total_traffic", - models.BigIntegerField(default=1073741824, verbose_name="总流量"), - ), - ( - "human_total_traffic", - models.CharField( - blank=True, - default="1GB", - max_length=255, - null=True, - verbose_name="节点总流量", - ), - ), - ( - "used_traffic", - models.BigIntegerField(default=0, verbose_name="已用流量"), - ), - ( - "human_used_traffic", - models.CharField( - blank=True, max_length=255, null=True, verbose_name="已用流量" - ), - ), - ( - "order", - models.PositiveSmallIntegerField(default=1, verbose_name="排序"), - ), - ( - "group", - models.CharField(default="谜之屋", max_length=32, verbose_name="分组名"), - ), - ], - options={ - "verbose_name_plural": "节点", - "db_table": "ss_node", - "ordering": ["-show", "order"], - }, - ), - migrations.CreateModel( - name="NodeInfoLog", - fields=[ - ( - "id", - models.AutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ("node_id", models.IntegerField(verbose_name="节点id")), - ("uptime", models.FloatField(verbose_name="更新时间")), - ("load", models.CharField(max_length=32, verbose_name="负载")), - ("log_time", models.IntegerField(verbose_name="日志时间")), - ], - options={ - "verbose_name_plural": "节点日志", - "db_table": "ss_node_info_log", - "ordering": ("-log_time",), - }, - ), - migrations.CreateModel( - name="NodeOnlineLog", - fields=[ - ( - "id", - models.AutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ("node_id", models.IntegerField(verbose_name="节点id")), - ("online_user", models.IntegerField(verbose_name="在线人数")), - ("log_time", models.IntegerField(verbose_name="日志时间")), - ], - options={"verbose_name_plural": "节点在线记录", "db_table": "ss_node_online_log"}, - ), - migrations.CreateModel( - name="SSUser", - fields=[ - ( - "id", - models.AutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ( - "last_check_in_time", - models.DateTimeField( - default=datetime.datetime(1970, 1, 1, 8, 0), - editable=False, - null=True, - verbose_name="最后签到时间", - ), - ), - ( - "password", - models.CharField( - db_column="passwd", - default=apps.utils.get_short_random_string, - max_length=32, - validators=[django.core.validators.MinLengthValidator(6)], - verbose_name="sspanel密码", - ), - ), - ( - "port", - models.IntegerField( - db_column="port", unique=True, verbose_name="端口" - ), - ), - ( - "last_use_time", - models.IntegerField( - db_column="t", - default=0, - editable=False, - help_text="时间戳", - verbose_name="最后使用时间", - ), - ), - ( - "upload_traffic", - models.BigIntegerField( - db_column="u", default=0, verbose_name="上传流量" - ), - ), - ( - "download_traffic", - models.BigIntegerField( - db_column="d", default=0, verbose_name="下载流量" - ), - ), - ( - "transfer_enable", - models.BigIntegerField( - db_column="transfer_enable", - default=5368709120, - verbose_name="总流量", - ), - ), - ( - "switch", - models.BooleanField( - db_column="switch", default=True, verbose_name="保留字段switch" - ), - ), - ( - "enable", - models.BooleanField( - db_column="enable", default=True, verbose_name="开启与否" - ), - ), - ( - "method", - models.CharField( - choices=[ - ("aes-256-cfb", "aes-256-cfb"), - ("aes-128-ctr", "aes-128-ctr"), - ("rc4-md5", "rc4-md5"), - ("salsa20", "salsa20"), - ("chacha20", "chacha20"), - ("none", "none"), - ], - default="aes-128-ctr", - max_length=32, - verbose_name="加密类型", - ), - ), - ( - "protocol", - models.CharField( - choices=[ - ("auth_sha1_v4", "auth_sha1_v4"), - ("auth_aes128_md5", "auth_aes128_md5"), - ("auth_aes128_sha1", "auth_aes128_sha1"), - ("auth_chain_a", "auth_chain_a"), - ("origin", "origin"), - ], - default="auth_chain_a", - max_length=32, - verbose_name="协议", - ), - ), - ( - "protocol_param", - models.CharField( - blank=True, max_length=128, null=True, verbose_name="协议参数" - ), - ), - ( - "obfs", - models.CharField( - choices=[ - ("plain", "plain"), - ("http_simple", "http_simple"), - ("http_simple_compatible", "http_simple_compatible"), - ("http_post", "http_post"), - ("tls1.2_ticket_auth", "tls1.2_ticket_auth"), - ], - default="http_simple", - max_length=32, - verbose_name="混淆", - ), - ), - ( - "obfs_param", - models.CharField( - blank=True, max_length=128, null=True, verbose_name="混淆参数" - ), - ), - ("level", models.PositiveIntegerField(default=0, verbose_name="用户等级")), - ( - "user", - models.OneToOneField( - on_delete=django.db.models.deletion.CASCADE, - related_name="ss_user", - to=settings.AUTH_USER_MODEL, - verbose_name="用户名", - ), - ), - ], - options={ - "verbose_name_plural": "SS用户", - "db_table": "user", - "ordering": ("-last_check_in_time",), - }, - ), - migrations.CreateModel( - name="TrafficLog", - fields=[ - ( - "id", - models.AutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ("user_id", models.IntegerField(verbose_name="用户id")), - ("node_id", models.IntegerField(verbose_name="节点id")), - ( - "upload_traffic", - models.BigIntegerField( - db_column="u", default=0, verbose_name="上传流量" - ), - ), - ( - "download_traffic", - models.BigIntegerField( - db_column="d", default=0, verbose_name="下载流量" - ), - ), - ("rate", models.FloatField(default=1.0, verbose_name="流量比例")), - ("traffic", models.CharField(max_length=32, verbose_name="流量记录")), - ("log_time", models.IntegerField(verbose_name="日志时间")), - ( - "log_date", - models.DateTimeField( - default=django.utils.timezone.now, verbose_name="记录日期" - ), - ), - ], - options={ - "verbose_name_plural": "流量记录", - "db_table": "user_traffic_log", - "ordering": ("-log_time",), - }, - ), - ] diff --git a/apps/ssserver/migrations/0002_auto_20180707_2200.py b/apps/ssserver/migrations/0002_auto_20180707_2200.py deleted file mode 100644 index b4f53f5581..0000000000 --- a/apps/ssserver/migrations/0002_auto_20180707_2200.py +++ /dev/null @@ -1,52 +0,0 @@ -# Generated by Django 2.0.5 on 2018-07-07 22:00 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [("ssserver", "0001_initial")] - - operations = [ - migrations.AlterField( - model_name="node", - name="country", - field=models.CharField( - choices=[ - ("US", "美国"), - ("CN", "中国"), - ("HK", "香港"), - ("JP", "日本"), - ("FR", "法国"), - ("DE", "德国"), - ("KR", "韩国"), - ("JE", "泽西岛"), - ("NZ", "新西兰"), - ("MX", "墨西哥"), - ("CA", "加拿大"), - ("BR", "巴西"), - ("CU", "古巴"), - ("CZ", "捷克"), - ("EG", "埃及"), - ("FI", "芬兰"), - ("GR", "希腊"), - ("GU", "关岛"), - ("IS", "冰岛"), - ("MO", "澳门"), - ("NL", "荷兰"), - ("NO", "挪威"), - ("PL", "波兰"), - ("IT", "意大利"), - ("IE", "爱尔兰"), - ("AR", "阿根廷"), - ("PT", "葡萄牙"), - ("AU", "澳大利亚"), - ("RU", "俄罗斯联邦"), - ("CF", "中非共和国"), - ], - default="CN", - max_length=2, - verbose_name="国家", - ), - ) - ] diff --git a/apps/ssserver/migrations/0003_auto_20180730_0916.py b/apps/ssserver/migrations/0003_auto_20180730_0916.py deleted file mode 100644 index 42934ebd41..0000000000 --- a/apps/ssserver/migrations/0003_auto_20180730_0916.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 2.0.7 on 2018-07-30 09:16 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [("ssserver", "0002_auto_20180707_2200")] - - operations = [ - migrations.RemoveField(model_name="node", name="human_total_traffic"), - migrations.RemoveField(model_name="node", name="human_used_traffic"), - migrations.AlterField( - model_name="node", - name="password", - field=models.CharField( - default="password", - help_text="单端口时需要", - max_length=32, - verbose_name="节点密码", - ), - ), - ] diff --git a/apps/ssserver/migrations/0004_auto_20180930_1537.py b/apps/ssserver/migrations/0004_auto_20180930_1537.py deleted file mode 100644 index 4b9f8d2000..0000000000 --- a/apps/ssserver/migrations/0004_auto_20180930_1537.py +++ /dev/null @@ -1,181 +0,0 @@ -# Generated by Django 2.0.7 on 2018-09-30 07:37 - -import apps.utils -import datetime -import django.core.validators -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [("ssserver", "0003_auto_20180730_0916")] - - operations = [ - migrations.CreateModel( - name="Suser", - fields=[ - ( - "id", - models.AutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ( - "user_id", - models.IntegerField( - db_column="user_id", - db_index=True, - unique=True, - verbose_name="user_id", - ), - ), - ( - "last_check_in_time", - models.DateTimeField( - default=datetime.datetime(1970, 1, 1, 8, 0), - editable=False, - null=True, - verbose_name="最后签到时间", - ), - ), - ( - "password", - models.CharField( - db_column="passwd", - default=apps.utils.get_short_random_string, - max_length=32, - validators=[django.core.validators.MinLengthValidator(6)], - verbose_name="sspanel密码", - ), - ), - ( - "port", - models.IntegerField( - db_column="port", unique=True, verbose_name="端口" - ), - ), - ( - "last_use_time", - models.IntegerField( - db_column="t", - default=0, - editable=False, - help_text="时间戳", - verbose_name="最后使用时间", - ), - ), - ( - "upload_traffic", - models.BigIntegerField( - db_column="u", default=0, verbose_name="上传流量" - ), - ), - ( - "download_traffic", - models.BigIntegerField( - db_column="d", default=0, verbose_name="下载流量" - ), - ), - ( - "transfer_enable", - models.BigIntegerField( - db_column="transfer_enable", - default=5368709120, - verbose_name="总流量", - ), - ), - ( - "switch", - models.BooleanField( - db_column="switch", default=True, verbose_name="保留字段switch" - ), - ), - ( - "enable", - models.BooleanField( - db_column="enable", default=True, verbose_name="开启与否" - ), - ), - ( - "method", - models.CharField( - choices=[ - ("aes-256-cfb", "aes-256-cfb"), - ("aes-128-ctr", "aes-128-ctr"), - ("rc4-md5", "rc4-md5"), - ("salsa20", "salsa20"), - ("chacha20", "chacha20"), - ("none", "none"), - ], - default="aes-128-ctr", - max_length=32, - verbose_name="加密类型", - ), - ), - ( - "protocol", - models.CharField( - choices=[ - ("auth_sha1_v4", "auth_sha1_v4"), - ("auth_aes128_md5", "auth_aes128_md5"), - ("auth_aes128_sha1", "auth_aes128_sha1"), - ("auth_chain_a", "auth_chain_a"), - ("origin", "origin"), - ], - default="auth_chain_a", - max_length=32, - verbose_name="协议", - ), - ), - ( - "protocol_param", - models.CharField( - blank=True, max_length=128, null=True, verbose_name="协议参数" - ), - ), - ( - "obfs", - models.CharField( - choices=[ - ("plain", "plain"), - ("http_simple", "http_simple"), - ("http_simple_compatible", "http_simple_compatible"), - ("http_post", "http_post"), - ("tls1.2_ticket_auth", "tls1.2_ticket_auth"), - ], - default="http_simple", - max_length=32, - verbose_name="混淆", - ), - ), - ( - "obfs_param", - models.CharField( - blank=True, max_length=255, null=True, verbose_name="混淆参数" - ), - ), - ], - options={ - "verbose_name_plural": "ss_db_user", - "db_table": "s_user", - "ordering": ("-last_check_in_time",), - }, - ), - migrations.AlterField( - model_name="node", - name="obfs_param", - field=models.CharField( - blank=True, default="", max_length=255, null=True, verbose_name="混淆参数" - ), - ), - migrations.AlterField( - model_name="ssuser", - name="obfs_param", - field=models.CharField( - blank=True, max_length=255, null=True, verbose_name="混淆参数" - ), - ), - ] diff --git a/apps/ssserver/migrations/0005_auto_20181003_1549.py b/apps/ssserver/migrations/0005_auto_20181003_1549.py deleted file mode 100644 index 88158a8b09..0000000000 --- a/apps/ssserver/migrations/0005_auto_20181003_1549.py +++ /dev/null @@ -1,38 +0,0 @@ -# Generated by Django 2.1.2 on 2018-10-03 07:49 - -import apps.utils -import django.core.validators -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [("ssserver", "0004_auto_20180930_1537")] - - operations = [ - migrations.AlterModelOptions( - name="suser", - options={ - "ordering": ("-last_check_in_time",), - "verbose_name_plural": "Ss用户", - }, - ), - migrations.AlterField( - model_name="suser", - name="last_check_in_time", - field=models.DateTimeField( - editable=False, null=True, verbose_name="最后签到时间" - ), - ), - migrations.AlterField( - model_name="suser", - name="password", - field=models.CharField( - db_column="passwd", - default=apps.utils.get_short_random_string, - max_length=32, - validators=[django.core.validators.MinLengthValidator(6)], - verbose_name="ss密码", - ), - ), - ] diff --git a/apps/ssserver/migrations/0006_auto_20181003_1759.py b/apps/ssserver/migrations/0006_auto_20181003_1759.py deleted file mode 100644 index da8942f060..0000000000 --- a/apps/ssserver/migrations/0006_auto_20181003_1759.py +++ /dev/null @@ -1,13 +0,0 @@ -# Generated by Django 2.0 on 2018-10-03 09:59 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [("ssserver", "0005_auto_20181003_1549")] - - operations = [ - migrations.RemoveField(model_name="ssuser", name="user"), - migrations.DeleteModel(name="SSUser"), - ] diff --git a/apps/ssserver/migrations/0007_auto_20181004_1032.py b/apps/ssserver/migrations/0007_auto_20181004_1032.py deleted file mode 100644 index 9490445a3f..0000000000 --- a/apps/ssserver/migrations/0007_auto_20181004_1032.py +++ /dev/null @@ -1,29 +0,0 @@ -# Generated by Django 2.0 on 2018-10-04 02:32 - -from django.db import migrations, models -import django.utils.timezone - - -class Migration(migrations.Migration): - - dependencies = [("ssserver", "0006_auto_20181003_1759")] - - operations = [ - migrations.AlterField( - model_name="trafficlog", - name="log_date", - field=models.DateField( - db_index=True, default=django.utils.timezone.now, verbose_name="记录日期" - ), - ), - migrations.AlterField( - model_name="trafficlog", - name="node_id", - field=models.IntegerField(db_index=True, verbose_name="节点id"), - ), - migrations.AlterField( - model_name="trafficlog", - name="user_id", - field=models.IntegerField(db_index=True, verbose_name="用户id"), - ), - ] diff --git a/apps/ssserver/migrations/0008_auto_20181009_0909.py b/apps/ssserver/migrations/0008_auto_20181009_0909.py deleted file mode 100644 index 6e60ddde13..0000000000 --- a/apps/ssserver/migrations/0008_auto_20181009_0909.py +++ /dev/null @@ -1,42 +0,0 @@ -# Generated by Django 2.0.7 on 2018-10-09 01:09 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [("ssserver", "0007_auto_20181004_1032")] - - operations = [ - migrations.DeleteModel(name="NodeInfoLog"), - migrations.AddField( - model_name="node", - name="ss_type", - field=models.SmallIntegerField( - choices=[(0, "SS"), (1, "SSR"), (2, "SS/SSR")], - default=2, - verbose_name="SS类型", - ), - ), - migrations.AlterField( - model_name="node", - name="obfs_param", - field=models.CharField( - blank=True, default="", max_length=255, verbose_name="混淆参数" - ), - ), - migrations.AlterField( - model_name="node", - name="port", - field=models.IntegerField( - blank=True, default=443, help_text="单端口多用户时需要", verbose_name="节点端口" - ), - ), - migrations.AlterField( - model_name="node", - name="protocol_param", - field=models.CharField( - blank=True, default="", max_length=128, verbose_name="协议参数" - ), - ), - ] diff --git a/apps/ssserver/migrations/0009_auto_20181122_0930.py b/apps/ssserver/migrations/0009_auto_20181122_0930.py deleted file mode 100644 index 959d9678e6..0000000000 --- a/apps/ssserver/migrations/0009_auto_20181122_0930.py +++ /dev/null @@ -1,53 +0,0 @@ -# Generated by Django 2.1.3 on 2018-11-22 01:30 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [("ssserver", "0008_auto_20181009_0909")] - - operations = [ - migrations.AlterField( - model_name="node", - name="country", - field=models.CharField( - choices=[ - ("US", "美国"), - ("CN", "中国"), - ("TW", "台湾"), - ("HK", "香港"), - ("JP", "日本"), - ("FR", "法国"), - ("DE", "德国"), - ("KR", "韩国"), - ("JE", "泽西岛"), - ("NZ", "新西兰"), - ("MX", "墨西哥"), - ("CA", "加拿大"), - ("BR", "巴西"), - ("CU", "古巴"), - ("CZ", "捷克"), - ("EG", "埃及"), - ("FI", "芬兰"), - ("GR", "希腊"), - ("GU", "关岛"), - ("IS", "冰岛"), - ("MO", "澳门"), - ("NL", "荷兰"), - ("NO", "挪威"), - ("PL", "波兰"), - ("IT", "意大利"), - ("IE", "爱尔兰"), - ("AR", "阿根廷"), - ("PT", "葡萄牙"), - ("AU", "澳大利亚"), - ("RU", "俄罗斯联邦"), - ("CF", "中非共和国"), - ], - default="CN", - max_length=2, - verbose_name="国家", - ), - ) - ] diff --git a/apps/ssserver/migrations/0010_auto_20181219_1632.py b/apps/ssserver/migrations/0010_auto_20181219_1632.py deleted file mode 100644 index 2d308939e9..0000000000 --- a/apps/ssserver/migrations/0010_auto_20181219_1632.py +++ /dev/null @@ -1,67 +0,0 @@ -# Generated by Django 2.0.4 on 2018-12-19 08:32 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [("ssserver", "0009_auto_20181122_0930")] - - operations = [ - migrations.AddField( - model_name="node", - name="speed_limit", - field=models.IntegerField(default=0, verbose_name="限速"), - ), - migrations.AddField( - model_name="suser", - name="speed_limit", - field=models.IntegerField( - db_column="speed_limit", default=0, verbose_name="限速" - ), - ), - migrations.AlterField( - model_name="node", - name="country", - field=models.CharField( - choices=[ - ("US", "美国"), - ("CN", "中国"), - ("GB", "英国"), - ("SG", "新加坡"), - ("TW", "台湾"), - ("HK", "香港"), - ("JP", "日本"), - ("FR", "法国"), - ("DE", "德国"), - ("KR", "韩国"), - ("JE", "泽西岛"), - ("NZ", "新西兰"), - ("MX", "墨西哥"), - ("CA", "加拿大"), - ("BR", "巴西"), - ("CU", "古巴"), - ("CZ", "捷克"), - ("EG", "埃及"), - ("FI", "芬兰"), - ("GR", "希腊"), - ("GU", "关岛"), - ("IS", "冰岛"), - ("MO", "澳门"), - ("NL", "荷兰"), - ("NO", "挪威"), - ("PL", "波兰"), - ("IT", "意大利"), - ("IE", "爱尔兰"), - ("AR", "阿根廷"), - ("PT", "葡萄牙"), - ("AU", "澳大利亚"), - ("RU", "俄罗斯联邦"), - ("CF", "中非共和国"), - ], - default="CN", - max_length=2, - verbose_name="国家", - ), - ), - ] diff --git a/apps/ssserver/migrations/__init__.py b/apps/ssserver/migrations/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/apps/ssserver/models.py b/apps/ssserver/models.py deleted file mode 100644 index 000f8c8269..0000000000 --- a/apps/ssserver/models.py +++ /dev/null @@ -1,576 +0,0 @@ -import time -import base64 -from random import choice, randint - -import pendulum -from django_prometheus.models import ExportModelOperationsMixin -from django.db.models import F -from django.conf import settings -from django.utils import timezone -from django.core import validators -from django.db import models, connection -from django.core.exceptions import ValidationError -from django.core.validators import MaxValueValidator, MinValueValidator - -from apps.utils import get_short_random_string, traffic_format, get_current_time, cache -from apps.constants import ( - METHOD_CHOICES, - PROTOCOL_CHOICES, - OBFS_CHOICES, - COUNTRIES_CHOICES, - NODE_TIME_OUT, -) - - -class Suser(ExportModelOperationsMixin("ss_user"), models.Model): - """与user通过user_id作为虚拟外键关联""" - - user_id = models.IntegerField( - verbose_name="user_id", db_column="user_id", unique=True, db_index=True - ) - last_check_in_time = models.DateTimeField( - verbose_name="最后签到时间", null=True, editable=False - ) - password = models.CharField( - verbose_name="ss密码", - max_length=32, - default=get_short_random_string, - db_column="passwd", - validators=[validators.MinLengthValidator(6)], - ) - port = models.IntegerField(verbose_name="端口", db_column="port", unique=True) - last_use_time = models.IntegerField( - verbose_name="最后使用时间", default=0, editable=False, help_text="时间戳", db_column="t" - ) - upload_traffic = models.BigIntegerField( - verbose_name="上传流量", default=0, db_column="u" - ) - download_traffic = models.BigIntegerField( - verbose_name="下载流量", default=0, db_column="d" - ) - transfer_enable = models.BigIntegerField( - verbose_name="总流量", - default=settings.DEFAULT_TRAFFIC, - db_column="transfer_enable", - ) - speed_limit = models.IntegerField( - verbose_name="限速", default=0, db_column="speed_limit" - ) - switch = models.BooleanField( - verbose_name="保留字段switch", default=True, db_column="switch" - ) - enable = models.BooleanField(verbose_name="开启与否", default=True, db_column="enable") - method = models.CharField( - verbose_name="加密类型", - default=settings.DEFAULT_METHOD, - max_length=32, - choices=METHOD_CHOICES, - ) - protocol = models.CharField( - verbose_name="协议", - default=settings.DEFAULT_PROTOCOL, - max_length=32, - choices=PROTOCOL_CHOICES, - ) - protocol_param = models.CharField( - verbose_name="协议参数", max_length=128, null=True, blank=True - ) - obfs = models.CharField( - verbose_name="混淆", - default=settings.DEFAULT_OBFS, - max_length=32, - choices=OBFS_CHOICES, - ) - obfs_param = models.CharField( - verbose_name="混淆参数", max_length=255, null=True, blank=True - ) - - class Meta: - verbose_name_plural = "Ss用户" - ordering = ("-last_check_in_time",) - db_table = "s_user" - - def __str__(self): - return self.user.username - - def clean(self): - """保证端口在1024<50000之间""" - if self.port: - if not 1024 < self.port < 50000: - raise ValidationError("端口必须在1024和50000之间") - - @classmethod - def get_today_checked_user_num(cls): - now = get_current_time() - midnight = pendulum.datetime( - year=now.year, month=now.month, day=now.day, tz=now.tz - ) - query = cls.objects.filter(last_check_in_time__gte=midnight) - return query.count() - - @classmethod - def get_never_checked_user_num(cls): - return cls.objects.filter(last_check_in_time=None).count() - - @classmethod - def get_never_used_num(cls): - """返回从未使用过的人数""" - return cls.objects.filter(last_use_time=0).count() - - @classmethod - def get_user_by_traffic(cls, num=10): - """返回流量用的最多的前num名用户""" - return cls.objects.all().order_by("-download_traffic")[:10] - - @classmethod - def get_users_by_level(cls, level): - """返回指大于等于指定等级的所有合法用户""" - from apps.sspanel.models import User - - user_ids = User.objects.filter(level__gte=level).values_list("id") - users = cls.objects.filter( - transfer_enable__gte=(F("upload_traffic") + F("download_traffic")), - user_id__in=user_ids, - ) - return users - - @classmethod - @cache.cached(ttl=60 * 60 * 5) - def get_user_configs_by_node_id(cls, node_id): - data = [] - node = Node.objects.filter(node_id=node_id).first() - if not node: - return data - user_list = cls.get_users_by_level(node.level) - for user in user_list: - cfg = { - "port": user.port, - "u": user.upload_traffic, - "d": user.download_traffic, - "transfer_enable": user.transfer_enable, - "passwd": user.password, - "enable": user.enable, - "user_id": user.user_id, - "id": user.user_id, - "method": user.method, - "obfs": user.obfs, - "obfs_param": user.obfs_param, - "protocol": user.protocol, - "protocol_param": user.protocol_param, - "speed_limit_per_user": user.speed_limit, - } - if node.speed_limit > 0: - if user.speed_limit > 0: - cfg["speed_limit_per_user"] = min( - user.speed_limit, node.speed_limit - ) - else: - cfg["speed_limit_per_user"] = node.speed_limit - - data.append(cfg) - return data - - @classmethod - def clear_get_user_configs_by_node_id_cache(cls): - node_ids = Node.get_node_ids_by_show(all=True) - keys = [] - for node_id in node_ids: - keys.append(cls.get_user_configs_by_node_id.make_cache_key(cls, node_id)) - return cache.delete_many(keys) - - @classmethod - def get_random_port(cls): - users = cls.objects.all().values_list("port") - port_list = [] - for user in users: - port_list.append(user[0]) - if len(port_list) == 0: - return 1025 - all_ports = [i for i in range(1025, max(port_list) + 1)] - try: - return choice(list(set(all_ports).difference(set(port_list)))) - except IndexError: - return max(port_list) + 1 - - @property - def user(self): - from apps.sspanel.models import User - - return User.objects.get(pk=self.user_id) - - @property - def today_is_checked(self): - if self.last_check_in_time: - return self.last_check_in_time.date() == timezone.now().date() - return False - - @property - def user_last_use_time(self): - t = pendulum.from_timestamp(self.last_use_time, tz=settings.TIME_ZONE) - return t - - @property - def used_traffic(self): - return traffic_format(self.download_traffic + self.upload_traffic) - - @property - def totla_transfer(self): - return traffic_format(self.transfer_enable) - - @property - def unused_traffic(self): - return traffic_format( - self.transfer_enable - self.upload_traffic - self.download_traffic - ) - - @property - def used_percentage(self): - try: - used = self.download_traffic + self.upload_traffic - return used / self.transfer_enable * 100 - except ZeroDivisionError: - return 100 - - def checkin(self): - if not self.today_is_checked: - traffic = randint( - settings.MIN_CHECKIN_TRAFFIC, settings.MAX_CHECKIN_TRAFFIC - ) - self.transfer_enable += traffic - self.last_check_in_time = get_current_time() - self.save() - return True, traffic - return False, 0 - - def reset_traffic(self, new_traffic): - self.transfer_enable = new_traffic - self.upload_traffic = 0 - self.download_traffic = 0 - - def increase_transfer(self, new_transfer): - self.transfer_enable += new_transfer - - -class Node(ExportModelOperationsMixin("node"), models.Model): - """线路节点""" - - SHOW_CHOICES = ((1, "显示"), (-1, "不显示")) - - NODE_TYPE_CHOICES = ((0, "多端口多用户"), (1, "单端口多用户")) - - CUSTOM_METHOD_CHOICES = ((0, "否"), (1, "是")) - - SS_TYPE_CHOICES = ((0, "SS"), (1, "SSR"), (2, "SS/SSR")) - - class Meta: - ordering = ["-show", "order"] - verbose_name_plural = "节点" - db_table = "ss_node" - - node_id = models.IntegerField("节点id", unique=True) - port = models.IntegerField("节点端口", default=443, blank=True, help_text="单端口多用户时需要") - password = models.CharField( - "节点密码", max_length=32, default="password", help_text="单端口时需要" - ) - country = models.CharField( - "国家", default="CN", max_length=2, choices=COUNTRIES_CHOICES - ) - custom_method = models.SmallIntegerField( - "自定义加密", choices=CUSTOM_METHOD_CHOICES, default=0 - ) - show = models.SmallIntegerField("是否显示", choices=SHOW_CHOICES, default=1) - node_type = models.SmallIntegerField("节点类型", choices=NODE_TYPE_CHOICES, default=0) - ss_type = models.SmallIntegerField("SS类型", choices=SS_TYPE_CHOICES, default=2) - name = models.CharField("名字", max_length=32) - info = models.CharField("节点说明", max_length=1024, blank=True, null=True) - server = models.CharField("服务器IP", max_length=128) - method = models.CharField( - "加密类型", default=settings.DEFAULT_METHOD, max_length=32, choices=METHOD_CHOICES - ) - traffic_rate = models.FloatField("流量比例", default=1.0) - protocol = models.CharField( - "协议", default=settings.DEFAULT_PROTOCOL, max_length=32, choices=PROTOCOL_CHOICES - ) - protocol_param = models.CharField("协议参数", max_length=128, default="", blank=True) - obfs = models.CharField( - "混淆", default=settings.DEFAULT_OBFS, max_length=32, choices=OBFS_CHOICES - ) - obfs_param = models.CharField("混淆参数", max_length=255, default="", blank=True) - level = models.PositiveIntegerField( - "节点等级", default=0, validators=[MaxValueValidator(9), MinValueValidator(0)] - ) - total_traffic = models.BigIntegerField("总流量", default=settings.GB) - used_traffic = models.BigIntegerField("已用流量", default=0) - speed_limit = models.IntegerField("限速", default=0) - order = models.PositiveSmallIntegerField("排序", default=1) - group = models.CharField("分组名", max_length=32, default="谜之屋") - - def __str__(self): - return self.name - - def get_ssr_link(self, ss_user): - """返回ssr链接""" - ssr_password = ( - base64.urlsafe_b64encode(bytes(ss_user.password, "utf8")) - .decode("utf8") - .replace("=", "") - ) - ssr_remarks = ( - base64.urlsafe_b64encode(bytes(self.name, "utf8")) - .decode("utf8") - .replace("=", "") - ) - ssr_group = ( - base64.urlsafe_b64encode(bytes(self.group, "utf8")) - .decode("utf8") - .replace("=", "") - ) - if self.node_type == 1: - # 单端口多用户 - ssr_password = ( - base64.urlsafe_b64encode(bytes(self.password, "utf8")) - .decode("utf8") - .replace("=", "") - ) - info = "{}:{}".format(ss_user.port, ss_user.password) - protocol_param = ( - base64.urlsafe_b64encode(bytes(info, "utf8")) - .decode("utf8") - .replace("=", "") - ) - obfs_param = ( - base64.urlsafe_b64encode(bytes(str(self.obfs_param), "utf8")) - .decode("utf8") - .replace("=", "") - ) - ssr_code = "{}:{}:{}:{}:{}:{}/?obfsparam={}&protoparam={}&remarks={}&group={}".format( - self.server, - self.port, - self.protocol, - self.method, - self.obfs, - ssr_password, - obfs_param, - protocol_param, - ssr_remarks, - ssr_group, - ) - elif self.custom_method == 1: - ssr_code = "{}:{}:{}:{}:{}:{}/?remarks={}&group={}".format( - self.server, - ss_user.port, - ss_user.protocol, - ss_user.method, - ss_user.obfs, - ssr_password, - ssr_remarks, - ssr_group, - ) - else: - ssr_code = "{}:{}:{}:{}:{}:{}/?remarks={}&group={}".format( - self.server, - ss_user.port, - self.protocol, - self.method, - self.obfs, - ssr_password, - ssr_remarks, - ssr_group, - ) - ssr_pass = ( - base64.urlsafe_b64encode(bytes(ssr_code, "utf8")) - .decode("utf8") - .replace("=", "") - ) - ssr_link = "ssr://{}".format(ssr_pass) - return ssr_link - - def get_ss_link(self, ss_user): - """返回ss链接""" - if self.custom_method == 1: - ss_code = "{}:{}@{}:{}".format( - ss_user.method, ss_user.password, self.server, ss_user.port - ) - else: - ss_code = "{}:{}@{}:{}".format( - self.method, ss_user.password, self.server, ss_user.port - ) - ss_pass = base64.urlsafe_b64encode(bytes(ss_code, "utf8")).decode("utf8") - ss_link = "ss://{}#{}".format(ss_pass, self.name) - return ss_link - - def get_node_link(self, ss_user): - """获取当前的节点链接""" - if self.ss_type == 0: - return self.get_ss_link(ss_user) - else: - return self.get_ssr_link(ss_user) - - def save(self, *args, **kwargs): - if self.node_type == 1: - self.custom_method = 0 - super(Node, self).save(*args, **kwargs) - - def human_total_traffic(self): - """总流量""" - return traffic_format(self.total_traffic) - - def human_used_traffic(self): - """已用流量""" - return traffic_format(self.used_traffic) - - @classmethod - def get_by_node_id(cls, node_id): - return cls.objects.get(node_id=node_id) - - @classmethod - def get_import_code(cls, user): - """获取该用户的所有节点的导入信息""" - ss_user = user.ss_user - sub_code_list = [] - node_list = cls.objects.filter(level__lte=user.level, show=1) - for node in node_list: - sub_code_list.append(node.get_node_link(ss_user)) - return "\n".join(sub_code_list) - - @classmethod - def get_node_ids_by_show(cls, show=1, all=False): - if all: - nodes = cls.objects.all().values_list("node_id") - else: - nodes = cls.objects.filter(show=show).values_list("node_id") - return [node[0] for node in nodes] - - # verbose_name - human_total_traffic.short_description = "总流量" - human_used_traffic.short_description = "使用流量" - - -class TrafficLog(ExportModelOperationsMixin("traffic_log"), models.Model): - """用户流量记录""" - - user_id = models.IntegerField("用户id", blank=False, null=False, db_index=True) - node_id = models.IntegerField("节点id", blank=False, null=False, db_index=True) - upload_traffic = models.BigIntegerField("上传流量", default=0, db_column="u") - download_traffic = models.BigIntegerField("下载流量", default=0, db_column="d") - rate = models.FloatField("流量比例", default=1.0, null=False) - traffic = models.CharField("流量记录", max_length=32, null=False) - log_time = models.IntegerField("日志时间", blank=False, null=False) - log_date = models.DateField( - "记录日期", default=timezone.now, blank=False, null=False, db_index=True - ) - - def __str__(self): - return self.traffic - - class Meta: - verbose_name_plural = "流量记录" - ordering = ("-log_time",) - db_table = "user_traffic_log" - - @property - def user(self): - from apps.sspanel.models import User - - return User.objects.get(pk=self.user_id) - - @property - def used_traffic(self): - return self.download_traffic + self.upload_traffic - - @classmethod - def get_user_traffic(cls, node_id, user_id): - logs = cls.objects.filter(node_id=node_id, user_id=user_id) - return traffic_format(sum([l.used_traffic for l in logs])) - - @classmethod - def get_traffic_by_date(cls, node_id, user_id, date): - logs = cls.objects.filter(node_id=node_id, user_id=user_id, log_date=date) - return round(sum([l.used_traffic for l in logs]) / settings.MB, 1) - - @classmethod - def truncate(cls): - with connection.cursor() as cursor: - cursor.execute("TRUNCATE TABLE {}".format(cls._meta.db_table)) - - -class NodeOnlineLog(ExportModelOperationsMixin("node_onlie_log"), models.Model): - """节点在线记录""" - - @classmethod - def totalOnlineUser(cls): - """返回所有节点的在线人数总和""" - count = 0 - node_ids = [o["node_id"] for o in Node.objects.filter(show=1).values("node_id")] - for node_id in node_ids: - o = cls.objects.filter(node_id=node_id).order_by("-log_time")[:1] - if o: - count += o[0].get_online_user() - return count - - @classmethod - def truncate(cls): - with connection.cursor() as cursor: - cursor.execute("TRUNCATE TABLE {}".format(cls._meta.db_table)) - - node_id = models.IntegerField("节点id", blank=False, null=False) - online_user = models.IntegerField("在线人数", blank=False, null=False) - log_time = models.IntegerField("日志时间", blank=False, null=False) - - def __str__(self): - return "节点:{}".format(self.node_id) - - def get_oneline_status(self): - """检测是否在线""" - if int(time.time()) - self.log_time > NODE_TIME_OUT: - return False - else: - return True - - def get_online_user(self): - """返回在线人数""" - if self.get_oneline_status() is True: - return self.online_user - else: - return 0 - - class Meta: - verbose_name_plural = "节点在线记录" - db_table = "ss_node_online_log" - - -class AliveIp(ExportModelOperationsMixin("aliveip_log"), models.Model): - @classmethod - def recent_alive(cls, node_id): - """ - 返回节点最近一分钟的在线ip - """ - ret = [] - seen = [] - now = pendulum.now() - last_now = now.subtract(minutes=1) - time_range = [str(last_now), str(now)] - logs = cls.objects.filter(node_id=node_id, log_time__range=time_range) - for log in logs: - if log.ip not in seen: - seen.append(log.ip) - ret.append(log) - return ret - - @classmethod - def truncate(cls): - with connection.cursor() as cursor: - cursor.execute("TRUNCATE TABLE {}".format(cls._meta.db_table)) - - @property - def node_name(self): - return Node.get_by_node_id(self.node_id).name - - node_id = models.IntegerField(verbose_name="节点id", blank=False, null=False) - ip = models.CharField(verbose_name="设备ip", max_length=128) - user = models.CharField(verbose_name="用户名", max_length=128) - log_time = models.DateTimeField("日志时间", auto_now=True) - - class Meta: - verbose_name_plural = "节点在线IP" - ordering = ["-log_time"] diff --git a/apps/ssserver/urls.py b/apps/ssserver/urls.py deleted file mode 100644 index 3da2e0f878..0000000000 --- a/apps/ssserver/urls.py +++ /dev/null @@ -1,14 +0,0 @@ -from django.urls import path -from . import views - - -app_name = "ssserver" -urlpatterns = [ - path("user/edit//", views.user_edit, name="user_edit"), - path("changesspass/", views.change_ss_pass, name="changesspass"), - path("changessmethod/", views.change_ss_method, name="changessmethod"), - path("changessprotocol/", views.change_ss_protocol, name="changessprotocol"), - path("changessobfs/", views.change_ss_obfs, name="changessobfs"), - path("subscribe/", views.subscribe, name="subscribe"), - path("node/config/", views.node_config, name="node_config"), -] diff --git a/apps/ssserver/views.py b/apps/ssserver/views.py deleted file mode 100644 index 743e6da0f0..0000000000 --- a/apps/ssserver/views.py +++ /dev/null @@ -1,215 +0,0 @@ -import json -import base64 -import binascii -from urllib import parse - -from django.urls import reverse -from django.conf import settings -from django.shortcuts import render -from django.contrib import messages -from django.shortcuts import get_object_or_404 -from django.views.decorators.http import require_http_methods -from django.http import StreamingHttpResponse, HttpResponseRedirect, HttpResponseNotFound -from django.contrib.auth.decorators import login_required, permission_required - -from .models import Suser, Node -from apps.sspanel.models import User -from apps.sspanel.forms import UserForm -from .forms import ChangeSsPassForm, SuserForm - - -@permission_required("ssesrver") -def user_edit(request, user_id): - """编辑ss_user的信息""" - ss_user = Suser.objects.get(user_id=user_id) - # 当为post请求时,修改数据 - if request.method == "POST": - # 对总流量部分进行修改,转换单GB - data = request.POST.copy() - data["transfer_enable"] = int(eval(data["transfer_enable"]) * settings.GB) - ssform = SuserForm(data, instance=ss_user) - userform = UserForm(data, instance=ss_user.user) - if ssform.is_valid() and userform.is_valid(): - ssform.save() - userform.save() - # 修改账户密码 - passwd = request.POST.get("resetpass") - if len(passwd) > 0: - user = ss_user.user - user.set_password(passwd) - user.save() - messages.success(request, "数据更新成功", extra_tags="修改成功") - return HttpResponseRedirect(reverse("sspanel:user_list")) - else: - messages.error(request, "数据填写错误", extra_tags="错误") - context = {"ssform": ssform, "userform": userform, "ss_user": ss_user} - return render(request, "backend/useredit.html", context=context) - # 当请求不是post时,渲染form - else: - # 特别初始化总流量字段 - data = {"transfer_enable": ss_user.transfer_enable // settings.GB} - ssform = SuserForm(initial=data, instance=ss_user) - userform = UserForm(instance=ss_user.user) - context = {"ssform": ssform, "userform": userform, "ss_user": ss_user} - return render(request, "backend/useredit.html", context=context) - - -@login_required -@require_http_methods(["POST"]) -def change_ss_pass(request): - """改变用户ss连接密码""" - ss_user = request.user.ss_user - - if request.method == "POST": - form = ChangeSsPassForm(request.POST) - - if form.is_valid(): - # 获取用户提交的password - ss_pass = request.POST.get("password") - ss_user.password = ss_pass - ss_user.save() - messages.success(request, "请及时更换客户端密码!", extra_tags="修改成功!") - return HttpResponseRedirect(reverse("sspanel:userinfo_edit")) - else: - messages.error(request, "新的客户端密码格式不正确!", extra_tags="修改失败!") - return HttpResponseRedirect(reverse("sspanel:userinfo_edit")) - else: - form = ChangeSsPassForm() - return render(request, "sspanel/sspasschanged.html", {"form": form}) - - -@login_required -@require_http_methods(["POST"]) -def change_ss_method(request): - """改变用户ss加密""" - ss_user = request.user.ss_user - ss_method = request.POST.get("method") - ss_user.method = ss_method - ss_user.save() - messages.success(request, "请及时更换客户端配置!", extra_tags="修改成功!") - return HttpResponseRedirect(reverse("sspanel:userinfo_edit")) - - -@login_required -@require_http_methods(["POST"]) -def change_ss_protocol(request): - """改变用户ss协议""" - ss_user = request.user.ss_user - ss_protocol = request.POST.get("protocol") - ss_user.protocol = ss_protocol - ss_user.save() - messages.success(request, "请及时更换客户端配置!", extra_tags="修改成功!") - return HttpResponseRedirect(reverse("sspanel:userinfo_edit")) - - -@login_required -@require_http_methods(["POST"]) -def change_ss_obfs(request): - """改变用户ss连接混淆""" - ss_user = request.user.ss_user - ss_obfs = request.POST.get("obfs") - ss_user.obfs = ss_obfs - ss_user.save() - messages.success(request, "请及时更换客户端配置!", extra_tags="修改成功!") - return HttpResponseRedirect(reverse("sspanel:userinfo_edit")) - - -def subscribe(request): - """ - 返回ssr订阅链接 - """ - url = request.build_absolute_uri() - token = parse.parse_qs(parse.urlparse(url).query).get("token", []) - if token: - try: - username = base64.b64decode(token[0]).decode() - except binascii.Error: - return HttpResponseNotFound() - else: - return HttpResponseNotFound() - # 验证token - user = get_object_or_404(User, username=username) - ss_user = user.ss_user - # 遍历该用户所有的节点 - if user.sub_type == User.SUB_TYPE_ALL: - node_list = Node.objects.filter(level__lte=user.level, show=1) - else: - node_list = Node.objects.filter( - level__lte=user.level, show=1, node_type=user.sub_type - ) - # 生成订阅链接部分 - sub_code = "MAX={}\n".format(len(node_list)) - for node in node_list: - sub_code = sub_code + node.get_node_link(ss_user) + "\n" - sub_code = base64.b64encode(bytes(sub_code, "utf8")).decode("ascii") - resp_ok = StreamingHttpResponse(sub_code) - resp_ok["Content-Type"] = "application/octet-stream; charset=utf-8" - resp_ok["Content-Disposition"] = "attachment; filename={}.txt".format(token) - resp_ok["Cache-Control"] = "no-store, no-cache, must-revalidate" - resp_ok["Content-Length"] = len(sub_code) - return resp_ok - - -@login_required -def node_config(request): - """返回节点json配置""" - user = request.user - ss_user = user.ss_user - node_list = Node.objects.filter(level__lte=user.level, show=1) - data = {"configs": []} - for node in node_list: - if node.node_type == 1: - # 单端口模式 - data["configs"].append( - { - "remarks": node.name, - "server_port": node.port, - "remarks_base64": base64.b64encode(bytes(node.name, "utf8")).decode( - "ascii" - ), - "enable": True, - "password": node.password, - "method": node.method, - "server": node.server, - "obfs": node.obfs, - "obfs_param": node.obfs_param, - "protocol": node.protocol, - "protocol_param": "{}:{}".format(ss_user.port, ss_user.password), - } - ) - elif node.custom_method == 1: - data["configs"].append( - { - "remarks": node.name, - "server_port": ss_user.port, - "remarks_base64": base64.b64encode(bytes(node.name, "utf8")).decode( - "ascii" - ), - "enable": True, - "password": ss_user.password, - "method": ss_user.method, - "server": node.server, - "obfs": ss_user.obfs, - "protocol": ss_user.protocol, - } - ) - else: - data["configs"].append( - { - "remarks": node.name, - "server_port": ss_user.port, - "remarks_base64": base64.b64encode(bytes(node.name, "utf8")).decode( - "ascii" - ), - "enable": True, - "password": ss_user.password, - "method": node.method, - "server": node.server, - "obfs": node.obfs, - "protocol": node.protocol, - } - ) - response = StreamingHttpResponse(json.dumps(data, ensure_ascii=False)) - response["Content-Type"] = "application/octet-stream" - response["Content-Disposition"] = 'attachment; filename="ss.json"' - return response diff --git a/apps/urls.py b/apps/urls.py index 9a2ab687ff..642c5dc469 100644 --- a/apps/urls.py +++ b/apps/urls.py @@ -6,10 +6,9 @@ urlpatterns = [ path("", index, name="index"), path("admin/", admin.site.urls, name="admin"), - path("", include("django.contrib.auth.urls")), + path("accounts/", include("django.contrib.auth.urls")), path("jet/", include("jet.urls", "jet")), path("prom/", include("django_prometheus.urls")), path("api/", include("apps.api.urls", namespace="api")), path("sspanel/", include("apps.sspanel.urls", namespace="sspanel")), - path("server/", include("apps.ssserver.urls", namespace="ssserver")), ] diff --git a/apps/utils.py b/apps/utils.py index 2354511c4a..cb4e5aeec7 100644 --- a/apps/utils.py +++ b/apps/utils.py @@ -109,11 +109,26 @@ def wrapper(request, *args, **kwargs): return wrapper -def get_current_time(): - return pendulum.now(tz=settings.TIME_ZONE) +def api_authorized(view_func): + @wraps(view_func) + def wrapper(request, *args, **kwargs): + token = request.GET.get("token", "") + if token != settings.TOKEN: + return JsonResponse({"msg": "auth error"}) + return view_func(request, *args, **kwargs) + + return wrapper -def global_settings(request): - global_variable = {"USE_SMTP": settings.USE_SMTP} +def handle_json_post(view_func): + @wraps(view_func) + def wrapper(request, *args, **kw): + if request.method == "POST": + request.json = json.loads(request.body) + return view_func(request, *args, **kw) + + return wrapper - return global_variable + +def get_current_time(): + return pendulum.now(tz=settings.TIME_ZONE) diff --git a/commands/clear_zombie_user.py b/commands/clear_zombie_user.py index 671533f492..669b934c7c 100644 --- a/commands/clear_zombie_user.py +++ b/commands/clear_zombie_user.py @@ -5,19 +5,14 @@ def clear_zombie_user(): """ 删除僵尸用户 """ - from apps.sspanel.models import User + from apps.sspanel.models import User, UserTraffic - users = User.objects.all() - count = 0 - for user in users: - try: - if user.ss_user.last_use_time == 0 and user.balance == 0: + for ut in UserTraffic.objects.all(): + if ut.last_use_time == UserTraffic.DEFAULT_USE_TIME: + user = User.get_by_pk(ut.user_id) + if user.balance == 0 and user.level > 0: user.delete() - count += 1 - except ObjectDoesNotExist: - user.delete() - count += 1 - print("clear user count: ", count) + print(f"delete zombie user {user.username}") if __name__ == "__main__": diff --git a/commands/croncmds.py b/commands/croncmds.py index b267fe7109..e125d8a0b7 100644 --- a/commands/croncmds.py +++ b/commands/croncmds.py @@ -1,40 +1,26 @@ +import os + import pendulum from django.conf import settings from django.utils import timezone -from apps.sspanel.models import User, UserOrder -from apps.ssserver.models import Node, NodeOnlineLog, TrafficLog, AliveIp -from django.core.mail import send_mail +from apps.sspanel.models import ( + User, + UserOrder, + UserOnLineIpLog, + UserTrafficLog, + SSNodeOnlineLog, + SSNode, + UserTraffic, +) + +os.environ["DJANGO_ENV"] = "production" def check_user_state(): """检测用户状态,将所有账号到期的用户状态重置""" - users = User.objects.filter(level__gt=0) - expire_user = [] - for user in users: - # 判断用户过期 - if timezone.now() - timezone.timedelta(days=1) > user.level_expire_time: - user.level = 0 - user.save() - ss_user = user.ss_user - ss_user.enable = False - ss_user.upload_traffic = 0 - ss_user.download_traffic = 0 - ss_user.transfer_enable = settings.DEFAULT_TRAFFIC - ss_user.save() - expire_user.append(user.email) - print( - "time: {} user: {} level timeout ".format( - timezone.now().strftime("%Y-%m-%d"), user.username - ) - ) - if expire_user and settings.EXPIRE_EMAIL_NOTICE: - send_mail( - "您的{0}账号已到期".format(settings.TITLE), - "您的{0}账号已到期,现被暂停使用。如需继续使用请前往 {1} 充值".format(settings.TITLE, settings.HOST), - settings.DEFAULT_FROM_EMAIL, - expire_user, - ) + User.check_and_disable_expired_users() + UserTraffic.check_and_disable_out_of_traffic_user() print("Time: {} CHECKED".format(timezone.now())) @@ -43,39 +29,37 @@ def auto_reset_traffic(): users = User.objects.filter(level=0) for user in users: - ss_user = user.ss_user - ss_user.download_traffic = 0 - ss_user.upload_traffic = 0 - ss_user.transfer_enable = settings.DEFAULT_TRAFFIC - ss_user.save() + ut = UserTraffic.get_by_user_id(user.pk) + ut.reset_traffic(settings.DEFAULT_TRAFFIC) + ut.save() print("Time {} all free user traffic reset! ".format(timezone.now())) def clean_traffic_log(): """清空七天前的所有流量记录""" dt = pendulum.now().subtract(days=7).date() - query = TrafficLog.objects.filter(log_date__lt=dt) + query = UserTrafficLog.objects.filter(date__lt=dt) count, res = query.delete() print("Time: {} traffic record removed!:{}".format(timezone.now(), count)) def clean_online_log(): """清空所有在线记录""" - count = TrafficLog.objects.count() - NodeOnlineLog.truncate() + count = SSNodeOnlineLog.objects.count() + SSNodeOnlineLog.truncate() print("Time {} online record removed!:{}".format(timezone.now(), count)) def clean_online_ip_log(): """清空在线ip记录""" - count = AliveIp.objects.count() - AliveIp.truncate() + count = UserOnLineIpLog.objects.count() + UserOnLineIpLog.truncate() print("Time: {} online ip log removed!:{}".format(timezone.now(), count)) def reset_node_traffic(): """月初重置节点使用流量""" - for node in Node.objects.all(): + for node in SSNode.objects.all(): node.used_traffic = 0 node.save() print("Time: {} all node traffic removed!".format(timezone.now())) diff --git a/commands/export_node_host.py b/commands/export_node_host.py deleted file mode 100644 index c820e471c5..0000000000 --- a/commands/export_node_host.py +++ /dev/null @@ -1,16 +0,0 @@ -def export_node_host(): - from apps.ssserver.models import Node - - hosts = [] - for node in Node.objects.all(): - hosts.append("'{}'".format(node.server)) - with open("node_host.txt", "w") as f: - f.writelines("\n".join(hosts)) - print("export node host down ! node num: ", len(hosts)) - - -if __name__ == "__main__": - from importlib import import_module - - import_module("__init__", "commands") - export_node_host() diff --git a/commands/print_user_count.py b/commands/print_user_count.py deleted file mode 100644 index 7b73f8e035..0000000000 --- a/commands/print_user_count.py +++ /dev/null @@ -1,11 +0,0 @@ -def print_user_count(): - from apps.sspanel.models import User - - print("total user count is : {}".format(User.objects.all().count())) - - -if __name__ == "__main__": - from importlib import import_module - - import_module("__init__", "commands") - print_user_count() diff --git a/configs/default/common.py b/configs/default/common.py index c61145fe09..bf133f9567 100644 --- a/configs/default/common.py +++ b/configs/default/common.py @@ -14,9 +14,10 @@ "django.contrib.messages", "django.contrib.staticfiles", "django_prometheus", - "django_crontab", # 定时任务相关 - "apps.sspanel", # 前端网站 - "apps.ssserver", + "django_crontab", + "anymail", + "apps.sspanel", + "v2", ] MIDDLEWARE = [ @@ -44,7 +45,6 @@ "django.template.context_processors.request", "django.contrib.auth.context_processors.auth", "django.contrib.messages.context_processors.messages", - "apps.utils.global_settings", ] }, } diff --git a/configs/default/email.py b/configs/default/email.py index ca1b0bbe25..f1ad6bf42f 100644 --- a/configs/default/email.py +++ b/configs/default/email.py @@ -1,13 +1,6 @@ # 是否开启邮件功能 USE_SMTP = True -# 是否开启ssl/tls -EMAIL_USE_TLS = False -EMAIL_USE_SSL = False - -# 我使用163邮箱作为smtp服务器 -EMAIL_HOST = "smtp.163.com" -EMAIL_PORT = 25 -EMAIL_HOST_USER = "USER" -EMAIL_HOST_PASSWORD = "PASS" -DEFAULT_FROM_EMAIL = "Ehco
" +ANYMAIL = {"MAILGUN_API_KEY": "", "MAILGUN_SENDER_DOMAIN": ""} +EMAIL_BACKEND = "anymail.backends.mailgun.EmailBackend" +DEFAULT_FROM_EMAIL = "mizhiwu@email.com" diff --git a/configs/default/sites.py b/configs/default/sites.py index bf65631763..28c9c7e704 100644 --- a/configs/default/sites.py +++ b/configs/default/sites.py @@ -1,11 +1,11 @@ # 网站域名设置(请正确填写,不然订阅功能会失效: -HOST = "http://127.0.0.1:8000/" +HOST = "http://127.0.0.1:8000" # 网站密钥 SECRET_KEY = "aasdasdas" # 是否开启注册 -ALLOW_REGISET = True +ALLOW_REGISTER = True # 默认的theme # 可选列表在 apps/constants.py 里的THEME_CHOICES里 @@ -34,7 +34,7 @@ # 支付订单提示信息 修改请保留 {} 用于动态生成金额 ALIPAY_TRADE_INFO = "谜之屋的{}元充值码" # 支付宝回掉接口 -ALIPAY_CALLBACK_URL = f'{HOST}/api/callback/alipay' +ALIPAY_CALLBACK_URL = f"{HOST}/api/callback/alipay" # 网站title TITLE = "谜之屋" @@ -54,3 +54,6 @@ # 是否开启用户到期邮件通知 EXPIRE_EMAIL_NOTICE = False + +# SHORT_URL_ALPHABET 请随机生成,且不要重复 +DEFAULT_ALPHABET = "qwertyuiopasdfghjklzxcvbnm" diff --git a/configs/development.py b/configs/development.py index 998a00eb6f..39cbf2ac76 100644 --- a/configs/development.py +++ b/configs/development.py @@ -11,5 +11,3 @@ DATABASES["default"].update( {"HOST": os.getenv("MYSQL_HOST", "127.0.0.1"), "PASSWORD": MYSQL_PASSWORD} ) - -EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend" diff --git a/configs/nginx/nginx.example.conf b/configs/nginx/nginx.example.conf index fd392a80ce..96eb298aa0 100644 --- a/configs/nginx/nginx.example.conf +++ b/configs/nginx/nginx.example.conf @@ -10,12 +10,12 @@ server } location /media { - alias /src/django-sspanel/media; # your Django project's media files - amend as required + alias /usr/src/app/media; # your Django project's media files - amend as required } location /static { - alias /src/django-sspanel/static; #静态文件地址,js/css + alias /usr/src/app/static; #静态文件地址,js/css expires 12h; } diff --git a/configs/production.py b/configs/production.py index 433ac8750a..3de898f47e 100644 --- a/configs/production.py +++ b/configs/production.py @@ -11,6 +11,3 @@ "USER": os.getenv("MYSQL_USER", "root"), } ) - - -EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend" diff --git a/docker-compose.yml b/docker-compose.yml index 74a0fba71d..2ec5c2628f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,50 +1,54 @@ version: '3' + +networks: + sspanel: + +volumes: + mysql_data: + services: - db: + nginx: + image: nginx + restart: always + container_name : nginx + volumes: + - ./configs/nginx/:/etc/nginx/conf.d + - .:/usr/src/app + ports: + - 80:80 + depends_on: + - web + networks: + - sspanel + mysql: image: mysql:5.6 - container_name : sspanel-db + container_name : mysql restart: always environment: MYSQL_ROOT_PASSWORD: yourpass MYSQL_DATABASE: sspanel volumes: - ./configs/mysqld/mysqld_charset.cnf:/etc/mysql/conf.d/mysqld_charset.cnf - - mysql-data:/var/lib/mysql + - mysql_data:/var/lib/mysql networks: - - sspanel_network + - sspanel web: - container_name : sspanel-web + container_name : web + restart: always build: . image: sspanel environment: MYSQL_PASSWORD: yourpass - MYSQL_HOST: db + MYSQL_HOST: mysql + DJANGO_ENV: production volumes: - - .:/src/django-sspanel + - .:/usr/src/app depends_on: - - db + - mysql networks: - - sspanel_network + - sspanel ports: - 8080:8080 - working_dir: /src/django-sspanel - command: uwsgi uwsgi.ini - nginx: - image: nginx - restart: always - container_name : sspanel-nginx - volumes: - - ./configs/nginx/:/etc/nginx/conf.d - - static:/src/django-sspanel/static - ports: - - 80:80 - depends_on: - - web - networks: - - sspanel_network -networks: - sspanel_network: -volumes: - static: - mysql-data: \ No newline at end of file + working_dir: /usr/src/app + command: uwsgi uwsgi.ini \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 7b1cf22757..7f7681217b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,10 +8,12 @@ python-alipay-sdk requests tomd uWSGI -Django -mysqlclient +Django>=2.2.1 +mysqlclient>=1.3.13 pycrypto gevent pendulum django-ratelimit -sentry-sdk>=0.7.8 \ No newline at end of file +sentry-sdk>=0.7.8 +short_url +django-anymail[mailgun] \ No newline at end of file diff --git a/static/sspanel/css/ehco.css b/static/sspanel/css/sspanel.css similarity index 100% rename from static/sspanel/css/ehco.css rename to static/sspanel/css/sspanel.css diff --git a/static/sspanel/js/ehcov1.js b/static/sspanel/js/sspanel.js similarity index 82% rename from static/sspanel/js/ehcov1.js rename to static/sspanel/js/sspanel.js index 866a0a3218..c5b74d832f 100644 --- a/static/sspanel/js/ehcov1.js +++ b/static/sspanel/js/sspanel.js @@ -128,6 +128,21 @@ document.addEventListener('DOMContentLoaded', function () { +function genRandomRgbaSet(num) { + colorData = [] + for (var i = 0; i < num; i++) { + var r = Math.floor(Math.random() * 256); // Random between 0-255 + var g = Math.floor(Math.random() * 256); // Random between 0-255 + var b = Math.floor(Math.random() * 256); // Random between 0-255 + var rgba = 'rgba(' + r + ',' + g + ',' + b + ',' + 0.2 + ')'; // Collect all to a string + colorData.push(rgba) + } + return colorData + +} + + + var getRandomColor = function () { var letters = '0123456789ABCDEF'; var color = '#'; @@ -230,3 +245,39 @@ var genLineChart = function (chartId, config) { } }) } +var genBarChart = function (chartId, config) { + /** + charId : 元素id 定位canvas用 + config : 配置信息 dict类型 + { + title: 图表名字 + labels :data对应的label + data_title: data的标题 + data: 数据 + } + **/ + var ctx = $('#' + chartId) + var myChart = new Chart(ctx, { + type: 'bar', + data: { + labels: config.labels, + datasets: [{ + label: config.data_title, + data: config.data, + backgroundColor: genRandomRgbaSet(config.data.length), + }] + }, + options: { + scales: { + yAxes: [{ + ticks: { + beginAtZero: true, + stepSize: 1, + suggestedMax: 7 + } + }] + } + } + }) +} + diff --git a/templates/backend/index.html b/templates/backend/index.html index 154e532917..1c4e0a3540 100644 --- a/templates/backend/index.html +++ b/templates/backend/index.html @@ -33,16 +33,14 @@

{% endif %} + \ No newline at end of file diff --git a/templates/base.html b/templates/base.html index 3667dce45f..fed5902a91 100644 --- a/templates/base.html +++ b/templates/base.html @@ -24,7 +24,7 @@ @@ -44,7 +44,7 @@

请不要下载来路不明的软件:

diff --git a/templates/sspanel/invite.html b/templates/sspanel/invite.html index d7e6e452be..3854dbb829 100644 --- a/templates/sspanel/invite.html +++ b/templates/sspanel/invite.html @@ -24,11 +24,11 @@

邀请码: - {% for code in codelist %} + {% for code in code_list %} - {{ code.time_created|date:"m月d日" }}: + {{ code.created_at|date:"m月d日" }} - + {{ code.code }} diff --git a/templates/sspanel/nodeinfo.html b/templates/sspanel/nodeinfo.html index e5126fbad4..1e5cc0e734 100644 --- a/templates/sspanel/nodeinfo.html +++ b/templates/sspanel/nodeinfo.html @@ -14,28 +14,21 @@

-
-
- {% for node in nodelists %} -
+
+
+ {% for node in node_list %} +
{% if node.online == True %}

- {{ node.ss_type_info }} - {{ node.node_type }} - 在线 -   {{ node.name }} + SS + 在线 {{ node.name }}

{% else %}

- {{ node.ss_type_info }} - {{ node.node_type }} - 掉线 -   {{ node.name }} + SS + 掉线 {{ node.name }}

{% endif %}

地区: @@ -63,7 +56,7 @@

配置 - 「{{ node.count }}」 + 「{{ node.online_user_count }}」

@@ -77,10 +70,6 @@

@@ -188,33 +139,47 @@

{% endif %}

-
{% endfor %} + {% if not forloop.last %}
{% endif %} + {% endfor %}
+
{% endblock main %} \ No newline at end of file diff --git a/templates/sspanel/rebaterecord.html b/templates/sspanel/rebaterecord.html deleted file mode 100644 index 763138e5a5..0000000000 --- a/templates/sspanel/rebaterecord.html +++ /dev/null @@ -1,56 +0,0 @@ -{% extends 'base.html' %} {% block main %} - -
-
-
-
-

- 返利记录:最新的10条记录 -

-

- 存钱罐.... -

-
-
-
-
-
- -
-
-

我的返利记录

- - - - - - - - - {% for r in records %} - - - - - - {% endfor %} -
ID时间金额
#{{ r.pk }}{{ r.rebatetime }}{{ r.money }}元
-
-
-
-{% endblock main %} \ No newline at end of file diff --git a/templates/sspanel/shop.html b/templates/sspanel/shop.html index 7173ea8e5f..4b9a2bc185 100644 --- a/templates/sspanel/shop.html +++ b/templates/sspanel/shop.html @@ -22,13 +22,13 @@

用户名:

-

{{ ss_user }}

+

{{ user }}

余额:

-

{{ ss_user.balance }}

+

{{ user.balance }}

diff --git a/templates/sspanel/user_settings.html b/templates/sspanel/user_settings.html new file mode 100644 index 0000000000..6c0fb4e21f --- /dev/null +++ b/templates/sspanel/user_settings.html @@ -0,0 +1,87 @@ +{% extends 'base.html' %} {% block main %} +
+
+
+
+

+ 用户资料改写处: +

+

+ 自定义连接信息.... +

+
+
+
+
+
+
+ +
+ +
+ 修改连接密码: +
+

当前密码:{{ user_ss_config.password }}

+
+ +
+
+ +
+
+
+ +
+ 自定义的加密: +
+

当前加密方式:{{ user_ss_config.method }}

+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+
+
+ +
+
+
+
+ +
+ + +{% endblock main %} \ No newline at end of file diff --git a/templates/sspanel/trafficlog.html b/templates/sspanel/user_traffic_log.html similarity index 81% rename from templates/sspanel/trafficlog.html rename to templates/sspanel/user_traffic_log.html index 0fc4d05ce5..8939dca949 100644 --- a/templates/sspanel/trafficlog.html +++ b/templates/sspanel/user_traffic_log.html @@ -18,14 +18,14 @@

用户名:

-

{{ ss_user }}

+

{{ user }}

使用流量:

- {{ ss_user.used_traffic }} + {{ user.user_ss_config.human_used_traffic }}

@@ -36,7 +36,7 @@

@@ -52,17 +52,15 @@

{% endblock main %} \ No newline at end of file diff --git a/templates/sspanel/userinfo.html b/templates/sspanel/userinfo.html index c7271bc57d..716902a6dd 100644 --- a/templates/sspanel/userinfo.html +++ b/templates/sspanel/userinfo.html @@ -74,7 +74,7 @@


- {% if user.ss_user.today_is_checked %} + {% if user.today_is_checkin %}

今天已经签到过了ಠ౪ಠ

{% else %}

今天还没有签到,点一下可以获得{{ min_traffic }}~{{ max_traffic }}流量

@@ -110,24 +110,18 @@

  • 端口: - {{ user.ss_user.port }} + {{ user.user_ss_config.port }}
  • 密码: - {{ user.ss_user.password }} + {{ user.user_ss_config.password }}
  • 加密: - {{ user.ss_user.method }} -
  • -
  • 协议: - {{ user.ss_user.protocol }} -
  • -
  • 混淆: - {{ user.ss_user.obfs }} + {{ user.user_ss_config.method }}

  • @@ -145,7 +139,7 @@

    点击按钮复制链接至客户端的订阅处

    - +

  • 总量: - {{ user.ss_user.totla_transfer }} + {{ user.user_ss_config.human_total_traffic }}
  • @@ -187,10 +181,10 @@

  • 使用: - {{ user.ss_user.used_traffic }}
  • + {{ user.user_ss_config.human_used_traffic }}
    -
    @@ -211,7 +205,7 @@

  • 等级: {{ user.level }} 级
  • - {% if user.ss_user.enable == True %} + {% if user.user_ss_config.enable == True %}
  • 状态: 正常使用
  • @@ -227,10 +221,10 @@

    {{ user_sub_type }}
  • 等级到期时间: - {{ user.expire_time }} + {{ user.level_expire_time }}
  • 上次使用时间: - {{ user.ss_user.user_last_use_time|date:"m月d日G点" }} + {{ user_traffic.last_use_time}}
  • @@ -248,22 +242,29 @@

    $('.copied').show(); $('.copied').fadeOut(1000); }); - // 端口重置Ajax部分 + // 端口重置 var ProtButton = $("#id-change-port") var changeport = function () { - url = "{% url 'api:changessport' %}" - $.getJSON(url, function (results) { + url = "{% url 'api:reset_ss_port' %}" + data = { + csrfmiddlewaretoken: '{{ csrf_token }}', + } + $.post(url, data, function (results) { swal(results.title, results.subtitle, results.status) port.textContent = results.subtitle.slice(6, -1) }) } ProtButton.click(changeport) - // 签到AJAX部分 + // 签到 var CheckButton = $("#id-checkin") var Checkin = function () { url = "{% url 'api:checkin' %}" - $.getJSON(url, function (results) { - swal(results.title, results.subtitle, results.status) + data = { + csrfmiddlewaretoken: '{{ csrf_token }}', + } + $.post(url, data, function (results) { + info = results + swal(info.title, info.subtitle, info.status) var box = $("#id-checkin-box") box.html('

    今天已经签到过了ಠ౪ಠ

    ') }) diff --git a/templates/sspanel/userinfoedit.html b/templates/sspanel/userinfoedit.html deleted file mode 100644 index 4bdd7f8cc1..0000000000 --- a/templates/sspanel/userinfoedit.html +++ /dev/null @@ -1,131 +0,0 @@ -{% extends 'base.html' %} {% block main %} -
    -
    -
    -
    -

    - 用户资料改写处: -

    -

    - 自定义连接信息.... -

    -
    -
    -
    -
    -
    -
    -
    -
    - 修改连接密码: -
    -

    当前密码:{{ ss_user.password }}

    -
    - {% csrf_token %} - -
    -
    - -
    -
    -
    -
    - 自定义的加密: -
    -

    当前加密方式:{{ ss_user.method }}

    -
    - {% csrf_token %} -
    -
    - -
    -
    -
    -
    -
    - -
    -
    -
    -
    -
    -
    -
    - -
    -
    -
    -
    - -
    -
    - 自定义协议: -
    -

    当前协议:{{ ss_user.protocol }}

    -
    - {% csrf_token %} -
    -
    - -
    -
    -
    -
    -
    - -
    -
    -
    -
    -
    -
    -
    - -
    -
    -
    -
    - 自定义混淆: -
    -

    当前混淆:{{ ss_user.obfs }}

    -
    - {% csrf_token %} -
    -
    - -
    -
    -
    -
    -
    - -
    -
    -
    -
    -
    -
    -
    - -
    -
    -
    -
    -
    - - - - - {% endblock main %} \ No newline at end of file diff --git a/uwsgi.ini b/uwsgi.ini index 3cc16a7e33..5dfa4cd914 100644 --- a/uwsgi.ini +++ b/uwsgi.ini @@ -3,6 +3,7 @@ plungins = python,gevent socket = 0.0.0.0:8080 module = apps.wsgi:application pidfile = /tmp/django-sspanel.pid +enable-threads = true gevent-early-monkey-patch = True gevent-wait-for-hub = True diff --git a/v2/__init__.py b/v2/__init__.py new file mode 100644 index 0000000000..545f927eda --- /dev/null +++ b/v2/__init__.py @@ -0,0 +1 @@ +default_app_config = "v2.apps.V2Config" diff --git a/v2/apps.py b/v2/apps.py new file mode 100644 index 0000000000..315adfb0d8 --- /dev/null +++ b/v2/apps.py @@ -0,0 +1,10 @@ +from django.apps import AppConfig + + +class V2Config(AppConfig): + name = "v2" + + def ready(self): + from apps.connector import register_connectors + + register_connectors()